Skip to content
Snippets Groups Projects
Commit bd403c14 authored by Antoine Gaudron-Desjardins's avatar Antoine Gaudron-Desjardins
Browse files

interface admin

parent e4477a47
No related branches found
No related tags found
1 merge request!53Interface admin
......@@ -214,7 +214,7 @@ def delete_comment(id: int, db: Session):
# Define CRUD operation for the news
def get_news(place: str, db: Session):
def get_news(place: str, admin: bool, db: Session):
""" Get the news for the given place """
current_date = datetime.now(tz=pytz.timezone("Europe/Paris"))
news = db.query(
......@@ -222,37 +222,50 @@ def get_news(place: str, db: Session):
models.News.place == place,
models.News.end_date >= current_date).order_by(
models.News.published_at.desc()).all()
if admin:
return news
opening_hours = db.query(
models.OpeningHours.open_time,
models.OpeningHours.close_time).filter(
models.OpeningHours.close_time,
models.OpeningHours.day).filter(
models.OpeningHours.place == place,
models.OpeningHours.day == current_date.weekday()).order_by(
models.OpeningHours.day >= current_date.weekday()).order_by(
models.OpeningHours.day,
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
if not opening_hours:
opening_hours = db.query(
models.OpeningHours.open_time,
models.OpeningHours.close_time,
models.OpeningHours.day).filter(
models.OpeningHours.place == place).order_by(
models.OpeningHours.day,
models.OpeningHours.open_time).all()
next_time_slot = None
for open_time, close_time, day in opening_hours:
next_date = current_date + timedelta(days=day-current_date.weekday())
if day < current_date.weekday():
next_date = next_date + timedelta(days=7)
next_time_slot = datetime.combine(next_date.date(), 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:
if current_date < pytz.timezone("Europe/Paris").localize(datetime.combine(next_date.date(), close_time)):
next_time_slot = datetime.combine(next_date.date(), open_time)
break
if next_time_slot:
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()
models.Closure.beginning_date <= next_time_slot,
models.Closure.end_date > next_time_slot).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)
published_at=current_date)
news.append(closure_news)
return news
......@@ -533,7 +546,7 @@ def create_closure(closure: schemas.Closure, db: Session):
db.add(db_closure)
db.commit()
db.refresh(db_closure)
return schemas.Closure(**closure.dict())
return schemas.Closure(**db_closure.__dict__)
def delete_closure(id: int, db: Session):
......
......@@ -68,7 +68,7 @@ class NewsBase(BaseModel):
class News(NewsBase):
"""Database news base schema"""
id: int
id: Optional[int]
published_at: datetime = Field(..., title="Publication date of the news")
class Config:
......
......@@ -19,7 +19,7 @@ router = APIRouter(prefix="/api/auth", tags=["auth"])
@router.get("/")
async def whoami(connect_id: str = Cookie(...), db: Session = Depends(get_db)):
user = crud.get_user(connect_id, db)
return user.username
return {"name": user.username, "admin": user.admin}
@router.get("/login")
......
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Cookie, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List
......@@ -34,8 +34,12 @@ async def get_closure(place: str, db: Session = Depends(get_db)):
@router.post('/closure', response_model=schemas.Closure)
async def create_closure(closure: schemas.Closure, db: Session = Depends(get_db)):
async def create_closure(closure: schemas.ClosureBase, connect_id: str = Cookie(...), db: Session = Depends(get_db)):
user = crud.get_user(connect_id, db)
if user.admin:
return crud.create_closure(closure, db)
else:
raise HTTPException(status_code=403, detail="Administrator privilege required")
@router.delete('/closure/{id}', response_model=None)
......
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Cookie, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List
import json
......@@ -12,17 +12,25 @@ router = APIRouter(prefix="/api", tags=["news"])
@router.get('/{place}/news', response_model=List[schemas.News])
async def get_news(place: str, db: Session = Depends(get_db)):
return crud.get_news(place, db)
async def get_news(place: str, admin: bool = False, db: Session = Depends(get_db)):
return crud.get_news(place, admin, db)
@router.post('/news', response_model=schemas.News)
async def create_news(news: schemas.NewsBase, db: Session = Depends(get_db)):
async def create_news(news: schemas.NewsBase, connect_id: str = Cookie(...), db: Session = Depends(get_db)):
user = crud.get_user(connect_id, db)
if user.admin:
saved_news = crud.create_news(news, db)
await manager.broadcast(json.dumps({"type": "news", "comment": saved_news.__dict__}, default=str))
return saved_news
else:
raise HTTPException(status_code=403, detail="Administrator privilege required")
@router.delete('/news/{id}', response_model=None)
async def delete_news(id: int, db: Session = Depends(get_db)):
async def delete_news(id: int, connect_id: str = Cookie(...), db: Session = Depends(get_db)):
user = crud.get_user(connect_id, db)
if user.admin:
return crud.delete_news(id, db)
else:
raise HTTPException(status_code=403, detail="Administrator privilege required")
import React, { useEffect, useState } from "react";
import axios from "axios";
export default function ClosureForm() {
const [places, setPlaces] = useState([]);
const fetchPlaces = () => {
axios
.get(`${process.env.REACT_APP_BASE_URL_BACK}/restaurants`)
.then((res) => {
setPlaces(res.data);
})
.catch((e) => {
console.log(e);
});
};
const Submit = (ev) => {
ev.preventDefault();
let { place, beginning_date, end_date } = ev.target.elements;
axios
.post(`${process.env.REACT_APP_BASE_URL_BACK}/closure`, {
place: place.value,
beginning_date: beginning_date.value,
end_date: end_date.value,
})
.then((res) => {
console.log(res);
})
.catch((e) => {
console.log(e);
});
};
useEffect(fetchPlaces, []);
return (
<form onSubmit={Submit} id="news-form-container">
<label htmlFor="place">Restaurant concerné</label>
<select name="place">
{places.map((place) => (
<option key={place.name}>{place.name}</option>
))}
</select>
<label htmlFor="beginning_date" className="news-form-label">
Date de fermeture
</label>
<input type="datetime-local" name="beginning_date" style={{ minHeight: "2rem" }} />
<label htmlFor="end_date" className="news-form-label">
Date de réouverture
</label>
<input type="datetime-local" name="end_date" style={{ minHeight: "2rem" }} />
<input id="submit_event" type="submit" className="news-form-label" />
</form>
);
}
import React, { useContext, useEffect, useRef, useState } from "react";
import axios from "axios";
import { AiOutlineInfoCircle } from "react-icons/ai";
import { AiOutlineInfoCircle, AiOutlineDelete } from "react-icons/ai";
import { BiSend } from "react-icons/bi";
import { BsChatText } from "react-icons/bs";
......@@ -9,7 +9,7 @@ import { getSiblings } from "../utils";
import "../styles/Comments.css";
export default function Messages({ place, infos, lastMessage }) {
export default function Messages({ place, infos, lastMessage, admin }) {
const [user] = useContext(User);
const [messages, setMessages] = useState([]);
const [newComment, setNewComment] = useState("");
......@@ -35,11 +35,29 @@ export default function Messages({ place, infos, lastMessage }) {
})
.catch((e) => {
console.log(e);
alert("Une erreur est survenue");
updateValue("");
});
}
};
const Delete = (ev) => {
if (infos && admin) {
ev.preventDefault();
axios
.delete(`${process.env.REACT_APP_BASE_URL_BACK}/news/${ev.target.id}`, {
withCredentials: true,
})
.then(() => {
setLoading(true);
})
.catch((e) => {
alert("Une erreur est survenue");
console.log(e);
});
}
};
const updateValue = (value) => {
setNewComment(value);
if (input.current) {
......@@ -81,7 +99,7 @@ export default function Messages({ place, infos, lastMessage }) {
.get(
`${process.env.REACT_APP_BASE_URL_BACK}/${encodeURIComponent(place)}/${
infos ? "news" : "comments"
}`,
}${admin ? "?admin=true" : ""}`,
)
.then((res) => {
setMessages(res.data);
......@@ -95,7 +113,7 @@ export default function Messages({ place, infos, lastMessage }) {
console.log(e);
setLoading(false);
});
}, []);
}, [place, loading]);
useEffect(() => {
if (chat.current) {
......@@ -127,7 +145,8 @@ export default function Messages({ place, infos, lastMessage }) {
}, [lastMessage]);
return (
<div className="comments-side-bar">
<div className={`comments-side-bar ${admin ? "comments-full-size" : ""}`}>
{!admin && (
<div className="comments-title">
{infos ? (
<>
......@@ -141,6 +160,7 @@ export default function Messages({ place, infos, lastMessage }) {
</>
)}
</div>
)}
<div ref={chat} className={`comments-scroll-bar ${infos && "infos-scroll-bar"}`}>
{!messages.length ? (
loading ? (
......@@ -162,6 +182,13 @@ export default function Messages({ place, infos, lastMessage }) {
return (
<div key={index} className="comment">
<div className={`comment-title${infos ? "-infos" : ""}`}>
{admin && (
<AiOutlineDelete
id={message.id}
onClick={Delete}
className="comment-delete-button"
/>
)}
{infos ? message.title : message.username}
</div>
<div className="comment-content">{message.content}</div>
......
import React, { useEffect, useState } from "react";
import axios from "axios";
import "../styles/NewsForm.css";
export default function NewsForm() {
const [places, setPlaces] = useState([]);
const fetchPlaces = () => {
axios
.get(`${process.env.REACT_APP_BASE_URL_BACK}/restaurants`)
.then((res) => {
setPlaces(res.data);
})
.catch((e) => {
console.log(e);
});
};
const Submit = (ev) => {
ev.preventDefault();
let { title, content, end_date, place } = ev.target.elements;
axios
.post(
`${process.env.REACT_APP_BASE_URL_BACK}/news`,
{
title: title.value,
content: content.value,
end_date: end_date.value,
place: place.value,
},
{ withCredentials: true },
)
.then(() => {
ev.target.reset();
alert("L'actualité a bien été postée !");
})
.catch((e) => {
console.log(e);
alert("Une erreur est survenue");
});
};
useEffect(fetchPlaces, []);
return (
<form onSubmit={Submit} id="news-form-container">
<label htmlFor="title">Titre</label>
<input name="title" />
<label htmlFor="content" className="news-form-label">
Contenu
</label>
<textarea name="content" style={{ resize: "none" }} />
<label htmlFor="end_date" className="news-form-label">
Date de fin
</label>
<input type="datetime-local" name="end_date" style={{ minHeight: "2rem" }} />
<label htmlFor="place" className="news-form-label">
Restaurant concerné
</label>
<select name="place">
{places.map((place) => (
<option key={place.name}>{place.name}</option>
))}
</select>
<input id="submit_event" type="submit" className="news-form-label" />
</form>
);
}
......@@ -3,3 +3,5 @@ export { default as Footer } from "./Footer";
export { default as WaitingTime } from "./WaitingTime";
export { default as Graph } from "./Graph";
export { default as Comments } from "./Comments";
export { default as NewsForm } from "./NewsForm";
export { default as ClosureForm } from "./ClosureForm";
......@@ -5,7 +5,7 @@ import useWebSocket from "react-use-websocket";
import axios from "axios";
import { Footer, Header } from "./components";
import { HomePage, RestaurantPage, NotFoundPage } from "./views";
import { HomePage, RestaurantPage, NotFoundPage, AdminPage } from "./views";
import "bootstrap/dist/css/bootstrap.min.css";
import "./styles/index.css";
......@@ -14,11 +14,12 @@ const socketUrl = `${process.env.REACT_APP_SOCKET_URL}/ws`;
export const User = createContext(null);
export default function App() {
const storedSession = localStorage.getItem("user");
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 [user, setUser] = useState(storedSession ? JSON.parse(storedSession) : null);
const { lastMessage } = useWebSocket(socketUrl, {
shouldReconnect: () => true,
});
......@@ -78,6 +79,9 @@ export default function App() {
path="/"
element={<HomePage {...{ restaurantsList, setSelection, loading }} />}
/>
{user?.admin && (
<Route path="/admin" element={<AdminPage lastmessage={lastMessage} />} />
)}
<Route
path="/:restaurant"
element={<RestaurantPage {...{ selection, setSelection, lastMessage }} />}
......
#admin-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding: 1rem;
}
#admin-news-list {
display: flex;
flex-direction: column;
align-items: center;
width: 40rem;
max-width: 90%;
height: 100%;
border: 1px solid white;
padding: 1rem;
margin: 1rem;
margin-top: 2rem;
overflow-y: auto;
}
.admin-select-option {
font-style: 1.2rem;
margin-bottom: 1rem;
}
#admin-select-place {
width: fit-content;
}
\ No newline at end of file
......@@ -32,6 +32,10 @@
overflow: hidden;
}
.comments-full-size {
width: 100%;
}
.comments-scroll-bar {
flex: 1;
display: flex;
......@@ -119,6 +123,10 @@
font-size : 1.2rem;
}
.comment-delete-button {
margin-left: 1rem;
}
@media only screen and (max-width: 600px) {
.comments-side-bar {
width: 0px;
......
#news-form-container {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 40rem;
max-width: 90%;
height: 100%;
border: 1px solid white;
padding: 1rem;
margin: 1rem;
margin-top: 2rem;
overflow-y: auto;
}
.news-form-label {
margin-top: 1rem;
}
input::placeholder {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
\ No newline at end of file
import React, { useEffect, useState } from "react";
import axios from "axios";
import { NewsForm, ClosureForm, Comments } from "../components";
import "../styles/Admin.css";
export default function AdminPage({ lastMessage }) {
const [form, setForm] = useState("");
function NewsList() {
const [place, setPlace] = useState(null);
const [places, setPlaces] = useState([]);
const fetchPlaces = () => {
axios
.get(`${process.env.REACT_APP_BASE_URL_BACK}/restaurants`)
.then((res) => {
setPlaces(res.data);
res.data.length && setPlace(res.data[0].name);
})
.catch((e) => {
console.log(e);
});
};
useEffect(fetchPlaces, []);
return (
<div id="admin-news-list">
<select id="admin-select-place" name="place" onChange={(ev) => setPlace(ev.target.value)}>
{places.map((place) => (
<option key={place.name}>{place.name}</option>
))}
</select>
<Comments place={place} lastMessage={lastMessage} infos admin />
</div>
);
}
let forms = {
news: <NewsForm />,
newsList: <NewsList />,
closure: <ClosureForm />,
};
return (
<div id="admin-container">
<select defaultValue="" onChange={(ev) => setForm(ev.target.value)}>
<option className="admin-select-option" value="">
-- Sélectionner une action --
</option>
<option className="admin-select-option" value="news">
Ajouter une actualité
</option>
<option className="admin-select-option" value="newsList">
Supprimer une actualité
</option>
<option className="admin-select-option" value="closure">
Renseigner une fermeture exceptionnelle
</option>
</select>
{form && forms[form]}
</div>
);
}
export { default as HomePage } from "./HomePage";
export { default as RestaurantPage } from "./Restaurant";
export { default as NotFoundPage } from "./NotFoundPage";
export { default as AdminPage } from "./AdminPage";
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment