Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion api_schemas/event_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from api_schemas.base_schema import BaseSchema
from db_models.priority_model import Priority_DB
from helpers.constants import MAX_EVENT_DESC, MAX_EVENT_TITLE
from helpers.types import ALCOHOL_EVENT_TYPES, EVENT_DOT_TYPES, datetime_utc
from helpers.types import ALCOHOL_EVENT_TYPES, EVENT_DOT_TYPES, GROUP_TYPE, datetime_utc
from pydantic import StringConstraints

if TYPE_CHECKING:
Expand Down Expand Up @@ -31,6 +31,8 @@ class EventRead(BaseSchema):
can_signup: bool
drink_package: bool
is_nollning_event: bool
mentor_group_types: list[GROUP_TYPE]
allow_other_mentors: bool
alcohol_event_type: str
dress_code: str
price: int
Expand Down Expand Up @@ -64,6 +66,8 @@ class EventCreate(BaseSchema):
can_signup: bool
drink_package: bool
is_nollning_event: bool
mentor_group_types: list[GROUP_TYPE]
allow_other_mentors: bool
alcohol_event_type: ALCOHOL_EVENT_TYPES
dress_code: str
price: int
Expand All @@ -90,11 +94,14 @@ class EventUpdate(BaseSchema):
can_signup: bool | None = None
drink_package: bool | None = None
is_nollning_event: bool | None = None
mentor_group_types: list[GROUP_TYPE] | None = None
allow_other_mentors: bool | None = None
priorities: list[str] | None = None
alcohol_event_type: ALCOHOL_EVENT_TYPES | None = None
dress_code: str | None = None
price: int | None = None
dot: EVENT_DOT_TYPES | None = None
lottery: bool | None = None


class AddEventTag(BaseSchema):
Expand Down
6 changes: 6 additions & 0 deletions api_schemas/user_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ class UserInEventRead(SimpleUserRead):
email: str


class UserForEventSignupRead(SimpleUserRead):
groups: list["GroupRead"]
standard_food_preferences: list[str] | None
other_food_preferences: str | None


class AdminUserReadForCarBookings(SimpleUserRead):
email: str
telephone_number: PhoneNumber
Expand Down
10 changes: 7 additions & 3 deletions db_models/event_model.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from helpers.types import ALCOHOL_EVENT_TYPES, EVENT_DOT_TYPES, datetime_utc
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey, String
from helpers.types import ALCOHOL_EVENT_TYPES, EVENT_DOT_TYPES, GROUP_TYPE, datetime_utc
from typing import TYPE_CHECKING, get_args
from sqlalchemy import JSON, ForeignKey, String
from sqlalchemy.orm import mapped_column, Mapped, relationship
from helpers.constants import MAX_EVENT_DESC, MAX_EVENT_DRESS_CODE, MAX_EVENT_LOCATION, MAX_EVENT_TITLE
from .base_model import BaseModel_DB
Expand Down Expand Up @@ -69,6 +69,10 @@ class Event_DB(BaseModel_DB):
)

is_nollning_event: Mapped[bool] = mapped_column(default=False)
mentor_group_types: Mapped[list[GROUP_TYPE]] = mapped_column(
JSON, default_factory=lambda: list(get_args(GROUP_TYPE))
)
allow_other_mentors: Mapped[bool] = mapped_column(default=False)

dot: Mapped[EVENT_DOT_TYPES] = mapped_column(default="None")

Expand Down
5 changes: 3 additions & 2 deletions routes/event_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
from helpers.types import ALLOWED_EXT, ALLOWED_IMG_SIZES, ALLOWED_IMG_TYPES, ASSETS_BASE_PATH
from pathlib import Path


event_router = APIRouter()


Expand Down Expand Up @@ -50,7 +49,9 @@ def get_event_priorities(db: DB_dependency):
return list(priorities)


