Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Features/105 GA booking request flow #159

Merged
merged 14 commits into from
Nov 3, 2023
Merged
2 changes: 1 addition & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ SMTP_PORT=
# SMTP user credentials
SMTP_USER=
SMTP_PASS=
# Authorized email address for sending emails, leave empty to default to organizer
# Authorized email address for sending emails
SMTP_SENDER=

# -- TIERS --
Expand Down
53 changes: 46 additions & 7 deletions backend/src/appointment/controller/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,9 @@ def create_event(
return event

def delete_events(self, start):
"""delete all events in given date range from the server"""
# Not used?
"""delete all events in given date range from the server
Not intended to be used in production. For cleaning purposes after testing only.
"""
pass


Expand Down Expand Up @@ -223,7 +224,9 @@ def create_event(
return event

def delete_events(self, start):
"""delete all events in given date range from the server"""
"""delete all events in given date range from the server
Not intended to be used in production. For cleaning purposes after testing only.
"""
calendar = self.client.calendar(url=self.url)
result = calendar.events()
count = 0
Expand Down Expand Up @@ -277,8 +280,8 @@ def send_vevent(
mail = InvitationMail(sender=organizer.email, to=attendee.email, attachments=[invite])
mail.send()

def available_slots_from_schedule(s: schemas.ScheduleBase):
"""This helper calculates a list of slots according to the given schedule."""
def available_slots_from_schedule(s: schemas.ScheduleBase) -> list[schemas.SlotBase]:
"""This helper calculates a list of slots according to the given schedule configuration."""
now = datetime.utcnow()
earliest_start = now + timedelta(minutes=s.earliest_booking)
farthest_end = now + timedelta(minutes=s.farthest_booking)
Expand Down Expand Up @@ -312,9 +315,9 @@ def available_slots_from_schedule(s: schemas.ScheduleBase):
pointer = next_date
return slots

def events_set_difference(a_list: list[schemas.SlotBase], b_list: list[schemas.Event]):
def events_set_difference(a_list: list[schemas.SlotBase], b_list: list[schemas.Event]) -> list[schemas.SlotBase]:
"""This helper removes all events from list A, which have a time collision with any event in list B
and returns all remaining elements from A as new list.
and returns all remaining elements from A as new list.
"""
available_slots = []
for a in a_list:
Expand All @@ -332,3 +335,39 @@ def events_set_difference(a_list: list[schemas.SlotBase], b_list: list[schemas.E
if not collision_found:
available_slots.append(a)
return available_slots

def existing_events_for_schedule(
schedule: schemas.Schedule,
calendars: list[schemas.Calendar],
subscriber: schemas.Subscriber,
google_client: GoogleClient,
db
) -> list[schemas.Event]:
"""This helper retrieves all events existing in given calendars for the scheduled date range
and returns all remaining elements from A as new list.
"""
existingEvents = []
# handle calendar events
for calendar in calendars:
if calendar.provider == CalendarProvider.google:
con = GoogleConnector(
db=db,
google_client=google_client,
calendar_id=calendar.user,
subscriber_id=subscriber.id,
google_tkn=subscriber.google_tkn,
)
else:
con = CalDavConnector(calendar.url, calendar.user, calendar.password)
devmount marked this conversation as resolved.
Show resolved Hide resolved
farthest_end = datetime.utcnow() + timedelta(minutes=schedule.farthest_booking)
start = schedule.start_date.strftime("%Y-%m-%d")
end = schedule.end_date.strftime("%Y-%m-%d") if schedule.end_date else farthest_end.strftime("%Y-%m-%d")
devmount marked this conversation as resolved.
Show resolved Hide resolved
existingEvents.extend(con.list_events(start, end))
# handle already requested time slots
for slot in schedule.slots:
existingEvents.append(schemas.Event(
title=schedule.name,
start=slot.start.isoformat(),
end=(slot.start + timedelta(minutes=slot.duration)).isoformat(),
))
return existingEvents
5 changes: 4 additions & 1 deletion backend/src/appointment/controller/google_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,10 @@ def sync_calendars(self, db, subscriber_id: int, token):
# add calendar
try:
repo.update_or_create_subscriber_calendar(
db=db, calendar=cal, calendar_url=calendar.get("id"), subscriber_id=subscriber_id
db=db,
calendar=cal,
calendar_url=calendar.get("id"),
subscriber_id=subscriber_id
)
except Exception as err:
logging.warning(
Expand Down
49 changes: 44 additions & 5 deletions backend/src/appointment/controller/mailer.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import jinja2
import validators

from datetime import datetime
from html import escape
from email import encoders
from email.mime.base import MIMEBase
Expand All @@ -36,22 +37,22 @@ def __init__(self, mime: tuple[str], filename: str, data: str):
class Mailer:
def __init__(
self,
sender: str,
to: str,
sender: str = os.getenv("SMTP_SENDER"),
subject: str = "",
html: str = "",
plain: str = "",
attachments: list = [Attachment],
attachments: list[Attachment] = [],
):
self.sender = os.getenv("SMTP_SENDER") or sender
self.sender = sender
self.to = to
self.subject = subject
self.body_html = html
self.body_plain = plain
self.attachments = attachments

def html(self):
"""provide email body as html"""
"""provide email body as html per default"""
return self.body_html

def text(self):
Expand Down Expand Up @@ -130,9 +131,47 @@ def __init__(self, *args, **kwargs):
"""init Mailer with invitation specific defaults"""
defaultKwargs = {
"subject": "[TBA] Invitation sent from Thunderbird Appointment",
"plain": "This message is sent from Appointment.",
"plain": "This message is sent from Thunderbird Appointment.",
}
super(InvitationMail, self).__init__(*args, **defaultKwargs, **kwargs)

def html(self):
return get_template("invite.jinja2").render()


class ConfirmationMail(Mailer):
def __init__(self, confirmUrl, denyUrl, attendee, date, *args, **kwargs):
"""init Mailer with confirmation specific defaults"""
self.attendee = attendee
self.date = date
self.confirmUrl = confirmUrl
self.denyUrl = denyUrl
defaultKwargs = {
"subject": "[TBA] Confirm booking request from Thunderbird Appointment",
"plain": """
{name} ({email}) just requested this time slot from your schedule: {date}

Visit this link to confirm the booking request:
{confirm}

Or this link if you want to deny it:
{deny}

This message is sent from Thunderbird Appointment.
devmount marked this conversation as resolved.
Show resolved Hide resolved
""".format(
name=self.attendee.name,
email=self.attendee.email,
date=self.date,
confirm=self.confirmUrl,
deny=self.denyUrl
),
}
super(ConfirmationMail, self).__init__(*args, **defaultKwargs, **kwargs)

def html(self):
return get_template("confirm.jinja2").render(
attendee=self.attendee,
date=self.date,
confirm=self.confirmUrl,
deny=self.denyUrl,
)
Loading