diff --git a/README.md b/README.md index 96294370772dd6a7ea60c7437c7c63c6f9412bb7..6be0232a3f8b924143ae4d7b56a5d0aef8aae9c7 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,6 @@ Navigate to [http://localhost:3000](http://localhost:3000) - Accéder à d'autre infos telle que l'API des cours sur demande à la DISI pour intégrer ça aux prédictions (ex: cours en promo complète juste implique plus d'attente) ## Coté dev -- Ajouter une table pour les fermetures exceptionnelles - Protéger l'interface OpenAPI et mettre en place une interface admin pour les news et potentiellement modération (avec authentification) - Permettre de définir les masques proprement et de manière à pouvoir généraliser à d'autre RU - Accorder la charte graphique si le service est intégré à d'autres appli (bordeau/blanc service de CS, charte graphique de VR) diff --git a/backend/db/crud.py b/backend/db/crud.py index 1431bf2b90e4eb40405e16bc01d03ed63d560040..df872ffc41042fd3a602dcdb7dcd8fccb57b2130 100644 --- a/backend/db/crud.py +++ b/backend/db/crud.py @@ -18,7 +18,7 @@ from db import models, schemas 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() + date, weekday, current_time = current_date.date(), current_date.weekday(), current_date.time() opening_hours = db.query( models.OpeningHours.open_time, models.OpeningHours.close_time).filter( @@ -26,17 +26,27 @@ def get_waiting_time(place: str, db: Session): models.OpeningHours.day == weekday).order_by( models.OpeningHours.open_time).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) + closure = db.query( + models.Closure).filter( + models.Closure.place == place, + models.Closure.beginning_date <= datetime.combine(date, time_slot.open_time), + models.Closure.end_date >= datetime.combine(date, time_slot.open_time)).order_by( + models.Closure.beginning_date).first() + if not closure: + 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, 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() @@ -46,11 +56,10 @@ def shift_time(t: time, delta: timedelta): def add_slot(slots_list, start_time, end_time, function): - average_waiting_time = function(start_time, end_time) - if average_waiting_time: + waiting_time = function(start_time, end_time) + if waiting_time: name = 60 * start_time.hour + start_time.minute - slots_list.append(schemas.RecordRead( - name=name, time=average_waiting_time)) + slots_list.append(schemas.RecordRead(name=name, time=waiting_time)) def get_avg_graph_points(place: str, weekday: int, min_time: time, @@ -88,12 +97,21 @@ 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() - 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) + 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() + closure = db.query( + models.Closure).filter( + models.Closure.place == place, + models.Closure.beginning_date <= current_date, + models.Closure.end_date >= current_date).first() + if not closure: + 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 [] @@ -130,18 +148,25 @@ def get_current_graph_points(place: str, current_date: date, def get_current_graph(place: str, db: Session): """ Get the waiting_time_graph for the current timeslot""" current_date = datetime.now(tz=pytz.timezone("Europe/Paris")) - weekday, day, current_time = current_date.weekday( - ), current_date.date(), current_date.time() - 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) + weekday, day, current_time = current_date.weekday(), current_date.date(), current_date.time() + opening_hours = db.query( + models.OpeningHours.open_time, + models.OpeningHours.close_time).filter( + models.OpeningHours.place == place, + models.OpeningHours.day == weekday).all() + closure = db.query( + models.Closure).filter( + models.Closure.place == place, + models.Closure.beginning_date <= current_date, + models.Closure.end_date >= current_date).first() + if not closure: + 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=[]) @@ -155,10 +180,15 @@ def get_comments(place: str, page: int, db: Session): models.Comments.published_at.desc(), models.Comments.id.desc()).all() else: - comments = db.query(models.Comments, models.Users.username).join(models.Users).filter(models.Comments.place == place).order_by( - models.Comments.published_at.desc(), models.Comments.id.desc()).slice((page - 1) * 20, page * 20).all() - comments_list = list(schemas.Comment( - **comment.__dict__, username=username) for comment, username in comments) + comments = db.query( + models.Comments, + models.Users.username).join( + models.Users).filter( + models.Comments.place == place).order_by( + models.Comments.published_at.desc(), + models.Comments.id.desc()).slice( + (page - 1) * 20, page * 20).all() + comments_list = [schemas.Comment(**comment.__dict__, username=username) for comment, username in comments] comments_list.reverse() return comments_list @@ -166,8 +196,7 @@ 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) @@ -187,10 +216,43 @@ def delete_comment(id: int, db: Session): def get_news(place: str, db: Session): """ Get the news for the given place """ + current_date = datetime.now(tz=pytz.timezone("Europe/Paris")) news = db.query( models.News).filter( - models.News.place == place).order_by( - models.News.published_at.desc()).all() + models.News.place == place, + models.News.end_date >= current_date).order_by( + models.News.published_at.desc()).all() + opening_hours = db.query( + models.OpeningHours.open_time, + models.OpeningHours.close_time).filter( + models.OpeningHours.place == place, + models.OpeningHours.day == current_date.weekday()).order_by( + models.OpeningHours.open_time).all() + next_timetable = None + for time_slot in opening_hours: + if current_date.time() < time_slot.open_time: + next_timetable=time_slot.open_time + break + if not next_timetable: + closure = db.query( + models.Closure).filter( + models.Closure.place == place, + models.Closure.beginning_date <= current_date, + models.Closure.end_date >= current_date).first() + else: + closure = db.query( + models.Closure).filter( + models.Closure.place == place, + models.Closure.beginning_date <= datetime.combine(current_date.date(), next_timetable), + models.Closure.end_date >= datetime.combine(current_date.date(), next_timetable)).first() + if closure: + closure_news = schemas.News( + title="Fermeture exceptionnelle", + content=f"{place} est exceptionnellement hors service jusqu'au {closure.end_date.strftime('%d/%m/%y à %Hh%M')}", + end_date=closure.end_date, + place=place, + published_at=closure.beginning_date) + news.append(closure_news) return news @@ -450,3 +512,32 @@ def delete_collaborative_record(id: int, db: Session): models.CollaborativeRecords.id == id).delete() db.commit() return + + +# Define CRUD operation for exceptional closure + +def get_closure(place: str, db: Session): + current_date = datetime.now(tz=pytz.timezone("Europe/Paris")) + closures = db.query( + models.Closure).filter( + models.Closure.place == place, + models.Closure.end_date >= current_date).order_by( + models.Closure.beginning_date).all() + return [schemas.Closure(**closure.__dict__) for closure in closures] + + +def create_closure(closure: schemas.Closure, db: Session): + db_closure = models.Closure(**closure.dict()) + db.add(db_closure) + db.commit() + db.refresh(db_closure) + return schemas.Closure(**closure.dict()) + + +def delete_closure(id: int, db: Session): + if id == 0: + db.query(models.Closure).delete() + else: + db.query(models.Closure).filter(models.Closure.id == id).delete() + db.commit() + return diff --git a/backend/db/models.py b/backend/db/models.py index cb8d31e9dfbdd5c8b88bc4e82997451656ac2885..c80580bdfbec9ff61ad427397134f7463cdc0e09 100644 --- a/backend/db/models.py +++ b/backend/db/models.py @@ -63,6 +63,16 @@ class OpeningHours(Base): close_time = Column(Time) +class Closure(Base): + """ Register exceptional closure for a period sql table model """ + __tablename__ = "closure" + + id = Column(Integer, primary_key=True, index=True) + place = Column(String(30)) + beginning_date = Column(DateTime) + end_date = Column(DateTime) + + class Users(Base): """ User sql table model """ __tablename__ = "users" diff --git a/backend/db/schemas.py b/backend/db/schemas.py index 7eaa3d4f7474b99ebf8fbd75e6fe1a9e354cbd9f..325d2071b40bdcb8c3061058154b8d9ab1c8a74c 100644 --- a/backend/db/schemas.py +++ b/backend/db/schemas.py @@ -100,6 +100,21 @@ class OpeningHours(OpeningHoursBase): orm_mode = True +class ClosureBase(BaseModel): + """ Closure schema base """ + place: str = Field(..., title="Name of the restaurant") + beginning_date: datetime = Field(..., title="Beginning date of closure") + end_date: datetime = Field(..., title="Ending date of closure") + + +class Closure(ClosureBase): + """ Closure schema """ + id: int + + class Config: + orm_mode = True + + class Restaurant(BaseModel): """Restaurant schema for reading""" name: str diff --git a/backend/main.py b/backend/main.py index 8b01abb83fdab29cda11a508b68a40640ae3cd56..d6c986ac7a837ba1f558c9d386bfb257ded706f8 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,43 +1,44 @@ -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware -from dotenv import load_dotenv -from threading import Thread -import os - -from db import database, models -from routers import * -from video_capture import handle_cameras - -app = FastAPI(docs_url="/api/docs", openapi_url="/api/openapi.json") - -# load environment variables -load_dotenv() - -origins = [ - os.getenv('WEB_ROOT'), -] - -app.add_middleware( - CORSMiddleware, - allow_origins=origins, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"] -) - - -@app.on_event("startup") -async def on_startup(): - # Database creation - models.Base.metadata.create_all(bind=database.engine) - t = Thread(target=handle_cameras) - t.start() - - -# Integration of routers -app.include_router(stats.router) -app.include_router(comments.router) -app.include_router(news.router) -app.include_router(authentication.router) -app.include_router(websocket.router) -app.include_router(records.router) +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from dotenv import load_dotenv +from threading import Thread +import os + +from db import database, models +from routers import * +from video_capture import handle_cameras + +app = FastAPI(docs_url="/api/docs", openapi_url="/api/openapi.json") + +# load environment variables +load_dotenv() + +origins = [ + os.getenv('WEB_ROOT'), +] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"] +) + + +@app.on_event("startup") +async def on_startup(): + # Database creation + models.Base.metadata.create_all(bind=database.engine) + t = Thread(target=handle_cameras) + t.start() + + +# Integration of routers +app.include_router(infos.router) +app.include_router(records.router) +app.include_router(stats.router) +app.include_router(news.router) +app.include_router(comments.router) +app.include_router(authentication.router) +app.include_router(websocket.router) diff --git a/backend/routers/__init__.py b/backend/routers/__init__.py index d88b0068a21617007209e3ba487efd5d103f01aa..ca73df7e38364f5c3193afdaa4ceae5e9b6ee649 100644 --- a/backend/routers/__init__.py +++ b/backend/routers/__init__.py @@ -4,3 +4,4 @@ from . import news from . import stats from . import websocket from . import records +from . import infos diff --git a/backend/routers/infos.py b/backend/routers/infos.py new file mode 100644 index 0000000000000000000000000000000000000000..16636c4ebb80b20d5bc2d9d897e958188dd10dbe --- /dev/null +++ b/backend/routers/infos.py @@ -0,0 +1,50 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from typing import List + +from db import schemas, crud +from db.database import get_db + + +router = APIRouter(prefix="/api", tags=["timetable"]) + + +# Manage opening hours + +@router.get('/{place}/opening_hours', response_model=List[schemas.OpeningHours]) +async def get_opening_hours(place: str, db: Session = Depends(get_db)): + return crud.get_opening_hours(place, db) + + +@router.post('/opening_hours', response_model=schemas.OpeningHours) +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) +async def delete_opening_hours(id: int, db: Session = Depends(get_db)): + return crud.delete_opening_hours(id, db) + + +# Manage exceptional closure + +@router.get('/{place}/closure', response_model=List[schemas.Closure]) +async def get_closure(place: str, db: Session = Depends(get_db)): + return crud.get_closure(place, db) + + +@router.post('/closure', response_model=schemas.Closure) +async def create_closure(closure: schemas.Closure, db: Session = Depends(get_db)): + return crud.create_closure(closure, db) + + +@router.delete('/closure/{id}', response_model=None) +async def delete_closure(id: int, db: Session = Depends(get_db)): + return crud.delete_closure(id, db) + + +# Render restaurants infos + +@router.get('/restaurants', response_model=List[schemas.Restaurant]) +async def get_restaurants(db: Session = Depends(get_db)): + return crud.get_restaurants(db) diff --git a/backend/routers/stats.py b/backend/routers/stats.py index ffe229fdeffc4a85d48b24c6625e23ebdf084a8d..ec8b09e3ffe20b1b6a3edc56457e3f015e2cb475 100644 --- a/backend/routers/stats.py +++ b/backend/routers/stats.py @@ -1,45 +1,23 @@ from fastapi import APIRouter, Depends from sqlalchemy.orm import Session -from typing import List from db import schemas, crud from db.database import get_db -router = APIRouter(prefix="/api") +router = APIRouter(prefix="/api", tags=["stats"]) -@router.get('/{place}/waiting_time', response_model=schemas.WaitingTime, tags=["stats"]) +@router.get('/{place}/waiting_time', response_model=schemas.WaitingTime) 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, tags=["stats"]) +@router.get('/{place}/stats/avg_graph', response_model=list) 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=schemas.Graph, tags=["stats"]) +@router.get('/{place}/stats/current_graph', response_model=schemas.Graph) 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], tags=["timetable"]) -async def get_opening_hours(place: str, db: Session = Depends(get_db)): - return crud.get_opening_hours(place, db) - - -@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, 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[schemas.Restaurant], tags=["timetable"]) -async def get_restaurants(db: Session = Depends(get_db)): - return crud.get_restaurants(db)