Skip to content

Commit

Permalink
Ajout de paramètres de limitation (#67)
Browse files Browse the repository at this point in the history
* Limitation du nombre d'animations par utilisateur - cf #53

* Limitation du nombre de participants à une animation - cf #51

* Controle du nombre de participants lors d'update
  • Loading branch information
amandine-sahl authored Jun 21, 2024
1 parent 5f317ba commit cbf5c7a
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 20 deletions.
51 changes: 50 additions & 1 deletion backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@
from marshmallow.exceptions import ValidationError as MarshmallowValidationError

from core.env import db
from core.routes import app_routes, QueryParamValidationError, EventIsFull
from core.routes import app_routes, QueryParamValidationError
from core.exceptions import (
EventIsFull,
UserEventNbExceded,
UserEventNbExcededUser,
UserEventNbExcededAdmin,
NotBookable,
ParticipantNbExceded,
)

mail = None

Expand Down Expand Up @@ -72,6 +80,47 @@ def handle_event_is_full_error(e):
422,
)

@app.errorhandler(NotBookable)
def handle_event_is_full_error(e):
return (
jsonify({"error": "L'animation n'est pas ouverte à la réservation"}),
422,
)

@app.errorhandler(UserEventNbExceded)
@app.errorhandler(UserEventNbExcededAdmin)
def handle_user_event_nb_exceded_error(e):
return (
jsonify(
{
"error": "La limite du nombre de réservation pour cet utilisateur est atteinte"
}
),
422,
)

@app.errorhandler(UserEventNbExcededUser)
def handle_user_event_nb_exceded_error(e):
return (
jsonify(
{
"error": "Vous avez atteint la limite du nombre de réservations possible par personne"
}
),
422,
)

@app.errorhandler(ParticipantNbExceded)
def handle_participant_nb_exceded_error(e):
return (
jsonify(
{
"error": "Vous ne pouvez pas inscrire autant de personnes sur une animation"
}
),
422,
)

@app.template_filter()
def format_date(value):
format_string = "EEEE d MMMM"
Expand Down
4 changes: 4 additions & 0 deletions backend/config/config.py.sample
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ ORGANISM_FOR_EMAIL_SUBJECT = ""

# Le nombre maximale de personnes que l'on peut mettre en liste d'attente sur un événement.
LISTE_ATTENTE_CAPACITY = 10
# Le nombre maximal d'animations auxquelles une personne peut s'inscrire
NB_ANIM_MAX_PER_USER = 3
# Le nombre maximal de participants que l'on peut indiquer lors de la création d'une réservation
NB_PARTICIPANTS_MAX_PER_ANIM_PER_USER = 10

# L'adresse où écoute le frontend du portail de réservation, par exemple : 'www.png-resa.fr' ou 'localhost:5000'.
PUBLIC_SERVER_NAME =
Expand Down
4 changes: 4 additions & 0 deletions backend/config/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@

# Le nombre maximale de personnes que l'on peut mettre en liste d'attente sur un événement.
LISTE_ATTENTE_CAPACITY = 10
# Le nombre maximal d'animations auxquelles une personne peut s'inscrire
NB_ANIM_MAX_PER_USER = 3
# Le nombre maximale de participant pouvant s'inscrire à une animation
NB_PARTICIPANTS_MAX_PER_ANIM_PER_USER = 10
# L'adresse où écoute le frontend du portail de réservation, par exemple : 'www.png-resa.fr' ou 'localhost:5000'.
PUBLIC_SERVER_NAME = "http://localhost:5173"

Expand Down
22 changes: 22 additions & 0 deletions backend/core/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class EventIsFull(Exception):
pass


class NotBookable(Exception):
pass


class UserEventNbExceded(Exception):
pass


class UserEventNbExcededAdmin(Exception):
pass


class UserEventNbExcededUser(Exception):
pass


class ParticipantNbExceded(Exception):
pass
37 changes: 33 additions & 4 deletions backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@
import json

from flask import current_app
from sqlalchemy import func, or_
from sqlalchemy import func, or_, select, extract
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import aliased

from .env import db

from core.exceptions import (
EventIsFull,
UserEventNbExceded,
NotBookable,
ParticipantNbExceded,
)


class GTEventsQuery:
def filter_properties(self, query, filters):
Expand Down Expand Up @@ -158,9 +165,31 @@ def massif(self):
def massif(cls):
return func.animations.get_secteur_name(cls.id)

def is_reservation_possible_for(self, nb_people):
def is_reservation_possible_for(self, nb_people, email):

if not self.bookable:
return False
raise NotBookable

# Test nombre de reservation par utilisateur
# Selection animations par utilisateur
from datetime import datetime

query = select(func.count(TReservations.id_reservation)).where(
TReservations.cancelled == False,
TReservations.email == email,
TReservations.confirmed == True,
extract("year", TReservations.meta_create_date) == datetime.today().year,
)
nb_reservation = db.session.scalar(query)

# On retranche un de façon a s'assurer que le nb d'animation
# ne sera pas suppérieur une fois l'animation ajoutée
if nb_reservation > current_app.config["NB_ANIM_MAX_PER_USER"] - 1:
raise UserEventNbExceded

if nb_people > current_app.config["NB_PARTICIPANTS_MAX_PER_ANIM_PER_USER"]:
raise ParticipantNbExceded

if not self.capacity:
return True
if self.sum_participants + nb_people <= self.capacity:
Expand All @@ -170,7 +199,7 @@ def is_reservation_possible_for(self, nb_people):
<= current_app.config["LISTE_ATTENTE_CAPACITY"]
):
return True
return False
raise EventIsFull