@event_router.patch("/confirmed/{event_id}", response_model=EventRead)
@event_router.patch(
"/confirmed/{event_id}", response_model=EventRead, dependencies=[Permission.require("manage", "Event")]
)
def confirm_places(
db: DB_dependency,
event_id: int,
Expand Down
23 changes: 20 additions & 3 deletions routes/event_signup_router.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
from typing import Annotated
from typing import Annotated, get_args
from fastapi import APIRouter, HTTPException, status
from api_schemas.user_schemas import UserForEventSignupRead
from database import DB_dependency
from db_models.event_model import Event_DB
from db_models.user_model import User_DB
from services.event_signup_service import signup_to_event, signoff_from_event, update_event_signup, check_me_signup
from db_models.group_model import Group_DB
from helpers.types import GROUP_TYPE
from services.event_signup_service import (
get_allowed_groups,
signup_to_event,
signoff_from_event,
update_event_signup,
check_me_signup,
)
from user.permission import Permission
from api_schemas.event_signup_schemas import EventSignupCreate, EventSignupRead, EventSignupUpdate


event_signup_router = APIRouter()


Expand Down Expand Up @@ -88,6 +96,15 @@ def get_me_event_signup(event_id: int, me: Annotated[User_DB, Permission.member(
return check_me_signup(event_id, me, db)


@event_signup_router.get("/me/{event_id}", response_model=UserForEventSignupRead)
def get_me_for_event_signup(event_id: int, user: Annotated[User_DB, Permission.member()], db: DB_dependency): # type: ignore
event = db.query(Event_DB).filter_by(id=event_id).one_or_none()
if event is None:
raise HTTPException(404, detail="Event not found")

return {**user.__dict__, "groups": get_allowed_groups(event, user)} # type: ignore


# @event_signup_router.get("/{event_id}", response_model=list[EventSignupRead])
# def get_all_signups(event_id: int, db: DB_dependency):
# signups = db.query(EventUser_DB).filter(EventUser_DB.event_id == event_id).all()
Expand Down
6 changes: 6 additions & 0 deletions services/event_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ def create_new_event(data: EventCreate, db: Session):
drink_package=data.drink_package,
location=data.location,
is_nollning_event=data.is_nollning_event,
mentor_group_types=data.mentor_group_types,
allow_other_mentors=data.allow_other_mentors,
dress_code=data.dress_code,
price=data.price,
alcohol_event_type=data.alcohol_event_type,
Expand Down Expand Up @@ -89,6 +91,10 @@ def update_event(event_id: int, data: EventUpdate, db: Session):
if not event:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Event not found")

if data.lottery is not None and data.lottery != event.lottery:
if event.event_users_confirmed or any(eu.confirmed_status for eu in event.event_users):
raise HTTPException(400, detail="Cannot change lottery after signups are confirmed")

if data.price is not None and data.price < 0:
raise HTTPException(400, detail="Price cannot be lower than 0")

Expand Down
55 changes: 55 additions & 0 deletions services/event_signup_service.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from datetime import UTC, datetime
from typing import get_args
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from db_models.event_model import Event_DB
from db_models.event_user_model import EventUser_DB
from db_models.user_model import User_DB
from db_models.group_model import Group_DB
from db_models.group_user_model import GroupUser_DB
from api_schemas.event_signup_schemas import EventSignupCreate, EventSignupUpdate
from helpers.constants import DEFAULT_USER_PRIORITY
from helpers.types import GROUP_TYPE


def signup_to_event(event: Event_DB, user: User_DB, data: EventSignupCreate, manage_permission: bool, db: Session):
Expand All @@ -30,6 +34,13 @@ def signup_to_event(event: Event_DB, user: User_DB, data: EventSignupCreate, man
):
raise HTTPException(400, detail="User already signed up to chosen event")

if (
manage_permission == False
and data.group_name is not None
and not is_group_allowed(event, user, data.group_name)
):
raise HTTPException(status.HTTP_403_FORBIDDEN, detail="User cannot sign up with this group")

signup = EventUser_DB(user=user, user_id=user.id, event=event, event_id=event.id)

for var, value in vars(data).items():
Expand Down Expand Up @@ -76,6 +87,13 @@ def update_event_signup(event: Event_DB, data: EventSignupUpdate, user_id: int,
if signup is None:
raise HTTPException(status.HTTP_404_NOT_FOUND)

if (
manage_permission == False
and data.group_name is not None
and not is_group_allowed(event, db.query(User_DB).filter(User_DB.id == user_id).one(), data.group_name)
):
raise HTTPException(status.HTTP_403_FORBIDDEN, detail="User cannot sign up with this group")

for var, value in vars(data).items():
if var == "priority" and not value:
setattr(signup, "priority", DEFAULT_USER_PRIORITY)
Expand All @@ -102,3 +120,40 @@ def check_me_signup(event_id: int, me: User_DB, db: Session):
raise HTTPException(404, detail="Signup not found")

return signup


def get_allowed_groups(event: Event_DB, user: User_DB):
allowed_groups: list[Group_DB] = []
if event.is_nollning_event:
allowed_group_types = event.mentor_group_types or list(get_args(GROUP_TYPE))
for gu in user.group_users:
if _is_group_allowed(gu, event, allowed_group_types):
allowed_groups.append(gu.group)
else:
allowed_groups = user.groups

return allowed_groups


def is_group_allowed(event: Event_DB, user: User_DB, group_name: str | None):
if event.is_nollning_event:
allowed_group_types = event.mentor_group_types or list(get_args(GROUP_TYPE))
is_event_allowed = False
for gu in user.group_users:
if group_name == gu.group.name:
if _is_group_allowed(gu, event, allowed_group_types):
is_event_allowed = True
break

if not is_event_allowed:
return False

return True


def _is_group_allowed(gu: GroupUser_DB, e: Event_DB, agt: list[GROUP_TYPE]):
if gu.group.group_type in agt:
return True
elif (gu.group_user_type == "Mentor") and e.allow_other_mentors:
return True
return False
51 changes: 51 additions & 0 deletions tests/basic_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,54 @@ def patch_sub_election(client, sub_election_id, token=None, **kwargs):
data = sub_election_data_factory(**kwargs)
headers = auth_headers(token) if token else {}
return client.patch(f"/sub-election/{sub_election_id}", json=data, headers=headers)


def event_data_factory(**kwargs):
"""Factory for creating event payloads with sensible default times."""
now = datetime.datetime.now(timezone.utc)
default_data = {
"starts_at": (now + datetime.timedelta(days=7)).isoformat(),
"ends_at": (now + datetime.timedelta(days=7, hours=3)).isoformat(),
"signup_start": (now - datetime.timedelta(days=1)).isoformat(),
"signup_end": (now + datetime.timedelta(days=6)).isoformat(),
"title_sv": "Testevenemang",
"title_en": "Test Event",
"description_sv": "Svensk beskrivning",
"description_en": "English description",
"location": "Kårhuset",
"max_event_users": 0,
"priorities": [],
"all_day": False,
"recurring": False,
"food": False,
"closed": False,
"can_signup": True,
"drink_package": False,
"is_nollning_event": False,
"mentor_group_types": ["Mentor", "Mission", "Default", "Committee"],
"allow_other_mentors": False,
"alcohol_event_type": "None",
"dress_code": "Ovve",
"price": 0,
"dot": "None",
"lottery": False,
}
return {**default_data, **kwargs}


def add_user_to_group(db_session, user, name, group_type, group_user_type="Mentee"):
"""Create a group of a given type and put the user in it. Returns the group."""
from db_models.group_model import Group_DB
from db_models.group_user_model import GroupUser_DB

group = Group_DB(name=name, group_type=group_type)
db_session.add(group)
db_session.commit()

group_user = GroupUser_DB(
user=user, user_id=user.id, group=group, group_id=group.id, group_user_type=group_user_type
)
db_session.add(group_user)
db_session.commit()

return group
40 changes: 40 additions & 0 deletions tests/basic_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,3 +281,43 @@ def open_sub_election(db_session, open_election, admin_post, member_post):
db_session.commit()
db_session.refresh(sub_election)
return sub_election


@pytest.fixture()
def event(client, admin_token, admin_council_id):
"""Create and return a plain event which is open for signup."""

response = client.post(
"/events/", json=event_data_factory(council_id=admin_council_id), headers=auth_headers(admin_token)
)
assert response.status_code in (200, 201), response.text

return response.json()


@pytest.fixture()
def nollning_event(client, admin_token, admin_council_id):
"""Create and return a nollning event which only accepts groups of type "Mentor"."""

response = client.post(
"/events/",
json=event_data_factory(council_id=admin_council_id, is_nollning_event=True, mentor_group_types=["Mentor"]),
headers=auth_headers(admin_token),
)
assert response.status_code in (200, 201), response.text

return response.json()


@pytest.fixture()
def mentor_group(db_session, membered_user):
"""Create a group of type "Mentor" with the member user as a mentee."""

return add_user_to_group(db_session, membered_user, "Fadderiet", "Mentor", "Mentee")


@pytest.fixture()
def mission_group(db_session, membered_user):
"""Create a group of type "Mission" with the member user as a mentee."""

return add_user_to_group(db_session, membered_user, "Uppdraget", "Mission", "Mentee")
Loading
Loading