Skip to content

Commit

Permalink
✨ Add support for custom confirmation email
Browse files Browse the repository at this point in the history
  • Loading branch information
otytlandsvik committed Jan 31, 2024
1 parent 8e7cd1e commit ed7f220
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 61 deletions.
54 changes: 32 additions & 22 deletions app/api/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
from starlette.responses import FileResponse
from starlette.background import BackgroundTasks
from uuid import uuid4, UUID
from app.utils.event_utils import event_has_started, event_starts_in, num_of_confirmed_participants, num_of_deprioritized_participants, should_penalize, valid_registration, validate_event_dates, validate_pos_update
from app.utils.event_utils import *
from app.utils.validation import validate_image_file_type, validate_uuid
from ..auth_helpers import authorize, authorize_admin, optional_authentication
from ..db import get_database, get_image_path, get_qr_path, get_export_path
from ..models import Event, EventDB, AccessTokenPayload, EventInput, EventUpdate, EventUserView, JoinEventPayload, Participant, ParticipantPosUpdate, Role, SetAttendancePayload
from ..models import *
from .utils import get_event_or_404, penalize
import pandas as pd
from .mail import send_mail
Expand Down Expand Up @@ -319,8 +319,8 @@ def update_event_options(request: Request, id: str, payload: JoinEventPayload, t

# Create a dictionary with the payload
values = payload.model_dump(exclude_unset=True)
update_dict = {f"participants.$.{
key}": value for key, value in values.items()}
update_dict = {f"""participants.$.{
key}""": value for key, value in values.items()}

# Update db field
res = db.events.update_one(
Expand Down Expand Up @@ -550,8 +550,22 @@ async def reorder_participants(request: Request, id: str, position_update: Parti

return Response(status_code=200)


@router.get('/{id}/confirm-message', dependencies=[Depends(validate_uuid)])
async def get_confirmation_message(request: Request, id: str, token: AccessTokenPayload = Depends(authorize_admin)):
db = get_database(request)
event = get_event_or_404(db, id)

try:
content = get_default_confirmation(event)
return {'message': content}
except FileNotFoundError:
raise HTTPException(500, "Error fetching default confirm message")



@router.post('/{id}/confirm', dependencies=[Depends(validate_uuid)])
async def confirmation(request: Request, id: str, token: AccessTokenPayload = Depends(authorize_admin)):
async def confirmation(request: Request, id: str, m: EventConfirmMessage, token: AccessTokenPayload = Depends(authorize_admin)):
async with lock:
db = get_database(request)
event = get_event_or_404(db, id)
Expand All @@ -569,11 +583,13 @@ async def confirmation(request: Request, id: str, token: AccessTokenPayload = De
raise HTTPException(
400, "Cannot send confirmation to a unpublished event")

# if users cannot join confirmations cannot be sent out
if not valid_registration(event["registrationOpeningDate"]):
raise HTTPException(
400, "Cannot send confirmations before registration is opened")

if m.msg != None and len(m.msg) > 5000:
raise HTTPException(400, "Email message is too long")

result = db.events.find_one_and_update(
{'eid': UUID(id)},
{"$set": {"confirmed": True}})
Expand Down Expand Up @@ -616,24 +632,18 @@ async def confirmation(request: Request, id: str, token: AccessTokenPayload = De
raise HTTPException(
500, "Unexpected error when updating participants")

# Use default confirmation email if no message is supplied
content = m.msg if m.msg != None else get_default_confirmation(event)

# Send email to all participants
if request.app.config.ENV == 'production':
with open("./app/assets/mails/event_confirmation.txt", 'r') as mail_content:
content = mail_content.read().replace(
"$EVENT_NAME$", event['title'])
content = content.replace(
"$DATE$", event['date'].strftime("%d %B, %Y"))
content = content.replace(
"$TIME$", event['date'].strftime("%H:%M:%S"))
content = content.replace("$LOCATION$", event['address'])
# send mail individual
for mail in mailingList:
confirmation_email = MailPayload(
to=[mail],
subject=f"Bekreftelse {event['title']}",
content=content,
)
send_mail(confirmation_email)
for mail in mailingList:
confirmation_email = MailPayload(
to=[mail],
subject=f"Bekreftelse {event['title']}",
content=content,
)
send_mail(confirmation_email)

return Response(status_code=200)

Expand Down
24 changes: 20 additions & 4 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@

from pydantic.fields import Field


class Status(str, Enum):
active = "active"
inactive = "inactive"

def __str__(self):
return self.value


class Role(str, Enum):
admin = "admin"
member = "member"
Expand All @@ -20,6 +22,7 @@ class Role(str, Enum):
def __str__(self):
return self.value


class MailPayload(BaseModel):
"""
Mail model used to define emails sent from server
Expand All @@ -37,6 +40,7 @@ class MailPayload(BaseModel):
to: List[EmailStr]
sent_by: str = "[email protected]"


class AccessTokenPayload(BaseModel):
exp: int
iat: int
Expand All @@ -60,6 +64,7 @@ class MemberInput(BaseModel):
graduated: bool
phone: Optional[str] = None


class AdminMemberUpdate(BaseModel):
realName: Optional[str] = None
role: Optional[Role] = None
Expand All @@ -76,6 +81,7 @@ class MemberUpdate(BaseModel):
classof: Optional[str] = None
phone: Optional[str] = None


class MemberDB(BaseModel, use_enum_values=True):
id: UUID4
realName: str
Expand Down Expand Up @@ -137,7 +143,8 @@ class Participant(BaseModel):


class ParticipantPosUpdate(BaseModel):
updateList: List[create_model('ParticipantPosUpdate', id=(UUID4, ...), pos=(int, ...))]
updateList: List[create_model(
'ParticipantPosUpdate', id=(UUID4, ...), pos=(int, ...))]


class EventInput(BaseModel):
Expand Down Expand Up @@ -190,6 +197,9 @@ class EventUpdate(BaseModel):
confirmed: Optional[bool] = None


class EventConfirmMessage(BaseModel):
msg: Optional[str] = None

class EventDB(Event):
participants: List[Participant]

Expand Down Expand Up @@ -227,7 +237,7 @@ class JoinEventPayload(BaseModel):
class PenaltyInput(BaseModel):
penalty: int = Field(
ge=0, description="Penalty must be larger or equal to 0")


class SetAttendancePayload(BaseModel):
member_id: Optional[str] = None
Expand All @@ -247,6 +257,7 @@ class JobItemPayload(BaseModel):
start_date: Optional[datetime] = None
due_date: Optional[datetime] = None


class UpdateJob(BaseModel):
company: Optional[str] = None
title: Optional[str] = None
Expand All @@ -260,6 +271,7 @@ class UpdateJob(BaseModel):
start_date: Optional[datetime] = None
due_date: Optional[datetime] = None


class JobItem(JobItemPayload):
id: UUID4

Expand All @@ -270,20 +282,24 @@ class UniqueVisitsStructure(BaseModel):
bloom_filter: bytes
timestamps: List[date]


class PageVisitsStructure(BaseModel):
# tracks
# tracks
url_dict: Dict[str, int]


class PageVisitStamp(BaseModel):
pass


class PageVisit(BaseModel):
page: str


class PageVisits(PageVisit):
start: date
end: date


class Stats(BaseModel):
date: date

61 changes: 44 additions & 17 deletions app/utils/event_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@
from fastapi import HTTPException
from datetime import datetime, timedelta


def validate_registartion_opening_time(event_date, opening_date):
try:
# registration Opening Time are defined as days hours:minutes before the event starts
opening_date = datetime.strptime(str(opening_date), "%Y-%m-%d %H:%M:%S")
opening_date = datetime.strptime(
str(opening_date), "%Y-%m-%d %H:%M:%S")
except ValueError:
raise HTTPException(400, "Invalid date format for when registration is opening")
raise HTTPException(
400, "Invalid date format for when registration is opening")
if opening_date >= event_date:
raise HTTPException(400, "Registration date must be before event start")
raise HTTPException(
400, "Registration date must be before event start")
return opening_date


def validate_event_dates(event):
try:
event_date = datetime.strptime(str(event.date), "%Y-%m-%d %H:%M:%S")
Expand All @@ -23,10 +28,12 @@ def validate_event_dates(event):
raise HTTPException(400, "Invalid date")

if event.registrationOpeningDate != None:
validate_registartion_opening_time(event.date, event.registrationOpeningDate)
validate_registartion_opening_time(
event.date, event.registrationOpeningDate)


# validates if the cancellation time is inside the acceptable time frame
def validate_cancellation_time(start_date):
""" validates if the cancellation time is inside the acceptable time frame """
cancellation_threshold = 24
now = datetime.now()
try:
Expand All @@ -35,7 +42,8 @@ def validate_cancellation_time(start_date):
diff = abs(start_date-date)
except ValueError:
return False
return diff>=timedelta(hours=cancellation_threshold)
return diff >= timedelta(hours=cancellation_threshold)


def should_penalize(event, user_id):
already_penalized = user_id in event["registeredPenalties"]
Expand All @@ -54,26 +62,31 @@ def should_penalize(event, user_id):
return True
return False


def valid_registration(opening_date):
# non specified opening date means registration is open
if opening_date == None:
return True
try:
# validates format
registration_start = datetime.strptime(str(opening_date), "%Y-%m-%d %H:%M:%S")
registration_start = datetime.strptime(
str(opening_date), "%Y-%m-%d %H:%M:%S")
except ValueError:
# sets registration open if field is malformed
return True
return datetime.now()>registration_start
return datetime.now() > registration_start


# validates position reorder input
# - id:
# - all participants in reorder list is already joined the event
# - pos
# - validates that all pos arguments are valid i.e between 0 and len(participants)
def validate_pos_update(participants, updateList):
"""
Validates position reorder input
- id:
- all participants in reorder list have already joined the event
- pos
- all pos arguments are valid i.e. between 0 and len(participants)
"""
valid_args = list(range(0, len(participants)))
joined_ids = [ p["id"] for p in participants ]
joined_ids = [p["id"] for p in participants]
for p in updateList:
try:
valid_args.remove(p.pos)
Expand All @@ -82,14 +95,16 @@ def validate_pos_update(participants, updateList):
return False
return len(valid_args) == 0 and len(joined_ids) == 0


def event_has_started(event):
try :
try:
start_date = datetime.strptime(str(event["date"]), "%Y-%m-%d %H:%M:%S")
current_time = datetime.now()
current_time = datetime.now()
return current_time > start_date
except ValueError:
return True



def event_starts_in(event, dt):
"""
Return whether event has started, calculated with the given
Expand All @@ -106,6 +121,18 @@ def event_starts_in(event, dt):
def num_of_deprioritized_participants(participants):
return sum(p["penalty"] > 1 for p in participants)


def num_of_confirmed_participants(participants):
return sum(p["confirmed"] == True for p in participants)


def get_default_confirmation(event):
with open("./app/assets/mails/event_confirmation.txt", 'r') as mail_content:
content = mail_content.read().replace(
"$EVENT_NAME$", event['title'])
content = content.replace(
"$DATE$", event['date'].strftime("%d %B, %Y"))
content = content.replace("$TIME$", event['date'].strftime("%H:%M"))
content = content.replace("$LOCATION$", event['address'])

return content
Loading

0 comments on commit ed7f220

Please sign in to comment.