Skip to content

Commit

Permalink
Deploy to stage (#129)
Browse files Browse the repository at this point in the history
* Booking page and general availability (#104)

* 🔨 enable booking page for general availability

* ❌ revert debugging changes

* Replace router.fullPath with window.location.href, as signed urls require a complete url.

* ➕ add route checks

* Fix docker dev server with the new sentry webpack stuff.

* Features/98 ga data structure (#108)

* Add models and schemas for Schedule and Availability

* Fix missing/incorrect back_populates= for Appointment and Schedule

* Add migration file for schedule and availability tables

* Respond to PR feedback

* Fix migration issue

Happened because two migrations were auto generated in parallel.

* Bugs/caldav connector clean up (#112)

* 🔨 fix value check on updating calendars

* 🔨 remove deprecated provider code

* 👕 fix linter issues

* Fix call on appointment id before NoneType check (#113)

* General documentation (#116)

* ➕ add general documentation

* ➕ add general documentation

* 📜 update component chart

* Backend testing (#117)

* 🔨 updated pytests

* ➕ extend health and authentication tests

* ➕ add authentication for test user

* 📜 document testing in readme

* ➕ add subscriber related tests

* 👕 fix linter issues

* 🔨 prevent connecting calendars manually

* 🔨 check tier limit on connecting calendars

* ➕ add calendar related tests

* 🔨 only allow appointment creations on connected calendars

* 🔨 cascade delete attendees on slot deletion

* ➕ add appointment related tests

* 📜 add hint for smtp server for testing

* ➕ prepare google calendar tests

* ➕ add google test env vars

* Schedule API endpoints (#114)

* ➕ add schedule API endpoints

* Get current backend tests (#122)

* General documentation (#116)

* ➕ add general documentation

* ➕ add general documentation

* 📜 update component chart

* Backend testing (#117)

* 🔨 updated pytests

* ➕ extend health and authentication tests

* ➕ add authentication for test user

* 📜 document testing in readme

* ➕ add subscriber related tests

* 👕 fix linter issues

* 🔨 prevent connecting calendars manually

* 🔨 check tier limit on connecting calendars

* ➕ add calendar related tests

* 🔨 only allow appointment creations on connected calendars

* 🔨 cascade delete attendees on slot deletion

* ➕ add appointment related tests

* 📜 add hint for smtp server for testing

* ➕ prepare google calendar tests

* ➕ add google test env vars

* 🔨 migrate data structure to current mockup

* ➕ endpoint for schedule creation

* ➕ add schedule test

* ❌ remove appointment type

* 🔨 improve model types and link verification

* ➕ time slots calculation from schedule config

* ➕ time slots comparison with remote events of assigned calendar

* ➕ compare schedule to all connected calendars

* ➕ check calendar connections first

* ➕ add test for invalid availability link

* ➕ check if actual booking slots exist

* Use localStorage to cache logged-in user (#124)

* Use localStorage to cache logged-in user

* Rename import to use existing variable name

* Update frontend/src/views/ProfileView.vue

Co-authored-by: Andreas <[email protected]>

---------

Co-authored-by: Andreas <[email protected]>

* Features/97 Schedules settings page (#128)

* Draft of GA Settings page (#126)

* Basic layout without form

* Finish styling header for general availability

* Finish fake step 1

* Add placeholders for forms, adds buttons

* Add placeholders for date and time inputs

* Add placeholder for step 3

* Change form inputs to correct types

* Additional styling on GA creation view

* Makes booking settings reactive, styles slot length

* Make sections toggle-able

* Set start/end time v-model refs

* Enable date picker for start/end date; add control to remove end date

* Sets default days; shows action buttons

* ➕ implement schedules page frontend

* ➕ implement schedule live preview

* 🔨 fix schedule preview on calendar navigation

* 🔨 fix calendar view tab navigation

* 🔨 fix names and calendar title in preview tooltip

* 🔨 handle unset schedule end date

* ➕ connect schedule page to actual schedule API endpoints

* ➕ add active flag for schedules

* ➕ extend toggle component

* ➕ add disabled state for tab bar

* ➕ implement schedule activation toggle

* 🔨 convert schedule times to utc

* 🔨 fix misaligned starting date for scheduled time slots

* 🔨 make list of weekdays aware to locale start day of week

* 🔨 fix calendar for different start of weeks

* 🔨 reload on locale change to update dayjs instance

* ➕ add option for event popup on right side

* ➕ add month navigation for booking page

* ➕ endpoint for availability booking

* 🔨 fix timezone back calculation on schedule config

* 🔨 fix remote event datetime format

* 🔨 show confirmation modal only on first schedule save

* ➕ implement ICS serving endpoint for schedule availabilities

---------

Co-authored-by: Melissa Autumn <[email protected]>
Co-authored-by: Chris Aquino <[email protected]>
  • Loading branch information
3 people authored Sep 27, 2023
1 parent e6e4f2d commit a846fb0
Show file tree
Hide file tree
Showing 51 changed files with 3,589 additions and 1,439 deletions.
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Invite others to grab times on your calendar. Choose a date. Make appointments a

## Get started

You can either build preconfigured docker containers (database, backend and frontend) or manually set up the application. A more detailed documentation can befound in the [docs folder](./docs/README.md).

### With Docker

```bash
Expand Down Expand Up @@ -69,15 +71,21 @@ Run application for development with hot reloading backend and frontend:
To run tests, first install Pytest
```bash
pip install pytest
pip install pytest httpx
```
Then `cd` into the project root und simply run
Create an Auth0 test user and add the credentials of that user to `AUTH0_TEST_USER` and `AUTH0_TEST_PASS` in your `.env`. Then `cd` into the project root und simply run
```bash
pytest
```
Note: Since tests include endpoints that trigger mail sending, there must be a running smtp server on your testing system. You can simply run the Python built in server (according to your environment configuration):
```bash
python -m smtpd -n -c DebuggingServer localhost:25
```
## Contributing
Contributions are very welcome. Please lint/format code before creating PRs.
Expand Down
35 changes: 23 additions & 12 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
# Appointment backend configuration.

# -- General --
# -- GENERAL --
# Logging level: DEBUG|INFO|WARNING|ERROR|CRITICAL
LOG_LEVEL=ERROR

# -- Frontend --
# -- FRONTEND --
FRONTEND_URL=http://localhost:8080
# Leave blank for no short url
SHORT_BASE_URL=

# -- Database --
# -- DATABASE --
DATABASE_URL=
DATABASE_SECRETS=
# Secret phrase for database encryption (e.g. create it by running `openssl rand -hex 32`)
DB_SECRET=

# -- Auth0 --
# -- AUTH0 --
# Management API
AUTH0_API_CLIENT_ID=
AUTH0_API_SECRET=
# Auth API
AUTH0_API_DOMAIN=
AUTH0_API_AUDIENCE=
# Roles, available in Auth0 User Management -> Roles
AUTH0_API_ADMIN_ROLE=rol_xyz
# Role keys, configurable in Auth0 User Management -> Roles
AUTH0_API_ROLE_ADMIN=
AUTH0_API_ROLE_BASIC=
AUTH0_API_ROLE_PLUS=
AUTH0_API_ROLE_PRO=

# -- Mail --
# -- MAIL --
# Connection security: SSL|STARTTLS|NONE
SMTP_SECURITY=SSL
# Address and port of the SMTP server
Expand All @@ -37,26 +40,34 @@ SMTP_PASS=
# Authorized email address for sending emails, leave empty to default to organizer
SMTP_SENDER=

# -- Tiers --
# -- TIERS --
# Max number of calendars to be simultanously connected for members of the basic tier
TIER_BASIC_CALENDAR_LIMIT=3
# Max number of calendars to be simultanously connected for members of the plus tier
TIER_PLUS_CALENDAR_LIMIT=5
# Max number of calendars to be simultanously connected for members of the pro tier
TIER_PRO_CALENDAR_LIMIT=10

# -- Google Auth --
# -- GOOGLE AUTH --
GOOGLE_AUTH_CLIENT_ID=
GOOGLE_AUTH_SECRET=
GOOGLE_AUTH_PROJECT_ID=
GOOGLE_AUTH_CALLBACK=http://localhost:5000/google/callback

# -- Signed URL Secret --
# -- SIGNED URL SECRET --
# Shared secret for url signing (e.g. create it by running `openssl rand -hex 32`)
SIGNED_SECRET=

# If empty, sentry will be disabled
SENTRY_DSN=

# Possible values: prod, dev
APP_ENV=dev

# -- TESTING --
AUTH0_TEST_USER=
AUTH0_TEST_PASS=
CALDAV_TEST_PRINCIPAL_URL=
CALDAV_TEST_CALENDAR_URL=
CALDAV_TEST_USER=
CALDAV_TEST_PASS=
GOOGLE_TEST_USER=
GOOGLE_TEST_PASS=
14 changes: 9 additions & 5 deletions backend/src/controller/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,15 @@ def persist_user(self, db: Session, user: Auth0User):

# Generate an initial short link hash if they don't have one already
if db_subscriber.short_link_hash is None:
repo.update_subscriber(db, schemas.SubscriberAuth(
email=db_subscriber.email,
username=db_subscriber.username,
short_link_hash=secrets.token_hex(32)
), db_subscriber.id)
repo.update_subscriber(
db,
schemas.SubscriberAuth(
email=db_subscriber.email,
username=db_subscriber.username,
short_link_hash=secrets.token_hex(32),
),
db_subscriber.id,
)

return db_subscriber
return None
Expand Down
115 changes: 78 additions & 37 deletions backend/src/controller/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
Handle connection to a CalDAV server.
"""
import json
import logging
from caldav import DAVClient
from google.oauth2.credentials import Credentials
from icalendar import Calendar, Event, vCalAddress, vText
from datetime import datetime, timedelta, timezone
from dateutil.parser import parse

from .google_client import GoogleClient
from ..database import schemas
Expand Down Expand Up @@ -119,8 +121,8 @@ def create_event(
"summary": event.title,
"location": event.location.name,
"description": "\n".join(description),
"start": {"dateTime": event.start + "+00:00"},
"end": {"dateTime": event.end + "+00:00"},
"start": {"dateTime": event.start},
"end": {"dateTime": event.end},
"attendees": [
{"displayName": organizer.name, "email": organizer.email},
{"displayName": attendee.name, "email": attendee.email},
Expand All @@ -136,20 +138,14 @@ def delete_events(self, start):


class CalDavConnector:
def __init__(self, provider: int, url: str, user: str, password: str, google_tkn: str = None):
def __init__(self, url: str, user: str, password: str):
# store credentials of remote location
self.provider = provider
self.provider = CalendarProvider.caldav
self.url = url
self.user = user
self.password = password
self.google_token = google_tkn
# connect to CalDAV server
if provider == CalendarProvider.google:
raise DeprecationWarning()

if provider == CalendarProvider.caldav:
# https://github.com/python-caldav/caldav/blob/master/examples/basic_usage_examples.py
self.client = DAVClient(url=url, username=user, password=password)
self.client = DAVClient(url=url, username=user, password=password)

def sync_calendars(self):
pass
Expand Down Expand Up @@ -207,30 +203,19 @@ def create_event(
organizer: schemas.Subscriber,
):
"""add a new event to the connected calendar"""
if self.provider == CalendarProvider.google:
googleEvent = {
"summary": event.title,
# 'location': event.location_url, # TODO handle location types
"description": event.description,
"start": {"dateTime": event.start + "+00:00"},
"end": {"dateTime": event.end + "+00:00"},
"attendees": [{"displaName": attendee.name, "email": attendee.email}],
}
self.client.events().insert(calendarId=self.user, body=googleEvent).execute()
if self.provider == CalendarProvider.caldav:
calendar = self.client.calendar(url=self.url)
# save event
caldavEvent = calendar.save_event(
dtstart=datetime.fromisoformat(event.start),
dtend=datetime.fromisoformat(event.end),
summary=event.title,
# TODO: handle location
description=event.description,
)
# save attendee data
caldavEvent.add_attendee((organizer.name, organizer.email))
caldavEvent.add_attendee((attendee.name, attendee.email))
caldavEvent.save()
calendar = self.client.calendar(url=self.url)
# save event
caldavEvent = calendar.save_event(
dtstart=datetime.fromisoformat(event.start),
dtend=datetime.fromisoformat(event.end),
summary=event.title,
# TODO: handle location
description=event.description,
)
# save attendee data
caldavEvent.add_attendee((organizer.name, organizer.email))
caldavEvent.add_attendee((attendee.name, attendee.email))
caldavEvent.save()
return event

def delete_events(self, start):
Expand All @@ -248,7 +233,7 @@ def delete_events(self, start):
class Tools:
def create_vevent(
self,
appointment: schemas.Appointment,
appointment: schemas.Appointment | schemas.AppointmentBase,
slot: schemas.Slot,
organizer: schemas.Subscriber,
):
Expand All @@ -274,7 +259,7 @@ def create_vevent(

def send_vevent(
self,
appointment: schemas.Appointment,
appointment: schemas.Appointment | schemas.AppointmentBase,
slot: schemas.Slot,
organizer: schemas.Subscriber,
attendee: schemas.AttendeeBase,
Expand All @@ -287,3 +272,59 @@ 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."""
now = datetime.utcnow()
earliest_start = now + timedelta(minutes=s.earliest_booking)
farthest_end = now + timedelta(minutes=s.farthest_booking)
start = datetime.combine(s.start_date, s.start_time)
end = min([datetime.combine(s.end_date, s.end_time), farthest_end]) if s.end_date else farthest_end
slots = []
# set the first date to an allowed weekday
weekdays = s.weekdays if type(s.weekdays) == list else json.loads(s.weekdays)
if not weekdays or len(weekdays) == 0:
weekdays = [1, 2, 3, 4, 5]
while start.isoweekday() not in weekdays:
start = start + timedelta(days=1)
# init date generation: pointer holds the current slot start datetime
pointer = start
counter = 0
# set fix event limit of 1000 for now for performance reasons. Can be removed later.
while pointer < end and counter < 1000:
counter += 1
if pointer >= earliest_start:
slots.append(schemas.SlotBase(start=pointer, duration=s.slot_duration))
next_start = pointer + timedelta(minutes=s.slot_duration)
# if the next slot still fits into the current day
if next_start.time() < s.end_time:
pointer = next_start
# if the next slot has to be on the next available day
else:
next_date = datetime.combine(pointer.date() + timedelta(days=1), s.start_time)
# check weekday and skip da if it isn't allowed
while next_date.isoweekday() not in weekdays:
next_date = next_date + timedelta(days=1)
pointer = next_date
return slots

def events_set_difference(a_list: list[schemas.SlotBase], b_list: list[schemas.Event]):
"""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.
"""
available_slots = []
for a in a_list:
a_start = a.start
a_end = a_start + timedelta(minutes=a.duration)
collision_found = False
for b in b_list:
b_start = parse(b.start)
b_end = parse(b.end)
# if there is an overlap of both date ranges, a collision was found
# see https://en.wikipedia.org/wiki/De_Morgan%27s_laws
if a_start.timestamp() < b_end.timestamp() and a_end.timestamp() > b_start.timestamp():
collision_found = True
break
if not collision_found:
available_slots.append(a)
return available_slots
12 changes: 6 additions & 6 deletions backend/src/controller/google_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ def get_credentials(self, code: str):
self.client.fetch_token(code=code)
return self.client.credentials
except Warning as e:
logging.error(f"[google.get_credentials] Google Warning: {str(e)}")
logging.error(f"[google_client.get_credentials] Google Warning: {str(e)}")
# This usually is the "Scope has changed" error.
raise GoogleScopeChanged()
except ValueError as e:
logging.error(f"[google.get_credentials] Value error while fetching credentials {str(e)}")
logging.error(f"[google_client.get_credentials] Value error while fetching credentials {str(e)}")
raise GoogleInvalidCredentials()

def get_email(self, token):
Expand All @@ -92,7 +92,7 @@ def list_calendars(self, token):
try:
response = request.execute()
except HttpError as e:
logging.warning(f"[google.list_calendars] Request Error: {e.status_code}/{e.error_details}")
logging.warning(f"[google_client.list_calendars] Request Error: {e.status_code}/{e.error_details}")

request = service.calendarList().list_next(request, response)

Expand All @@ -108,7 +108,7 @@ def list_events(self, calendar_id, time_min, time_max, token):
try:
response = request.execute()
except HttpError as e:
logging.warning(f"[google.list_events] Request Error: {e.status_code}/{e.error_details}")
logging.warning(f"[google_client.list_events] Request Error: {e.status_code}/{e.error_details}")

request = service.events().list_next(request, response)

Expand All @@ -120,7 +120,7 @@ def create_event(self, calendar_id, body, token):
try:
response = service.events().insert(calendarId=calendar_id, sendUpdates="all", body=body).execute()
except HttpError as e:
logging.warning(f"[google.add_event] Request Error: {e.status_code}/{e.error_details}")
logging.warning(f"[google_client.create_event] Request Error: {e.status_code}/{e.error_details}")

return response

Expand Down Expand Up @@ -148,7 +148,7 @@ def sync_calendars(self, db, subscriber_id: int, token):
)
except Exception as err:
logging.warning(
f"[controller.google.sync_calendars] Error occurred while creating calendar. Error: {str(err)}"
f"[google_client.sync_calendars] Error occurred while creating calendar. Error: {str(err)}"
)
error_occurred = True
return error_occurred
Loading

0 comments on commit a846fb0

Please sign in to comment.