Merge branch 'quentin' of https://gitlab.telecom-paris.fr/pact/2022-2023/pact71 into quentin
59
code/Interface_Lounes/determinationGenre.py
Normal file
@@ -0,0 +1,59 @@
|
||||
import cv2
|
||||
import mediapipe as mp
|
||||
|
||||
mp_drawing = mp.solutions.drawing_utils
|
||||
mp_face_mesh = mp.solutions.face_mesh
|
||||
|
||||
drawing_spec = mp_drawing.DrawingSpec(thickness=1, circle_radius=1)
|
||||
cap = cv2.VideoCapture(0)
|
||||
with mp_face_mesh.FaceMesh(
|
||||
max_num_faces=1,
|
||||
refine_landmarks=True,
|
||||
min_detection_confidence=0.5,
|
||||
min_tracking_confidence=0.5) as face_mesh:
|
||||
while cap.isOpened():
|
||||
success, image = cap.read()
|
||||
if not success:
|
||||
print("Ignoring empty camera frame.")
|
||||
continue
|
||||
# Initialize the face mesh model
|
||||
face_mesh = mp_face_mesh.FaceMesh(static_image_mode=False, max_num_faces=1, min_detection_confidence=0.5)
|
||||
|
||||
# Load the input image
|
||||
# lecture de la vidéo
|
||||
ret, frame = cap.read()
|
||||
# conversion de l'image en RGB
|
||||
image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
# Process the image and extract the landmarks
|
||||
results = face_mesh.process(image)
|
||||
if results.multi_face_landmarks:
|
||||
landmarks = results.multi_face_landmarks[0]
|
||||
|
||||
# Define the landmark indices for the corners of the eyes and the tip of the nose
|
||||
left_eye = [33, 133, 246, 161, 160, 159, 158, 157, 173, 133]
|
||||
right_eye = [362, 263, 373, 380, 381, 382, 384, 385, 386, 362]
|
||||
nose_tip = 4
|
||||
|
||||
# Calculate the distance between the eyes and the nose tip
|
||||
left_eye_x = landmarks.landmark[left_eye[0]].x * image.shape[1]
|
||||
right_eye_x = landmarks.landmark[right_eye[0]].x * image.shape[1]
|
||||
nose_x = landmarks.landmark[nose_tip].x * image.shape[1]
|
||||
eye_distance = abs(left_eye_x - right_eye_x)
|
||||
nose_distance = abs(nose_x - (left_eye_x + right_eye_x) / 2)
|
||||
|
||||
# Determine the gender based on the eye and nose distances
|
||||
if eye_distance > 1.5 * nose_distance:
|
||||
gender = "Female"
|
||||
else:
|
||||
gender = "Male"
|
||||
|
||||
# Draw the landmarks on the image
|
||||
cv2.putText(image, gender, (10, 50),cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
|
||||
# affichage de la vidéo
|
||||
cv2.imshow('Video', cv2.cvtColor(image, cv2.COLOR_RGB2BGR))
|
||||
if cv2.waitKey(10) & 0xFF == ord('q'):
|
||||
break
|
||||
|
||||
# libération de la caméra et des ressources
|
||||
cap.release()
|
||||
cv2.destroyAllWindows()
|
||||
88
code/Interface_Lounes/reconnaissancePouce.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
import mediapipe as mp
|
||||
mp_drawing = mp.solutions.drawing_utils
|
||||
mp_drawing_styles = mp.solutions.drawing_styles
|
||||
mp_hands = mp.solutions.hands
|
||||
|
||||
def prodScalaire(V1,V2):
|
||||
return V1[0]*V2[0]+V1[1]*V2[1]/(np.sqrt(V1[0]**2+V1[1]**2)*np.sqrt(V2[0]**2+V2[1]**2))
|
||||
|
||||
def reconnaissancePouce(handLandmarks):
|
||||
etatDuPouce=["neutre","baissé","levé"]
|
||||
i=0
|
||||
j=0
|
||||
for cpt in range (0,4):
|
||||
V1=[handLandmarks[(4*cpt)+6][0]-handLandmarks[(4*cpt)+5][0],handLandmarks[(4*cpt)+6][1]-handLandmarks[(4*cpt)+5][1]]
|
||||
V2=[handLandmarks[(4*cpt)+8][0]-handLandmarks[(4*cpt)+6][0],handLandmarks[(4*cpt)+8][1]-handLandmarks[(4*cpt)+6][1]]
|
||||
j=np.dot(V1,V2)
|
||||
if (j>0.005):
|
||||
return etatDuPouce[0]
|
||||
V1=[handLandmarks[4][0]-handLandmarks[1][0],handLandmarks[4][1]-handLandmarks[1][1]]
|
||||
V2=[handLandmarks[2][0]-handLandmarks[1][0],handLandmarks[2][1]-handLandmarks[1][1]]
|
||||
if((np.dot(V1,V2))>0 and (handLandmarks[4][1]>handLandmarks[2][1])):
|
||||
i=1
|
||||
elif(np.dot(V1,V2)>0 and handLandmarks[4][1]<handLandmarks[2][1]):
|
||||
i=2
|
||||
return etatDuPouce[i]
|
||||
|
||||
|
||||
cap = cv2.VideoCapture(0)
|
||||
with mp_hands.Hands(
|
||||
model_complexity=0,
|
||||
min_detection_confidence=0.5,
|
||||
min_tracking_confidence=0.5) as hands:
|
||||
while cap.isOpened():
|
||||
success, image = cap.read()
|
||||
if not success:
|
||||
print("Ignoring empty camera frame.")
|
||||
# If loading a video, use 'break' instead of 'continue'.
|
||||
continue
|
||||
|
||||
# To improve performance, optionally mark the image as not writeable to
|
||||
# pass by reference.
|
||||
image.flags.writeable = False
|
||||
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
||||
results = hands.process(image)
|
||||
|
||||
# Draw the hand annotations on the image.
|
||||
image.flags.writeable = True
|
||||
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
|
||||
if results.multi_hand_landmarks:
|
||||
for hand_landmarks in results.multi_hand_landmarks:
|
||||
mp_drawing.draw_landmarks(
|
||||
image,
|
||||
hand_landmarks,
|
||||
mp_hands.HAND_CONNECTIONS,
|
||||
mp_drawing_styles.get_default_hand_landmarks_style(),
|
||||
mp_drawing_styles.get_default_hand_connections_style())
|
||||
|
||||
# Set variable to keep landmarks positions (x and y)
|
||||
handLandmarks = []
|
||||
if results.multi_hand_landmarks:
|
||||
for hand_landmarks in results.multi_hand_landmarks:
|
||||
# Fill list with x and y positions of each landmark
|
||||
for landmarks in hand_landmarks.landmark:
|
||||
handLandmarks.append([landmarks.x, landmarks.y])
|
||||
|
||||
cv2.putText(image, reconnaissancePouce(handLandmarks), (50, 450), cv2.FONT_HERSHEY_SIMPLEX, 3, (255, 0, 0), 10)
|
||||
|
||||
# Flip the image horizontally for a selfie-view display.
|
||||
cv2.imshow('MediaPipe Hands', cv2.flip(image, 1))
|
||||
if cv2.waitKey(5) & 0xFF == 27:
|
||||
break
|
||||
cap.release()
|
||||
|
||||
|
||||
|
||||
|
||||
""" etatDuPouce=["neutre","baissé","levé"]
|
||||
i=0
|
||||
|
||||
if results.multi_hand_landmarks:
|
||||
|
||||
if(results.multi_hand_landmarks.gestures.categories[0].categoryName==Thumb_Up):
|
||||
cv2.putText(image, str(results.multi_hand_landmarks.gestures.categories[0].categoryName), (50, 450), cv2.FONT_HERSHEY_SIMPLEX, 3, (255, 0, 0), 10)
|
||||
else:
|
||||
cv2.putText(image, "raté", (50, 450), cv2.FONT_HERSHEY_SIMPLEX, 3, (255, 0, 0), 10)
|
||||
"""
|
||||
@@ -1,75 +0,0 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
import torch.optim as optim
|
||||
from torchvision import datasets, transforms
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
trainSet = datasets.ImageFolder(r'C:\Users\kesha\Desktop\TelecomParis\PACT\DownloadedDataset\train',
|
||||
transform = transforms.ToTensor())
|
||||
valSet = datasets.ImageFolder(r'C:\Users\kesha\Desktop\TelecomParis\PACT\DownloadedDataset\val',
|
||||
transform = transforms.ToTensor())
|
||||
|
||||
trainloader = torch.utils.data.DataLoader(trainSet,
|
||||
batch_size = 50,
|
||||
shuffle = True)
|
||||
|
||||
valloader = torch.utils.data.DataLoader(valSet,
|
||||
batch_size = 50,
|
||||
shuffle = True)
|
||||
|
||||
class Net(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
#nn.Conv2d(channels_in, out_channels/number of filters, kernel size)
|
||||
self.conv1 = nn.Conv2d(3, 16, 3)
|
||||
self.pool = nn.MaxPool2d(2, 2)
|
||||
self.conv2 = nn.Conv2d(16, 32, 3)
|
||||
self.conv3 = nn.Conv2d(32, 64, 3)
|
||||
self.fc1 = nn.Linear(64*14*14, 16)
|
||||
self.fc2 = nn.Linear(16, 6)
|
||||
|
||||
def forward(self, x):
|
||||
x = self.pool(F.relu(self.conv1(x)))
|
||||
#size = 16*126*126 then 16*63*63
|
||||
x = self.pool(F.relu(self.conv2(x)))
|
||||
#size = 32*61*61 then 32*30*30
|
||||
x = self.pool(F.relu(self.conv3(x)))
|
||||
#size = 64*28*28 then 64*14*14
|
||||
x = torch.flatten(x, 1)
|
||||
x = F.relu(self.fc1(x))
|
||||
x = self.fc2(x)
|
||||
return x
|
||||
|
||||
net = Net()
|
||||
print(net)
|
||||
|
||||
criterion = nn.CrossEntropyLoss()
|
||||
optimizer = optim.RMSprop(net.parameters(), lr=0.001)
|
||||
|
||||
device = torch.device('cuda')
|
||||
for epoch in range(1, 7):
|
||||
print('Starting epoch ' + str(epoch))
|
||||
current_loss = 0
|
||||
Epoch = []
|
||||
Loss = []
|
||||
for i, data in enumerate(trainloader, 0):
|
||||
inputs, labels = data
|
||||
|
||||
#très important
|
||||
optimizer.zero_grad()
|
||||
|
||||
output = net(inputs)
|
||||
loss = criterion(output, labels)
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
current_loss += loss.item()
|
||||
print('epoch: ', epoch, " loss: ", current_loss)
|
||||
Loss.append(current_loss)
|
||||
Epoch.append(epoch)
|
||||
|
||||
plt.plot(Epoch, Loss)
|
||||
plt.title('Valeur de la fonction cost en fonction de l\'epoch')
|
||||
plt.show()
|
||||
#to save a model: torch.save(net.state_dict(), file_location)
|
||||
27
code/backend_reconnaissance/.dockerignore
Normal file
@@ -0,0 +1,27 @@
|
||||
**/__pycache__
|
||||
**/.venv
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
1
code/backend_reconnaissance/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.wav
|
||||
19
code/backend_reconnaissance/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM python:3.8
|
||||
|
||||
#Ne pas créer les fichiers .pyc
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
#Afficher les logs directement dans le terminal
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
#Installation des dépendances de opencv
|
||||
RUN apt-get update
|
||||
RUN apt-get install ffmpeg libsm6 libxext6 portaudio19-dev python3-pyaudio pulseaudio -y
|
||||
|
||||
# Installation des dépendances python
|
||||
COPY requirements.txt .
|
||||
RUN python -m pip install -r requirements.txt
|
||||
# Création du répertoire de travail
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
|
||||
CMD ["python", "main.py"]
|
||||
BIN
code/backend_reconnaissance/__pycache__/hands.cpython-310.pyc
Normal file
BIN
code/backend_reconnaissance/audio_data.zip
Normal file
22
code/backend_reconnaissance/audio_data/metadata.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"ennuyant": {
|
||||
"grade": 2,
|
||||
"display": "Ennuyant"
|
||||
},
|
||||
"genial": {
|
||||
"grade": 9,
|
||||
"display": "Génial"
|
||||
},
|
||||
"j_ai_beaucoup_aime": {
|
||||
"grade": 9,
|
||||
"display": "J'ai beaucoup aimé"
|
||||
},
|
||||
"j_ai_trouve_ca_genial": {
|
||||
"grade": 10,
|
||||
"display": "J'ai trouvé ça génial"
|
||||
},
|
||||
"nul": {
|
||||
"grade": 0,
|
||||
"display": "Nul"
|
||||
}
|
||||
}
|
||||
145
code/backend_reconnaissance/audio_detector.py
Normal file
@@ -0,0 +1,145 @@
|
||||
import librosa
|
||||
import os
|
||||
import numpy as np
|
||||
import scipy.spatial.distance as dist
|
||||
import pyaudio
|
||||
import wave
|
||||
import json
|
||||
|
||||
def dp(distmat):
|
||||
N,M = distmat.shape
|
||||
# Initialisons the cost matrix
|
||||
costmat =np.zeros((N+1,M+1))
|
||||
for i in range (1,N+1):
|
||||
costmat[i,0]=np.inf
|
||||
for i in range (1,M+1):
|
||||
costmat[0,i]=np.inf
|
||||
|
||||
for i in range (N):
|
||||
for j in range (M):
|
||||
#on calcule le cout minimal pour chaque chemin.pour atteindre the costmat[i][j] il y a trois chemins possibles on choisit celui de cout minimal
|
||||
penalty = [
|
||||
costmat[i,j], # cas T==0
|
||||
costmat[i,j+1] , # cas T==1
|
||||
costmat[i+1,j]] # cas T==2
|
||||
ipenalty = np.argmin(penalty)
|
||||
costmat[i+1,j+1] = distmat[i,j] + penalty[ipenalty]
|
||||
|
||||
#enlever les valeurs de l infini
|
||||
costmat = costmat[1: , 1:]
|
||||
return (costmat, costmat[-1, -1]/(N+M))
|
||||
def calculate_mfcc(audio, sr):
|
||||
# Define parameters for MFCC calculation
|
||||
n_mfcc = 13
|
||||
n_fft = 2048
|
||||
hop_length = 512
|
||||
fmin = 0
|
||||
fmax = sr/2
|
||||
|
||||
# Calculate MFCCs
|
||||
mfccs = librosa.feature.mfcc(y=audio, sr=sr, n_mfcc=n_mfcc, n_fft=n_fft, hop_length=hop_length, fmin=fmin, fmax=fmax)
|
||||
return mfccs.T
|
||||
def calculate_dtw_cost(mfccs_query , mfccs_train):
|
||||
distmat = dist.cdist(mfccs_query, mfccs_train,"cosine")
|
||||
costmat,mincost = dp(distmat)
|
||||
return mincost
|
||||
def recognize_speech(audio_query, audio_train_list, sr):#sr frequence d echantillonnage
|
||||
# Calculate MFCCs for query audio
|
||||
mfccs_query = calculate_mfcc(audio_query, sr)
|
||||
|
||||
# Calculate DTW cost for each audio in training data
|
||||
dtw_costs = []
|
||||
for audio_train in audio_train_list:
|
||||
mfccs_train = calculate_mfcc(audio_train, sr)
|
||||
mincost = calculate_dtw_cost(mfccs_query, mfccs_train)
|
||||
dtw_costs.append(mincost)
|
||||
|
||||
# Find index of word with lowest DTW cost
|
||||
index = np.argmin(dtw_costs)
|
||||
|
||||
# Return recognized word
|
||||
return index
|
||||
|
||||
def record_audio(filename, duration, sr):
|
||||
chunk = 1024
|
||||
sample_format = pyaudio.paInt16
|
||||
channels = 1
|
||||
record_seconds = duration
|
||||
filename = f"{filename}.wav"
|
||||
|
||||
p = pyaudio.PyAudio()
|
||||
|
||||
stream = p.open(format=sample_format,
|
||||
channels=channels,
|
||||
rate=sr,
|
||||
frames_per_buffer=chunk,
|
||||
input=True)
|
||||
|
||||
frames = []
|
||||
|
||||
print(f"Enregistrement en cours...")
|
||||
|
||||
for i in range(0, int(sr / chunk * record_seconds)):
|
||||
data = stream.read(chunk)
|
||||
frames.append(data)
|
||||
|
||||
stream.stop_stream()
|
||||
stream.close()
|
||||
|
||||
p.terminate()
|
||||
|
||||
|
||||
print("Enregistrement terminé")
|
||||
|
||||
wf = wave.open(filename, "wb")
|
||||
wf.setnchannels(channels)
|
||||
wf.setsampwidth(p.get_sample_size(sample_format))
|
||||
wf.setframerate(sr)
|
||||
wf.writeframes(b"".join(frames))
|
||||
wf.close()
|
||||
|
||||
print(f"Fichier enregistré sous {filename}")
|
||||
|
||||
def coupe_silence(signal):
|
||||
t = 0
|
||||
if signal[t] == 0 :
|
||||
p = 0
|
||||
while signal[t+p] == 0 :
|
||||
if p == 88 :
|
||||
signal = signal[:t] + signal[t+p:]
|
||||
coupe_silence(signal)
|
||||
else :
|
||||
p = p+1
|
||||
|
||||
def init_database():
|
||||
data_dir = "audio_data/"
|
||||
words = []
|
||||
files = []
|
||||
for word in os.listdir(data_dir):
|
||||
if not os.path.isfile(os.path.join(data_dir, word)):
|
||||
for file in os.listdir(os.path.join(data_dir,word)):
|
||||
if os.path.isfile(os.path.join(data_dir, word,file)):
|
||||
print(word,os.path.join(data_dir, word,file))
|
||||
words.append(word)
|
||||
files.append(os.path.join(data_dir, word,file))
|
||||
return words,files
|
||||
|
||||
def get_word_metadata(word):
|
||||
with open("audio_data/metadata.json") as f:
|
||||
data = json.loads(f.read())
|
||||
return data[word]
|
||||
|
||||
#Todo : detecte si pas de note donnée
|
||||
def get_grade():
|
||||
sr = 44100 # fréquence d'échantillonnage
|
||||
duration = 6 # durée d'enregistrement en secondes
|
||||
filename = "recording" # nom du fichier à enregistrer
|
||||
data_dir = "audio_data/"
|
||||
record_audio(filename, duration, sr)
|
||||
audio_query, sr = librosa.load(f'{filename}.wav', sr=sr)
|
||||
coupe_silence(audio_query)
|
||||
words, files = init_database()
|
||||
audio_train_list = [librosa.load(file, sr=sr)[0] for file in files]
|
||||
recognized_word_index = recognize_speech(audio_query, audio_train_list, sr)
|
||||
recognized_word = words[recognized_word_index]
|
||||
return get_word_metadata(recognized_word)
|
||||
97
code/backend_reconnaissance/hand_detector.py
Normal file
@@ -0,0 +1,97 @@
|
||||
import cv2
|
||||
import mediapipe as mp
|
||||
import numpy as np
|
||||
|
||||
class HandDetector():
|
||||
def __init__(self):
|
||||
self.mp_drawing = mp.solutions.drawing_utils
|
||||
self.mp_drawing_styles = mp.solutions.drawing_styles
|
||||
self.mp_hands = mp.solutions.hands
|
||||
self.cap = cv2.VideoCapture(0)
|
||||
self.hands = self.mp_hands.Hands(
|
||||
model_complexity=0,
|
||||
min_detection_confidence=0.5,
|
||||
min_tracking_confidence=0.5)
|
||||
#Paramètres
|
||||
self.BUFFER_LENGTH = 30
|
||||
self.DETECTION_THRESHOLD = 3/4
|
||||
|
||||
self.resultBuffer = []
|
||||
|
||||
def reset(self):
|
||||
self.resultBuffer = []
|
||||
|
||||
def reconnaissancePouce(self,handLandmarks):
|
||||
etatDuPouce=["neutre","thumbs_down","thumbs_up"]
|
||||
i=0
|
||||
j=0
|
||||
for cpt in range (0,4):
|
||||
V1=[handLandmarks[(4*cpt)+6][0]-handLandmarks[(4*cpt)+5][0],handLandmarks[(4*cpt)+6][1]-handLandmarks[(4*cpt)+5][1]]
|
||||
V2=[handLandmarks[(4*cpt)+8][0]-handLandmarks[(4*cpt)+6][0],handLandmarks[(4*cpt)+8][1]-handLandmarks[(4*cpt)+6][1]]
|
||||
j=np.dot(V1,V2)
|
||||
if (j>0.005):
|
||||
return etatDuPouce[0]
|
||||
V1=[handLandmarks[4][0]-handLandmarks[1][0],handLandmarks[4][1]-handLandmarks[1][1]]
|
||||
V2=[handLandmarks[2][0]-handLandmarks[1][0],handLandmarks[2][1]-handLandmarks[1][1]]
|
||||
if((np.dot(V1,V2))>0 and (handLandmarks[4][1]>handLandmarks[2][1])):
|
||||
i=1
|
||||
elif(np.dot(V1,V2)>0 and handLandmarks[4][1]<handLandmarks[2][1]):
|
||||
i=2
|
||||
return etatDuPouce[i]
|
||||
|
||||
|
||||
def detect(self):
|
||||
if self.cap.isOpened():
|
||||
success, image = self.cap.read()
|
||||
if not success:
|
||||
print("Ignoring empty camera frame.")
|
||||
# If loading a video, use 'break' instead of 'continue'.
|
||||
return False
|
||||
|
||||
# To improve performance, optionally mark the image as not writeable to
|
||||
# pass by reference.
|
||||
image.flags.writeable = False
|
||||
results = self.hands.process(image)
|
||||
# print(results)
|
||||
if results.multi_hand_landmarks:
|
||||
handsPositions = []
|
||||
for hand_landmarks in results.multi_hand_landmarks:
|
||||
handLandmarks = []
|
||||
# Fill list with x and y positions of each landmark
|
||||
for landmarks in hand_landmarks.landmark:
|
||||
handLandmarks.append([landmarks.x, landmarks.y])
|
||||
#On ajoute la position de chaque mains a une liste
|
||||
handsPositions.append(self.reconnaissancePouce(handLandmarks))
|
||||
|
||||
#On calcule le résultat suivant la position des deux mains
|
||||
if(len(handsPositions) == 2):
|
||||
if(handsPositions[0] == handsPositions[1]):
|
||||
thumbState = handsPositions[0]
|
||||
elif(handsPositions[0] == "neutre"):
|
||||
thumbState = handsPositions[1]
|
||||
elif(handsPositions[1] == "neutre"):
|
||||
thumbState = handsPositions[0]
|
||||
else:
|
||||
thumbState = "neutre"
|
||||
else:
|
||||
thumbState = handsPositions[0]
|
||||
|
||||
self.resultBuffer.append(thumbState)
|
||||
if(len(self.resultBuffer) > self.BUFFER_LENGTH):
|
||||
self.resultBuffer.pop(0)
|
||||
|
||||
thumbsUpCount = sum(map(lambda x : x == "thumbs_up", self.resultBuffer))
|
||||
thumbsDownCount = sum(map(lambda x : x == "thumbs_down", self.resultBuffer))
|
||||
|
||||
if(thumbsUpCount > self.DETECTION_THRESHOLD * self.BUFFER_LENGTH):
|
||||
result = "thumbs_up"
|
||||
elif(thumbsDownCount > self.DETECTION_THRESHOLD * self.BUFFER_LENGTH):
|
||||
result = "thumbs_down"
|
||||
else:
|
||||
result = False
|
||||
|
||||
if(thumbState != "neutre"):
|
||||
return thumbState, handLandmarks[9], np.linalg.norm(np.array(handLandmarks[9]) - np.array(handLandmarks[0])), result
|
||||
return False
|
||||
|
||||
|
||||
5
code/backend_reconnaissance/main.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from manager import Manager
|
||||
if __name__ == "__main__":
|
||||
print("backend started")
|
||||
m = Manager()
|
||||
m.loop()
|
||||
92
code/backend_reconnaissance/manager.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from hand_detector import HandDetector
|
||||
from audio_detector import get_grade
|
||||
from network import ApiClient, WebsocketServer
|
||||
import time
|
||||
|
||||
#Classe qui coordonne les différents modules et qui s'occupe de construire l'avis au fur et a mesure
|
||||
class Manager():
|
||||
def __init__(self):
|
||||
self.state = 0
|
||||
self.defualtAvis = {
|
||||
"note": None,
|
||||
"commentaire": None,
|
||||
"notes_autres": {}
|
||||
}
|
||||
|
||||
self.TIMEOUT_CAMERA = 5
|
||||
|
||||
self.avis = self.defualtAvis
|
||||
self.server = WebsocketServer(None)
|
||||
self.server.start()
|
||||
self.handDetector = HandDetector()
|
||||
self.api = ApiClient()
|
||||
self.timeLastChange = time.time()
|
||||
self.isLastHandPacketEmpty = False
|
||||
print("Backend ready")
|
||||
|
||||
#Boucle principale
|
||||
def loop(self):
|
||||
while(True):
|
||||
if(self.state == 0):
|
||||
self.sleep()
|
||||
if(self.state == 1):
|
||||
self.camera()
|
||||
if(self.state == 2):
|
||||
self.audio()
|
||||
if(self.state == 3):
|
||||
self.thankYou()
|
||||
time.sleep(0.01)
|
||||
|
||||
#Fonction qui est executée pendant que la borne est en veille, reveille la borne si une main est detectée
|
||||
def sleep(self):
|
||||
res = self.handDetector.detect()
|
||||
if(res != False):
|
||||
self.state = 1
|
||||
self.timeLastChange = time.time()
|
||||
self.server.sendMessage({"type": "state", "state": 1})
|
||||
|
||||
#Envoie la position de la main a l'écran et passe a l'étape suivante si une main est detectée pendant assez longtemps
|
||||
def camera(self):
|
||||
if(time.time() - self.timeLastChange > self.TIMEOUT_CAMERA):
|
||||
self.server.sendMessage({"type":"reset"})
|
||||
self.reset()
|
||||
return
|
||||
|
||||
res = self.handDetector.detect()
|
||||
if(res != False):
|
||||
state, coords, size, finalDecision = res
|
||||
self.server.sendMessage({"type": "effects", "effects": [{"type": state, "x":coords[0], "y": coords[1], "width": size, "height": size}]})
|
||||
self.isLastHandPacketEmpty = False
|
||||
if(finalDecision != False):
|
||||
self.avis["note"] = 10 if finalDecision == "thumbs_up" else 0
|
||||
self.state = 2
|
||||
self.timeLastChange = time.time()
|
||||
self.server.sendMessage({"type": "state", "state": 2})
|
||||
elif self.isLastHandPacketEmpty == False:
|
||||
self.server.sendMessage({"type":"effects","effects":[]})
|
||||
self.isLastHandPacketEmpty = True
|
||||
|
||||
def audio(self):
|
||||
result = get_grade()
|
||||
if(result != False):
|
||||
self.server.sendMessage({"type":"new_grade","word":result["display"]})
|
||||
self.avis["notes_autres"]["test"] = result["grade"]
|
||||
time.sleep(3)
|
||||
self.state = 3
|
||||
self.timeLastChange = time.time()
|
||||
self.server.sendMessage({"type": "state", "state": 3})
|
||||
|
||||
def thankYou(self):
|
||||
time.sleep(10)
|
||||
print("Reseting...")
|
||||
self.timeLastChange = time.time()
|
||||
self.server.sendMessage({"type": "state", "state": 0})
|
||||
res = self.api.send(self.avis["note"],self.avis["notes_autres"]["test"])
|
||||
print(res.text)
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.state = 0
|
||||
self.avis = self.defualtAvis
|
||||
self.handDetector.reset()
|
||||
|
||||
49
code/backend_reconnaissance/network.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import requests
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import websockets
|
||||
|
||||
class WebsocketServer(threading.Thread):
|
||||
def __init__(self, onMessage, port=os.getenv("PORT"), host=os.getenv("HOST")):
|
||||
threading.Thread.__init__(self)
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.messageQueue = []
|
||||
self.onMessage = onMessage
|
||||
|
||||
def run(self):
|
||||
print("server thread started")
|
||||
asyncio.run(self.runServer())
|
||||
|
||||
async def runServer(self):
|
||||
async with websockets.serve(self.handler, self.host, self.port):
|
||||
await asyncio.Future()
|
||||
|
||||
async def handler(self,websocket):
|
||||
while True:
|
||||
for msg in self.messageQueue:
|
||||
# print("sending", json.dumps(msg))
|
||||
await websocket.send(json.dumps(msg))
|
||||
self.messageQueue.pop(0)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
def sendMessage(self,message):
|
||||
self.messageQueue.append(message)
|
||||
|
||||
class ApiClient():
|
||||
def __init__(self, host=os.getenv("API_HOST"), port=os.getenv("API_PORT")):
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
def send(self,note,note_autre):
|
||||
#Exemple ajout d'un commentaire depuis la borne (site ou geste)
|
||||
avis = {
|
||||
"note": note,
|
||||
"source": "borne",
|
||||
"commentaire":"",
|
||||
#Optionel
|
||||
"notes_autre": '{"proprete":'+str(note_autre)+',"calme":10}',
|
||||
}
|
||||
return requests.post("http://"+self.host+":"+self.port+"/add_review", data=avis)
|
||||
BIN
code/backend_reconnaissance/recording.wav
Normal file
8
code/backend_reconnaissance/requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
websockets
|
||||
requests
|
||||
opencv-python
|
||||
mediapipe
|
||||
numpy
|
||||
pyaudio
|
||||
librosa
|
||||
scipy
|
||||
@@ -1,444 +1,428 @@
|
||||
-- phpMyAdmin SQL Dump
|
||||
-- version 4.9.5deb2
|
||||
-- https://www.phpmyadmin.net/
|
||||
--
|
||||
-- Host: localhost:3306
|
||||
-- Generation Time: Dec 26, 2022 at 10:31 AM
|
||||
-- Server version: 8.0.31-0ubuntu0.20.04.1
|
||||
-- PHP Version: 7.4.3
|
||||
|
||||
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||
SET AUTOCOMMIT = 0;
|
||||
START TRANSACTION;
|
||||
SET time_zone = "+00:00";
|
||||
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
|
||||
--
|
||||
-- Database: `telereview`
|
||||
--
|
||||
CREATE DATABASE IF NOT EXISTS `telereview` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||||
USE `telereview`;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `borne_auteurs`
|
||||
--
|
||||
|
||||
CREATE TABLE `borne_auteurs` (
|
||||
`id` int NOT NULL,
|
||||
`sexe` tinytext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
|
||||
`age` tinyint DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `borne_avis`
|
||||
--
|
||||
|
||||
CREATE TABLE `borne_avis` (
|
||||
`id` int NOT NULL,
|
||||
`id_auteur` int NOT NULL,
|
||||
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`note_principale` tinyint NOT NULL,
|
||||
`commentaire` text NOT NULL,
|
||||
`source_id` int NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `borne_criteres`
|
||||
--
|
||||
|
||||
CREATE TABLE `borne_criteres` (
|
||||
`id` int NOT NULL,
|
||||
`nom` text NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
--
|
||||
-- Dumping data for table `borne_criteres`
|
||||
--
|
||||
|
||||
INSERT INTO `borne_criteres` (`id`, `nom`) VALUES
|
||||
(1, 'proprete'),
|
||||
(2, 'calme'),
|
||||
(3, 'attente');
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `borne_notes_autre`
|
||||
--
|
||||
|
||||
CREATE TABLE `borne_notes_autre` (
|
||||
`id` int NOT NULL,
|
||||
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`critere_id` int NOT NULL,
|
||||
`avis_id` int NOT NULL,
|
||||
`note` int NOT NULL COMMENT 'Note sur 10'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `reseaux_sociaux_auteurs`
|
||||
--
|
||||
|
||||
CREATE TABLE `reseaux_sociaux_auteurs` (
|
||||
`id` int NOT NULL,
|
||||
`nom_utilisateur` text NOT NULL,
|
||||
`source_id` int NOT NULL,
|
||||
`lien` text NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `reseaux_sociaux_avis`
|
||||
--
|
||||
|
||||
CREATE TABLE `reseaux_sociaux_avis` (
|
||||
`id` int NOT NULL,
|
||||
`date` date NOT NULL,
|
||||
`source_id` int NOT NULL,
|
||||
`note` tinyint DEFAULT NULL,
|
||||
`commentaire` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
|
||||
`auteur_id` int NOT NULL,
|
||||
`lien_source` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `sources`
|
||||
--
|
||||
|
||||
CREATE TABLE `sources` (
|
||||
`id` int NOT NULL,
|
||||
`nom` text NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
--
|
||||
-- Dumping data for table `sources`
|
||||
--
|
||||
|
||||
INSERT INTO `sources` (`id`, `nom`) VALUES
|
||||
(1, 'website'),
|
||||
(2, 'borne'),
|
||||
(3, 'instagram');
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `stats_autres_annee`
|
||||
--
|
||||
|
||||
CREATE TABLE `stats_autres_annee` (
|
||||
`id` int NOT NULL,
|
||||
`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`critere_id` int NOT NULL,
|
||||
`note` float NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `stats_autres_jour`
|
||||
--
|
||||
|
||||
CREATE TABLE `stats_autres_jour` (
|
||||
`id` int NOT NULL,
|
||||
`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`critere_id` int NOT NULL,
|
||||
`note` float NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `stats_autres_mois`
|
||||
--
|
||||
|
||||
CREATE TABLE `stats_autres_mois` (
|
||||
`id` int NOT NULL,
|
||||
`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`critere_id` int NOT NULL,
|
||||
`note` float NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `stats_autres_semaine`
|
||||
--
|
||||
|
||||
CREATE TABLE `stats_autres_semaine` (
|
||||
`id` int NOT NULL,
|
||||
`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`critere_id` int NOT NULL,
|
||||
`note` float NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `stats_general_annee`
|
||||
--
|
||||
|
||||
CREATE TABLE `stats_general_annee` (
|
||||
`id` int NOT NULL,
|
||||
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`moyenne_globale` float NOT NULL,
|
||||
`moyenne_site` float NOT NULL,
|
||||
`moyenne_borne` float NOT NULL,
|
||||
`dist_age` text NOT NULL COMMENT 'Distribution de l''age des auteurs',
|
||||
`dist_sexe` text NOT NULL COMMENT 'Distribution du sexe des auteurs'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `stats_general_jour`
|
||||
--
|
||||
|
||||
CREATE TABLE `stats_general_jour` (
|
||||
`id` int NOT NULL,
|
||||
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`moyenne_globale` float DEFAULT NULL,
|
||||
`moyenne_site` float DEFAULT NULL,
|
||||
`moyenne_borne` float DEFAULT NULL,
|
||||
`dist_age` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT 'Distribution de l''age des auteurs',
|
||||
`dist_sexe` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT 'Distribution du sexe des auteurs'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `stats_general_mois`
|
||||
--
|
||||
|
||||
CREATE TABLE `stats_general_mois` (
|
||||
`id` int NOT NULL,
|
||||
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`moyenne_globale` float NOT NULL,
|
||||
`moyenne_site` float NOT NULL,
|
||||
`moyenne_borne` float NOT NULL,
|
||||
`dist_age` text NOT NULL COMMENT 'Distribution de l''age des auteurs',
|
||||
`dist_sexe` text NOT NULL COMMENT 'Distribution du sexe des auteurs'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `stats_general_semaine`
|
||||
--
|
||||
|
||||
CREATE TABLE `stats_general_semaine` (
|
||||
`id` int NOT NULL,
|
||||
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`moyenne_globale` float NOT NULL,
|
||||
`moyenne_site` float NOT NULL,
|
||||
`moyenne_borne` float NOT NULL,
|
||||
`dist_age` text NOT NULL COMMENT 'Distribution de l''age des auteurs',
|
||||
`dist_sexe` text NOT NULL COMMENT 'Distribution du sexe des auteurs'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
--
|
||||
-- Indexes for dumped tables
|
||||
--
|
||||
|
||||
--
|
||||
-- Indexes for table `borne_auteurs`
|
||||
--
|
||||
ALTER TABLE `borne_auteurs`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `borne_avis`
|
||||
--
|
||||
ALTER TABLE `borne_avis`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `borne_criteres`
|
||||
--
|
||||
ALTER TABLE `borne_criteres`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `borne_notes_autre`
|
||||
--
|
||||
ALTER TABLE `borne_notes_autre`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `reseaux_sociaux_auteurs`
|
||||
--
|
||||
ALTER TABLE `reseaux_sociaux_auteurs`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `reseaux_sociaux_avis`
|
||||
--
|
||||
ALTER TABLE `reseaux_sociaux_avis`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `sources`
|
||||
--
|
||||
ALTER TABLE `sources`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `stats_autres_annee`
|
||||
--
|
||||
ALTER TABLE `stats_autres_annee`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `stats_autres_jour`
|
||||
--
|
||||
ALTER TABLE `stats_autres_jour`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `stats_autres_mois`
|
||||
--
|
||||
ALTER TABLE `stats_autres_mois`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `stats_autres_semaine`
|
||||
--
|
||||
ALTER TABLE `stats_autres_semaine`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `stats_general_annee`
|
||||
--
|
||||
ALTER TABLE `stats_general_annee`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `stats_general_jour`
|
||||
--
|
||||
ALTER TABLE `stats_general_jour`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `stats_general_mois`
|
||||
--
|
||||
ALTER TABLE `stats_general_mois`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `stats_general_semaine`
|
||||
--
|
||||
ALTER TABLE `stats_general_semaine`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for dumped tables
|
||||
--
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `borne_auteurs`
|
||||
--
|
||||
ALTER TABLE `borne_auteurs`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `borne_avis`
|
||||
--
|
||||
ALTER TABLE `borne_avis`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `borne_criteres`
|
||||
--
|
||||
ALTER TABLE `borne_criteres`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `borne_notes_autre`
|
||||
--
|
||||
ALTER TABLE `borne_notes_autre`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `reseaux_sociaux_auteurs`
|
||||
--
|
||||
ALTER TABLE `reseaux_sociaux_auteurs`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `reseaux_sociaux_avis`
|
||||
--
|
||||
ALTER TABLE `reseaux_sociaux_avis`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `sources`
|
||||
--
|
||||
ALTER TABLE `sources`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `stats_autres_annee`
|
||||
--
|
||||
ALTER TABLE `stats_autres_annee`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `stats_autres_jour`
|
||||
--
|
||||
ALTER TABLE `stats_autres_jour`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `stats_autres_mois`
|
||||
--
|
||||
ALTER TABLE `stats_autres_mois`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `stats_autres_semaine`
|
||||
--
|
||||
ALTER TABLE `stats_autres_semaine`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `stats_general_annee`
|
||||
--
|
||||
ALTER TABLE `stats_general_annee`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `stats_general_jour`
|
||||
--
|
||||
ALTER TABLE `stats_general_jour`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `stats_general_mois`
|
||||
--
|
||||
ALTER TABLE `stats_general_mois`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `stats_general_semaine`
|
||||
--
|
||||
ALTER TABLE `stats_general_semaine`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
COMMIT;
|
||||
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||
SET AUTOCOMMIT = 0;
|
||||
START TRANSACTION;
|
||||
|
||||
|
||||
--
|
||||
-- Database: `telereview`
|
||||
--
|
||||
CREATE DATABASE IF NOT EXISTS `telereview`;
|
||||
USE `telereview`;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `borne_auteurs`
|
||||
--
|
||||
|
||||
CREATE TABLE `borne_auteurs` (
|
||||
`id` int NOT NULL,
|
||||
`sexe` tinytext ,
|
||||
`age` tinyint DEFAULT NULL
|
||||
) ;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `borne_avis`
|
||||
--
|
||||
|
||||
CREATE TABLE `borne_avis` (
|
||||
`id` int NOT NULL,
|
||||
`id_auteur` int NOT NULL,
|
||||
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`note_principale` tinyint NOT NULL,
|
||||
`commentaire` text NOT NULL,
|
||||
`source_id` int NOT NULL
|
||||
) ;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `borne_criteres`
|
||||
--
|
||||
|
||||
CREATE TABLE `borne_criteres` (
|
||||
`id` int NOT NULL,
|
||||
`nom` text NOT NULL
|
||||
) ;
|
||||
|
||||
--
|
||||
-- Dumping data for table `borne_criteres`
|
||||
--
|
||||
|
||||
INSERT INTO `borne_criteres` (`id`, `nom`) VALUES
|
||||
(1, 'proprete'),
|
||||
(2, 'calme'),
|
||||
(3, 'attente');
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `borne_notes_autre`
|
||||
--
|
||||
|
||||
CREATE TABLE `borne_notes_autre` (
|
||||
`id` int NOT NULL,
|
||||
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`critere_id` int NOT NULL,
|
||||
`avis_id` int NOT NULL,
|
||||
`note` int NOT NULL COMMENT 'Note sur 10'
|
||||
) ;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `reseaux_sociaux_auteurs`
|
||||
--
|
||||
|
||||
CREATE TABLE `reseaux_sociaux_auteurs` (
|
||||
`id` int NOT NULL,
|
||||
`nom_utilisateur` text NOT NULL,
|
||||
`source_id` int NOT NULL,
|
||||
`lien` text NOT NULL
|
||||
) ;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `reseaux_sociaux_avis`
|
||||
--
|
||||
|
||||
CREATE TABLE `reseaux_sociaux_avis` (
|
||||
`id` int NOT NULL,
|
||||
`date` date NOT NULL,
|
||||
`source_id` int NOT NULL,
|
||||
`note` tinyint DEFAULT NULL,
|
||||
`commentaire` text ,
|
||||
`auteur_id` int NOT NULL,
|
||||
`lien_source` text
|
||||
) ;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `sources`
|
||||
--
|
||||
|
||||
CREATE TABLE `sources` (
|
||||
`id` int NOT NULL,
|
||||
`nom` text NOT NULL
|
||||
) ;
|
||||
|
||||
--
|
||||
-- Dumping data for table `sources`
|
||||
--
|
||||
|
||||
INSERT INTO `sources` (`id`, `nom`) VALUES
|
||||
(1, 'website'),
|
||||
(2, 'borne');
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `stats_autres_annee`
|
||||
--
|
||||
|
||||
CREATE TABLE `stats_autres_annee` (
|
||||
`id` int NOT NULL,
|
||||
`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`critere_id` int NOT NULL,
|
||||
`note` float NOT NULL
|
||||
) ;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `stats_autres_jour`
|
||||
--
|
||||
|
||||
CREATE TABLE `stats_autres_jour` (
|
||||
`id` int NOT NULL,
|
||||
`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`critere_id` int NOT NULL,
|
||||
`note` float NOT NULL
|
||||
) ;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `stats_autres_mois`
|
||||
--
|
||||
|
||||
CREATE TABLE `stats_autres_mois` (
|
||||
`id` int NOT NULL,
|
||||
`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`critere_id` int NOT NULL,
|
||||
`note` float NOT NULL
|
||||
) ;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `stats_autres_semaine`
|
||||
--
|
||||
|
||||
CREATE TABLE `stats_autres_semaine` (
|
||||
`id` int NOT NULL,
|
||||
`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`critere_id` int NOT NULL,
|
||||
`note` float NOT NULL
|
||||
) ;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `stats_general_annee`
|
||||
--
|
||||
|
||||
CREATE TABLE `stats_general_annee` (
|
||||
`id` int NOT NULL,
|
||||
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`nb_avis` int NOT NULL,
|
||||
`moyenne_globale` float DEFAULT NULL,
|
||||
`moyenne_site` float DEFAULT NULL,
|
||||
`moyenne_borne` float DEFAULT NULL,
|
||||
`dist_age` text DEFAULT NULL COMMENT 'Distribution de l''age des auteurs',
|
||||
`dist_sexe` text DEFAULT NULL COMMENT 'Distribution du sexe des auteurs'
|
||||
) ;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `stats_general_jour`
|
||||
--
|
||||
|
||||
CREATE TABLE `stats_general_jour` (
|
||||
`id` int NOT NULL,
|
||||
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`nb_avis` int NOT NULL,
|
||||
`moyenne_globale` float DEFAULT NULL,
|
||||
`moyenne_site` float DEFAULT NULL,
|
||||
`moyenne_borne` float DEFAULT NULL,
|
||||
`dist_age` text DEFAULT NULL COMMENT 'Distribution de l''age des auteurs',
|
||||
`dist_sexe` text DEFAULT NULL COMMENT 'Distribution du sexe des auteurs'
|
||||
) ;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `stats_general_mois`
|
||||
--
|
||||
|
||||
CREATE TABLE `stats_general_mois` (
|
||||
`id` int NOT NULL,
|
||||
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`nb_avis` int NOT NULL,
|
||||
`moyenne_globale` float DEFAULT NULL,
|
||||
`moyenne_site` float DEFAULT NULL,
|
||||
`moyenne_borne` float DEFAULT NULL,
|
||||
`dist_age` text DEFAULT NULL COMMENT 'Distribution de l''age des auteurs',
|
||||
`dist_sexe` text DEFAULT NULL COMMENT 'Distribution du sexe des auteurs'
|
||||
) ;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `stats_general_semaine`
|
||||
--
|
||||
|
||||
CREATE TABLE `stats_general_semaine` (
|
||||
`id` int NOT NULL,
|
||||
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`nb_avis` int NOT NULL,
|
||||
`moyenne_globale` float DEFAULT NULL,
|
||||
`moyenne_site` float DEFAULT NULL,
|
||||
`moyenne_borne` float DEFAULT NULL,
|
||||
`dist_age` text DEFAULT NULL COMMENT 'Distribution de l''age des auteurs',
|
||||
`dist_sexe` text DEFAULT NULL COMMENT 'Distribution du sexe des auteurs'
|
||||
) ;
|
||||
|
||||
--
|
||||
-- Indexes for dumped tables
|
||||
--
|
||||
|
||||
--
|
||||
-- Indexes for table `borne_auteurs`
|
||||
--
|
||||
ALTER TABLE `borne_auteurs`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `borne_avis`
|
||||
--
|
||||
ALTER TABLE `borne_avis`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `borne_criteres`
|
||||
--
|
||||
ALTER TABLE `borne_criteres`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `borne_notes_autre`
|
||||
--
|
||||
ALTER TABLE `borne_notes_autre`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `reseaux_sociaux_auteurs`
|
||||
--
|
||||
ALTER TABLE `reseaux_sociaux_auteurs`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `reseaux_sociaux_avis`
|
||||
--
|
||||
ALTER TABLE `reseaux_sociaux_avis`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `sources`
|
||||
--
|
||||
ALTER TABLE `sources`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `stats_autres_annee`
|
||||
--
|
||||
ALTER TABLE `stats_autres_annee`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `stats_autres_jour`
|
||||
--
|
||||
ALTER TABLE `stats_autres_jour`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `stats_autres_mois`
|
||||
--
|
||||
ALTER TABLE `stats_autres_mois`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `stats_autres_semaine`
|
||||
--
|
||||
ALTER TABLE `stats_autres_semaine`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `stats_general_annee`
|
||||
--
|
||||
ALTER TABLE `stats_general_annee`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `stats_general_jour`
|
||||
--
|
||||
ALTER TABLE `stats_general_jour`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `stats_general_mois`
|
||||
--
|
||||
ALTER TABLE `stats_general_mois`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `stats_general_semaine`
|
||||
--
|
||||
ALTER TABLE `stats_general_semaine`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for dumped tables
|
||||
--
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `borne_auteurs`
|
||||
--
|
||||
ALTER TABLE `borne_auteurs`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `borne_avis`
|
||||
--
|
||||
ALTER TABLE `borne_avis`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `borne_criteres`
|
||||
--
|
||||
ALTER TABLE `borne_criteres`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `borne_notes_autre`
|
||||
--
|
||||
ALTER TABLE `borne_notes_autre`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `reseaux_sociaux_auteurs`
|
||||
--
|
||||
ALTER TABLE `reseaux_sociaux_auteurs`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `reseaux_sociaux_avis`
|
||||
--
|
||||
ALTER TABLE `reseaux_sociaux_avis`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `sources`
|
||||
--
|
||||
ALTER TABLE `sources`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `stats_autres_annee`
|
||||
--
|
||||
ALTER TABLE `stats_autres_annee`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `stats_autres_jour`
|
||||
--
|
||||
ALTER TABLE `stats_autres_jour`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `stats_autres_mois`
|
||||
--
|
||||
ALTER TABLE `stats_autres_mois`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `stats_autres_semaine`
|
||||
--
|
||||
ALTER TABLE `stats_autres_semaine`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `stats_general_annee`
|
||||
--
|
||||
ALTER TABLE `stats_general_annee`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `stats_general_jour`
|
||||
--
|
||||
ALTER TABLE `stats_general_jour`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `stats_general_mois`
|
||||
--
|
||||
ALTER TABLE `stats_general_mois`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `stats_general_semaine`
|
||||
--
|
||||
ALTER TABLE `stats_general_semaine`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
COMMIT;
|
||||
112
code/docker-compose.yaml
Normal file
@@ -0,0 +1,112 @@
|
||||
version: "3.9"
|
||||
services:
|
||||
|
||||
#Base de donnée mysql de la borne sur laquelle est stockée tous les avis et les stats
|
||||
db:
|
||||
image: mysql:latest
|
||||
container_name: db
|
||||
expose:
|
||||
- 3306
|
||||
volumes:
|
||||
- ./db:/docker-entrypoint-initdb.d
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost", "-uroot"] # Command to check health.
|
||||
interval: 5s # Interval between health checks.
|
||||
timeout: 5s # Timeout for each health checking.
|
||||
retries: 20 # Hou many times retries.
|
||||
start_period: 10s # Estimated time to boot.
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: telereview
|
||||
MYSQL_DATABASE: telereview
|
||||
|
||||
#Interface d'aministration pour la bdd
|
||||
phpmyadmin:
|
||||
image: phpmyadmin:latest
|
||||
restart: always
|
||||
container_name: phpmyadmin
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PMA_ARBITRARY: 1
|
||||
PMA_HOST: db
|
||||
PMA_USER: root
|
||||
PMA_PASSWORD: telereview
|
||||
ports:
|
||||
- 8000:80
|
||||
|
||||
#API de gestion des avis, permet d'ajouter ou de récuperer des avis ou les stats sur les avis par des requêtes HTTP
|
||||
reviews_api:
|
||||
container_name: reviews_api
|
||||
expose:
|
||||
- 8080
|
||||
ports:
|
||||
- 8080:8080
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DB_USER=root
|
||||
- DB_PASSWORD=telereview
|
||||
- DB_HOST=db
|
||||
- DB_NAME=telereview
|
||||
- PORT=8080
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
build: ./reviews_api
|
||||
restart: always
|
||||
|
||||
# Serveur web de l'interface de la borne
|
||||
interface_borne:
|
||||
image: httpd:latest
|
||||
volumes:
|
||||
- ./interface_borne:/usr/local/apache2/htdocs/
|
||||
container_name: interface_borne
|
||||
ports:
|
||||
- 8888:80
|
||||
|
||||
#Serveur web de l'interface admin
|
||||
interface_admin:
|
||||
image: httpd:latest
|
||||
volumes:
|
||||
- ./interface_admin/out:/usr/local/apache2/htdocs/
|
||||
container_name: interface_admin
|
||||
ports:
|
||||
- 800:80
|
||||
|
||||
#Formulaire de retour d'avis
|
||||
formulaire:
|
||||
image: httpd:latest
|
||||
volumes:
|
||||
- ./formulaire:/usr/local/apache2/htdocs/
|
||||
container_name: formulaire
|
||||
ports:
|
||||
- 80:80
|
||||
# #Backend de la borne : scripts pythons de reconnaissances video et audio
|
||||
# #Envoient les infos a l'interface de la borne par websocket pour mettre a jour l'interface rapidement
|
||||
# #Met a jour les avis en faisant des requêtes a l'API
|
||||
backend_reconnaissance:
|
||||
build: ./backend_reconnaissance
|
||||
container_name: backend_reconnaissance
|
||||
restart: always
|
||||
devices:
|
||||
- /dev/video3:/dev/video0
|
||||
- /dev/snd:/dev/snd
|
||||
environment:
|
||||
- PORT=5000
|
||||
- HOST=backend_reconnaissance
|
||||
- API_HOST=reviews_api
|
||||
- API_PORT=8080
|
||||
ports:
|
||||
#Ce container est le serveur websocker dont le client est l'interface de la borne qui tourne dans le navigateur
|
||||
- 5000:5000
|
||||
user: root
|
||||
|
||||
video_loopback:
|
||||
build: ./video_loopback
|
||||
container_name: video_loopback
|
||||
restart: always
|
||||
devices:
|
||||
- /dev/video0:/dev/video0
|
||||
- /dev/video2:/dev/video1
|
||||
- /dev/video3:/dev/video2
|
||||
3
code/interface_admin/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.next
|
||||
package-lock.json
|
||||
out
|
||||
34
code/interface_admin/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
|
||||
|
||||
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
|
||||
|
||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
58
code/interface_admin/components/Avis.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react'
|
||||
import { Card, Col, Row, Table } from 'react-bootstrap';
|
||||
import { BsPersonFill } from 'react-icons/bs';
|
||||
import styles from '../styles/Avis.module.css'
|
||||
|
||||
export default function Avis({review}) {
|
||||
const {date, note_principale,notes_autres, commentaire, sexe_auteur, nom_source, age_auteur} = review;
|
||||
return (
|
||||
<Card>
|
||||
<Card.Title>Avis</Card.Title>
|
||||
<Card.Body>
|
||||
<Row>
|
||||
<h2>Auteur</h2>
|
||||
<Col xs={1}>
|
||||
<BsPersonFill className={styles.personIcon} />
|
||||
</Col>
|
||||
<Col className='d-flex flex-column'>
|
||||
<p>Age : {age_auteur}</p>
|
||||
<p>Sexe : {sexe_auteur}</p>
|
||||
<p>Date de publication : {date}</p>
|
||||
<p>Source : {nom_source}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<h2>Notes</h2>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Critère</th>
|
||||
<th>Note</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Général</td>
|
||||
<td>{note_principale} / 10</td>
|
||||
</tr>
|
||||
{notes_autres && notes_autres.map(({ critere, note }) => {
|
||||
return <tr key={critere}>
|
||||
<td>{critere}</td>
|
||||
<td>{note}/10</td>
|
||||
</tr>
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Row>
|
||||
<Row>
|
||||
<Card>
|
||||
<Card.Header>Commentaire</Card.Header>
|
||||
<Card.Body>
|
||||
{commentaire}
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Row>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
34
code/interface_admin/components/AvisList.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react'
|
||||
import { Table } from 'react-bootstrap'
|
||||
import styles from '../styles/AvisList.module.css'
|
||||
|
||||
export default function AvisList({ avis }) {
|
||||
const router = useRouter();
|
||||
function handleClick(id) {
|
||||
router.push(`/avis/${id}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Note globale</th>
|
||||
<th>Commentaire</th>
|
||||
<th>Source</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{avis.map(({ id, note_principale, commentaire, date, nom_source }) => {
|
||||
return <tr onClick={() => handleClick(id)} key={id} className={styles.row}>
|
||||
<td>{date}</td>
|
||||
<td>{note_principale} / 10</td>
|
||||
<td>{commentaire}</td>
|
||||
<td>{nom_source}</td>
|
||||
</tr>
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
41
code/interface_admin/components/ComparativeBarChart.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import { Bar } from 'react-chartjs-2'
|
||||
import Chart from 'chart.js/auto'; //NE SURTOUT PAS SUPPRIMER CET IMPORT
|
||||
|
||||
export default function ComparativeBarChart({ xlabels, data0, label0, data1, label1}) {
|
||||
return (
|
||||
<Bar
|
||||
options={{
|
||||
responsive: true,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
},
|
||||
y: {
|
||||
stacked: true
|
||||
}
|
||||
}
|
||||
}}
|
||||
data={{
|
||||
labels: xlabels,
|
||||
datasets: [
|
||||
{
|
||||
label: label0,
|
||||
data: data0,
|
||||
backgroundColor: "#FF3B30",
|
||||
stack: "stack0"
|
||||
},
|
||||
{
|
||||
label: label1,
|
||||
data: data1,
|
||||
backgroundColor: "#0000FF",
|
||||
stack: "stack1"
|
||||
}
|
||||
]
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
23
code/interface_admin/components/Menu.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import Container from 'react-bootstrap/Container';
|
||||
import Nav from 'react-bootstrap/Nav';
|
||||
import Navbar from 'react-bootstrap/Navbar';
|
||||
|
||||
export default function Menu() {
|
||||
return (
|
||||
<Navbar bg="light" expand="lg">
|
||||
<Container>
|
||||
<Navbar.Brand href="#home">Téléreview</Navbar.Brand>
|
||||
<Navbar.Toggle aria-controls="basic-navbar-nav" />
|
||||
<Navbar.Collapse id="basic-navbar-nav">
|
||||
<Nav className="me-auto">
|
||||
<Link href="/" passHref legacyBehavior><Nav.Link>Accueil</Nav.Link></Link>
|
||||
<Link href="/stats" passHref legacyBehavior><Nav.Link>Statistiques</Nav.Link></Link>
|
||||
<Link href="/avis" passHref legacyBehavior><Nav.Link>Avis</Nav.Link></Link>
|
||||
</Nav>
|
||||
</Navbar.Collapse>
|
||||
</Container>
|
||||
</Navbar>
|
||||
)
|
||||
}
|
||||
34
code/interface_admin/components/StatBarChart.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React, { useRef } from 'react'
|
||||
import { Bar } from 'react-chartjs-2'
|
||||
import Chart from 'chart.js/auto'; //NE SURTOUT PAS SUPPRIMER CET IMPORT
|
||||
|
||||
export default function StatBarChart({labels, data}) {
|
||||
return (
|
||||
<Bar
|
||||
options={{
|
||||
redraw: true,
|
||||
responsive: true,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
},
|
||||
y: {
|
||||
stacked: true
|
||||
}
|
||||
}
|
||||
}}
|
||||
data={{
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
data: data,
|
||||
backgroundColor: "#FF3B30",
|
||||
},
|
||||
]
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
3
code/interface_admin/config/reviewsApi.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const api = {
|
||||
HOST: 'localhost:8080'
|
||||
}
|
||||
32
code/interface_admin/hooks/review.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { api } from "../config/reviewsApi";
|
||||
|
||||
|
||||
function useReview(reviewId) {
|
||||
const [review, setReview] = useState({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
async function fetchData(id) {
|
||||
const response = await fetch('http://' + api.HOST + `/borne/get_review?id=${id}`)
|
||||
if (response.ok) {
|
||||
const jsonData = await response.json();
|
||||
setReview(jsonData);
|
||||
setLoading(false);
|
||||
setError(false);
|
||||
} else {
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (reviewId) {
|
||||
fetchData(reviewId);
|
||||
}
|
||||
}, [reviewId])
|
||||
|
||||
return { review, loading, error }
|
||||
}
|
||||
|
||||
export default useReview;
|
||||
30
code/interface_admin/hooks/reviews.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { api } from "../config/reviewsApi";
|
||||
|
||||
export default function useReviews() {
|
||||
const [reviews, setReviews] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
async function fetchLastReviews(limit=100) {
|
||||
setLoading(true);
|
||||
const response = await fetch('http://' + api.HOST + '/borne/get_last_reviews', {
|
||||
method: 'GET'
|
||||
})
|
||||
if(response.ok) {
|
||||
let json = await response.json()
|
||||
setReviews(json);
|
||||
setError(false);
|
||||
setLoading(false);
|
||||
}else {
|
||||
setLoading(false);
|
||||
setError(true);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchLastReviews();
|
||||
}, [])
|
||||
|
||||
return {reviews, error, loading, fetchLastReviews};
|
||||
}
|
||||
26
code/interface_admin/hooks/stats.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { api } from "../config/reviewsApi";
|
||||
|
||||
export default function useStats(limit, interval) {
|
||||
const [stats, setStats] = useState({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
async function fetchData(limit, interval) {
|
||||
const response = await fetch("http://" + api.HOST + `/borne/get_stats?interval=${interval}&limit=${limit}`)
|
||||
if(response.ok) {
|
||||
const data = await response.json();
|
||||
setStats(data);
|
||||
setError(false);
|
||||
}else {
|
||||
setError(true)
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(limit, interval);
|
||||
}, [limit, interval])
|
||||
|
||||
return {stats, loading, error};
|
||||
}
|
||||
6
code/interface_admin/next.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
26
code/interface_admin/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "interface-admin",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"export": "next export",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/font": "13.1.6",
|
||||
"bootstrap": "^5.2.3",
|
||||
"chart.js": "^4.2.0",
|
||||
"date-fns": "^2.29.3",
|
||||
"eslint": "8.33.0",
|
||||
"eslint-config-next": "13.1.6",
|
||||
"next": "13.1.6",
|
||||
"react": "18.2.0",
|
||||
"react-bootstrap": "^2.7.0",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-icons": "^4.7.1"
|
||||
}
|
||||
}
|
||||
13
code/interface_admin/pages/_app.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import Menu from '../components/Menu'
|
||||
import '../styles/globals.css'
|
||||
import 'bootstrap/dist/css/bootstrap.css';
|
||||
import { Container } from 'react-bootstrap';
|
||||
|
||||
export default function App({ Component, pageProps }) {
|
||||
return <>
|
||||
<Menu />
|
||||
<Container fluid="md">
|
||||
<Component {...pageProps} />
|
||||
</Container>
|
||||
</>
|
||||
}
|
||||
16
code/interface_admin/pages/_document.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Html, Head, Main, NextScript } from 'next/document'
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
12
code/interface_admin/pages/avis/[id].js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react'
|
||||
import Avis from '../../components/Avis';
|
||||
import useReview from '../../hooks/review';
|
||||
export default function AvisPage() {
|
||||
const router = useRouter();
|
||||
const {id} = router.query;
|
||||
const {review, loading, error} = useReview(id);
|
||||
return (
|
||||
!loading && !error && <Avis review={review}/>
|
||||
)
|
||||
}
|
||||
74
code/interface_admin/pages/avis/index.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Card, Container, Form, Row } from 'react-bootstrap'
|
||||
import AvisList from '../../components/AvisList';
|
||||
import useReviews from '../../hooks/reviews';
|
||||
import styles from '../../styles/AvisListPage.module.css'
|
||||
|
||||
export default function AvisListPage() {
|
||||
const [minGrade, setMinGrade] = useState(0);
|
||||
const [maxGrade, setMaxGrade] = useState(10);
|
||||
const [sources, setSources] = useState({'borne': true, 'website': true})
|
||||
const [filteredReviews, setFilteredREviews] = useState([])
|
||||
|
||||
const {reviews, error, loading} = useReviews();
|
||||
|
||||
useEffect(() => {
|
||||
const newReviews = reviews.filter((review) => review.note_principale >= minGrade && review.note_principale <= maxGrade && sources[review.nom_source])
|
||||
setFilteredREviews(newReviews)
|
||||
}, [reviews, minGrade, maxGrade, sources])
|
||||
|
||||
useEffect(() => {
|
||||
if(minGrade > maxGrade) {
|
||||
setMinGrade(maxGrade);
|
||||
}
|
||||
}, [maxGrade]);
|
||||
useEffect(() => {
|
||||
if(minGrade > maxGrade) {
|
||||
setMaxGrade(minGrade);
|
||||
}
|
||||
}, [minGrade])
|
||||
|
||||
|
||||
return (
|
||||
<Container fluid>
|
||||
<Card>
|
||||
<Card.Header>Tous les avis</Card.Header>
|
||||
<Card.Body>
|
||||
<Row>
|
||||
<Form>
|
||||
<Form.Group>
|
||||
<Form.Label>Types d'avis</Form.Label>
|
||||
<Form.Check
|
||||
type="switch"
|
||||
label="Borne"
|
||||
onChange={(e) => setSources({...sources, 'borne': e.target.checked})}
|
||||
checked={sources['borne']}
|
||||
/>
|
||||
<Form.Check
|
||||
type="switch"
|
||||
label="QR Code"
|
||||
onChange={(e) => setSources({...sources, 'website': e.target.checked})}
|
||||
checked={sources['website']}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label>Note</Form.Label>
|
||||
<div className='d-flex flex-row justify-content-around col-md-6'>
|
||||
<div>Min : {minGrade}/10</div>
|
||||
<div className={styles.sliderContainer}>
|
||||
<input type="range" value={minGrade} onChange={(e) => setMinGrade(Number(e.target.value))} min="0" max="10" step="1" className={styles.slider}></input>
|
||||
<input type="range" value={maxGrade} onChange={(e) => setMaxGrade(Number(e.target.value))} min="0" max="10" step="1" className={styles.slider}></input>
|
||||
</div>
|
||||
<div>Max : {maxGrade}/10</div>
|
||||
</div>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Row>
|
||||
<Row>
|
||||
{!loading && !error && <AvisList avis={filteredReviews} />}
|
||||
</Row>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Container >
|
||||
)
|
||||
}
|
||||
128
code/interface_admin/pages/index.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import Head from 'next/head'
|
||||
import { Card, Container } from 'react-bootstrap'
|
||||
import ComparativeBarChart from '../components/ComparativeBarChart'
|
||||
import { useEffect, useState } from 'react'
|
||||
import styles from "../styles/Home.module.css"
|
||||
import useStats from '../hooks/stats'
|
||||
import getDay from 'date-fns/getDay'
|
||||
import getWeek from '../util'
|
||||
|
||||
|
||||
export default function Home() {
|
||||
const [datasets, setDatasets] = useState(null);
|
||||
const [averages, setAverages] = useState(null);
|
||||
const [differences, setDifferences] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (datasets) {
|
||||
let newAverages = []
|
||||
let newDifferences = []
|
||||
for (let i = 0; i < datasets.length; i++) {
|
||||
let currentEntriesCount = 0;
|
||||
let previousEntriesCount = 0;
|
||||
for (let x of datasets[i].current) {
|
||||
if (x != null) {
|
||||
currentEntriesCount++;
|
||||
}
|
||||
}
|
||||
for (let x of datasets[i].previous) {
|
||||
if (x != null) {
|
||||
previousEntriesCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentEntriesCount != 0) {
|
||||
newAverages[i] = datasets[i].current.reduce((a, b) => a + b) / currentEntriesCount;
|
||||
if (previousEntriesCount > 0) {
|
||||
newDifferences[i] = newAverages[i] - datasets[i].previous.reduce((a, b) => a + b) / datasets[i].previous.length
|
||||
} else {
|
||||
newDifferences[i] = newAverages[i]
|
||||
}
|
||||
} else {
|
||||
newDifferences[i] = 0;
|
||||
newAverages[i] = 0;
|
||||
}
|
||||
}
|
||||
setAverages(newAverages);
|
||||
setDifferences(newDifferences);
|
||||
}
|
||||
}, [datasets]);
|
||||
|
||||
const { stats, loading, error } = useStats(14, "jour");
|
||||
|
||||
useEffect(() => {
|
||||
if (!error && !loading) {
|
||||
let reviewCount = [null, null, null, null, null, null, null];
|
||||
let reviewCountPrev = [null, null, null, null, null, null, null]
|
||||
let reviewAvg = [null, null, null, null, null, null, null]
|
||||
let reviewAvgPrev = [null, null, null, null, null, null, null]
|
||||
|
||||
for (let i = 0; i < stats.length; i++) {
|
||||
let date = new Date(Date.parse(stats[i].date))
|
||||
let now = new Date();
|
||||
let day = (date.getDay() - 1) % 7;
|
||||
let week = getWeek(date, 1);
|
||||
let thisWeek = getWeek(now, 1);
|
||||
if (week == thisWeek) {
|
||||
reviewCount[day] = stats[i].nb_avis;
|
||||
reviewAvg[day] = stats[i].moyenne_globale;
|
||||
} else if (week = thisWeek - 1) {
|
||||
reviewAvgPrev[day] = stats[i].moyenne_globale;
|
||||
reviewCountPrev[day] = stats[i].nb_avis;
|
||||
}
|
||||
}
|
||||
setDatasets([
|
||||
{ title: "Nombre d'avis", current: reviewCount, previous: reviewCountPrev },
|
||||
{ title: "Notes moyennes", current: reviewAvg, previous: reviewAvgPrev }
|
||||
])
|
||||
}
|
||||
}, [stats]);
|
||||
|
||||
function dataVisualizer(title, current, previous, average, difference) {
|
||||
return <div key={title}>
|
||||
<h3>{title}</h3>
|
||||
<Card className={styles.averageCard}>
|
||||
<Card.Title>Moyenne</Card.Title>
|
||||
<Card.Body className={styles.averageCardBody}>
|
||||
<div
|
||||
className={styles.averageMainValue}
|
||||
>
|
||||
{Math.round(average * 1e2) / 1e2}
|
||||
</div>
|
||||
<div
|
||||
className={[styles.averageCardSecondaryValue, difference >= 0 ? styles.averagePositive : styles.averageNegative].join(' ')}
|
||||
>
|
||||
{(difference >= 0 ? "+" : "-") + Math.round(difference * 1e2) / 1e2}
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
<ComparativeBarChart
|
||||
xlabels={["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"]}
|
||||
label0="Cette semaine"
|
||||
label1="La semaine dernière"
|
||||
data0={current}
|
||||
data1={previous}
|
||||
/>
|
||||
<hr />
|
||||
</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Telereview</title>
|
||||
<meta name="description" content="Page d'accueil" />
|
||||
</Head>
|
||||
<Container fluid>
|
||||
<Card>
|
||||
<Card.Header as="h2">Vos performances cette semaine</Card.Header>
|
||||
<Card.Body>
|
||||
{datasets && averages && differences && datasets.map((set, i) => dataVisualizer(set.title, set.current, set.previous, averages[i], differences[i]))}
|
||||
</Card.Body>
|
||||
<div className='col col-12 col-lg-8 mx-auto'>
|
||||
</div>
|
||||
</Card>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
68
code/interface_admin/pages/stats.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Card, Container, Form, Row } from 'react-bootstrap';
|
||||
import StatBarChart from '../components/StatBarChart';
|
||||
import useStats from '../hooks/stats';
|
||||
|
||||
export default function Stats() {
|
||||
const [statName,setStatName] = useState("moyenne_globale")
|
||||
const [timeInterval, setTimeInterval] = useState("jour")
|
||||
const [chartReady, setChartReady] = useState(false);
|
||||
const [xlabels, setXlabels] = useState([]);
|
||||
const [plotData, setPlotData] = useState([]);
|
||||
|
||||
const {loading, error, stats} = useStats(10,timeInterval);
|
||||
|
||||
useEffect(() => {
|
||||
if(!loading && !error) {
|
||||
let newXlabels = [];
|
||||
let newPlotData = [];
|
||||
for(let i = 0; i < stats.length; i++) {
|
||||
newXlabels.push(stats[i].date);
|
||||
newPlotData.push(stats[i][statName]);
|
||||
}
|
||||
setXlabels(newXlabels);
|
||||
setPlotData(newPlotData);
|
||||
setChartReady(true);
|
||||
}else {
|
||||
setChartReady(false);
|
||||
}
|
||||
}, [stats, statName, timeInterval, loading, error])
|
||||
|
||||
|
||||
return (
|
||||
<Container fluid>
|
||||
<Card>
|
||||
<Card.Header>Tous les avis</Card.Header>
|
||||
<Card.Body>
|
||||
<Row>
|
||||
<Form>
|
||||
<Form.Group>
|
||||
<Form.Label>Statistique</Form.Label>
|
||||
<Form.Select value={statName} onChange={(e) => setStatName(e.target.value)}>
|
||||
<option value="moyenne_globale">Moyenne globale</option>
|
||||
<option value="nb_avis">Nombre d'avis</option>
|
||||
<option value="moyenne_site">Moyenne du formulaire</option>
|
||||
<option value = "moyenne_borne">Moyenne sur la borne</option>
|
||||
<option value="dist_sexes">Distribution sexes</option>
|
||||
</Form.Select>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label>Periode</Form.Label>
|
||||
<Form.Select value={timeInterval} onChange={(e) => setTimeInterval(e.target.value)}>
|
||||
<option value="jour">Jour</option>
|
||||
<option value="semaine">Semaine</option>
|
||||
<option value="mois">Mois</option>
|
||||
<option value = "annee">Année</option>
|
||||
</Form.Select>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Row>
|
||||
<Row>
|
||||
{error && <p>Error</p>}
|
||||
{chartReady && <StatBarChart data={plotData} labels={xlabels} />}
|
||||
</Row>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
BIN
code/interface_admin/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 25 KiB |
1
code/interface_admin/public/next.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
code/interface_admin/public/thirteen.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="31" fill="none"><g opacity=".9"><path fill="url(#a)" d="M13 .4v29.3H7V6.3h-.2L0 10.5V5L7.2.4H13Z"/><path fill="url(#b)" d="M28.8 30.1c-2.2 0-4-.3-5.7-1-1.7-.8-3-1.8-4-3.1a7.7 7.7 0 0 1-1.4-4.6h6.2c0 .8.3 1.4.7 2 .4.5 1 .9 1.7 1.2.7.3 1.6.4 2.5.4 1 0 1.7-.2 2.5-.5.7-.3 1.3-.8 1.7-1.4.4-.6.6-1.2.6-2s-.2-1.5-.7-2.1c-.4-.6-1-1-1.8-1.4-.8-.4-1.8-.5-2.9-.5h-2.7v-4.6h2.7a6 6 0 0 0 2.5-.5 4 4 0 0 0 1.7-1.3c.4-.6.6-1.3.6-2a3.5 3.5 0 0 0-2-3.3 5.6 5.6 0 0 0-4.5 0 4 4 0 0 0-1.7 1.2c-.4.6-.6 1.2-.6 2h-6c0-1.7.6-3.2 1.5-4.5 1-1.3 2.2-2.3 3.8-3C25 .4 26.8 0 28.8 0s3.8.4 5.3 1.1c1.5.7 2.7 1.7 3.6 3a7.2 7.2 0 0 1 1.2 4.2c0 1.6-.5 3-1.5 4a7 7 0 0 1-4 2.2v.2c2.2.3 3.8 1 5 2.2a6.4 6.4 0 0 1 1.6 4.6c0 1.7-.5 3.1-1.4 4.4a9.7 9.7 0 0 1-4 3.1c-1.7.8-3.7 1.1-5.8 1.1Z"/></g><defs><linearGradient id="a" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient><linearGradient id="b" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
code/interface_admin/public/vercel.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
||||
|
After Width: | Height: | Size: 629 B |
5
code/interface_admin/styles/Avis.module.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.personIcon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* font-size: 50px; */
|
||||
}
|
||||
6
code/interface_admin/styles/AvisList.module.css
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
/* ==== TABLEAU ==== */
|
||||
.row:hover {
|
||||
cursor: pointer;
|
||||
background-color: #EEE;
|
||||
}
|
||||
57
code/interface_admin/styles/AvisListPage.module.css
Normal file
@@ -0,0 +1,57 @@
|
||||
/* ==== SLIDER ==== */
|
||||
.sliderContainer {
|
||||
position: relative;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.sliderContainer > input[type=range]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
pointer-events: all;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 1px #C6C6C6;
|
||||
cursor: pointer;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.sliderContainer > input[type=range]::-moz-range-thumb {
|
||||
z-index: 99;
|
||||
pointer-events: all;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 1px #C6C6C6;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sliderContainer > input[type=range]::-webkit-slider-thumb:hover {
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.sliderContainer >input[type=range]::-webkit-slider-thumb:active {
|
||||
box-shadow: inset 0 0 3px #387bbe, 0 0 9px #387bbe;
|
||||
-webkit-box-shadow: inset 0 0 3px #387bbe, 0 0 9px #387bbe;
|
||||
}
|
||||
.sliderContainer > input[type=range]::-moz-range-thumb:hover {
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.sliderContainer >input[type=range]::-moz-range-thumb:active {
|
||||
box-shadow: inset 0 0 3px #387bbe, 0 0 9px #387bbe;
|
||||
-webkit-box-shadow: inset 0 0 3px #387bbe, 0 0 9px #387bbe;
|
||||
}
|
||||
|
||||
.sliderContainer >input[type="range"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
background-color: #C6C6C6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
29
code/interface_admin/styles/Home.module.css
Normal file
@@ -0,0 +1,29 @@
|
||||
.averageCard {
|
||||
width: min-content;
|
||||
margin: 0 auto;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.averageCardBody {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.averageMainValue {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.averageSecondaryValue {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.averagePositive {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.averageNegative {
|
||||
color: red;
|
||||
}
|
||||
34
code/interface_admin/util.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Returns the week number for this date. dowOffset is the day of week the week
|
||||
* "starts" on for your locale - it can be from 0 to 6. If dowOffset is 1 (Monday),
|
||||
* the week returned is the ISO 8601 week number.
|
||||
* @param int dowOffset
|
||||
* @return int
|
||||
*/
|
||||
export default function getWeek (date,dowOffset) {
|
||||
/*getWeek() was developed by Nick Baicoianu at MeanFreePath: http://www.meanfreepath.com */
|
||||
|
||||
dowOffset = typeof(dowOffset) == 'number' ? dowOffset : 0; //default dowOffset to zero
|
||||
var newYear = new Date(date.getFullYear(),0,1);
|
||||
var day = newYear.getDay() - dowOffset; //the day of week the year begins on
|
||||
day = (day >= 0 ? day : day + 7);
|
||||
var daynum = Math.floor((date.getTime() - newYear.getTime() -
|
||||
(date.getTimezoneOffset()-newYear.getTimezoneOffset())*60000)/86400000) + 1;
|
||||
var weeknum;
|
||||
//if the year starts before the middle of a week
|
||||
if(day < 4) {
|
||||
weeknum = Math.floor((daynum+day-1)/7) + 1;
|
||||
if(weeknum > 52) {
|
||||
nYear = new Date(date.getFullYear() + 1,0,1);
|
||||
nday = nYear.getDay() - dowOffset;
|
||||
nday = nday >= 0 ? nday : nday + 7;
|
||||
/*if the next year starts before the middle of
|
||||
the week, it is week #1 of that year*/
|
||||
weeknum = nday < 4 ? 1 : 53;
|
||||
}
|
||||
}
|
||||
else {
|
||||
weeknum = Math.floor((daynum+day-1)/7);
|
||||
}
|
||||
return weeknum;
|
||||
};
|
||||
7
code/interface_borne/assets/css/bootstrap-grid.min.css
vendored
Normal file
56
code/interface_borne/assets/css/main.css
Normal file
@@ -0,0 +1,56 @@
|
||||
* {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.page {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#camera > video, #camera > canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#camera > video {
|
||||
z-index: 0;
|
||||
}
|
||||
#camera > canvas {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
width: max-content;
|
||||
height: 300px;
|
||||
margin: auto;
|
||||
background: #A6CC00;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
border: 3px #6B8000 solid;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.instructions > .title {
|
||||
border-bottom: 3px #6B8000 solid;
|
||||
}
|
||||
|
||||
.instructions > table, .instructions > th,.instructions > td {
|
||||
border: 1px solid #6B8000;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
3
code/interface_borne/assets/img/logo_tp.svg
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
code/interface_borne/assets/img/thumbs_down.png
Normal file
|
After Width: | Height: | Size: 202 KiB |
BIN
code/interface_borne/assets/img/thumbs_up.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
22
code/interface_borne/assets/js/audio_page.js
Normal file
@@ -0,0 +1,22 @@
|
||||
class AudioPage {
|
||||
constructor() {
|
||||
this.isEnabled = false;
|
||||
this.DOMElement = document.getElementById("audio");
|
||||
|
||||
}
|
||||
set enabled(isEnabled) {
|
||||
this.isEnabled = isEnabled;
|
||||
this.DOMElement.style.display = isEnabled ? "block" : "none";
|
||||
document.getElementById("grade").innerHTML = "";
|
||||
}
|
||||
|
||||
setGrade(grade) {
|
||||
if(this.isEnabled) {
|
||||
document.getElementById("grade").innerHTML = grade.toString();
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
document.getElementById("grade").innerHTML = "";
|
||||
}
|
||||
}
|
||||
153
code/interface_borne/assets/js/camera_page.js
Normal file
@@ -0,0 +1,153 @@
|
||||
class CameraPage {
|
||||
constructor() {
|
||||
|
||||
this.spinnerWeight = 10;
|
||||
this.spinnerColor = "#0F0FFF"
|
||||
|
||||
this.canvas = document.getElementById("overlay-canvas");
|
||||
this.ctx = this.canvas.getContext("2d");
|
||||
this.video = document.getElementById("camera-video");
|
||||
this.width;
|
||||
this.height; //calcule automatiquement en fonction de la largeur du flux vidéo
|
||||
this.videoWidth;
|
||||
this.videoHeight;
|
||||
this.streaming = false;
|
||||
this.activeEffects = [];
|
||||
this.images = {};
|
||||
this._startup();
|
||||
this._loadImages();
|
||||
this._enabled = false;
|
||||
this.DOMElement = document.getElementById("camera");
|
||||
}
|
||||
|
||||
set enabled(val) {
|
||||
this._enabled = val;
|
||||
this.DOMElement.style.display = val ? "block" : "none";
|
||||
if (val) {
|
||||
this._frame();
|
||||
this.video.play();
|
||||
}else {
|
||||
this.video.pause();
|
||||
}
|
||||
}
|
||||
|
||||
get enabled() {
|
||||
return this._enabled;
|
||||
}
|
||||
|
||||
_startup() {
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({ video: true, audio: false })
|
||||
.then((stream) => {
|
||||
this.video.srcObject = stream;
|
||||
this.video.play();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Erreur pendant la lecture de la camera: ${err}`);
|
||||
});
|
||||
|
||||
this.video.addEventListener(
|
||||
"canplay",
|
||||
(ev) => {
|
||||
if (!this.streaming) {
|
||||
//calcul de la taille de la vidéo en fonction de la taille de la fenêtre pour qu'elle soit toujours visible
|
||||
let aspectRatio = this.video.videoWidth / this.video.videoHeight;
|
||||
if (window.innerHeight * aspectRatio > window.innerWidth) {
|
||||
this.width = window.innerWidth;
|
||||
this.height = window.innerWidth / aspectRatio;
|
||||
} else {
|
||||
this.width = window.innerHeight * aspectRatio;
|
||||
this.height = window.innerHeight;
|
||||
}
|
||||
|
||||
this.videoHeight = this.video.videoHeight;
|
||||
this.videoWidth = this.video.videoWidth;
|
||||
|
||||
this.video.setAttribute("width", this.width);
|
||||
this.video.setAttribute("height", this.height);
|
||||
this.canvas.setAttribute("width", this.width);
|
||||
this.canvas.setAttribute("height", this.height);
|
||||
this.streaming = true;
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
_loadImages() {
|
||||
this.images.thumbsUp = new Image();
|
||||
this.images.thumbsUp.src = "assets/img/thumbs_up.png";
|
||||
this.images.thumbsDown = new Image();
|
||||
this.images.thumbsDown.src = "assets/img/thumbs_down.png";
|
||||
}
|
||||
|
||||
_frame() {
|
||||
if (this.streaming && this.enabled && this.width && this.height) {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this._drawEffects();
|
||||
}
|
||||
if (this.enabled) {
|
||||
requestAnimationFrame(() => this._frame());
|
||||
}
|
||||
}
|
||||
|
||||
_scaleEffect(x, y, width, height) {
|
||||
let xScale = this.width / this.videoWidth;
|
||||
let yScale = this.height / this.videoHeight;
|
||||
return {
|
||||
x: x * xScale,
|
||||
y: y * yScale,
|
||||
width: width * xScale,
|
||||
height: height * yScale
|
||||
}
|
||||
}
|
||||
|
||||
_drawEffects() {
|
||||
for (let effect of this.activeEffects) {
|
||||
let { x, y, width, height } = this._scaleEffect(effect.x, effect.y, effect.width, effect.height);
|
||||
width = width * this.videoWidth * 2;
|
||||
height = height * this.videoHeight * 2;
|
||||
x = x * this.videoWidth - width / 2;
|
||||
y = y * this.videoHeight - height / 2;
|
||||
console.log(width, height);
|
||||
if (effect.type == "thumbs_down") {
|
||||
this._drawThumbsDown(x, y, width, height);
|
||||
}
|
||||
if (effect.type == "thumbs_up") {
|
||||
this._drawThumbsUp(x, y, width, height);
|
||||
}
|
||||
if (effect.type == "loading") {
|
||||
this._drawLoading(x, y, width, effect.progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_drawLoading(x, y, radius, progress) {
|
||||
this.ctx.lineWidth = this.spinnerWeight;
|
||||
this.ctx.strokeStyle = this.spinnerColor;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(x, y, radius, 0, progress * 2 * Math.PI);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
|
||||
_drawThumbsDown(x, y, width, height) {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.drawImage(this.images.thumbsDown, x, y, width, height);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
|
||||
_drawThumbsUp(x, y, width, height) {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.drawImage(this.images.thumbsUp, x, y, width, height);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
|
||||
setEffects(effects) {
|
||||
this.activeEffects = effects;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.activeEffects = [];
|
||||
}
|
||||
}
|
||||
5
code/interface_borne/assets/js/main.js
Normal file
@@ -0,0 +1,5 @@
|
||||
let stateManager;
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
stateManager = new StateManager();
|
||||
}, false);
|
||||
22
code/interface_borne/assets/js/network.js
Normal file
@@ -0,0 +1,22 @@
|
||||
class WebsocketClient {
|
||||
constructor(onNewEffects, onNewState, onNewGrade, onReset) {
|
||||
this.socket = new WebSocket("ws://localhost:5000");
|
||||
this.socket.addEventListener("open", (event) => {
|
||||
this.socket.send("connected");
|
||||
console.log("connected")
|
||||
});
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
let msg = JSON.parse(event.data);
|
||||
if (msg.type == "effects") {
|
||||
onNewEffects(msg.effects);
|
||||
}else if(msg.type == "state") {
|
||||
onNewState(msg.state);
|
||||
}else if(msg.type == "new_grade") {
|
||||
onNewGrade(Number(msg.grade));
|
||||
}else if(msg.type == "reset") {
|
||||
onReset();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
12
code/interface_borne/assets/js/sleeping_page.js
Normal file
@@ -0,0 +1,12 @@
|
||||
class SleepingPage {
|
||||
constructor(onWakeUp) {
|
||||
this.onWakeUp = onWakeUp;
|
||||
this.isEnabled = false;
|
||||
this.DOMElement = document.getElementById("sleeping-page");
|
||||
|
||||
}
|
||||
set enabled(isEnabled) {
|
||||
this.isEnabled = isEnabled;
|
||||
this.DOMElement.style.display = isEnabled ? "block" : "none";
|
||||
}
|
||||
}
|
||||
63
code/interface_borne/assets/js/state_manager.js
Normal file
@@ -0,0 +1,63 @@
|
||||
const STATE = {
|
||||
sleeping: 0,
|
||||
video: 1,
|
||||
audio: 2,
|
||||
thankYou: 3,
|
||||
};
|
||||
|
||||
class StateManager {
|
||||
constructor() {
|
||||
this._state = STATE.sleeping;
|
||||
this._cameraPage = new CameraPage();
|
||||
this._sleepingPage = new SleepingPage();
|
||||
this._audioPage = new AudioPage();
|
||||
this._thankYouPage = new ThankYouPage();
|
||||
|
||||
this.wsClient = new WebsocketClient(
|
||||
(effects) => {
|
||||
this.setState(STATE.video);
|
||||
this._cameraPage.setEffects(effects)
|
||||
},
|
||||
(state) => this.setState(state),
|
||||
(grade) => this._audioPage.setGrade(grade),
|
||||
() => this.reset(),
|
||||
);
|
||||
|
||||
this._sleepingPage.enabled = true;
|
||||
this._cameraPage.enabled = false;
|
||||
this._audioPage.enabled = false;
|
||||
this._thankYouPage.enabled = false;
|
||||
}
|
||||
|
||||
setState(newState) {
|
||||
console.log({current:this._state,new:newState})
|
||||
if(this._state == STATE.sleeping && newState == STATE.video) {
|
||||
this._cameraPage.enabled = true;
|
||||
this._sleepingPage.enabled = false;
|
||||
this._state = newState;
|
||||
}else if(this._state == STATE.video && newState == STATE.audio) {
|
||||
this._cameraPage.enabled = false;
|
||||
this._audioPage.enabled = true;
|
||||
this._state = newState;
|
||||
}else if(this._state == STATE.audio && newState == STATE.thankYou) {
|
||||
this._audioPage.enabled = false;
|
||||
this._thankYouPage.enabled = true;
|
||||
this._state = newState;
|
||||
}else if(this._state == STATE.thankYou && newState == STATE.sleeping) {
|
||||
this._thankYouPage.enabled = false;
|
||||
this._sleepingPage.enabled = true;
|
||||
this._state = newState;
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this._state = 0;
|
||||
this._cameraPage.enabled = false;
|
||||
this._audioPage.enabled = false;
|
||||
this._thankYouPage.enabled = false;
|
||||
this._audioPage.reset();
|
||||
this._cameraPage.reset();
|
||||
this._sleepingPage.enabled = true;
|
||||
|
||||
}
|
||||
}
|
||||
11
code/interface_borne/assets/js/thank_you_page.js
Normal file
@@ -0,0 +1,11 @@
|
||||
class ThankYouPage {
|
||||
constructor() {
|
||||
this.isEnabled = false;
|
||||
this.DOMElement = document.getElementById("thank-you");
|
||||
}
|
||||
|
||||
set enabled(isEnabled) {
|
||||
this.isEnabled = isEnabled;
|
||||
this.DOMElement.style.display = isEnabled ? "block" : "none";
|
||||
}
|
||||
}
|
||||
68
code/interface_borne/index.html
Normal file
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="assets/css/main.css">
|
||||
<!-- <link rel="stylesheet" href="assets/css/bootstrap-grid.min.css"> -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">
|
||||
<title>Téléreview</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="sleeping-page" class="page">
|
||||
<div class="instructions">
|
||||
<div class="title">
|
||||
<h1>Votre avis nous intéresse</h1>
|
||||
</div>
|
||||
<span>Faites un</span>
|
||||
<img width=50 src="assets/img/thumbs_up.png">
|
||||
<span>ou un</span>
|
||||
<img width=50 src="assets/img/thumbs_down.png">
|
||||
<span> avec votre main pour commencer</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="camera">
|
||||
<canvas id="overlay-canvas"></canvas>
|
||||
<video id="camera-video"></video>
|
||||
</div>
|
||||
<div id="audio">
|
||||
<div class="instructions">
|
||||
<div class="title">
|
||||
<h1>Dites-nous en plus</h1>
|
||||
</div>
|
||||
<p>Donnez une note sur 10 au critère suivant</p>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Critère</td>
|
||||
<th>Note / 10</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Calme</td>
|
||||
<td><span id="grade"></span>/10</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div id="thank-you">
|
||||
<div class="instructions">
|
||||
<div class="title">
|
||||
<h1>Merci pour votre avis</h1>
|
||||
</div>
|
||||
<span>Nous esperons vous revoir bientôt</span>
|
||||
</div>
|
||||
</div>
|
||||
<script src="assets/js/camera_page.js"></script>
|
||||
<script src="assets/js/network.js"></script>
|
||||
<script src="assets/js/thank_you_page.js"></script>
|
||||
<script src="assets/js/audio_page.js"></script>
|
||||
<script src="assets/js/sleeping_page.js"></script>
|
||||
<script src="assets/js/state_manager.js"></script>
|
||||
<script src="assets/js/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
24
code/reviews_api/.dockerignore
Normal file
@@ -0,0 +1,24 @@
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
15
code/reviews_api/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM node:lts-alpine
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
#installation des dépendances
|
||||
COPY ["package.json", "package-lock.json*", "npm-shrinkwrap.json*", "./"]
|
||||
RUN npm install --production --silent && mv node_modules ../
|
||||
|
||||
#On copie le code source
|
||||
COPY . .
|
||||
#On change le propriétaire du dossier
|
||||
RUN chown -R node /usr/src/app
|
||||
#On change l'utilisateur
|
||||
USER node
|
||||
|
||||
CMD ["node", "index.js"]
|
||||
@@ -1,31 +1,35 @@
|
||||
# Installation
|
||||
* Pour faire fonctinoner le serveur sur vos machines il y a 3 choses a faire
|
||||
1. Installer node js : https://nodejs.org/en/download/
|
||||
2. Ouvrir un terminal et aller dans ce dossier (code/server) et tapper `npm install` pour installer les pacakges nécessaires
|
||||
3. copier le fichier `.env_template` et le nommer `.env` et remplir les variables (cela est fait pour le pas mettre les mots de passes sur le gitlab, faites attention de ne jamais commit le fichier .env !!)
|
||||
4. pour lancer le serveur faire `node index.js`
|
||||
|
||||
# Utilisation
|
||||
## Avis laissés sur la borne (hors réseaux sociaux)
|
||||
### Routes GET :
|
||||
- `/borne/get_last_reviews?limit=LIM` : renvoie les LIM derniers avis sur la borne
|
||||
- `/borne/get_review?id=ID` : renvoie les infos sur l'avis d'in ID
|
||||
- `/borne/get_criteres` : renvoie les criteres de notations valide pour les notes autres
|
||||
- `/borne/notes_autres?critere=CRIT&limit=LIM` : renvoie les LIM dernières notes sur le critère CRIT
|
||||
- `/borne/notes_autres?id=ID&limit=LIM` : renvoie toutes les notes spécifiques liées à l'avis ID
|
||||
|
||||
### Routes POST
|
||||
- `/add_review` : Ajoute une review et un auteur, paramètres POST :
|
||||
* [OBLIGATOIRE] `note` : note principale de la review entre 0 et 10 compris
|
||||
* [OBLIGATOIRE] `source` : nom de la source de l'avis, doit être `borne` ou `website` pour resp la borne et le site
|
||||
* `auteur_age` : age de l'auteur
|
||||
* `auteur_sexe` : sexe de l'auteur (valeurs valide 'f', 'h', 'a')
|
||||
* `commentaire` : Commentaire laissé avec l'avis
|
||||
* `notes autres` : sous la forme
|
||||
```json
|
||||
{
|
||||
"critere1": 8,
|
||||
"critere2": 2,
|
||||
"critere3": 0
|
||||
}
|
||||
```
|
||||
# Serveur de traitement des données
|
||||
Ce serveur s'occupe de fournir une API web pour pourvoir ajouter des avis sur la borne, réxupérer des avis, calculer et fournir les statistiques sur ces avis
|
||||
|
||||
# Installation (Si vous voulez le lancer hors du container docker)
|
||||
* Pour faire fonctinoner le serveur sur vos machines il y a 3 choses a faire
|
||||
1. Installer node js : https://nodejs.org/en/download/
|
||||
2. Ouvrir un terminal et aller dans ce dossier (code/server) et tapper `npm install` pour installer les pacakges nécessaires
|
||||
3. copier le fichier `.env_template` et le nommer `.env` et remplir les variables (cela est fait pour le pas mettre les mots de passes sur le gitlab, faites attention de ne jamais commit le fichier .env !!)
|
||||
4. pour lancer le serveur faire `node index.js`
|
||||
|
||||
# Utilisation
|
||||
## Avis laissés sur la borne (hors réseaux sociaux)
|
||||
### Routes GET :
|
||||
- `/borne/get_last_reviews?limit=LIM` : renvoie les LIM derniers avis sur la borne
|
||||
- `/borne/get_review?id=ID` : renvoie les infos sur l'avis d'in ID
|
||||
- `/borne/get_criteres` : renvoie les criteres de notations valide pour les notes autres
|
||||
- `/borne/notes_autres?critere=CRIT&limit=LIM` : renvoie les LIM dernières notes sur le critère CRIT
|
||||
- `/borne/notes_autres?id=ID&limit=LIM` : renvoie toutes les notes spécifiques liées à l'avis ID
|
||||
- `get_stats?interval=INTERVAL&limit=LIM`: interval est "jour", "mois", "annee", "semaine", l'interval de calcul de données demandé, LIM est le nombre des stats à récupérer
|
||||
|
||||
### Routes POST
|
||||
- `/add_review` : Ajoute une review et un auteur, paramètres POST :
|
||||
* [OBLIGATOIRE] `note` : note principale de la review entre 0 et 10 compris
|
||||
* [OBLIGATOIRE] `source` : nom de la source de l'avis, doit être `borne` ou `website` pour resp la borne et le site
|
||||
* `auteur_age` : age de l'auteur
|
||||
* `auteur_sexe` : sexe de l'auteur (valeurs valide 'f', 'h', 'a')
|
||||
* `commentaire` : Commentaire laissé avec l'avis
|
||||
* `notes autres` : sous la forme
|
||||
```json
|
||||
{
|
||||
"critere1": 8,
|
||||
"critere2": 2,
|
||||
"critere3": 0
|
||||
}
|
||||
```
|
||||
@@ -1,200 +1,237 @@
|
||||
import conn from '../database.js';
|
||||
|
||||
/**
|
||||
* Renvoie les derniers avis laissés sur la borne trié par ordre chronologque décroissant
|
||||
* @param {Number} limit Nombre d'avis a afficher
|
||||
* @returns Une liste d'objets de la forme {id: id de l'avis, date: date de l'avis, note_principale: note sur 10, commentaire: avis textuel, nom_source, sexe_auteur, age_auteur}
|
||||
*/
|
||||
const getLastReviews = (limit=10) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let sql = `SELECT borne_avis.id,date,note_principale,commentaire,sources.nom as nom_source, borne_auteurs.sexe as sexe_auteur, borne_auteurs.age as age_auteur
|
||||
FROM borne_avis
|
||||
JOIN sources ON sources.id = source_id
|
||||
JOIN borne_auteurs ON borne_auteurs.id = id_auteur
|
||||
ORDER BY borne_avis.id DESC LIMIT ?`;
|
||||
conn.query(sql, [limit], (err, res) => {
|
||||
if(err) {
|
||||
reject(err);
|
||||
}else {
|
||||
resolve(res)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Renvoie les informations sur un avis avec un ID spécifique
|
||||
* @param {Number} id Id de la review
|
||||
* @returns Un objet de la forme des objets dans la table borne_avis ayant l'id id s'il existe, renvoie une erreur sinon
|
||||
*/
|
||||
const getReviewFromId = (id) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let sql = `SELECT * FROM borne_avis WHERE id = ? LIMIT 1`;
|
||||
conn.query(sql, [id], (err, res) => {
|
||||
if(err) {
|
||||
reject(err);
|
||||
}else {
|
||||
if(res.length != 1) {
|
||||
reject(new Error("Avis avec cet ID non trouvé"))
|
||||
}else {
|
||||
resolve(res[0])
|
||||
}
|
||||
if(res.length != 1) {
|
||||
reject(new Error("Avis avec cet ID non trouvé"))
|
||||
}else {
|
||||
resolve(res[0])
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Renvoie tout les critères de notation valides
|
||||
* @returns Une liste d'objets de la forme {id,nom}
|
||||
*/
|
||||
const getCriteres = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let sql = `SELECT * FROM borne_criteres`;
|
||||
conn.query(sql, [limit], (err, res) => {
|
||||
if(err) {
|
||||
reject(err);
|
||||
}else {
|
||||
resolve(res)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Renvoie les "limit" notes les plus récentes laissées pour un critère spécifié
|
||||
* @param {String} critere Nom de critère
|
||||
* @param {Number} limit Nombre max de note a afficher
|
||||
* @returns une liste d'objets de la forme {id,date,critere,note,avis_id:id de l'avis lié a cette note}
|
||||
*/
|
||||
const getNotesAutresFromCritere = (critere,limit=10) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let sql = `SELECT borne_notes_autre.id as id,date,borne_criteres.nom as critere, note, avis_id
|
||||
FROM borne_notes_autre
|
||||
JOIN borne_criteres ON borne_criteres.id = critere_id
|
||||
WHERE borne_criteres.nom = ?
|
||||
ORDER BY borne_notes_autre.id DESC LIMIT ? ;`;
|
||||
conn.query(sql, [critere,limit], (err, res) => {
|
||||
if(err) {
|
||||
reject(err);
|
||||
}else {
|
||||
resolve(res)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Renvoie toutes les notes sur des critères spécifiques laissée pour un avis spécifique
|
||||
* @param {Number} reviewId Id de l'avis
|
||||
* @returns une liste d'objets de la forme {id,critere:nom du critère, note:note sur 10}
|
||||
*/
|
||||
const getNotesAutresFromReview = (reviewId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let sql = `SELECT borne_notes_autre.id as id,borne_criteres.nom as critere, note
|
||||
FROM borne_notes_autre
|
||||
JOIN borne_criteres on borne_criteres.id = critere_id
|
||||
WHERE avis_id = ?
|
||||
ORDER BY borne_notes_autre.id DESC`;
|
||||
conn.query(sql, [reviewId], (err, res) => {
|
||||
if(err) {
|
||||
reject(err);
|
||||
}else {
|
||||
resolve(res)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Renvoie les dernières statistiques
|
||||
* @param {String} interval Interval de temps de la statistique, valeurs possibles : "jour","mois","semaine","annee"
|
||||
* @param {Number} limit Nombre max de stats a renvoyer
|
||||
* @returns Une liste d'objet ou chaque objet correspond a une stat sur une periode donnée (par ex une stat hebdo datée du 07/01 correspond a une stat sur la semaine du 01/01 au 07/01), ces objets sont de la forme
|
||||
*/
|
||||
const getStats = (interval, limit=10) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if(!["jour","mois","semaine","annee"].includes(interval)) {
|
||||
reject(new Error("Invalid time interval"));
|
||||
return;
|
||||
}
|
||||
let sql = `SELECT * FROM stats_general_${interval} ORDER BY id DESC LIMIT ?;`;
|
||||
conn.query(sql, [limit], (err, res) => {
|
||||
if(err) {
|
||||
reject(err);
|
||||
}else {
|
||||
resolve(res)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
Ces fonction sont des handlers pour les routes express, elles sont appelées par les routes et renvoient les données au format JSON
|
||||
*/
|
||||
export const handleGetLastReviews = (req, res) => {
|
||||
getLastReviews(req.query.limit)
|
||||
.then((reviews) => {
|
||||
res.send(reviews);
|
||||
})
|
||||
.catch((err) => {
|
||||
res.status(500).send("Error: " + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
export const handleGetReview = (req, res) => {
|
||||
getReviewFromId(req.query.id)
|
||||
.then((review) => {
|
||||
res.send(review);
|
||||
})
|
||||
.catch((err) => {
|
||||
res.status(500).send("Error: " + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
export const handleGetCriteres = (req, res) => {
|
||||
getCriteres()
|
||||
.then((criteres) => {
|
||||
res.send(criteres);
|
||||
})
|
||||
.catch((err) => {
|
||||
res.status(500).send("Error: " + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
export const handleGetNotesAutres = (req, res) => {
|
||||
if(req.query.critere) {
|
||||
getNotesAutresFromCritere(req.query.critere, req.query.limit)
|
||||
.then((notes) => {
|
||||
res.send(notes);
|
||||
})
|
||||
.catch((err) => {
|
||||
res.status(500).send("Error: " + err.message);
|
||||
});
|
||||
}else if(req.query.id) {
|
||||
getNotesAutresFromReview(req.query.id)
|
||||
.then((notes) => {
|
||||
res.send(notes);
|
||||
})
|
||||
.catch((err) => {
|
||||
res.status(500).send("Error: " + err.message);
|
||||
});
|
||||
}else {
|
||||
res.status(500).send("Error: no critere or id specified");
|
||||
}
|
||||
}
|
||||
|
||||
export const handleGetStats = (req, res) => {
|
||||
getStats(req.query.interval, req.query.limit)
|
||||
.then((stats) => {
|
||||
res.send(stats);
|
||||
})
|
||||
.catch((err) => {
|
||||
res.status(500).send("Error: " + err.message);
|
||||
});
|
||||
import conn from '../database.js';
|
||||
|
||||
/**
|
||||
* Renvoie les derniers avis laissés sur la borne trié par ordre chronologque décroissant
|
||||
* @param {Number} limit Nombre d'avis a afficher
|
||||
* @returns Une liste d'objets de la forme {id: id de l'avis, date: date de l'avis, note_principale: note sur 10, commentaire: avis textuel, nom_source, sexe_auteur, age_auteur}
|
||||
*/
|
||||
const getLastReviews = (limit = 10) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let sql = `SELECT borne_avis.id,date,note_principale,commentaire,sources.nom as nom_source, borne_auteurs.sexe as sexe_auteur, borne_auteurs.age as age_auteur
|
||||
FROM borne_avis
|
||||
JOIN sources ON sources.id = source_id
|
||||
JOIN borne_auteurs ON borne_auteurs.id = id_auteur
|
||||
ORDER BY borne_avis.id DESC LIMIT ?`;
|
||||
conn.query(sql, [limit], (err, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(res)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Renvoie les informations sur un avis avec un ID spécifique
|
||||
* @param {Number} id Id de la review
|
||||
* @returns Un objet de la forme des objets dans la table borne_avis ayant l'id id s'il existe, renvoie une erreur sinon
|
||||
*/
|
||||
const getReviewFromId = (id) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let sql = `SELECT borne_avis.id,date,note_principale,commentaire,sources.nom as nom_source, borne_auteurs.sexe as sexe_auteur, borne_auteurs.age as age_auteur
|
||||
FROM borne_avis
|
||||
JOIN sources ON sources.id = source_id
|
||||
JOIN borne_auteurs ON borne_auteurs.id = id_auteur
|
||||
WHERE borne_avis.id = ?
|
||||
LIMIT 1`;
|
||||
conn.query(sql, [id], (err, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
if (res.length != 1) {
|
||||
reject(new Error("Avis avec cet ID non trouvé"))
|
||||
} else {
|
||||
resolve(res[0])
|
||||
}
|
||||
if (res.length != 1) {
|
||||
reject(new Error("Avis avec cet ID non trouvé"))
|
||||
} else {
|
||||
resolve(res[0])
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Renvoie tout les critères de notation valides
|
||||
* @returns Une liste d'objets de la forme {id,nom}
|
||||
*/
|
||||
const getCriteres = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let sql = `SELECT * FROM borne_criteres`;
|
||||
conn.query(sql, [limit], (err, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(res)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Renvoie les "limit" notes les plus récentes laissées pour un critère spécifié
|
||||
* @param {String} critere Nom de critère
|
||||
* @param {Number} limit Nombre max de note a afficher
|
||||
* @returns une liste d'objets de la forme {id,date,critere,note,avis_id:id de l'avis lié a cette note}
|
||||
*/
|
||||
const getNotesAutresFromCritere = (critere, limit = 10) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let sql = `SELECT borne_notes_autre.id as id,date,borne_criteres.nom as critere, note, avis_id
|
||||
FROM borne_notes_autre
|
||||
JOIN borne_criteres ON borne_criteres.id = critere_id
|
||||
WHERE borne_criteres.nom = ?
|
||||
ORDER BY borne_notes_autre.id DESC LIMIT ? ;`;
|
||||
conn.query(sql, [critere, limit], (err, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(res)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Renvoie toutes les notes sur des critères spécifiques laissée pour un avis spécifique
|
||||
* @param {Number} reviewId Id de l'avis
|
||||
* @returns une liste d'objets de la forme {id,critere:nom du critère, note:note sur 10}
|
||||
*/
|
||||
const getNotesAutresFromReview = (reviewId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let sql = `SELECT borne_notes_autre.id as id,borne_criteres.nom as critere, note
|
||||
FROM borne_notes_autre
|
||||
JOIN borne_criteres on borne_criteres.id = critere_id
|
||||
WHERE avis_id = ?
|
||||
ORDER BY borne_notes_autre.id DESC`;
|
||||
conn.query(sql, [reviewId], (err, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(res)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Renvoie les dernières statistiques
|
||||
* @param {String} interval Interval de temps de la statistique, valeurs possibles : "jour","mois","semaine","annee"
|
||||
* @param {Number} limit Nombre max de stats a renvoyer
|
||||
* @returns Une liste d'objet ou chaque objet correspond a une stat sur une periode donnée (par ex une stat hebdo datée du 07/01 correspond a une stat sur la semaine du 01/01 au 07/01), ces objets sont de la forme
|
||||
*/
|
||||
const getStats = (interval, limit = 10) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!["jour", "mois", "semaine", "annee"].includes(interval)) {
|
||||
reject(new Error("Invalid time interval"));
|
||||
return;
|
||||
}
|
||||
let sql = `SELECT * FROM stats_general_${interval} ORDER BY id DESC LIMIT ?;`;
|
||||
conn.query(sql, [limit], (err, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(res)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
Ces fonction sont des handlers pour les routes express, elles sont appelées par les routes et renvoient les données au format JSON
|
||||
*/
|
||||
export const handleGetLastReviews = (req, res) => {
|
||||
if (req.query.limit) {
|
||||
getLastReviews(Number(req.query.limit))
|
||||
.then((reviews) => {
|
||||
res.send(reviews);
|
||||
})
|
||||
.catch((err) => {
|
||||
res.status(500).send("Error: " + err.message);
|
||||
});
|
||||
} else {
|
||||
getLastReviews()
|
||||
.then((reviews) => {
|
||||
res.send(reviews);
|
||||
})
|
||||
.catch((err) => {
|
||||
res.status(500).send("Error: " + err.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const handleGetReview = (req, res) => {
|
||||
getReviewFromId(req.query.id)
|
||||
.then((review) => {
|
||||
getNotesAutresFromReview(req.query.id).then((notesAutres) => {
|
||||
res.send({ ...review, notes_autres: notesAutres });
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
res.status(500).send("Error: " + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
export const handleGetCriteres = (req, res) => {
|
||||
getCriteres()
|
||||
.then((criteres) => {
|
||||
res.send(criteres);
|
||||
})
|
||||
.catch((err) => {
|
||||
res.status(500).send("Error: " + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
export const handleGetNotesAutres = (req, res) => {
|
||||
if (req.query.critere) {
|
||||
if (req.query.limit) {
|
||||
getNotesAutresFromCritere(req.query.critere, Number(req.query.limit))
|
||||
.then((notes) => {
|
||||
res.send(notes);
|
||||
})
|
||||
.catch((err) => {
|
||||
res.status(500).send("Error: " + err.message);
|
||||
});
|
||||
} else {
|
||||
getNotesAutresFromCritere(req.query.critere)
|
||||
.then((notes) => {
|
||||
res.send(notes);
|
||||
})
|
||||
.catch((err) => {
|
||||
res.status(500).send("Error: " + err.message);
|
||||
});
|
||||
}
|
||||
} else if (req.query.id) {
|
||||
getNotesAutresFromReview(req.query.id)
|
||||
.then((notes) => {
|
||||
res.send(notes);
|
||||
})
|
||||
.catch((err) => {
|
||||
res.status(500).send("Error: " + err.message);
|
||||
});
|
||||
} else {
|
||||
res.status(500).send("Error: no critere or id specified");
|
||||
}
|
||||
}
|
||||
|
||||
export const handleGetStats = (req, res) => {
|
||||
if (req.query.limit) {
|
||||
getStats(req.query.interval, Number(req.query.limit))
|
||||
.then((stats) => {
|
||||
res.send(stats);
|
||||
})
|
||||
.catch((err) => {
|
||||
res.status(500).send("Error: " + err.message);
|
||||
});
|
||||
} else {
|
||||
getStats(req.query.interval)
|
||||
.then((stats) => {
|
||||
res.send(stats);
|
||||
})
|
||||
.catch((err) => {
|
||||
res.status(500).send("Error: " + err.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,84 +1,84 @@
|
||||
import { Auteur, Review } from './structures.js';
|
||||
import conn from '../database.js';
|
||||
import {getSourceId} from '../utils.js';
|
||||
|
||||
|
||||
/**
|
||||
* Ajoute un nouvel auteur de commentaire a la BDD
|
||||
* @param {Auteur} author L'auteur a ajouter
|
||||
* @returns une Promise qui renvoie l'id de l'utilisateur
|
||||
*/
|
||||
const addAuteur = (author) => {
|
||||
return new Promise((resolve,reject) => {
|
||||
const sql = "INSERT INTO borne_auteurs (age, sexe) VALUES (?);"
|
||||
conn.query(sql, [[author.age, author.sexe]], (err, res) => {
|
||||
if(err) {
|
||||
reject(err)
|
||||
}else {
|
||||
resolve(res.insertId);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
//Ajoute une note sur un critère spécifique dans la BDD
|
||||
const addSpecificRating = (reviewId, label, value) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sql = "INSERT INTO borne_notes_autre(critere_id, avis_id, note) VALUES ((SELECT id FROM borne_criteres WHERE borne_criteres.nom = ?), ?, ?)"
|
||||
conn.query(sql, [label,reviewId, value], (err, res) => {
|
||||
if(err) {
|
||||
reject(err);
|
||||
}else {
|
||||
resolve(res.insertId);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ajoute un avis a la base de donnée
|
||||
* @param {Review} review la review a ajouter
|
||||
* @param {Number} authorId l'ID de l'auteur de l'avis dans la BDD
|
||||
* @param {Number} sourceId l'ID de la source de l'avis dans la BDD
|
||||
* @returns une Promise qui renvoie l'id de l'avis
|
||||
*/
|
||||
const addReview = (review, authorId, sourceId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sql = "INSERT INTO borne_avis (id_auteur, note_principale, commentaire, source_id) VALUES (?);"
|
||||
conn.query(sql, [[authorId, review.note, review.commentaire, sourceId]], (err, res) => {
|
||||
if(err) {
|
||||
reject(err)
|
||||
}else {
|
||||
resolve(res.insertId);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Traite une requête POST qui contient les valeurs nécessaires pour ajouter un nouvel avis dans la BDD
|
||||
* Essaie d'ajouter l'avis et l'auteur dans la BDD, sinon renvoie l'erreur avec un code 500
|
||||
* @param {*} req requete
|
||||
* @param {*} res reponse
|
||||
*/
|
||||
export const addReviewFromRequest = async (req,res) => {
|
||||
try {
|
||||
let notes_autre = {}
|
||||
try{
|
||||
notes_autre = JSON.parse(req.body.notes_autre);
|
||||
}catch(err){};
|
||||
|
||||
const author = new Auteur(req.body.auteur_age,req.body.auteur_sexe);
|
||||
const review = new Review(author, req.body.note, req.body.source, req.body.commentaire, notes_autre)
|
||||
let authorId = await addAuteur(author);
|
||||
let sourceId = await getSourceId(review.source);
|
||||
let reviewId = await addReview(review, authorId, sourceId );
|
||||
for(let label in review.notesAutre) {
|
||||
await addSpecificRating(reviewId, label, review.notesAutre[label]);
|
||||
}
|
||||
res.send("success")
|
||||
}catch(err) {
|
||||
res.status(500).send("Error : " + err.message)
|
||||
}
|
||||
import { Auteur, Review } from './structures.js';
|
||||
import conn from '../database.js';
|
||||
import {getSourceId} from '../utils.js';
|
||||
|
||||
|
||||
/**
|
||||
* Ajoute un nouvel auteur de commentaire a la BDD
|
||||
* @param {Auteur} author L'auteur a ajouter
|
||||
* @returns une Promise qui renvoie l'id de l'utilisateur
|
||||
*/
|
||||
const addAuteur = (author) => {
|
||||
return new Promise((resolve,reject) => {
|
||||
const sql = "INSERT INTO borne_auteurs (age, sexe) VALUES (?);"
|
||||
conn.query(sql, [[author.age, author.sexe]], (err, res) => {
|
||||
if(err) {
|
||||
reject(err)
|
||||
}else {
|
||||
resolve(res.insertId);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
//Ajoute une note sur un critère spécifique dans la BDD
|
||||
const addSpecificRating = (reviewId, label, value) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sql = "INSERT INTO borne_notes_autre(critere_id, avis_id, note) VALUES ((SELECT id FROM borne_criteres WHERE borne_criteres.nom = ?), ?, ?)"
|
||||
conn.query(sql, [label,reviewId, value], (err, res) => {
|
||||
if(err) {
|
||||
reject(err);
|
||||
}else {
|
||||
resolve(res.insertId);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ajoute un avis a la base de donnée
|
||||
* @param {Review} review la review a ajouter
|
||||
* @param {Number} authorId l'ID de l'auteur de l'avis dans la BDD
|
||||
* @param {Number} sourceId l'ID de la source de l'avis dans la BDD
|
||||
* @returns une Promise qui renvoie l'id de l'avis
|
||||
*/
|
||||
const addReview = (review, authorId, sourceId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sql = "INSERT INTO borne_avis (id_auteur, note_principale, commentaire, source_id) VALUES (?);"
|
||||
conn.query(sql, [[authorId, review.note, review.commentaire, sourceId]], (err, res) => {
|
||||
if(err) {
|
||||
reject(err)
|
||||
}else {
|
||||
resolve(res.insertId);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Traite une requête POST qui contient les valeurs nécessaires pour ajouter un nouvel avis dans la BDD
|
||||
* Essaie d'ajouter l'avis et l'auteur dans la BDD, sinon renvoie l'erreur avec un code 500
|
||||
* @param {*} req requete
|
||||
* @param {*} res reponse
|
||||
*/
|
||||
export const addReviewFromRequest = async (req,res) => {
|
||||
try {
|
||||
let notes_autre = {}
|
||||
try{
|
||||
notes_autre = JSON.parse(req.body.notes_autre);
|
||||
}catch(err){};
|
||||
|
||||
const author = new Auteur(req.body.auteur_age,req.body.auteur_sexe);
|
||||
const review = new Review(author, req.body.note, req.body.source, req.body.commentaire, notes_autre)
|
||||
let authorId = await addAuteur(author);
|
||||
let sourceId = await getSourceId(review.source);
|
||||
let reviewId = await addReview(review, authorId, sourceId );
|
||||
for(let label in review.notesAutre) {
|
||||
await addSpecificRating(reviewId, label, review.notesAutre[label]);
|
||||
}
|
||||
res.send("success")
|
||||
}catch(err) {
|
||||
res.status(500).send("Error : " + err.message)
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,52 @@
|
||||
//Liste des valeurs valides pour le sexe d'un utilisateur
|
||||
const validSexes = ["h","f","a"];
|
||||
|
||||
//Classe pour représenter un avis laissé sur la borne que ce soit par l'interface web ou par un geste
|
||||
export class Review {
|
||||
/**
|
||||
* Constructeur
|
||||
* @param {Auteur} auteur L'auteur de l'avis
|
||||
* @param {Number} note La note principale entre 0 et 10 compris
|
||||
* @param {String} source La source de l'avis, doit être une entrée dans la table soruces de la bdd
|
||||
* @param {String} commentaire Le commentaire lié a l'avis
|
||||
* @param {Object} notesAutre Les notes secondaires laissées, sous la forme d'un object ou les clés sont les labels de chaque notes et les valeurs sont des notes entre 0 et 10 compris
|
||||
*/
|
||||
constructor(auteur, note, source, commentaire=null, notesAutre={}) {
|
||||
this.auteur = auteur;
|
||||
this.note = note;
|
||||
this.source = source;
|
||||
this.commentaire = commentaire;
|
||||
this.notesAutre = notesAutre;
|
||||
|
||||
//On vérifie si toutes les données sont correctes
|
||||
if(note < 0 || note > 10) {
|
||||
throw new Error("Note principale invalide");
|
||||
}
|
||||
for(let nom in notesAutre) {
|
||||
if(notesAutre[nom] < 0 || notesAutre[nom] > 10) {
|
||||
throw new Error("Note " + notesAutre[nom] +"/10 invalide");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Classe qui représente l'auteur d'un avis
|
||||
export class Auteur {
|
||||
/**
|
||||
* Constructeur
|
||||
* @param {Number} age L'age de l'auteur
|
||||
* @param {String} sexe Le sexe de la personne (doit être dans l'array validSexes défini au début de ce fichier)
|
||||
*/
|
||||
constructor(age=null, sexe=null) {
|
||||
this.age = age;
|
||||
this.sexe = sexe;
|
||||
|
||||
//Verification des données
|
||||
if(sexe != undefined && !validSexes.includes(sexe) ) {
|
||||
throw new Error("Sexe invalide");
|
||||
}
|
||||
if(age != undefined && age <= 0) {
|
||||
throw new Error("Age invalide");
|
||||
}
|
||||
}
|
||||
//Liste des valeurs valides pour le sexe d'un utilisateur
|
||||
const validSexes = ["h","f","a"];
|
||||
|
||||
//Classe pour représenter un avis laissé sur la borne que ce soit par l'interface web ou par un geste
|
||||
export class Review {
|
||||
/**
|
||||
* Constructeur
|
||||
* @param {Auteur} auteur L'auteur de l'avis
|
||||
* @param {Number} note La note principale entre 0 et 10 compris
|
||||
* @param {String} source La source de l'avis, doit être une entrée dans la table soruces de la bdd
|
||||
* @param {String} commentaire Le commentaire lié a l'avis
|
||||
* @param {Object} notesAutre Les notes secondaires laissées, sous la forme d'un object ou les clés sont les labels de chaque notes et les valeurs sont des notes entre 0 et 10 compris
|
||||
*/
|
||||
constructor(auteur, note, source, commentaire=null, notesAutre={}) {
|
||||
this.auteur = auteur;
|
||||
this.note = note;
|
||||
this.source = source;
|
||||
this.commentaire = commentaire;
|
||||
this.notesAutre = notesAutre;
|
||||
|
||||
//On vérifie si toutes les données sont correctes
|
||||
if(note < 0 || note > 10) {
|
||||
throw new Error("Note principale invalide");
|
||||
}
|
||||
for(let nom in notesAutre) {
|
||||
if(notesAutre[nom] < 0 || notesAutre[nom] > 10) {
|
||||
throw new Error("Note " + notesAutre[nom] +"/10 invalide");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Classe qui représente l'auteur d'un avis
|
||||
export class Auteur {
|
||||
/**
|
||||
* Constructeur
|
||||
* @param {Number} age L'age de l'auteur
|
||||
* @param {String} sexe Le sexe de la personne (doit être dans l'array validSexes défini au début de ce fichier)
|
||||
*/
|
||||
constructor(age=null, sexe=null) {
|
||||
this.age = age;
|
||||
this.sexe = sexe;
|
||||
|
||||
//Verification des données
|
||||
if(sexe != undefined && !validSexes.includes(sexe) ) {
|
||||
throw new Error("Sexe invalide");
|
||||
}
|
||||
if(age != undefined && age <= 0) {
|
||||
throw new Error("Age invalide");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import * as dotenv from 'dotenv'
|
||||
import mysql from 'mysql2';
|
||||
dotenv.config();
|
||||
|
||||
const conn = mysql.createConnection({
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
multipleStatements: true,
|
||||
});
|
||||
|
||||
conn.connect();
|
||||
|
||||
import * as dotenv from 'dotenv'
|
||||
import mysql from 'mysql2';
|
||||
dotenv.config();
|
||||
|
||||
const conn = mysql.createConnection({
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
multipleStatements: true,
|
||||
});
|
||||
|
||||
conn.connect();
|
||||
|
||||
export default conn;
|
||||
@@ -1,28 +1,33 @@
|
||||
import * as dotenv from 'dotenv';
|
||||
import express from 'express';
|
||||
import bodyParser from 'body-parser';
|
||||
import { addReviewFromRequest } from './borne/post_handler.js';
|
||||
import { addSocialReviewFromRequest } from './reseaux_sociaux/post_handler.js';
|
||||
import { startCronJobs } from './stats/update_stats.js';
|
||||
import * as borneHandler from './borne/get_handler.js';
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(bodyParser.urlencoded({extended:true}))
|
||||
dotenv.config()
|
||||
app.post('/add_review', (req,res) => addReviewFromRequest(req,res));
|
||||
app.post('/add_social_review', (req,res) => addSocialReviewFromRequest(req,res));
|
||||
|
||||
app.get('/borne/get_last_reviews', borneHandler.handleGetLastReviews);
|
||||
app.get('/borne/get_review', borneHandler.handleGetReview);
|
||||
app.get('/borne/get_criteres', borneHandler.handleGetCriteres);
|
||||
app.get('/borne/notes_autres', borneHandler.handleGetNotesAutres);
|
||||
app.get('/borne/get_stats', borneHandler.handleGetStats);
|
||||
|
||||
|
||||
startCronJobs();
|
||||
|
||||
app.listen(process.env.PORT, () => {
|
||||
console.log("Server démaré sur le port " + process.env.PORT)
|
||||
})
|
||||
|
||||
import * as dotenv from 'dotenv';
|
||||
import express from 'express';
|
||||
import bodyParser from 'body-parser';
|
||||
import { addReviewFromRequest } from './borne/post_handler.js';
|
||||
import { addSocialReviewFromRequest } from './reseaux_sociaux/post_handler.js';
|
||||
import { startCronJobs, manualUpdateStats } from './stats/update_stats.js';
|
||||
import * as borneHandler from './borne/get_handler.js';
|
||||
import cors from "cors";
|
||||
|
||||
const app = express();
|
||||
app.use(bodyParser.urlencoded({extended:true}))
|
||||
app.use(cors({origin:'*'}))
|
||||
dotenv.config()
|
||||
app.post('/add_review', (req,res) => addReviewFromRequest(req,res));
|
||||
app.post('/add_social_review', (req,res) => addSocialReviewFromRequest(req,res));
|
||||
|
||||
app.get('/borne/get_last_reviews', borneHandler.handleGetLastReviews);
|
||||
app.get('/borne/get_review', borneHandler.handleGetReview);
|
||||
app.get('/borne/get_criteres', borneHandler.handleGetCriteres);
|
||||
app.get('/borne/notes_autres', borneHandler.handleGetNotesAutres);
|
||||
app.get('/borne/get_stats', borneHandler.handleGetStats);
|
||||
|
||||
app.get('/update_stats', (req, res) => {
|
||||
manualUpdateStats();
|
||||
res.send("OK");
|
||||
})
|
||||
|
||||
startCronJobs();
|
||||
|
||||
app.listen(process.env.PORT, () => {
|
||||
console.log("Server démaré sur le port " + process.env.PORT)
|
||||
})
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Telereview",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.20.1",
|
||||
"cron": "^2.1.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "^4.18.2",
|
||||
"mysql2": "^2.3.3"
|
||||
}
|
||||
}
|
||||
{
|
||||
"name": "server",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Telereview",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.20.1",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^2.1.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "^4.18.2",
|
||||
"mysql2": "^2.3.3"
|
||||
}
|
||||
}
|
||||
@@ -1,85 +1,85 @@
|
||||
import { ReseauxAuteur, ReseauxReview } from './structures.js';
|
||||
import conn from '../database.js';
|
||||
import {getSourceId} from '../utils.js';
|
||||
|
||||
/**Récupérer l'id d'un auteur particulier dans la base de donnée s'il existe
|
||||
* @param {ReseauxAuteur} author L'auteur dont on veut l'id*
|
||||
* @returns Une Promise qui donne l'id de l'utilisateur ou null si il n'existe pas
|
||||
*/
|
||||
const getAuteurId = (author) => {
|
||||
return new Promise(async (resolve,reject) => {
|
||||
const sql = "SELECT id FROM reseaux_sociaux_auteurs WHERE nom_utilisateur = ? AND source_id = ? AND lien = ?;"
|
||||
let sourceId = await getSourceId(author.source);
|
||||
let query = conn.query(sql, [author.nom, sourceId, author.lien], (err, res) => {
|
||||
if(err) {
|
||||
reject(new Error(err.message))
|
||||
}else {
|
||||
if(res.length > 0) {
|
||||
resolve(res[0].id);
|
||||
}else {
|
||||
resolve(null);
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ajoute un auteur de commentaire sur les réseaux sociaux a la bdd
|
||||
* @param {ReseauxAuteur} author
|
||||
* @returns Une Promise qui donne l'id de l'utilisateur
|
||||
*/
|
||||
const addAuteur = (author, sourceId) => {
|
||||
return new Promise((resolve,reject) => {
|
||||
const sql = "INSERT INTO reseaux_sociaux_auteurs (nom_utilisateur, source_id, lien) VALUES (?);"
|
||||
conn.query(sql, [[author.nom,sourceId, author.lien]], (err, res) => {
|
||||
if(err) {
|
||||
reject(new Error(err.message))
|
||||
}else {
|
||||
resolve(res.insertId);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute un avis dans la BDD
|
||||
* @param {ReseauxReview} review L'avis a ajouter
|
||||
* @param {Number} authorId ID de l'auteur dans la bdd
|
||||
* @param {Number} sourceId ID Du réseau social source dans la bdd
|
||||
* @returns une Promise qui renvoie l'ID de la review
|
||||
*/
|
||||
const addReview = (review, authorId, sourceId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sql = "INSERT INTO reseaux_sociaux_avis (source_id, note, commentaire, auteur_id, lien_source, date) VALUES (?);"
|
||||
conn.query(sql, [[sourceId, review.note, review.commentaire, authorId, review.lien, review.date]], (err, res) => {
|
||||
if(err) {
|
||||
reject(new Error(err.message))
|
||||
}else {
|
||||
resolve(res.insertId);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Traite une requête POST pour ajouter un avis récupéré sur les réseaux sociaux
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
export const addSocialReviewFromRequest = async (req,res) => {
|
||||
try {
|
||||
const author = new ReseauxAuteur(req.body.auteur_nom, req.body.source, req.body.auteur_lien);
|
||||
const review = new ReseauxReview(author, req.body.source, req.body.date, req.body.note, req.body.commentaire, req.body.lien)
|
||||
let sourceId = await getSourceId(review.source);
|
||||
let authorId = await getAuteurId(author);
|
||||
if(authorId == null) {
|
||||
authorId = await addAuteur(author, sourceId);
|
||||
}
|
||||
await addReview(review, authorId, sourceId);
|
||||
res.send("success")
|
||||
}catch(err) {
|
||||
res.status(500).send("Error : " + err.message)
|
||||
}
|
||||
import { ReseauxAuteur, ReseauxReview } from './structures.js';
|
||||
import conn from '../database.js';
|
||||
import {getSourceId} from '../utils.js';
|
||||
|
||||
/**Récupérer l'id d'un auteur particulier dans la base de donnée s'il existe
|
||||
* @param {ReseauxAuteur} author L'auteur dont on veut l'id*
|
||||
* @returns Une Promise qui donne l'id de l'utilisateur ou null si il n'existe pas
|
||||
*/
|
||||
const getAuteurId = (author) => {
|
||||
return new Promise(async (resolve,reject) => {
|
||||
const sql = "SELECT id FROM reseaux_sociaux_auteurs WHERE nom_utilisateur = ? AND source_id = ? AND lien = ?;"
|
||||
let sourceId = await getSourceId(author.source);
|
||||
let query = conn.query(sql, [author.nom, sourceId, author.lien], (err, res) => {
|
||||
if(err) {
|
||||
reject(new Error(err.message))
|
||||
}else {
|
||||
if(res.length > 0) {
|
||||
resolve(res[0].id);
|
||||
}else {
|
||||
resolve(null);
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ajoute un auteur de commentaire sur les réseaux sociaux a la bdd
|
||||
* @param {ReseauxAuteur} author
|
||||
* @returns Une Promise qui donne l'id de l'utilisateur
|
||||
*/
|
||||
const addAuteur = (author, sourceId) => {
|
||||
return new Promise((resolve,reject) => {
|
||||
const sql = "INSERT INTO reseaux_sociaux_auteurs (nom_utilisateur, source_id, lien) VALUES (?);"
|
||||
conn.query(sql, [[author.nom,sourceId, author.lien]], (err, res) => {
|
||||
if(err) {
|
||||
reject(new Error(err.message))
|
||||
}else {
|
||||
resolve(res.insertId);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute un avis dans la BDD
|
||||
* @param {ReseauxReview} review L'avis a ajouter
|
||||
* @param {Number} authorId ID de l'auteur dans la bdd
|
||||
* @param {Number} sourceId ID Du réseau social source dans la bdd
|
||||
* @returns une Promise qui renvoie l'ID de la review
|
||||
*/
|
||||
const addReview = (review, authorId, sourceId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sql = "INSERT INTO reseaux_sociaux_avis (source_id, note, commentaire, auteur_id, lien_source, date) VALUES (?);"
|
||||
conn.query(sql, [[sourceId, review.note, review.commentaire, authorId, review.lien, review.date]], (err, res) => {
|
||||
if(err) {
|
||||
reject(new Error(err.message))
|
||||
}else {
|
||||
resolve(res.insertId);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Traite une requête POST pour ajouter un avis récupéré sur les réseaux sociaux
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
export const addSocialReviewFromRequest = async (req,res) => {
|
||||
try {
|
||||
const author = new ReseauxAuteur(req.body.auteur_nom, req.body.source, req.body.auteur_lien);
|
||||
const review = new ReseauxReview(author, req.body.source, req.body.date, req.body.note, req.body.commentaire, req.body.lien)
|
||||
let sourceId = await getSourceId(review.source);
|
||||
let authorId = await getAuteurId(author);
|
||||
if(authorId == null) {
|
||||
authorId = await addAuteur(author, sourceId);
|
||||
}
|
||||
await addReview(review, authorId, sourceId);
|
||||
res.send("success")
|
||||
}catch(err) {
|
||||
res.status(500).send("Error : " + err.message)
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,39 @@
|
||||
import { Review } from "../borne/structures.js";
|
||||
|
||||
export class ReseauxReview extends Review{
|
||||
/**
|
||||
*
|
||||
* @param {ReseauxAuteur} auteur Auteur de l'avis
|
||||
* @param {String} source La source de l'avis
|
||||
* @param {String} date La date de l'avis au format YYYY-MM-DD
|
||||
* @param {Number} note Nombre entre 0 et 10, la note attribuée
|
||||
* @param {String} commentaire Le commentaire laissé par l'utilisateur
|
||||
* @param {String} lien Lien vers le commentaire
|
||||
*/
|
||||
constructor(auteur, source, date, note=null, commentaire=null, lien=null,) {
|
||||
super(auteur,note,source,commentaire);
|
||||
this.lien = lien;
|
||||
this.date = date;
|
||||
if((typeof lien !== "string" && lien != null)) {
|
||||
throw new Error("Lien invalide");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ReseauxAuteur {
|
||||
/**
|
||||
*
|
||||
* @param {String} nom Nom de l'utilisateur
|
||||
* @param {String} source Réseau social de provenance de l'utilisateur
|
||||
* @param {String} lien Lien vers le profil de l'utilisateur
|
||||
*/
|
||||
constructor(nom, source, lien=null) {
|
||||
this.nom = nom;
|
||||
this.source = source;
|
||||
this.lien = lien;
|
||||
|
||||
if((typeof this.nom !== "string") || (typeof this.source !== "string")){
|
||||
throw new Error("Auteur invalide");
|
||||
}
|
||||
}
|
||||
import { Review } from "../borne/structures.js";
|
||||
|
||||
export class ReseauxReview extends Review{
|
||||
/**
|
||||
*
|
||||
* @param {ReseauxAuteur} auteur Auteur de l'avis
|
||||
* @param {String} source La source de l'avis
|
||||
* @param {String} date La date de l'avis au format YYYY-MM-DD
|
||||
* @param {Number} note Nombre entre 0 et 10, la note attribuée
|
||||
* @param {String} commentaire Le commentaire laissé par l'utilisateur
|
||||
* @param {String} lien Lien vers le commentaire
|
||||
*/
|
||||
constructor(auteur, source, date, note=null, commentaire=null, lien=null,) {
|
||||
super(auteur,note,source,commentaire);
|
||||
this.lien = lien;
|
||||
this.date = date;
|
||||
if((typeof lien !== "string" && lien != null)) {
|
||||
throw new Error("Lien invalide");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ReseauxAuteur {
|
||||
/**
|
||||
*
|
||||
* @param {String} nom Nom de l'utilisateur
|
||||
* @param {String} source Réseau social de provenance de l'utilisateur
|
||||
* @param {String} lien Lien vers le profil de l'utilisateur
|
||||
*/
|
||||
constructor(nom, source, lien=null) {
|
||||
this.nom = nom;
|
||||
this.source = source;
|
||||
this.lien = lien;
|
||||
|
||||
if((typeof this.nom !== "string") || (typeof this.source !== "string")){
|
||||
throw new Error("Auteur invalide");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +1,54 @@
|
||||
/*
|
||||
Dans cette requête il faut replacer DAY_COUNT_DELAY par le nombre de jours sur lequel calculer les stats, STATS_GENERAL_TABLE_NAME par la table dans laquelle mettre les stats globales (par exemple stats_general_jour si les valeurs sont calculées sur 1 jour) et pareil pour STATS_AUTRES_TABLE_NAME
|
||||
C'est un peu bizzare comme technique mais j'ai pas trouvé de meilleur solution
|
||||
*/
|
||||
SET @date_limite = DATE_ADD(NOW(), INTERVAL -DAY_COUNT_DELAY DAY);
|
||||
|
||||
/*
|
||||
On récupère les notes notes moyennes sur la periode, en séparant global, borne et site
|
||||
*/
|
||||
|
||||
SELECT @moyenne_globale:=AVG(note_principale)
|
||||
FROM borne_avis
|
||||
WHERE borne_avis.date > @date_limite;
|
||||
|
||||
SELECT @moyenne_borne:=AVG(note_principale)
|
||||
FROM borne_avis
|
||||
JOIN sources ON sources.id = borne_avis.source_id
|
||||
WHERE borne_avis.date > @date_limite AND sources.nom = "borne";
|
||||
|
||||
SELECT @moyenne_site:=AVG(note_principale)
|
||||
FROM borne_avis
|
||||
JOIN sources ON sources.id = borne_avis.source_id
|
||||
WHERE borne_avis.date > @date_limite AND sources.nom = "website";
|
||||
|
||||
/*
|
||||
On récupère la distribution de sexes
|
||||
*/
|
||||
|
||||
SELECT @stats_f:=COUNT(*) FROM borne_avis
|
||||
JOIN borne_auteurs ON borne_avis.id_auteur = borne_auteurs.id
|
||||
WHERE sexe='f' AND date > @date_limite;
|
||||
SELECT @stats_h:=COUNT(*) FROM borne_avis
|
||||
JOIN borne_auteurs ON borne_avis.id_auteur = borne_auteurs.id
|
||||
WHERE sexe='h' AND date > @date_limite;
|
||||
SELECT @stats_a:=COUNT(*) FROM borne_avis
|
||||
JOIN borne_auteurs ON borne_avis.id_auteur = borne_auteurs.id
|
||||
WHERE sexe='a' AND date > @date_limite;
|
||||
|
||||
SET @dist_sexe = CONCAT(@stats_f,",",@stats_h,",",@stats_a);
|
||||
|
||||
INSERT INTO STATS_GENERAL_TABLE_NAME (moyenne_globale, moyenne_borne, moyenne_site, dist_sexe) VALUES (@moyenne_globale, @moyenne_borne, @moyenne_site, @dist_sexe);
|
||||
|
||||
INSERT INTO STATS_AUTRES_TABLE_NAME (critere_id, note)
|
||||
SELECT critere_id, AVG(note) as moyenne FROM borne_notes_autre
|
||||
WHERE borne_notes_autre.date > @date_limite
|
||||
GROUP BY critere_id
|
||||
|
||||
/*
|
||||
TODO : Calcul de la distribution d'age
|
||||
/*
|
||||
Dans cette requête il faut replacer DAY_COUNT_DELAY par le nombre de jours sur lequel calculer les stats, STATS_GENERAL_TABLE_NAME par la table dans laquelle mettre les stats globales (par exemple stats_general_jour si les valeurs sont calculées sur 1 jour) et pareil pour STATS_AUTRES_TABLE_NAME
|
||||
C'est un peu bizzare comme technique mais j'ai pas trouvé de meilleur solution
|
||||
*/
|
||||
SET @date_limite = DATE_ADD(NOW(), INTERVAL -DAY_COUNT_DELAY DAY);
|
||||
|
||||
/*
|
||||
On récupère les notes notes moyennes sur la periode, en séparant global, borne et site
|
||||
*/
|
||||
|
||||
SELECT @nb_avis:=COUNT(*)
|
||||
FROM borne_avis
|
||||
WHERE borne_avis.date > @date_limite;
|
||||
|
||||
SELECT @moyenne_globale:=AVG(note_principale)
|
||||
FROM borne_avis
|
||||
WHERE borne_avis.date > @date_limite;
|
||||
|
||||
SELECT @moyenne_borne:=AVG(note_principale)
|
||||
FROM borne_avis
|
||||
JOIN sources ON sources.id = borne_avis.source_id
|
||||
WHERE borne_avis.date > @date_limite AND sources.nom = "borne";
|
||||
|
||||
SELECT @moyenne_site:=AVG(note_principale)
|
||||
FROM borne_avis
|
||||
JOIN sources ON sources.id = borne_avis.source_id
|
||||
WHERE borne_avis.date > @date_limite AND sources.nom = "website";
|
||||
|
||||
/*
|
||||
On récupère la distribution de sexes
|
||||
*/
|
||||
|
||||
SELECT @stats_f:=COUNT(*) FROM borne_avis
|
||||
JOIN borne_auteurs ON borne_avis.id_auteur = borne_auteurs.id
|
||||
WHERE sexe='f' AND date > @date_limite;
|
||||
SELECT @stats_h:=COUNT(*) FROM borne_avis
|
||||
JOIN borne_auteurs ON borne_avis.id_auteur = borne_auteurs.id
|
||||
WHERE sexe='h' AND date > @date_limite;
|
||||
SELECT @stats_a:=COUNT(*) FROM borne_avis
|
||||
JOIN borne_auteurs ON borne_avis.id_auteur = borne_auteurs.id
|
||||
WHERE sexe='a' AND date > @date_limite;
|
||||
|
||||
SET @dist_sexe = CONCAT(@stats_f,",",@stats_h,",",@stats_a);
|
||||
|
||||
INSERT INTO STATS_GENERAL_TABLE_NAME (moyenne_globale, nb_avis, moyenne_borne, moyenne_site, dist_sexe) VALUES (@moyenne_globale, @nb_avis, @moyenne_borne, @moyenne_site, @dist_sexe);
|
||||
|
||||
INSERT INTO STATS_AUTRES_TABLE_NAME (critere_id, note)
|
||||
SELECT critere_id, AVG(note) as moyenne FROM borne_notes_autre
|
||||
WHERE borne_notes_autre.date > @date_limite
|
||||
GROUP BY critere_id
|
||||
|
||||
/*
|
||||
TODO : Calcul de la distribution d'age
|
||||
*/
|
||||
@@ -1,69 +1,76 @@
|
||||
import conn from '../database.js';
|
||||
import fs from "fs";
|
||||
import { CronJob } from 'cron';
|
||||
|
||||
/**
|
||||
* Calcules les stats sur une periode donnée et les stocke dans la BDD
|
||||
* @param {Number} timePeriod Periode de temps sur laquelle calculer les données. Par exemple 7 si on veut faire les stats des 7 derniers jours
|
||||
* @param {String} generalTableName Nom de la table dans laquelle mettre les statistiques générales (tables valides : stats_general_jour stats_general_semaine stats_general_mois stats_general_annee)
|
||||
* @param {*} otherTableName Nom de la table dans laquelle mettre les statistiques spécifiques (tables valides : stats_autres_jour stats_autres_autres_autres_mois stats_general_annee)
|
||||
* @returns Une Promise qui résout si la requête a fonctionnée
|
||||
*/
|
||||
const computeStats = async (timePeriod, generalTableName, otherTableName) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let sql = fs.readFileSync("stats/update_request.sql").toString();
|
||||
sql = sql
|
||||
.replace(new RegExp("DAY_COUNT_DELAY",'g'), timePeriod.toString())
|
||||
.replace(new RegExp("STATS_GENERAL_TABLE_NAME", 'g'), generalTableName)
|
||||
.replace(new RegExp('STATS_AUTRES_TABLE_NAME', 'g'), otherTableName)
|
||||
conn.query(sql, (err, res) => {
|
||||
if(err) {
|
||||
reject(err)
|
||||
}else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
export const startCronJobs = () => {
|
||||
//Update les stats journalières tout les jours a minuit
|
||||
new CronJob(
|
||||
'0 0 * * * * ',
|
||||
() => {
|
||||
computeStats(1,"stats_general_jour","stats_autres_jour");
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
|
||||
//update les stats de la semaine tous les lundis a minuit
|
||||
new CronJob(
|
||||
'0 0 * * 1 * ',
|
||||
() => {
|
||||
computeStats(7,"stats_general_semaine","stats_autres_jour");
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
|
||||
//update les stats mensuelles les 1er du mois a minuit
|
||||
new CronJob(
|
||||
'0 0 1 * * * ',
|
||||
() => {
|
||||
computeStats(30, "stats_general_mois","stats_autres_mois");
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
|
||||
//update les stats annuelles les premire de l'an a minuit
|
||||
new CronJob(
|
||||
'0 0 1 1 * * ',
|
||||
() => {
|
||||
computeStats(365, "stats_general_annee","stats_autres_annee");
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
console.log("All cronjobs initiated")
|
||||
import conn from '../database.js';
|
||||
import fs from "fs";
|
||||
import { CronJob } from 'cron';
|
||||
|
||||
/**
|
||||
* Calcules les stats sur une periode donnée et les stocke dans la BDD
|
||||
* @param {Number} timePeriod Periode de temps sur laquelle calculer les données. Par exemple 7 si on veut faire les stats des 7 derniers jours
|
||||
* @param {String} generalTableName Nom de la table dans laquelle mettre les statistiques générales (tables valides : stats_general_jour stats_general_semaine stats_general_mois stats_general_annee)
|
||||
* @param {*} otherTableName Nom de la table dans laquelle mettre les statistiques spécifiques (tables valides : stats_autres_jour stats_autres_autres_autres_mois stats_general_annee)
|
||||
* @returns Une Promise qui résout si la requête a fonctionnée
|
||||
*/
|
||||
const computeStats = async (timePeriod, generalTableName, otherTableName) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let sql = fs.readFileSync("stats/update_request.sql").toString();
|
||||
sql = sql
|
||||
.replace(new RegExp("DAY_COUNT_DELAY",'g'), timePeriod.toString())
|
||||
.replace(new RegExp("STATS_GENERAL_TABLE_NAME", 'g'), generalTableName)
|
||||
.replace(new RegExp('STATS_AUTRES_TABLE_NAME', 'g'), otherTableName)
|
||||
conn.query(sql, (err, res) => {
|
||||
if(err) {
|
||||
reject(err)
|
||||
}else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
export const startCronJobs = () => {
|
||||
//Update les stats journalières tout les jours a minuit
|
||||
new CronJob(
|
||||
'0 0 * * * * ',
|
||||
() => {
|
||||
computeStats(1,"stats_general_jour","stats_autres_jour");
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
|
||||
//update les stats de la semaine tous les lundis a minuit
|
||||
new CronJob(
|
||||
'0 0 * * 1 * ',
|
||||
() => {
|
||||
computeStats(7,"stats_general_semaine","stats_autres_jour");
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
|
||||
//update les stats mensuelles les 1er du mois a minuit
|
||||
new CronJob(
|
||||
'0 0 1 * * * ',
|
||||
() => {
|
||||
computeStats(30, "stats_general_mois","stats_autres_mois");
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
|
||||
//update les stats annuelles les premire de l'an a minuit
|
||||
new CronJob(
|
||||
'0 0 1 1 * * ',
|
||||
() => {
|
||||
computeStats(365, "stats_general_annee","stats_autres_annee");
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
console.log("All cronjobs initiated")
|
||||
}
|
||||
|
||||
export function manualUpdateStats() {
|
||||
computeStats(1, "stats_general_jour", "stats_autres_jour");
|
||||
computeStats(7, "stats_general_semaine", "stats_autres_jour");
|
||||
computeStats(30, "stats_general_mois", "stats_autres_mois");
|
||||
computeStats(365, "stats_general_annee", "stats_autres_annee");
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
import conn from './database.js';
|
||||
|
||||
/**
|
||||
* Renvoie l'ID dans la BDD d'une source de donnée
|
||||
* @param {String} source la source dont on veut récup l'id
|
||||
* @returns une promise qui renvoie l'id de la source
|
||||
*/
|
||||
export const getSourceId = (source) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sql = "SELECT id from sources WHERE nom = ?";
|
||||
conn.query(sql, [source], (err, res) => {
|
||||
if(res.length == 0) {
|
||||
reject(new Error("Invalid source"))
|
||||
}else {
|
||||
resolve(res[0].id);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
import conn from './database.js';
|
||||
|
||||
/**
|
||||
* Renvoie l'ID dans la BDD d'une source de donnée
|
||||
* @param {String} source la source dont on veut récup l'id
|
||||
* @returns une promise qui renvoie l'id de la source
|
||||
*/
|
||||
export const getSourceId = (source) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sql = "SELECT id from sources WHERE nom = ?";
|
||||
conn.query(sql, [source], (err, res) => {
|
||||
if(res.length == 0) {
|
||||
reject(new Error("Invalid source"))
|
||||
}else {
|
||||
resolve(res[0].id);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import requests
|
||||
#Exemple ajout d'un commentaire depuis la borne (site ou geste)
|
||||
avis = {
|
||||
"note": 8,
|
||||
"source": "borne",
|
||||
#Optionel
|
||||
"auteur_age": 20,
|
||||
"notes_autre": '{"proprete":8,"calme":10}',
|
||||
"auteur_sexe": 'f',
|
||||
"commentaire": "Commentaire"
|
||||
}
|
||||
|
||||
# res = requests.post("http://localhost:8080/add_review", data=avis)
|
||||
# print(res.text)
|
||||
|
||||
#Exemple ajout d'un commentaire trouvé sur les réseaux sociaux
|
||||
avis = {
|
||||
"auteur_nom": "michel",
|
||||
"source": "instagram",
|
||||
"note": 8,
|
||||
"date": "2022-12-24",
|
||||
#Optionel
|
||||
"commentaire": "J'ai beaucoup aimé !",
|
||||
"lien": "https://instagram.com/si_insta_avait_des_liens_vers_des_commentaires_faudrait_le_mettre_ici",
|
||||
"auteur_lien": "https://instagram.com/michel",
|
||||
}
|
||||
|
||||
res = requests.post("http://localhost:8080/add_social_review", data=avis)
|
||||
print(res.text)
|
||||
2
code/setup.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
sudo modprobe v4l2loopback devices=2
|
||||
3
code/video_loopback/Dockerfile
Normal file
@@ -0,0 +1,3 @@
|
||||
FROM alpine:latest
|
||||
RUN apk add --no-cache ffmpeg
|
||||
CMD ["ffmpeg","-video_size","640x480","-f","video4linux2","-i","/dev/video0","-codec","copy","-f","v4l2","/dev/video1","-codec","copy","-f","v4l2","/dev/video2", "-loglevel","debug"]
|
||||