diff --git a/backend/db/crud.py b/backend/db/crud.py index 12b744fbaf93d49dd858b89ec2aa52830fd0068e..c0b31deeb2425cf8965ac5eece72a9313d3f1eaa 100644 --- a/backend/db/crud.py +++ b/backend/db/crud.py @@ -4,6 +4,7 @@ Module to interact with the database from datetime import date, datetime, time, timedelta from sqlalchemy.orm import Session from sqlalchemy.sql import func +from sqlalchemy import Time, Date, cast from uuid import uuid4 import secrets import pytz @@ -17,87 +18,59 @@ def get_waiting_time(place: str, db: Session): """ Get the last estimated waiting time for the given place """ current_date = datetime.now(tz=pytz.timezone("Europe/Paris")) weekday, current_time = current_date.weekday(), current_date.time() - data = {"status": False, "waiting_time": None, "next_timetable": None} - first_timeslot = get_timeslot(place, weekday, True, db) - if first_timeslot and current_time < first_timeslot[0]: - data["next_timetable"] = "{:d}h{:02d}".format( - first_timeslot[0].hour, first_timeslot[0].minute) - return data - elif first_timeslot and current_time <= first_timeslot[1]: - last_record = db.query( - models.Records.waiting_time - ).filter( - models.Records.place == place - ).order_by( - models.Records.date.desc() - ).first() - if last_record: - waiting_time = last_record.waiting_time - waiting_time = round(waiting_time.total_seconds() / 60) - data["status"] = True - data["waiting_time"] = waiting_time - return data - second_timeslot = get_timeslot(place, weekday, False, db) - if second_timeslot and current_time < second_timeslot[0]: - data["next_timetable"] = "{:d}h{:02d}".format( - second_timeslot[0].hour, second_timeslot[0].minute) - return data - elif second_timeslot and current_time <= second_timeslot[1]: - last_record = db.query( - models.Records.waiting_time - ).filter( - models.Records.place == place - ).order_by( - models.Records.date.desc() - ).first() - if last_record: - waiting_time = last_record.waiting_time - waiting_time = round(waiting_time.total_seconds() / 60) - data["status"] = True - data["waiting_time"] = waiting_time - return data - return data + opening_hours = db.query(models.OpeningHours.open_time, models.OpeningHours.close_time).filter( + models.OpeningHours.place == place, models.OpeningHours.day == weekday).all() + for time_slot in opening_hours: + if current_time < time_slot.open_time: + return schemas.WaitingTime(next_timetable=time_slot.open_time.strftime('%Hh%M')) + elif current_time <= time_slot.close_time: + limit = datetime.combine(date.today(), time_slot.open_time) + last_record = db.query(models.Records.waiting_time).filter(models.Records.place == place).filter( + models.Records.date >= limit).order_by(models.Records.date.desc()).first() + waiting_time = None + if last_record: + waiting_time = round( + last_record.waiting_time.total_seconds() / 60) + return schemas.WaitingTime(status=True, waiting_time=waiting_time) + return schemas.WaitingTime() + + +## Define some utils function +def shift_time(t: time, delta: timedelta): + return (datetime.combine(date(1, 1, 1), t) + delta).time() + +def add_slot(slots_list, start_time, end_time, function): + average_waiting_time = function(start_time, end_time) + if average_waiting_time: + name = 60 * start_time.hour + start_time.minute + slots_list.append(schemas.RecordRead(name=name, time=average_waiting_time)) def get_avg_graph_points(place: str, weekday: int, min_time: time, max_time: time, interval: timedelta, db: Session): """ Get the average waiting time for each interval between two time steps """ - def shift_time(t: time, delta: timedelta): - return (datetime.combine(date(1, 1, 1), t) + delta).time() - def avg_time_query(start_time, end_time): records = db.query( - (func.round( + func.round( func.avg( - 3600 * func.extract('HOUR', models.Records.waiting_time) + - 60 * func.extract('MINUTE', models.Records.waiting_time) + - func.extract('SECOND', models.Records.waiting_time)) - )) / 60 + 60 * func.extract('HOUR', models.Records.waiting_time) + + func.extract('MINUTE', models.Records.waiting_time)) + ) ).filter( models.Records.place == place, func.weekday(models.Records.date) == weekday, - (func.extract('HOUR', models.Records.date) > start_time.hour) | - ((func.extract('HOUR', models.Records.date) == start_time.hour) & - (func.extract('MINUTE', models.Records.date) >= start_time.minute)), - (func.extract('HOUR', models.Records.date) < end_time.hour) | - ((func.extract('HOUR', models.Records.date) == end_time.hour) & - (func.extract('MINUTE', models.Records.date) < end_time.minute)), - ).one() - if records[0]: + cast(models.Records.date, Time) >= start_time, + cast(models.Records.date, Time) <= end_time, + ).first() + if records[0] or records[0] == 0: return int(records[0]) return None - def add_slot(slots_list, start_time, end_time): - average_waiting_time = avg_time_query(start_time, end_time) - if average_waiting_time: - name = 60 * start_time.hour + start_time.minute - slots_list.append({'name': name, 'time': average_waiting_time}) - stats = [] start_time, end_time = min_time, shift_time(min_time, interval) while start_time < max_time: - add_slot(stats, start_time, end_time) + add_slot(stats, start_time, end_time, avg_time_query) start_time, end_time = end_time, shift_time(end_time, interval) return stats @@ -108,13 +81,12 @@ def get_avg_graph(place: str, db: Session): for the current or next available timeslot""" current_date = datetime.now(tz=pytz.timezone("Europe/Paris")) weekday, current_time = current_date.weekday(), current_date.time() - first_timeslot = get_timeslot(place, weekday, True, db) - if first_timeslot and current_time <= first_timeslot[1]: - return get_avg_graph_points( - place, weekday, first_timeslot[0], first_timeslot[1], timedelta(minutes=5), db) - second_timeslot = get_timeslot(place, weekday, False, db) - if second_timeslot and current_time <= second_timeslot[1]: - return get_avg_graph_points(place, weekday, second_timeslot[0], second_timeslot[1], timedelta(minutes=5), db) + opening_hours = db.query(models.OpeningHours.open_time, models.OpeningHours.close_time).filter( + models.OpeningHours.place == place, models.OpeningHours.day == weekday).order_by(models.OpeningHours.open_time).all() + + for time_slot in opening_hours: + if current_time <= time_slot.close_time: + return get_avg_graph_points(place, weekday, time_slot.open_time, time_slot.close_time, timedelta(minutes=5), db) return [] @@ -122,43 +94,27 @@ def get_current_graph_points(place: str, current_date: date, min_time: time, max_time: time, interval: timedelta, db: Session): """ Get the waiting time for each interval between two time steps for the current timeslot """ - def shift_time(t: time, delta: timedelta): - return (datetime.combine(date(1, 1, 1), t) + delta).time() - - def avg_time_query(start_time, end_time): + def current_time_query(start_time, end_time): records = db.query( - (func.round( + func.round( func.avg( - 3600 * func.extract('HOUR', models.Records.waiting_time) + - 60 * func.extract('MINUTE', models.Records.waiting_time) + - func.extract('SECOND', models.Records.waiting_time)) - )) / 60 + 60 * func.extract('HOUR', models.Records.waiting_time) + + func.extract('MINUTE', models.Records.waiting_time)) + ) ).filter( models.Records.place == place, - func.extract('YEAR', models.Records.date) == current_date.year, - func.extract('MONTH', models.Records.date) == current_date.month, - func.extract('DAY', models.Records.date) == current_date.day, - (func.extract('HOUR', models.Records.date) > start_time.hour) | - ((func.extract('HOUR', models.Records.date) == start_time.hour) & - (func.extract('MINUTE', models.Records.date) >= start_time.minute)), - (func.extract('HOUR', models.Records.date) < end_time.hour) | - ((func.extract('HOUR', models.Records.date) == end_time.hour) & - (func.extract('MINUTE', models.Records.date) < end_time.minute)), - ).one() - if records[0]: + cast(models.Records.date, Date) == current_date, + cast(models.Records.date, Time) >= start_time, + cast(models.Records.date, Time) <= end_time + ).first() + if records[0] or records[0] == 0: return int(records[0]) return None - def add_slot(slots_list, start_time, end_time): - average_waiting_time = avg_time_query(start_time, end_time) - if average_waiting_time: - name = 60 * start_time.hour + start_time.minute - slots_list.append({'name': name, 'time': average_waiting_time}) - stats = [] start_time, end_time = min_time, shift_time(min_time, interval) while start_time < max_time: - add_slot(stats, start_time, end_time) + add_slot(stats, start_time, end_time, current_time_query) start_time, end_time = end_time, shift_time(end_time, interval) return stats @@ -169,23 +125,17 @@ def get_current_graph(place: str, db: Session): current_date = datetime.now(tz=pytz.timezone("Europe/Paris")) weekday, day, current_time = current_date.weekday( ), current_date.date(), current_date.time() - first_timeslot = get_timeslot(place, weekday, True, db) - if first_timeslot and current_time <= first_timeslot[0]: - return [], None, None - elif first_timeslot and current_time <= first_timeslot[1]: - points = get_current_graph_points(place, day, first_timeslot[0], current_time, timedelta(minutes=5), db) - start_time = 60 * first_timeslot[0].hour + first_timeslot[0].minute - end_time = 60 * first_timeslot[1].hour + first_timeslot[1].minute - return points, start_time, end_time - second_timeslot = get_timeslot(place, weekday, False, db) - if second_timeslot and current_time <= second_timeslot[0]: - return [], None, None - elif second_timeslot and current_time <= second_timeslot[1]: - points = get_current_graph_points(place, day, second_timeslot[0], current_time, timedelta(minutes=5), db) - start_time = 60 * second_timeslot[0].hour + second_timeslot[0].minute - end_time = 60 * second_timeslot[1].hour + second_timeslot[1].minute - return points, start_time, end_time - return [], None, None + opening_hours = db.query(models.OpeningHours.open_time, models.OpeningHours.close_time).filter( + models.OpeningHours.place == place, models.OpeningHours.day == weekday).all() + + for time_slot in opening_hours: + if time_slot.open_time <= current_time <= time_slot.close_time: + points = get_current_graph_points( + place, day, time_slot.open_time, current_time, timedelta(minutes=5), db) + start_time = 60 * time_slot.open_time.hour + time_slot.open_time.minute + end_time = 60 * time_slot.close_time.hour + time_slot.close_time.minute + return schemas.Graph(data=points, start=start_time, end=end_time) + return schemas.Graph(data=[]) # Define CRUD operation for the comments @@ -210,7 +160,8 @@ def get_comments(place: str, page: int, db: Session): 20, page * 20).all() - comments_list = list(schemas.Comment(**comment.__dict__, username=username) for comment, username in comments) + comments_list = list(schemas.Comment( + **comment.__dict__, username=username) for comment, username in comments) comments_list.reverse() return comments_list @@ -218,7 +169,8 @@ def get_comments(place: str, page: int, db: Session): def create_comment(user: schemas.User, place: str, new_comments: schemas.CommentBase, db: Session): """ Add a new comment to the database """ date = datetime.now(tz=pytz.timezone("Europe/Paris")) - db_comment = models.Comments(**new_comments.dict(), published_at=date, place=place, user_id=user.id) + db_comment = models.Comments( + **new_comments.dict(), published_at=date, place=place, user_id=user.id) db.add(db_comment) db.commit() db.refresh(db_comment) @@ -273,24 +225,11 @@ def get_opening_hours(place: str, db: Session): ).filter( models.OpeningHours.place == place ).order_by( - models.OpeningHours.day, models.OpeningHours.timeslot.desc() + models.OpeningHours.day, models.OpeningHours.open_time ).all() return opening_hours -def get_timeslot(place: str, day: int, timeslot: bool, db: Session): - """ Get the opening hours for the given place and timeslot""" - opening_hours = db.query( - models.OpeningHours.open_time, - models.OpeningHours.close_time, - ).filter( - models.OpeningHours.place == place, - models.OpeningHours.day == day, - models.OpeningHours.timeslot == timeslot - ).first() - return opening_hours - - def create_opening_hours( new_opening_hours: schemas.OpeningHoursBase, db: Session): """ Add opening hours to the database """ @@ -315,56 +254,19 @@ def delete_opening_hours(id: int, db: Session): # Restaurants information def get_restaurants(db: Session): - current_date = datetime.now(tz=pytz.timezone("Europe/Paris")) - weekday, current_time = current_date.weekday(), current_date.time() - restaurant_names = [ - r.place for r in db.query( - models.OpeningHours.place).distinct()] + weekday = datetime.now(tz=pytz.timezone("Europe/Paris")).weekday() + places = db.query(models.OpeningHours.place).distinct() restaurants = [] - for name in restaurant_names: - restaurant = {} - restaurant["name"] = name - first_timeslot = get_timeslot(name, weekday, True, db) - second_timeslot = get_timeslot(name, weekday, False, db) - - if (first_timeslot and first_timeslot[0] <= current_time < first_timeslot[1]) or ( - second_timeslot and second_timeslot[0] <= current_time < second_timeslot[1]): - restaurant["status"] = True - else: - restaurant["status"] = False - - if first_timeslot and second_timeslot: - restaurant["timetable"] = ( - f"{first_timeslot[0].hour:{'0'}{2}}h{first_timeslot[0].minute:{'0'}{2}}-" - f"{first_timeslot[1].hour:{'0'}{2}}h{first_timeslot[1].minute:{'0'}{2}} / " - f"{second_timeslot[0].hour:{'0'}{2}}h{second_timeslot[0].minute:{'0'}{2}}-" - f"{second_timeslot[1].hour:{'0'}{2}}h{second_timeslot[1].minute:{'0'}{2}}") - elif first_timeslot: - restaurant["timetable"] = ( - f"{first_timeslot[0].hour:{'0'}{2}}h{first_timeslot[0].minute:{'0'}{2}}-" - f"{first_timeslot[1].hour:{'0'}{2}}h{first_timeslot[1].minute:{'0'}{2}}") - else: - restaurant["timeslot"] = "-" - - if restaurant["status"]: - last_record = db.query( - models.Records - ).filter( - models.Records.place == name - ).order_by( - models.Records.date.desc() - ).first() - if last_record: - waiting_time = last_record.waiting_time - restaurant["waiting_time"] = round( - waiting_time.total_seconds() / 60) - else: - restaurant["waiting_time"] = None - else: - restaurant["waiting_time"] = None - - restaurants.append(restaurant) + for place in places: + opening_hours = db.query(models.OpeningHours).filter( + models.OpeningHours.place == place.place, models.OpeningHours.day == weekday).all() + opening_hours_formated = [ + f"{row.open_time.strftime('%Hh%M')}-{row.close_time.strftime('%Hh%M')}" for row in opening_hours] + timetable = "/".join(opening_hours_formated) + infos = get_waiting_time(place.place, db) + restaurants.append(schemas.Restaurant( + **infos.dict(), name=place.place, timetable=timetable)) return restaurants @@ -375,8 +277,10 @@ def init_user(db: Session): """ Add a news to the database """ cookie = uuid4() state = secrets.token_urlsafe(30) - expiration_date = datetime.now(tz=pytz.timezone("Europe/Paris")) + timedelta(minutes=10) - db_user = models.Users(state=state, cookie=cookie, expiration_date=expiration_date) + expiration_date = datetime.now(tz=pytz.timezone( + "Europe/Paris")) + timedelta(minutes=10) + db_user = models.Users(state=state, cookie=cookie, + expiration_date=expiration_date) db.add(db_user) db.commit() db.refresh(db_user) @@ -400,8 +304,10 @@ def delete_state(user: schemas.User, db: Session): def update_user(user: schemas.User, user_info: dict, db: Session): full_name = f"{user_info['firstName']} {user_info['lastName']}" - expiration_date = datetime.now(tz=pytz.timezone("Europe/Paris")) + timedelta(days=3) - existing_user = db.query(models.Users).filter(models.Users.username == full_name).first() + expiration_date = datetime.now( + tz=pytz.timezone("Europe/Paris")) + timedelta(days=3) + existing_user = db.query(models.Users).filter( + models.Users.username == full_name).first() if existing_user: existing_user.cookie = user.cookie existing_user.expiration_date = expiration_date diff --git a/backend/db/models.py b/backend/db/models.py index ff985da4d23f28c5b5868fb0bd7ded7aedcb53dc..a03399ebe7678f792a6aad4185ec53a33eef8e17 100644 --- a/backend/db/models.py +++ b/backend/db/models.py @@ -48,7 +48,6 @@ class OpeningHours(Base): id = Column(Integer, primary_key=True, index=True) place = Column(String(30)) day = Column(Integer) - timeslot = Column(Boolean) open_time = Column(Time) close_time = Column(Time) diff --git a/backend/db/schemas.py b/backend/db/schemas.py index 3a170468277526f19c502167aa4caa1d7cc802fd..991d910e96f3721e693910558d6a33b4c1511aca 100644 --- a/backend/db/schemas.py +++ b/backend/db/schemas.py @@ -1,11 +1,12 @@ """ Pydantic schemas for the magasin app """ -from typing import Optional +from typing import List, Optional from datetime import datetime, timedelta, time from pydantic import BaseModel, Field +# Records data structure class RecordBase(BaseModel): """Records base schema""" place: str = Field(..., @@ -24,6 +25,13 @@ class Record(RecordBase): orm_mode = True +class RecordRead(BaseModel): + """ Data structure for record in graph """ + name: int + time: int + + +# Comments Data structure class CommentBase(BaseModel): """Comments base schema""" content: str = Field(..., title="Content of the comment posted") @@ -40,6 +48,7 @@ class Comment(CommentBase): orm_mode = True +# News data structure class NewsBase(BaseModel): """News sql table model""" title: str = Field(..., title="Title of the news") @@ -57,13 +66,19 @@ class News(NewsBase): orm_mode = True +# Stats data structure +class WaitingTime(BaseModel): + """Waiting time schema for reading""" + status: bool = Field(default=False, title="Status of the restaurant for the current hour") + waiting_time: Optional[timedelta] = Field(default=None, title="Waiting time for the restaurant") + next_timetable: Optional[str] = Field(default=None, title="Next time the restaurant will be open") + + class OpeningHoursBase(BaseModel): """Database opening_hours base schema""" place: str = Field(..., title="Name of the RU corresponding the given record") day: int = Field(..., title="Day of the week") - timeslot: bool = Field(..., - title="Service slot (True for midday, False for evening)") open_time: time = Field(..., title="Opening time") close_time: time = Field(..., title="Closing time") @@ -76,6 +91,22 @@ class OpeningHours(OpeningHoursBase): orm_mode = True +class Restaurant(BaseModel): + """Restaurant schema for reading""" + name: str + status: bool + waiting_time: Optional[timedelta] = Field(default=None, title="Waiting time for the restaurant") + timetable: str + + +class Graph(BaseModel): + """ Data structure for current graph display """ + data: List[RecordRead] = Field(title="Last records list for the restaurant") + start: Optional[int] = Field(default=None, title="Opening of the RU") + end: Optional[int] = Field(default=None, title="Closure of the RU") + + +# User data structure class User(BaseModel): """Database user base schema""" id: int diff --git a/backend/main.py b/backend/main.py index 8aa44d91b3ac105d5d226debef40ad13fbb6ed79..ce0938157c9ad0872df0693744cab36fb523f231 100644 --- a/backend/main.py +++ b/backend/main.py @@ -2,14 +2,14 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from dotenv import load_dotenv import os -from db.database import get_db from fastapi import Depends from sqlalchemy.orm import Session -from db import schemas from typing import List from threading import Thread +import json -from db import database, models +from db import database, models, schemas +from db.database import get_db from routers import * from video_capture import handle_cameras @@ -59,6 +59,7 @@ async def post_records(record: schemas.RecordBase, db: Session = Depends(get_db) db.add(db_record) db.commit() db.refresh(db_record) + await websocket.manager.broadcast(json.dumps({"type": "data"})) return db_record diff --git a/backend/routers/__init__.py b/backend/routers/__init__.py index 091fddcb03a0abf8097c5bdd36461a30dd173e1a..745afdca4556b73e7a36084eca5963eadfbfa487 100644 --- a/backend/routers/__init__.py +++ b/backend/routers/__init__.py @@ -1,6 +1,5 @@ from . import authentication from . import comments from . import news -from . import opening_hours from . import stats from . import websocket diff --git a/backend/routers/authentication.py b/backend/routers/authentication.py index df409dfcfe6f9caa9e336ae1a0dea47744f67f4f..32ab07f5d4969da1bfb9ad9f3d22dff5498f963f 100644 --- a/backend/routers/authentication.py +++ b/backend/routers/authentication.py @@ -74,6 +74,8 @@ async def login(code: Optional[str] = None, state: Optional[str] = None, connect @router.get("/logout") async def delete_session(connect_id: str = Cookie(...), db: Session = Depends(get_db)): response = RedirectResponse(f"{os.getenv('AUTH_ROOT')}/logout?{urlencode({'redirect_logout': os.getenv('WEB_ROOT')})}") - response.delete_cookie(key="connect_id") - crud.end_session(connect_id, db) - return response + try: + response.delete_cookie(key="connect_id") + crud.end_session(connect_id, db) + finally: + return response diff --git a/backend/routers/news.py b/backend/routers/news.py index b76cdc55dbfe26020d06fa67c0ef4199c410d750..7c4b8136fda0bd567297b83c6c41d859399452b6 100644 --- a/backend/routers/news.py +++ b/backend/routers/news.py @@ -1,9 +1,11 @@ from fastapi import APIRouter, Depends from sqlalchemy.orm import Session from typing import List +import json from db import schemas, crud from db.database import get_db +from .websocket import manager router = APIRouter(prefix="/api", tags=["news"]) @@ -16,7 +18,9 @@ async def get_news(place: str, db: Session = Depends(get_db)): @router.post('/news', response_model=schemas.News) async def create_news(news: schemas.NewsBase, db: Session = Depends(get_db)): - return crud.create_news(news, db) + saved_news = crud.create_news(news, db) + await manager.broadcast(json.dumps({"type": "news", "comment": saved_news.__dict__}, default=str)) + return saved_news @router.delete('/news/{id}', response_model=None) diff --git a/backend/routers/stats.py b/backend/routers/stats.py index 066bd7681327a979c025b66690616742d5f168ec..ffe229fdeffc4a85d48b24c6625e23ebdf084a8d 100644 --- a/backend/routers/stats.py +++ b/backend/routers/stats.py @@ -1,51 +1,45 @@ from fastapi import APIRouter, Depends from sqlalchemy.orm import Session -from typing import List, Tuple +from typing import List from db import schemas, crud from db.database import get_db -router = APIRouter(prefix="/api", tags=["stats"]) +router = APIRouter(prefix="/api") -@router.get('/{place}/waiting_time', response_model=dict) +@router.get('/{place}/waiting_time', response_model=schemas.WaitingTime, tags=["stats"]) async def waiting_time(place: str, db: Session = Depends(get_db)): return crud.get_waiting_time(place, db) -@router.get('/{place}/stats/avg_graph', response_model=list) +@router.get('/{place}/stats/avg_graph', response_model=list, tags=["stats"]) async def stats(place: str, db: Session = Depends(get_db)): return crud.get_avg_graph(place, db) -@router.get('/{place}/stats/current_graph', response_model=Tuple[list, int, int]) +@router.get('/{place}/stats/current_graph', response_model=schemas.Graph, tags=["stats"]) async def stats(place: str, db: Session = Depends(get_db)): return crud.get_current_graph(place, db) @router.get('/{place}/opening_hours', - response_model=List[schemas.OpeningHours]) + response_model=List[schemas.OpeningHours], tags=["timetable"]) async def get_opening_hours(place: str, db: Session = Depends(get_db)): return crud.get_opening_hours(place, db) -@router.get('/{place}/opening_hours/{day}/{timeslot}', - response_model=List[schemas.OpeningHours]) -async def get_timeslot(place: str, day: int, timeslot: bool, db: Session = Depends(get_db)): - return crud.get_timeslot(place, day, timeslot, db) - - -@router.post('/opening_hours', response_model=schemas.OpeningHours) +@router.post('/opening_hours', response_model=schemas.OpeningHours, tags=["timetable"]) async def create_opening_hours(opening_hours: schemas.OpeningHoursBase, db: Session = Depends(get_db)): return crud.create_opening_hours(opening_hours, db) -@router.delete('/opening_hours/{id}', response_model=None) +@router.delete('/opening_hours/{id}', response_model=None, tags=["timetable"]) async def delete_opening_hours(id: int, db: Session = Depends(get_db)): return crud.delete_opening_hours(id, db) -@router.get('/restaurants', response_model=List[dict]) +@router.get('/restaurants', response_model=List[schemas.Restaurant], tags=["timetable"]) async def get_restaurants(db: Session = Depends(get_db)): return crud.get_restaurants(db) diff --git a/backend/video_capture.py b/backend/video_capture.py index 96afe5a8d8795b13600286463e4add0348025c4f..9806146f1aad3258b8e71d350cf91b4c6093d04f 100644 --- a/backend/video_capture.py +++ b/backend/video_capture.py @@ -3,11 +3,13 @@ from datetime import datetime, timedelta import numpy as np import keras from utils.preprocessing import fix_singular_shape, norm_by_imagenet -from db import models from dotenv import load_dotenv +import json import os +from db import models from db.database import SessionLocal +from routers.websocket import manager def handle_cameras(): @@ -56,7 +58,6 @@ def handle_cameras(): axis=0) pred_map = np.squeeze(model.predict(input_image)) count_prediction = np.sum(pred_map) - print(count_prediction) waiting_time = timedelta( seconds=camera['b_factor'] + int(count_prediction) * @@ -68,7 +69,7 @@ def handle_cameras(): waiting_time=waiting_time) db.add(db_record) db.commit() - db.refresh(db_record) + manager.broadcast(json.dumps({"type": "data"})) camera['count'] += 1 else: camera["cap"] = cv2.VideoCapture( diff --git a/frontend/src/components/Comments.js b/frontend/src/components/Comments.js index 714bec6aeb12f3d677c47252e4a619ec7f8b07a1..1f7975a963cc88c874bada47d9431c0e63e3b09a 100644 --- a/frontend/src/components/Comments.js +++ b/frontend/src/components/Comments.js @@ -30,10 +30,7 @@ export default function Messages({ place, infos, lastMessage }) { { withCredentials: true }, ) .then((res) => { - setMessages((messages) => { - messages.push(res.data); - return messages; - }); + setMessages((old_messages) => [...old_messages, res.data]); updateValue(""); }) .catch((e) => { @@ -102,8 +99,12 @@ export default function Messages({ place, infos, lastMessage }) { useEffect(() => { if (chat.current) { - let position = chat.current.scrollHeight - chat.current.clientHeight; - chat.current.scrollTop = position; + if (infos) { + chat.current.scrollTop = 0; + } else { + let position = chat.current.scrollHeight - chat.current.clientHeight; + chat.current.scrollTop = position; + } } }, [chat.current, messages.length]); @@ -117,11 +118,11 @@ export default function Messages({ place, infos, lastMessage }) { useEffect(() => { if (lastMessage?.data) { let new_message = JSON.parse(lastMessage.data); - if (new_message.type == "comment" && new_message.comment.username != user) { - setMessages((messages) => { - messages.push(new_message.comment); - return messages; - }); + if ( + (new_message.type == "news" && infos) || + (new_message.type == "comment" && !infos && new_message.comment.username != user) + ) { + setMessages((old_messages) => [...old_messages, new_message.comment]); } } }, [lastMessage]); diff --git a/frontend/src/components/Graph.js b/frontend/src/components/Graph.js index 8a4acc1860f11c67bb95f6a060f1d6b0a86c4d5c..5aca5c7becd299d5044001ef27531efc5ac9fb59 100644 --- a/frontend/src/components/Graph.js +++ b/frontend/src/components/Graph.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import axios from "axios"; import { Line, @@ -32,33 +32,44 @@ function formatXAxis(value) { return Math.floor(value / 60).toString() + "h" + (value % 60).toString().padStart(2, "0"); } -export default function Graph({ place }) { - const [checked, setChecked] = React.useState(false); - const [currentData, setCurrentData] = React.useState([[], 0, 0]); +export default function Graph({ place, lastMessage }) { + const [checked, setChecked] = useState(false); + const [currentData, setCurrentData] = useState({ data: [], start: 0, end: 0 }); + const [avgData, setAvgData] = useState([]); + const [reload, setReload] = useState(true); - React.useEffect(() => { - axios - .get( - `${process.env.REACT_APP_BASE_URL_BACK}/${encodeURIComponent(place)}/stats/current_graph`, - ) - .then((response) => { - setCurrentData(response.data); - }); - }, []); + useEffect(() => { + if (reload) { + axios + .get( + `${process.env.REACT_APP_BASE_URL_BACK}/${encodeURIComponent(place)}/stats/current_graph`, + ) + .then((response) => { + setCurrentData(response.data); + }); - const [avgData, setAvgData] = React.useState([[], 0, 0]); + axios + .get(`${process.env.REACT_APP_BASE_URL_BACK}/${encodeURIComponent(place)}/stats/avg_graph`) + .then((response) => { + setAvgData(response.data); + }); - React.useEffect(() => { - axios - .get(`${process.env.REACT_APP_BASE_URL_BACK}/${encodeURIComponent(place)}/stats/avg_graph`) - .then((response) => { - setAvgData(response.data); - }); - }, []); + setReload(false); + } + }, [reload]); + + useEffect(() => { + if (lastMessage?.data) { + let new_message = JSON.parse(lastMessage.data); + if (new_message.type == "data") { + setReload(true); + } + } + }, [lastMessage]); return ( <> - {!!currentData[0].length && ( + {!!currentData?.data?.length && ( <div style={{ height: "60%", padding: "3rem" }}> <div className="graph"> <ResponsiveContainer width="100%" height="100%"> @@ -95,12 +106,12 @@ export default function Graph({ place }) { tickLine={false} tick={{ fill: "#FFFFFF", fontSize: "18" }} ticks={[...Array(4).keys()].map( - (i) => currentData[1] + (i * (currentData[2] - currentData[1])) / 3, + (i) => currentData.start + (i * (currentData.end - currentData.start)) / 3, )} dataKey="name" type="number" interval="preserveStartEnd" - domain={[currentData[1], currentData[2]]} + domain={[currentData.start, currentData.end]} tickFormatter={formatXAxis} /> <YAxis @@ -117,7 +128,7 @@ export default function Graph({ place }) { /> <Tooltip content={<CustomTooltip />} /> <Area - data={currentData[0]} + data={currentData.data} type="monotone" dataKey="time" stroke="#FFFFFF" diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js index c7eeadea94d9f9dd54a4f600bd7a0bdf99218153..b3ab6f54adf318f7a5b1ddd484107eec09de05d8 100644 --- a/frontend/src/components/Header.js +++ b/frontend/src/components/Header.js @@ -39,7 +39,7 @@ export default function Header({ selection, setSelection }) { <div id="header-timetable"> {selection ? ( width ? ( - `Horaires : ${selection.timetable}` + selection.timetable && `Horaires : ${selection.timetable}` ) : ( selection.timetable ) diff --git a/frontend/src/components/WaitingTime.js b/frontend/src/components/WaitingTime.js index 7af4d42ce100a9915adf04e972f4716e5f10141f..7e12c19bd80563c942a30ad8f46559c0c6c5ae76 100644 --- a/frontend/src/components/WaitingTime.js +++ b/frontend/src/components/WaitingTime.js @@ -1,18 +1,31 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import axios from "axios"; import "../styles/WaitingTime.css"; -export default function WaitingTime({ place }) { +export default function WaitingTime({ place, lastMessage }) { const [data, setData] = useState({ status: true, waiting_time: null, next_timetable: null }); + const [reload, setReload] = useState(true); - React.useEffect(() => { - axios - .get(`${process.env.REACT_APP_BASE_URL_BACK}/${encodeURIComponent(place)}/waiting_time`) - .then((res) => { - setData(res.data); - }); - }, []); + useEffect(() => { + if (reload) { + axios + .get(`${process.env.REACT_APP_BASE_URL_BACK}/${encodeURIComponent(place)}/waiting_time`) + .then((res) => { + setData(res.data); + }); + setReload(false); + } + }, [reload]); + + useEffect(() => { + if (lastMessage?.data) { + let new_message = JSON.parse(lastMessage.data); + if (new_message.type == "data") { + setReload(true); + } + } + }, [lastMessage]); return ( <div id="waiting-time-parent"> diff --git a/frontend/src/index.js b/frontend/src/index.js index c793ad7462c0020ac6ad20348c92f0186743150d..28592a4fe01255010457d4e46c7edf453e19adee 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -5,7 +5,7 @@ import useWebSocket from "react-use-websocket"; import axios from "axios"; import { Footer, Header } from "./components"; -import { HomePage, RestaurantPage, NotFoundPage, DetailsPage } from "./views"; +import { HomePage, RestaurantPage, NotFoundPage } from "./views"; import "bootstrap/dist/css/bootstrap.min.css"; import "./styles/index.css"; @@ -17,23 +17,28 @@ export default function App() { const [restaurantsList, setRestaurantsList] = useState([]); const [selection, setSelection] = useState(null); const [loading, setLoading] = useState(true); + const [reload, setReload] = useState(true); const [user, setUser] = useState(localStorage.getItem("user")); const { lastMessage } = useWebSocket(socketUrl, { shouldReconnect: () => true, }); useEffect(() => { - axios - .get(`${process.env.REACT_APP_BASE_URL_BACK}/restaurants`) - .then((res) => { - setRestaurantsList(res.data); - setLoading(false); - }) - .catch((e) => { - console.log(e); - setLoading(false); - }); - }, []); + if (reload) { + axios + .get(`${process.env.REACT_APP_BASE_URL_BACK}/restaurants`) + .then((res) => { + setRestaurantsList(res.data); + setLoading(false); + setReload(false); + }) + .catch((e) => { + console.log(e); + setLoading(false); + setReload(false); + }); + } + }, [reload]); useEffect(() => { let path = window.location.pathname.split("/"); @@ -46,6 +51,15 @@ export default function App() { } }, [restaurantsList]); + useEffect(() => { + if (lastMessage?.data) { + let new_message = JSON.parse(lastMessage.data); + if (new_message.type == "data") { + setReload(true); + } + } + }, [lastMessage]); + return ( <div className="app"> <User.Provider value={[user, setUser]}> @@ -61,9 +75,7 @@ export default function App() { <Route path="/:restaurant" element={<RestaurantPage {...{ selection, setSelection, lastMessage }} />} - > - <Route path="details" element={<DetailsPage selection={selection} />} /> - </Route> + /> <Route path="*" element={<NotFoundPage />} /> </Routes> </div> diff --git a/frontend/src/views/Details.js b/frontend/src/views/Details.js deleted file mode 100644 index fe564b8827e7c68c69c4733330a3766d30989454..0000000000000000000000000000000000000000 --- a/frontend/src/views/Details.js +++ /dev/null @@ -1,5 +0,0 @@ -import React from "react"; - -export default function Details() { - return <div>details page</div>; -} diff --git a/frontend/src/views/Restaurant.js b/frontend/src/views/Restaurant.js index 5b0d73f7ce1cd7e907ce13a6164b21daa6af5486..a309473c4e7a71292f16dbf7341955ff3e078bd0 100644 --- a/frontend/src/views/Restaurant.js +++ b/frontend/src/views/Restaurant.js @@ -9,7 +9,7 @@ export default function RestaurantPage({ selection, lastMessage }) { <> {selection && ( <div className="restaurant-container"> - <Comments place={selection.name} infos /> + <Comments place={selection.name} lastMessage={lastMessage} infos /> <div className="restaurant-container" id="restaurant-main-page"> <WaitingTime place={selection.name} lastMessage={lastMessage} /> <Graph place={selection.name} type="current" lastMessage={lastMessage} /> diff --git a/frontend/src/views/index.js b/frontend/src/views/index.js index 82ebf8645a723d6d8d63880bf45fe1351a82a6e1..77ab82fcd6c65ec32cf59292d0e7eea5cb8681cc 100644 --- a/frontend/src/views/index.js +++ b/frontend/src/views/index.js @@ -1,4 +1,3 @@ export { default as HomePage } from "./HomePage"; export { default as RestaurantPage } from "./Restaurant"; export { default as NotFoundPage } from "./NotFoundPage"; -export { default as DetailsPage } from "./Details.js";