diff --git a/backend/db/crud.py b/backend/db/crud.py index 0910e780699224a206f43b05adbec442f3ba9e48..62e470cb5091db82eefed71195ab7a5209a8de2c 100644 --- a/backend/db/crud.py +++ b/backend/db/crud.py @@ -2,6 +2,7 @@ Module to interact with the database """ from datetime import date, datetime, time, timedelta +from fastapi import HTTPException from sqlalchemy.orm import Session from sqlalchemy.sql import func from sqlalchemy import Time, Date, cast @@ -44,7 +45,8 @@ 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)) + slots_list.append(schemas.RecordRead( + name=name, time=average_waiting_time)) def get_avg_graph_points(place: str, weekday: int, min_time: time, @@ -149,18 +151,8 @@ 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 = 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_list.reverse() @@ -290,9 +282,18 @@ def init_user(db: Session): def get_user(cookie: str, db: Session): """ Get user infos """ - user = db.query(models.Users).filter(models.Users.cookie == cookie).one() + try: + user = db.query(models.Users).filter( + models.Users.cookie == cookie).one() + except BaseException: + raise HTTPException(status_code=401, detail="Invalid cookie") + if pytz.timezone("Europe/Paris").localize(user.expiration_date) < datetime.now(tz=pytz.timezone("Europe/Paris")): - return + user.cookie = None + db.add(user) + db.commit() + raise HTTPException(status_code=401, detail="Expired cookie") + return user @@ -329,6 +330,111 @@ def update_user(user: schemas.User, user_info: dict, db: Session): def end_session(cookie: str, db: Session): user = db.query(models.Users).filter(models.Users.cookie == cookie).one() user.expiration_date = datetime.now(tz=pytz.timezone("Europe/Paris")) + user.cookie = None db.add(user) db.commit() return + + +def delete_user(cookie: str, db: Session): + db.query(models.Users).filter(models.Users.cookie == cookie).delete() + db.commit() + return + + +# Define CRUD operations for data collection + +def get_records(place: str, db: Session): + records = db.query(models.Records).filter( + models.Records.place == place).order_by(models.Records.date.desc()).all() + return records + + +def create_record(record: schemas.RecordBase, db: Session): + db_record = models.Records(**record.dict()) + db.add(db_record) + db.commit() + db.refresh(db_record) + return db_record + + +def delete_record(id: int, db: Session): + if id == 0: + db.query(models.Records).delete() + else: + db.query(models.Records).filter(models.Records.id == id).delete() + db.commit() + return + + +def get_collaborative_records(place: str, db: Session): + records = db.query(models.CollaborativeRecords).filter( + models.CollaborativeRecords.place == place).order_by(models.CollaborativeRecords.date.desc()).all() + return [schemas.CollaborativeRecords(**record.__dict__) for record in records] + + +def create_collaborative_record(user: schemas.User, place: str, db: Session): + current_date = datetime.now(tz=pytz.timezone("Europe/Paris")) + date, weekday, current_time = current_date.date( + ), current_date.weekday(), current_date.time() + + try: + time_slot = db.query( + models.OpeningHours).filter( + models.OpeningHours.place == place, + models.OpeningHours.day == weekday, + models.OpeningHours.open_time <= current_time, + models.OpeningHours.close_time >= current_time).one() + except BaseException: + raise HTTPException(status_code=404, detail="No restaurant opened") + + last_record = db.query(models.CollaborativeRecords).filter( + models.CollaborativeRecords.user_id == user.id).order_by(models.CollaborativeRecords.date.desc()).first() + if not last_record or last_record.date <= datetime.combine(date, time_slot.open_time): + db_record = models.CollaborativeRecords( + user_id=user.id, place=place, date=current_date) + db.add(db_record) + db.commit() + db.refresh(db_record) + return db_record + + raise HTTPException(status_code=406, detail="Client already registered") + + +def update_collaborative_record(user: schemas.User, db: Session): + current_date = datetime.now(tz=pytz.timezone("Europe/Paris")) + date, weekday, current_time = current_date.date( + ), current_date.weekday(), current_date.time() + last_record = db.query(models.CollaborativeRecords).filter( + models.CollaborativeRecords.user_id == user.id).order_by(models.CollaborativeRecords.date.desc()).first() + + try: + time_slot = db.query( + models.OpeningHours).filter( + models.OpeningHours.place == last_record.place, + models.OpeningHours.day == weekday, + models.OpeningHours.open_time <= current_time, + models.OpeningHours.close_time >= current_time).one() + except BaseException: + raise HTTPException(status_code=404, detail="No restaurant opened") + + if last_record.date >= datetime.combine(date, time_slot.open_time) and not last_record.waiting_time: + last_record.waiting_time = current_date - \ + pytz.timezone("Europe/Paris").localize(last_record.date) + print(last_record.waiting_time) + db.add(last_record) + db.commit() + db.refresh(last_record) + return schemas.CollaborativeRecords(**last_record.__dict__) + + raise HTTPException(status_code=406, detail="Client already registered") + + +def delete_collaborative_record(id: int, db: Session): + if id == 0: + db.query(models.CollaborativeRecords).delete() + else: + db.query(models.CollaborativeRecords).filter( + models.CollaborativeRecords.id == id).delete() + db.commit() + return diff --git a/backend/db/models.py b/backend/db/models.py index a03399ebe7678f792a6aad4185ec53a33eef8e17..c65788de3b1f1f4f096a9857c42941eb3ef892c9 100644 --- a/backend/db/models.py +++ b/backend/db/models.py @@ -1,12 +1,23 @@ """ Models of the database for magasin app """ -from sqlalchemy import Column, ForeignKey, Integer, DateTime, Float, Interval, String, Text, Boolean, Time +from sqlalchemy import Column, ForeignKey, Integer, DateTime, Float, Interval, String, Text, Time from sqlalchemy.orm import relationship from db.database import Base +class CollaborativeRecords(Base): + """CollaborativeRecords sql table model""" + __tablename__ = "collection" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id")) + place = Column(String(30)) + date = Column(DateTime) + waiting_time = Column(Interval) + + class Records(Base): """Records sql table model""" __tablename__ = "records" @@ -62,3 +73,4 @@ class Users(Base): cookie = Column(String(50)) expiration_date = Column(DateTime) comments = relationship("Comments") + comments = relationship("CollaborativeRecords") diff --git a/backend/db/schemas.py b/backend/db/schemas.py index 991d910e96f3721e693910558d6a33b4c1511aca..ac8fd093813e32a4d9fa71a77c7ce6b4d470258b 100644 --- a/backend/db/schemas.py +++ b/backend/db/schemas.py @@ -31,7 +31,16 @@ class RecordRead(BaseModel): time: int +class CollaborativeRecords(BaseModel): + """CollaborativeRecords schema""" + user_id: int = Field(..., title="Id of the user timed") + place: str = Field(..., title="Name of the RU corresponding the given record") + date: datetime = Field(..., title="Date of the record") + waiting_time: Optional[timedelta] = Field(default=None, title="Caculated waiting time for timed person") + + # Comments Data structure + class CommentBase(BaseModel): """Comments base schema""" content: str = Field(..., title="Content of the comment posted") diff --git a/backend/main.py b/backend/main.py index ce0938157c9ad0872df0693744cab36fb523f231..8b01abb83fdab29cda11a508b68a40640ae3cd56 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,15 +1,10 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from dotenv import load_dotenv -import os -from fastapi import Depends -from sqlalchemy.orm import Session -from typing import List from threading import Thread -import json +import os -from db import database, models, schemas -from db.database import get_db +from db import database, models from routers import * from video_capture import handle_cameras @@ -45,29 +40,4 @@ app.include_router(comments.router) app.include_router(news.router) app.include_router(authentication.router) app.include_router(websocket.router) - - -@app.get('/api/records', response_model=List[schemas.Record]) -async def get_records(place: str, db: Session = Depends(get_db)): - return db.query(models.Records).filter(models.Records.place == - place).order_by(models.Records.date.desc()).all() - - -@app.post('/api/records', response_model=schemas.Record) -async def post_records(record: schemas.RecordBase, db: Session = Depends(get_db)): - db_record = models.Records(**record.dict()) - db.add(db_record) - db.commit() - db.refresh(db_record) - await websocket.manager.broadcast(json.dumps({"type": "data"})) - return db_record - - -@app.delete('/api/records', response_model=None) -async def del_records(id: int, db: Session = Depends(get_db)): - if id == 0: - db.query(models.Records).delete() - else: - db.query(models.Records).filter(models.Records.id == id).delete() - db.commit() - return +app.include_router(records.router) diff --git a/backend/requirements.txt b/backend/requirements.txt index eb0a04d7c485d8c6f59433fe74a5a318f8fa5686..f55e8e57b67551d26ddd898a81cd432dddc09da7 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,18 +1,19 @@ -anyio==3.6.1 -asgiref==3.5.2 -click==8.1.3 -fastapi==0.78.0 -h11==0.13.0 -idna==3.3 -keras==2.9.0 -numpy==1.23.0 -opencv-python==4.6.0.66 -pydantic==1.9.1 -sniffio==1.2.0 -starlette==0.19.1 -typing-extensions==4.2.0 -uvicorn==0.17.6 -SQLAlchemy==1.4.19 -python-dotenv==0.18.0 -PyMySQL==1.0.2 -pytz==2022.1 \ No newline at end of file +anyio==3.6.1 +asgiref==3.5.2 +click==8.1.3 +fastapi==0.78.0 +h11==0.13.0 +idna==2.8 +keras==2.9.0 +numpy==1.23.0 +opencv-python==4.6.0.66 +pydantic==1.9.1 +sniffio==1.2.0 +starlette==0.19.1 +typing-extensions==4.2.0 +uvicorn==0.17.6 +SQLAlchemy==1.4.19 +python-dotenv==0.18.0 +PyMySQL==1.0.2 +pytz==2022.1 +requests==2.25.1 \ No newline at end of file diff --git a/backend/routers/__init__.py b/backend/routers/__init__.py index 745afdca4556b73e7a36084eca5963eadfbfa487..d88b0068a21617007209e3ba487efd5d103f01aa 100644 --- a/backend/routers/__init__.py +++ b/backend/routers/__init__.py @@ -3,3 +3,4 @@ from . import comments from . import news from . import stats from . import websocket +from . import records diff --git a/backend/routers/records.py b/backend/routers/records.py new file mode 100644 index 0000000000000000000000000000000000000000..f6f603ac15e7bf7a8b87f5f4ee881f27edafc86f --- /dev/null +++ b/backend/routers/records.py @@ -0,0 +1,59 @@ +from fastapi import APIRouter, HTTPException, Response, Cookie, 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.get('/records', response_model=List[schemas.Record], tags=["records"]) +async def get_records(place: str, db: Session = Depends(get_db)): + return crud.get_records(place, db) + + +@router.post('/records', response_model=schemas.Record, tags=["records"]) +async def stats(record: schemas.RecordBase, db: Session = Depends(get_db)): + return crud.create_record(record, db) + + +@router.delete('/records', response_model=None, tags=["records"]) +async def stats(id: int, db: Session = Depends(get_db)): + return crud.delete_record(id, db) + + +@router.get('/collection', response_model=List[schemas.CollaborativeRecords], tags=["data collection"]) +async def get_collection_records(place: str, db: Session = Depends(get_db)): + return crud.get_collaborative_records(place, db) + + +@router.post("/collection/start/{place}", response_model=schemas.CollaborativeRecords, tags=["data collection"]) +async def create_new_record(response: Response, place: str, connect_id: str = Cookie(default=None), db: Session = Depends(get_db)): + if connect_id: + try: + user = crud.get_user(connect_id, db) + except HTTPException: + response.delete_cookie("connect_id") + user = crud.init_user(db) + else: + user = crud.init_user(db) + db_record = crud.create_collaborative_record(user, place, db) + response.set_cookie(key="connect_id", value=user.cookie) + return schemas.CollaborativeRecords(**db_record.__dict__) + + +@router.post("/collection/stop", response_model=schemas.CollaborativeRecords, tags=["data collection"]) +async def end_new_record(response: Response, connect_id: str = Cookie(...), db: Session = Depends(get_db)): + user = crud.get_user(connect_id, db) + db_record = crud.update_collaborative_record(user, db) + if not user.username: + crud.delete_user(user.cookie, db) + response.delete_cookie(key="connect_id") + return db_record + + +@router.delete('/collection', response_model=None, tags=["data collection"]) +async def delete_record(id: int, db: Session = Depends(get_db)): + return crud.delete_collaborative_record(id, db) diff --git a/frontend/src/components/Graph.js b/frontend/src/components/Graph.js index 5aca5c7becd299d5044001ef27531efc5ac9fb59..f019a6b42e9d28473d650a20742f2ae7d59ae69d 100644 --- a/frontend/src/components/Graph.js +++ b/frontend/src/components/Graph.js @@ -81,18 +81,6 @@ export default function Graph({ place, lastMessage }) { bottom: 5, }} > - {checked ? ( - <Line - data={avgData} - type="monotone" - dataKey="time" - stroke="#FF0000" - strokeWidth={2} - dot={false} - /> - ) : ( - <div /> - )} <defs> <linearGradient id="colorGradient" x1="0" y1="0" x2="0" y2="1"> <stop offset="10%" stopColor="#ff0000" stopOpacity={0.55} /> @@ -136,6 +124,18 @@ export default function Graph({ place, lastMessage }) { fillOpacity={1} fill="url(#colorGradient)" /> + {checked ? ( + <Line + data={avgData} + type="monotone" + dataKey="time" + stroke="#FF0000" + strokeWidth={2} + dot={false} + /> + ) : ( + <div /> + )} </ComposedChart> </ResponsiveContainer> </div> diff --git a/frontend/src/components/Timetable.js b/frontend/src/components/Timetable.js deleted file mode 100644 index d39800c2bc0e7352b302015978ffdb1237149fb3..0000000000000000000000000000000000000000 --- a/frontend/src/components/Timetable.js +++ /dev/null @@ -1,41 +0,0 @@ -import React from "react"; -import Table from "react-bootstrap/Table"; - -import "../styles/Timetable.css"; - -export default function Timetable(schedule) { - const timetable = schedule.schedule; - return ( - <div className="timetable"> - <Table className="table table-striped table-bordered"> - <tbody> - <tr> - <th>Lundi</th> - <th>{timetable["LundiMidi"] != null ? timetable["LundiMidi"] : "-"}</th> - <th>{timetable["LundiSoir"] != null ? timetable["LundiSoir"] : "-"}</th> - </tr> - <tr> - <th>Mardi</th> - <th>{timetable["MardiMidi"] != null ? timetable["MardiMidi"] : "-"}</th> - <th>{timetable["MardiSoir"] != null ? timetable["MardiSoir"] : "-"}</th> - </tr> - <tr> - <th>Mercredi</th> - <th>{timetable["MercrediMidi"] != null ? timetable["MercrediMidi"] : "-"}</th> - <th>{timetable["MercrediSoir"] != null ? timetable["MercrediSoir"] : "-"}</th> - </tr> - <tr> - <th>Jeudi</th> - <th>{timetable["JeudiMidi"] != null ? timetable["JeudiMidi"] : "-"}</th> - <th>{timetable["JeudiSoir"] != null ? timetable["JeudiSoir"] : "-"}</th> - </tr> - <tr> - <th>Vendredi</th> - <th>{timetable["VendrediMidi"] != null ? timetable["VendrediMidi"] : "-"}</th> - <th>{timetable["VendrediSoir"] != null ? timetable["VendrediSoir"] : "-"}</th> - </tr> - </tbody> - </Table> - </div> - ); -} diff --git a/frontend/src/components/index.js b/frontend/src/components/index.js index 2ed4f885eb4c32b29c408a9496126867c343bf1d..de9175be93666cca57812a64f53e7e8eaf74791a 100644 --- a/frontend/src/components/index.js +++ b/frontend/src/components/index.js @@ -1,6 +1,5 @@ export { default as Header } from "./Header"; export { default as Footer } from "./Footer"; -export { default as Timetable } from "./Timetable"; export { default as WaitingTime } from "./WaitingTime"; export { default as Graph } from "./Graph"; export { default as Comments } from "./Comments"; diff --git a/frontend/src/styles/Timetable.css b/frontend/src/styles/Timetable.css deleted file mode 100644 index 595feb386391a884990cabef5d60f02e68a62a8e..0000000000000000000000000000000000000000 --- a/frontend/src/styles/Timetable.css +++ /dev/null @@ -1,5 +0,0 @@ -.timetable{ - display: inline-block; - border-radius: 5px; - text-align: center; -} \ No newline at end of file diff --git a/frontend/src/styles/restaurant.css b/frontend/src/styles/restaurant.css index 7b3f642eb2720c313d1c9beff252be044236c506..639835729cd42edf8daed25f00d902c2948d236b 100644 --- a/frontend/src/styles/restaurant.css +++ b/frontend/src/styles/restaurant.css @@ -8,9 +8,50 @@ width: 50%; flex-direction: column; align-content: center; + align-items: center; overflow: hidden; } +#restaurant-start-button { + width: fit-content; + border: none; + border-radius: 5px; + padding: 0.1rem; + padding-left: 0.3rem; + padding-right: 0.3rem; + margin-top: 2rem; +} + +.restaurant-button-disabled { + background-color: rgb(66, 75, 83); + color: #b9b9b9; +} + +.restaurant-button-active { + background-color: rgb(33, 37, 41); + color: white; +} + +.restaurant-button-active:hover { + box-shadow: 0px 0px 5px white; +} + +#restaurant-end-button { + width: fit-content; + background-color: #83000d; + color: white; + border: none; + border-radius: 5px; + padding: 0.1rem; + padding-left: 0.3rem; + padding-right: 0.3rem; + margin-top: 2rem; +} + +#restaurant-end-button:hover { + box-shadow: 0px 0px 5px rgb(33, 37, 41); +} + @media only screen and (max-width: 600px) { #restaurant-main-page { width: 100%; diff --git a/frontend/src/views/Restaurant.js b/frontend/src/views/Restaurant.js index a309473c4e7a71292f16dbf7341955ff3e078bd0..f4113205874ed501e30af255efe95cdf3a7a884a 100644 --- a/frontend/src/views/Restaurant.js +++ b/frontend/src/views/Restaurant.js @@ -1,10 +1,50 @@ -import React from "react"; +import React /*, { useState } */ from "react"; +// import axios from "axios"; import { Graph, WaitingTime, Comments } from "../components"; import "../styles/restaurant.css"; +// const instance = axios.create({ +// withCredentials: true, +// baseURL: process.env.REACT_APP_BASE_URL_BACK, +// }); + export default function RestaurantPage({ selection, lastMessage }) { + // const [started, setStarted] = useState(false); + // const [disabled, setDisabled] = useState(false); + + // const Start = () => { + // instance + // .post(`collection/start/${encodeURIComponent(selection.name)}`) + // .then(() => { + // setStarted(true); + // }) + // .catch((e) => { + // setDisabled(true); + // alert( + // "Il semblerait que tu aies déjà renseigné un temps d'attente sur ce créneau. Merci de ta participation, n'hésite pas à te chronométrer de nouveau la prochaine fois !", + // ); + // console.log(e.response.data); + // }); + // }; + + // const End = () => { + // instance + // .post("collection/stop") + // .then(() => { + // setStarted(false); + // setDisabled(true); + // }) + // .catch((e) => { + // setDisabled(true); + // alert( + // "Il semblerait que tu aies déjà renseigné un temps d'attente sur ce créneau. Merci de ta participation, n'hésite pas à te chronométrer de nouveau la prochaine fois !", + // ); + // console.log(e); + // }); + // }; + return ( <> {selection && ( @@ -13,6 +53,14 @@ export default function RestaurantPage({ selection, lastMessage }) { <div className="restaurant-container" id="restaurant-main-page"> <WaitingTime place={selection.name} lastMessage={lastMessage} /> <Graph place={selection.name} type="current" lastMessage={lastMessage} /> + {/* <button + id={`restaurant-${started ? "end" : "start"}-button`} + onClick={started ? End : Start} + className={disabled ? "restaurant-button-disabled" : "restaurant-button-active"} + disabled={disabled} + > + {started ? "Fini !!" : "Départ !!"} + </button> */} </div> <Comments place={selection.name} lastMessage={lastMessage} /> {/*<Graph place={selection.name} type="avg" />*/}