class GTCancellationReason(db.Model):
Expand Down
53 changes: 40 additions & 13 deletions backend/core/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from email_validator import validate_email, EmailNotValidError, EmailSyntaxError
from flask import jsonify, request, Blueprint, render_template, session, current_app

from sqlalchemy import select
from sqlalchemy import select, func, extract

from core.models import (
db,
Expand Down Expand Up @@ -33,7 +33,11 @@
get_mail_subject,
stringify,
)

from core.exceptions import (
UserEventNbExceded,
UserEventNbExcededAdmin,
UserEventNbExcededUser,
)

app_routes = Blueprint("app_routes", __name__)

Expand Down Expand Up @@ -257,10 +261,6 @@ def get_reservations():
)


class EventIsFull(Exception):
pass


class BodyParamValidationError(Exception):
pass

Expand All @@ -275,8 +275,12 @@ def _post_reservations_by_user(post_data):
f"Event with ID {reservation.id_event} not found"
)

if not event.is_reservation_possible_for(reservation.nb_participants):
raise EventIsFull
try:
event.is_reservation_possible_for(
reservation.nb_participants, reservation.email
)
except UserEventNbExceded:
raise UserEventNbExcededUser

reservation.token = generate_token()

Expand Down Expand Up @@ -308,8 +312,12 @@ def _post_reservations_by_admin(post_data):
f"Event with ID {reservation.id_event} not found"
)

if not event.is_reservation_possible_for(reservation.nb_participants):
raise EventIsFull
try:
event.is_reservation_possible_for(
reservation.nb_participants, reservation.email
)
except UserEventNbExceded:
raise UserEventNbExcededAdmin

if reservation.confirmed and reservation.liste_attente is None:
if not event.capacity:
Expand Down Expand Up @@ -390,9 +398,13 @@ def confirm_reservation():
return jsonify({"error": "Reservation already confirmed"}), 400

event = resa.event
if not event.is_reservation_possible_for(resa.nb_participants):
raise EventIsFull
elif not event.capacity:

try:
event.is_reservation_possible_for(resa.nb_participants, resa.email)
except UserEventNbExceded:
raise UserEventNbExcededUser

if not event.capacity:
resa.liste_attente = False
elif event.sum_participants + resa.nb_participants <= event.capacity:
resa.liste_attente = False
Expand All @@ -417,11 +429,26 @@ def update_reservation(reservation_id):
if not reservation:
return jsonify({"error": f"Reservation #{reservation_id} not found"}), 404

event = db.session.get(GTEvents, reservation.id_event)
old_nb_participants = reservation.nb_participants

if not event:
raise BodyParamValidationError(
f"Event with ID {reservation.id_event} not found"
)

post_data = request.get_json()
post_data["digitizer"] = session["user"]
validated_data = TReservationsUpdateSchema().load(post_data)

for k, v in validated_data.items():
setattr(reservation, k, v)
# On retranche l'ancien nombre de participants
if not event.is_reservation_possible_for(
reservation.nb_participants - old_nb_participants
):
raise EventIsFull

db.session.add(reservation)
db.session.commit()

Expand Down
48 changes: 47 additions & 1 deletion backend/test/test_api_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from sqlalchemy import select
from core.models import GTEvents, TReservations
from core.models import TTokens
from core.routes import EventIsFull
from core.exceptions import EventIsFull
from core.env import db

from .utils import login
Expand Down Expand Up @@ -41,6 +41,21 @@
"num_departement": "48",
"confirmed": True,
}
TEST_RESERVATION_1_PERSONNE = {
"nom": "BLAIR",
"prenom": "Eric",
"commentaire": "saisie test",
"tel": "00 00 00 00 00 ",
"email": "trop.resa@test.fr",
"nb_adultes": 1,
"nb_moins_6_ans": 0,
"nb_6_8_ans": 0,
"nb_9_12_ans": 0,
"nb_plus_12_ans": 0,
"num_departement": "48",
"confirmed": True,
}

TEST_BILAN = {
"commentaire": "test bilan",
"nb_6_8_ans": 1,
Expand Down Expand Up @@ -242,6 +257,37 @@ def test_post_export_and_cancel_one_reservation(self, events):
assert resa.cancelled == True
assert resa.cancel_by == "admin"

def test_post_limit_nb_animations(self, events):
login(self.client)
# Create reservation
event = db.session.scalars(
select(GTEvents)
.where(GTEvents.name == "Pytest bookable")
.order_by(GTEvents.id.desc())
).first()

data_resa = TEST_RESERVATION_1_PERSONNE
data_resa["id_event"] = event.id

nb_limit_per_user = current_app.config["NB_ANIM_MAX_PER_USER"]

# Création du nombre de reservation spécifié dans NB_ANIM_MAX_PER_USER
for loop in range(nb_limit_per_user):
resp = post_json(
self.client, url_for("app_routes.post_reservations"), data_resa
)
assert resp == 200

# Ajout de 1 reservation ce qui doit retourner une erreur
resp = post_json(
self.client, url_for("app_routes.post_reservations"), data_resa
)
assert resp.status_code == 422
assert (
json_of_response(resp)["error"]
== "La limite du nombre de réservation pour cet utilisateur est atteinte"
)

# def test_post_one_bilan(self):
# # POST
# event = GTEvents.query.limit(1).one()
Expand Down
Loading

0 comments on commit cbf5c7a

Please sign in to comment.