diff --git a/README.md b/README.md
index bc8b07f..5d61874 100644
--- a/README.md
+++ b/README.md
@@ -159,17 +159,22 @@ Email templates can be configured in the Django Admin interface. Jinja2 syntax
is used for the templates. The following variables are available in the
templates:
-- `event_name` - The name of the event
-- `event_start` - The start time (datetime) of the event
-- `event_end` - The end time (datetime) of the event
-- `event_created` - The datetime the event was created
-- `event_is_deleted` - Whether the event was deleted
-- `event_is_cancelled` - Whether the event was cancelled
-- `event_is_recurring` - Whether the event is recurring
-- `event_organizer` - The organizer of the event
-- `overlap_event_name` - The name of the overlap event
-- `overlap_event_start` - The start time (datetime) of the overlap event
-- `overlap_event_end` - The end time (datetime) of the overlap event
-- `overlap_event_created` - The datetime the overlap event was created
-- `overlap_event_organizer` - The organizer of the overlap event
+- `name` - The name of the event
+- `start` - The start time (datetime) of the event
+- `end` - The end time (datetime) of the event
+- `created` - The datetime the event was created
+- `is_deleted` - Whether the event was deleted
+- `is_cancelled` - Whether the event was cancelled
+- `is_recurring` - Whether the event is recurring
+- `is_priorized` - Whether the event is prioritized
+- `is_deprioritized` - Whether the event is deprioritized in favor of another
+ event
+- `organizer` - The organizer of the event
+- `overlap_events` - A list of overlap events, each containing:
+ - `name` - The name of the overlap event
+ - `start` - The start time (datetime) of the overlap event
+ - `end` - The end time (datetime) of the overlap event
+ - `created` - The datetime the overlap event was created
+ - `organizer` - The organizer of the overlap event
+ - `is_priorized` - Whether the overlap event is prioritized
- `calendar_name` - The name of the calendar
\ No newline at end of file
diff --git a/src/booking.py b/src/booking.py
index 43ae964..689edaa 100644
--- a/src/booking.py
+++ b/src/booking.py
@@ -3,20 +3,21 @@ import secrets
from datetime import datetime
from pathlib import Path
from uuid import UUID
-import markdown
+import markdown
import requests
import shortuuid
-from django.views.decorators.csrf import csrf_exempt
+from django.core.signing import Signer, BadSignature
from django.core.validators import URLValidator
from django.db import models
from django.shortcuts import render, get_object_or_404
from django.utils import timezone
+from django.views.decorators.csrf import csrf_exempt
from ics import Calendar as ICS_Calendar, Event as ICS_Event
from nanodjango import Django
from shortuuid.django_fields import ShortUUIDField
-from clear_overlaps import clear_overlaps as clear
+from clear_bookings import clear
DEBUG = os.getenv("DJANGO_DEBUG", False)
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY") \
@@ -64,6 +65,7 @@ from ninja import NinjaAPI, ModelSchema
from ninja.security import HttpBearer, django_auth
from ninja.errors import HttpError
+
@app.admin
class EmailTemplate(models.Model):
"""
@@ -77,24 +79,29 @@ class EmailTemplate(models.Model):
If not set, no email will be sent.
Jinja2 template syntax can be used in the subject and body.
Available variables:
- - event_name: The name of the event
- - event_start: The start time (datetime) of the event
- - event_end: The end time (datetime) of the event
- - event_created: The datetime the event was created
- - event_is_deleted: Whether the event was deleted
- - event_is_cancelled: Whether the event was cancelled
- - event_is_recurring: Whether the event is recurring
- - event_organizer: The organizer of the event
- - overlap_event_name: The name of the overlap event
- - overlap_event_start: The start time (datetime) of the overlap event
- - overlap_event_end: The end time (datetime) of the overlap event
- - overlap_event_created: The datetime the overlap event was created
- - overlap_event_organizer: The organizer of the overlap event
+ - name: The name of the event
+ - start: The start time (datetime) of the event
+ - end: The end time (datetime) of the event
+ - created: The datetime the event was created
+ - is_deleted: Whether the event was deleted
+ - is_cancelled: Whether the event was cancelled
+ - is_recurring: Whether the event is recurring
+ - organizer: The organizer of the event
+ - is_prioritized: Whether the event is prioritized
+ - is_deprioritized: Whether the event was deprioritized in favor of another event
+ - overlap_events: List of overlap events, each with:
+ - name: The name of the overlap event
+ - start: The start time (datetime) of the overlap event
+ - end: The end time (datetime) of the overlap event
+ - created: The datetime the overlap event was created
+ - organizer: The organizer of the overlap event
+ - is_prioritized: Whether the overlap event is prioritized
- calendar_name: The name of the calendar""")
def __str__(self):
return self.name
+
@app.admin
class Calendar(models.Model):
"""
@@ -227,6 +234,95 @@ class Event(models.Model):
return string if not self.cancelled else f"{string} - CANCELLED"
+@app.admin(
+ list_display=("token", "is_used", "created_at", "updated_at", "notes"),
+ list_filter=("is_used",),
+ read_only_fields=("created_at", "token", "signature"))
+class PriorityEventToken(models.Model):
+ """
+ Priority event token model to store tokens for priority events.
+ """
+ id = models.AutoField(primary_key=True)
+ token = models.CharField(max_length=14, unique=True, editable=False)
+ signature = models.CharField(max_length=80, blank=True, editable=False)
+ notes = models.TextField(blank=True)
+ is_used = models.BooleanField(default=False)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ @classmethod
+ def generate_token(cls):
+ return f"#{secrets.token_hex(6).upper()}#"
+
+ def save(self, *args, **kwargs):
+ if not self.token:
+ self.token = self.generate_token()
+ return super().save(*args, **kwargs)
+
+ def redeem(self, uuid: str) -> str | bool:
+ """
+ Use the token to prioritize the event.
+ :return: Returns the signature or False if the token or the uuid is not
+ valid.
+ """
+ # Check for a valid uuid
+ try:
+ uuid = UUID(uuid, version=4)
+ except ValueError:
+ print(f"Failed to decode UUID {uuid}")
+ return False
+
+ if not self.is_used:
+ self.signature = self._sign(uuid)
+ self.is_used = True
+ self.save()
+ return True
+ return False
+
+ def validate_signature(self, uuid: str) -> bool:
+ """
+ Validate the signature of the uuid.
+ :param uuid: The uuid4 of the priority event.
+ :return:
+ """
+ try:
+ uuid = UUID(uuid, version=4)
+ except ValueError:
+ print(f"Failed to decode UUID {uuid}")
+ return False
+
+ if not self.signature:
+ print(f"No signature found for token '{self.token}'")
+ return False
+
+ return self._validate(uuid, self.signature)
+
+ @staticmethod
+ def _sign(uuid: UUID) -> str:
+ """
+ Generate a signed uuid.
+ :return: The signature.
+ """
+ signer = Signer()
+ return signer.sign(uuid.__str__())
+
+ @staticmethod
+ def _validate(uuid: UUID, signature: str) -> bool:
+ """
+ Validate the signature of the uuid.
+ :return: True if the signature matches the uuid, False otherwise.
+ """
+ signer = Signer()
+ try:
+ return signer.unsign(signature) == uuid.__str__()
+ except BadSignature as e:
+ print(e)
+ return False
+
+ def __str__(self):
+ return self.token
+
+
@app.admin(readonly_fields=('key',))
class APIKey(models.Model):
id = models.AutoField(primary_key=True)
@@ -341,7 +437,9 @@ def delete_event(request, calendar: str, event_id: str):
event.cancel()
return 204, None
-@api.delete("/clear-bookings", response={200: dict, 204: None, 401: None, 404: None})
+
+@api.delete("/clear-bookings",
+ response={200: dict, 204: None, 401: None, 404: None})
@csrf_exempt
def clear_bookings(request, calendar: str = None, test: bool = False):
user = get_user(request)
@@ -349,7 +447,7 @@ def clear_bookings(request, calendar: str = None, test: bool = False):
# Get optional calendar name from the request
if calendar:
cal = get_object_or_404(Calendar, name=calendar,
- auto_clear_bookings=True)
+ auto_clear_bookings=True)
if user not in cal.users.all():
raise HttpError(401,
f"User not authorised to clear bookings in calendar '{cal.name}'")
@@ -362,16 +460,18 @@ def clear_bookings(request, calendar: str = None, test: bool = False):
calendars = user.calendars.filter(auto_clear_bookings=True)
if not calendars:
- return 204, None # No bookings to clear
+ return 204, None # No bookings to clear
result = clear(list(calendars), is_test=test)
if all(len(r) == 0 for r in result.values() if type(r) is list):
- return 204, None # No bookings to clear
+ return 204, None # No bookings to clear
else:
return 200, result
+
app.route("api/", include=api.urls)
+
@app.route("/")
@csrf_exempt
def home(request):
diff --git a/src/clear_overlaps.py b/src/clear_bookings.py
similarity index 60%
rename from src/clear_overlaps.py
rename to src/clear_bookings.py
index f345d94..8fa07ac 100644
--- a/src/clear_overlaps.py
+++ b/src/clear_bookings.py
@@ -1,9 +1,11 @@
import os
+import re
import smtplib
import ssl
from datetime import datetime, date, timedelta
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
+from email.utils import format_datetime
from typing import NamedTuple
import pytz
from pprint import pprint
@@ -23,11 +25,14 @@ DavEvent = NamedTuple("DavEvent", [
("status", str),
("organizer", str),
("obj", caldav.Event),
+ ("is_cancelled", bool),
+ ("is_recurring", bool),
+ ("is_prioritized", bool),
])
-def clear_overlaps(target_calendars: list, is_test: bool=False) -> dict:
+def clear(target_calendars: list, is_test: bool=False) -> dict:
"""
- Clear overlapping events in calendars.
+ Clear overlapping and cancelled events in calendars.
:param target_calendars: List of calendars to check for overlaps.
:param is_test: Do not delete events, just print what would be deleted.
:return: Dictionary with results of the operation.
@@ -71,7 +76,6 @@ def clear_overlaps(target_calendars: list, is_test: bool=False) -> dict:
end=date.today() + timedelta(days=horizon),
event=True,
expand=True,
- #split_expanded=False,
)
events = []
@@ -81,7 +85,7 @@ def clear_overlaps(target_calendars: list, is_test: bool=False) -> dict:
# Delete cancelled non-recurring events if not in test mode
if (component.name == "VEVENT"
and is_cancelled_event(event)
- and not is_recurring_event(event)):
+ and not event.is_recurring):
if not is_test:
event.delete()
@@ -129,6 +133,9 @@ def clear_overlaps(target_calendars: list, is_test: bool=False) -> dict:
status=component.get("status", "CONFIRMED"),
organizer=component.get("organizer", "").replace("mailto:", "").strip(),
obj=event,
+ is_cancelled=is_cancelled_event(event),
+ is_recurring=is_recurring_event(event),
+ is_prioritized=is_prioritized_event(event),
)
)
@@ -140,7 +147,8 @@ def clear_overlaps(target_calendars: list, is_test: bool=False) -> dict:
event = overlap["event"]
overlaps_with = overlap["overlaps_with"]
is_deleted = False
- is_canceled = False
+ is_cancelled = False
+ is_deprioritized = any(ov.is_prioritized for ov in overlaps_with)
result = {
"event": {
"uid": event.uid,
@@ -149,51 +157,69 @@ def clear_overlaps(target_calendars: list, is_test: bool=False) -> dict:
"end": event.end,
"created": event.created,
"organizer": event.organizer,
+ "is_recurring": event.is_recurring,
+ "is_prioritized": event.is_prioritized,
+ "is_deprioritized": is_deprioritized,
+ "cancellation_type": "deleted" if is_deleted else "cancelled",
},
- "overlaps_with": {
- "uid": overlaps_with.uid,
- "name": overlaps_with.name,
- "start": overlaps_with.start,
- "end": overlaps_with.end,
- "created": overlaps_with.created,
- "organizer": overlaps_with.organizer,
- },
+ "overlaps_with": [{
+ "uid": ov.uid,
+ "name": ov.name,
+ "start": ov.start,
+ "end": ov.end,
+ "created": ov.created,
+ "organizer": ov.organizer,
+ "is_recurring": ov.is_recurring,
+ "is_prioritized": ov.is_prioritized,
+ } for ov in overlaps_with],
"calendar": calendar.id,
}
try:
# If this is not a recurring event, we can delete it directly
- if not is_recurring_event(event.obj):
+ if not event.is_recurring:
# but only if we are not in test mode
if not is_test:
event.obj.delete()
is_deleted = True
results["deleted_overlapping_events"].append(result)
print("Deleted overlapping event:")
- pprint(result)
- print("------")
- # If this is a recurring event, and not already canceled,
+ # If this is a recurring event, and not already cancelled,
# we need to cancel it now
- elif not is_cancelled_event(event.obj):
+ elif not event.is_cancelled:
if not is_test:
event.obj.decline_invite()
- is_canceled = True
+ is_cancelled = True
results["cancelled_overlapping_recurring_events"].append(result)
print("Cancelled overlapping recurring event:")
- pprint(result)
- print("------")
+
# Send email to organizer of the event if not in test mode
email_template = tcal_by_name[calendar.id].email_template
if email_template and not is_test:
try:
send_mail_to_organizer(event, overlaps_with, calendar,
- is_deleted, is_canceled,
- email_template)
+ is_deleted, is_cancelled,
+ is_deprioritized, email_template)
+ result["email_sent"] = True
except Exception as e:
print("Failed to send email to organizer for event "
f"{event.name}: {e}")
- continue
+ result["email_sent"] = False
+ elif email_template and is_test:
+ print("Email template found, but not sending email in test mode.")
+ send_mail_to_organizer(event, overlaps_with, calendar,
+ is_deleted, is_cancelled,
+ is_deprioritized, email_template,
+ is_test=True)
+ result["email_sent"] = True
+ else:
+ result["email_sent"] = False
+
+ # Print results
+ pprint(result)
+ print("------")
+
except Exception as e:
print(f"Failed to delete event {event.uid}: {e}")
print("------")
@@ -211,7 +237,6 @@ def find_overlapping_events(events: list[DavEvent]) -> dict:
as values.
"""
overlapping_events = {}
- checked_events = {}
# Order events by created time
events = sorted(events, key=lambda item: item.created)
@@ -229,25 +254,40 @@ def find_overlapping_events(events: list[DavEvent]) -> dict:
continue
for compare_event in events:
- # Skip if the compare_events is:
- # 1. The same event or belongs to the same series as the compare event
- # 2. Already checked
- # 3. Already in the overlapping events dictionary
- if event.uid == compare_event.uid \
- or checked_events.get(create_event_identifier(compare_event)) \
- or overlapping_events.get(create_event_identifier(compare_event)):
+ # Skip if the compare_event is same event or belongs to the same
+ # series as the compare event
+ if event.uid == compare_event.uid:
continue
# Check if the events overlap
if event_starts_later(event, compare_event) or \
event_starts_earlier(event, compare_event) or \
event_inbetween(event, compare_event):
+
+ # If both events are prioritized, skip the overlap
+ if compare_event.is_prioritized and event.is_prioritized:
+ print(f"Skipping overlap between prioritized events: "
+ f"'{event.name}' and '{compare_event.name}'")
+ continue
+
+ # If the compare_event is prioritized and the event is not,
+ # skip the overlap
+ elif compare_event.is_prioritized and not event.is_prioritized:
+ print(f"Skipping overlap between event '{event.name}' "
+ f"and prioritized event '{compare_event.name}'")
+ continue
+
# Add to overlapping events dictionary
- overlapping_events.update({
- create_event_identifier(compare_event): {"event": compare_event,
- "overlaps_with": event}})
- # Add to checked events dictionary
- checked_events.update({create_event_identifier(event): event})
+ event_identifier = create_event_identifier(compare_event)
+ if e := overlapping_events.get(event_identifier):
+ # If the event is already in the dictionary, extend the overlaps
+ e["overlaps_with"].append(event)
+ else:
+ # Create a new entry for the overlapping event
+ overlapping_events[event_identifier] = {
+ "event": compare_event,
+ "overlaps_with": [event]
+ }
return overlapping_events
@@ -277,22 +317,74 @@ def is_cancelled_event(event: CalendarObjectResource) -> bool:
"""
return event.icalendar_component.get("status") == "CANCELLED"
+def is_prioritized_event(event: CalendarObjectResource) -> bool:
+ """
+ Check if the event is a prioritized event by finding and validating the
+ priority token.
+ :param event:
+ :return:
+ """
+ from booking import PriorityEventToken
+ description = event.icalendar_component.get("DESCRIPTION", "")
+ uid = event.icalendar_component.get("uid")
+
+ # Check if there is a token in the event description
+ token_pattern = r"\#[A-Z1-9]{12}\#"
+ match = re.search(token_pattern, description)
+ if match:
+ token = match.group()
+ print(f"Priority token found in event description: {token}")
+ token_obj = PriorityEventToken.objects.get(token=token)
+
+ # Check if the token object exists in the database
+ if not token_obj:
+ print(f"Priority token '{token}' not found in database.")
+ return False
+
+ # If token is already used, verify signature
+ if token_obj.is_used:
+ # Check if the signature is valid
+
+ if token_obj.validate_signature(uid):
+ print(f"Priority token '{token}' is valid.")
+ return True
+ else:
+ print(f"Priority token '{token}' is invalid.")
+ # TODO: Notify about invalid token usage
+ return False
+
+ # If the token hasn't been used yet, redeem it
+ else:
+ print(f"Redeeming priority token '{token}' for event '{uid}'.")
+ token_obj.redeem(uid)
+ # TODO: Notify about token redemption
+ return True
+
+ # If no token is found, return False
+ return False
+
+
def send_mail_to_organizer(
event: DavEvent,
- overlap_event: DavEvent,
+ overlap_events: list[DavEvent],
calendar: caldav.Calendar,
is_deleted: bool,
- is_canceled: bool,
+ is_cancelled: bool,
+ is_deprioritized: bool,
email_template,
+ is_test: bool=False
):
"""
Send email to organizer of the event.
:param event: Event that was declined
- :param overlap_event: Event that overlaps with the declined event
+ :param overlap_events: List of events which overlap with the declined event
:param calendar: Calendar to send the email from
:param is_deleted: True if the event was deleted, False otherwise
- :param is_canceled: True if the event was canceled, False otherwise
+ :param is_cancelled: True if the event was cancelled, False otherwise
+ :param is_deprioritized: True if the event was deprioritized in favor of
+ another event, False otherwise
:param email_template: Email template to use for the email
+ :param is_test: If True, do not send the email, just print it
"""
# Check if environment variables for SMTP are set
if not all([
@@ -306,19 +398,24 @@ def send_mail_to_organizer(
# Prepare the email content
context = {
- "event_name": event.name,
- "event_start": event.start,
- "event_end": event.end,
- "event_created": event.created,
- "event_is_deleted": is_deleted,
- "event_is_canceled": is_canceled,
- "event_is_recurring": is_recurring_event(event.obj),
- "event_organizer": event.organizer,
- "overlap_event_name": overlap_event.name,
- "overlap_event_start": overlap_event.start,
- "overlap_event_end": overlap_event.end,
- "overlap_event_created": overlap_event.created,
- "overlap_event_organizer": overlap_event.organizer,
+ "name": event.name,
+ "start": event.start,
+ "end": event.end,
+ "created": event.created,
+ "is_deleted": is_deleted,
+ "is_cancelled": is_cancelled,
+ "is_recurring": is_recurring_event(event.obj),
+ "organizer": event.organizer,
+ "is_prioritized": event.is_prioritized,
+ "is_deprioritized": is_deprioritized,
+ "overlap_events": [{
+ "name": overlap_event.name,
+ "start": overlap_event.start,
+ "end": overlap_event.end,
+ "created": overlap_event.created,
+ "organizer": overlap_event.organizer,
+ "is_prioritized": overlap_event.is_prioritized,
+ } for overlap_event in overlap_events],
"calendar_name": calendar.get_display_name(),
}
@@ -331,7 +428,7 @@ def send_mail_to_organizer(
if bcc := os.getenv("SMTP_BCC"):
message["Bcc"] = bcc
recipients += bcc.split(",")
- message["Date"] = datetime.now().astimezone(tz).strftime( "%d/%m/%Y %H:%M")
+ message["Date"] = format_datetime(datetime.now().astimezone(tz))
# Try to render subject and body from the email template
message["Subject"] = Template(email_template.subject).render(**context)
@@ -342,22 +439,27 @@ def send_mail_to_organizer(
ssl_context = ssl.create_default_context()
# Send the email with starttls or SSL based on environment variable
- print(f"Sending email to {', '.join(recipients)} about event '{event.name}' "
- f"overlap with '{overlap_event.name}'.")
+ overlaps = ", ".join([f"'{overlap.name}'" for overlap in overlap_events])
+ print(f"Sending email to {', '.join(recipients)} with subject: {message['Subject']}:")
+ print('"""')
+ print(body)
+ print('"""')
if os.getenv('SMTP_STARTTLS', False):
print("Using STARTTLS for SMTP connection.")
- with smtplib.SMTP(os.getenv('SMTP_SERVER'),
- os.getenv('SMTP_PORT', 587)) as server:
- server.starttls(context=ssl_context)
- server.login(os.getenv("SMTP_USER_NAME", os.getenv("SMTP_EMAIL")), os.getenv("SMTP_PASSWORD"))
- server.sendmail(os.getenv("SMTP_EMAIL"), recipients, message.as_string())
+ if not is_test:
+ with smtplib.SMTP(os.getenv('SMTP_SERVER'),
+ os.getenv('SMTP_PORT', 587)) as server:
+ server.starttls(context=ssl_context)
+ server.login(os.getenv(" ", os.getenv("SMTP_EMAIL")), os.getenv("SMTP_PASSWORD"))
+ server.sendmail(os.getenv("SMTP_EMAIL"), recipients, message.as_string())
else:
print("Using SSL for SMTP connection.")
- with smtplib.SMTP_SSL("mail.extrasolar.space",
- os.getenv('SMTP_PORT', 465),
- context=ssl_context) as server:
- server.login(os.getenv("SMTP_USER_NAME", os.getenv("SMTP_EMAIL")), os.getenv("SMTP_PASSWORD"))
- server.sendmail(os.getenv("SMTP_EMAIL"), recipients, message.as_string())
+ if not is_test:
+ with smtplib.SMTP_SSL("mail.extrasolar.space",
+ os.getenv('SMTP_PORT', 465),
+ context=ssl_context) as server:
+ server.login(os.getenv("SMTP_USER_NAME", os.getenv("SMTP_EMAIL")), os.getenv("SMTP_PASSWORD"))
+ server.sendmail(os.getenv("SMTP_EMAIL"), recipients, message.as_string())
def handle_date(date_value: datetime | date) -> datetime:
"""
diff --git a/src/migrations/0010_priorityeventtoken_alter_emailtemplate_body.py b/src/migrations/0010_priorityeventtoken_alter_emailtemplate_body.py
new file mode 100644
index 0000000..5a6ffa3
--- /dev/null
+++ b/src/migrations/0010_priorityeventtoken_alter_emailtemplate_body.py
@@ -0,0 +1,32 @@
+# Generated by Django 5.1.4 on 2025-07-11 16:46
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("booking", "0009_alter_emailtemplate_body"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="PriorityEventToken",
+ fields=[
+ ("id", models.AutoField(primary_key=True, serialize=False)),
+ ("token", models.CharField(editable=False, max_length=14, unique=True)),
+ ("signature", models.CharField(blank=True, max_length=80)),
+ ("notes", models.TextField(blank=True)),
+ ("is_used", models.BooleanField(default=False)),
+ ("created_at", models.DateTimeField(auto_now_add=True)),
+ ("updated_at", models.DateTimeField(auto_now=True)),
+ ],
+ ),
+ migrations.AlterField(
+ model_name="emailtemplate",
+ name="body",
+ field=models.TextField(
+ help_text=" Email template to use for sending notifications about deleted events.
\n If not set, no email will be sent.
\n Jinja2 template syntax can be used in the subject and body.
\n Available variables:
\n - name: The name of the event
\n - start: The start time (datetime) of the event
\n - end: The end time (datetime) of the event
\n - created: The datetime the event was created
\n - is_deleted: Whether the event was deleted
\n - is_cancelled: Whether the event was cancelled
\n - is_recurring: Whether the event is recurring
\n - organizer: The organizer of the event
\n - is_prioritized: Whether the event is prioritized
\n - overlap_events: List of overlap events, each with:
\n - name: The name of the overlap event
\n - start: The start time (datetime) of the overlap event
\n - end: The end time (datetime) of the overlap event
\n - created: The datetime the overlap event was created
\n - organizer: The organizer of the overlap event
\n - is_prioritized: Whether the overlap event is prioritized
\n - calendar_name: The name of the calendar"
+ ),
+ ),
+ ]
diff --git a/src/migrations/0011_alter_emailtemplate_body_and_more.py b/src/migrations/0011_alter_emailtemplate_body_and_more.py
new file mode 100644
index 0000000..47d0259
--- /dev/null
+++ b/src/migrations/0011_alter_emailtemplate_body_and_more.py
@@ -0,0 +1,25 @@
+# Generated by Django 5.1.4 on 2025-07-14 09:09
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("booking", "0010_priorityeventtoken_alter_emailtemplate_body"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="emailtemplate",
+ name="body",
+ field=models.TextField(
+ help_text=" Email template to use for sending notifications about deleted events.
\n If not set, no email will be sent.
\n Jinja2 template syntax can be used in the subject and body.
\n Available variables:
\n - name: The name of the event
\n - start: The start time (datetime) of the event
\n - end: The end time (datetime) of the event
\n - created: The datetime the event was created
\n - is_deleted: Whether the event was deleted
\n - is_cancelled: Whether the event was cancelled
\n - is_recurring: Whether the event is recurring
\n - organizer: The organizer of the event
\n - is_prioritized: Whether the event is prioritized
\n - is_deprioritized: Whether the event was deprioritized in favor of another event
\n - overlap_events: List of overlap events, each with:
\n - name: The name of the overlap event
\n - start: The start time (datetime) of the overlap event
\n - end: The end time (datetime) of the overlap event
\n - created: The datetime the overlap event was created
\n - organizer: The organizer of the overlap event
\n - is_prioritized: Whether the overlap event is prioritized
\n - calendar_name: The name of the calendar"
+ ),
+ ),
+ migrations.AlterField(
+ model_name="priorityeventtoken",
+ name="signature",
+ field=models.CharField(blank=True, editable=False, max_length=80),
+ ),
+ ]