Compare commits

..

8 Commits
1.1.0 ... main

Author SHA1 Message Date
Marc Koch 0b3edff6f0
🔖 Version 1.2.0 2025-07-14 20:55:02 +02:00
Marc Koch 76c2bf1631
♻️ reorders imports 2025-07-14 20:15:56 +02:00
Marc Koch e3e9ac3c58
💄show duration in event list display 2025-07-14 20:13:31 +02:00
Marc Koch 14dd63ad72
🐛 fix: declined events not cancelled
Events were declined but not cancelled what lead to a loop of allready declined events getting declined again in every run.
2025-07-14 19:54:20 +02:00
Marc Koch f053f4456f
💄 improve list display of calendars amd events
Enhances the calendar admin interface by adding more relevant
fields to the list display, such as associated users,
auto-clearing settings, and the email template used.

Also improves the event admin by including calendar, creation
date, start, and end times to the list display,
and orders events by calendar in addition to start and end times.
2025-07-14 19:52:51 +02:00
Marc Koch 3fa2763ebf
🐛 fix: corrects placement of cancellation type assignment 2025-07-14 16:40:32 +02:00
Marc Koch c3b1d42cef
Merge branch 'priority_events' 2025-07-14 15:41:53 +02:00
Marc Koch 4bd038bf10
adds prioritized event support
This commit introduces support for prioritized events, allowing certain events to take precedence over others during overlap resolution.

This is achieved by:
- Introducing a `PriorityEventToken` model to manage tokens for priority events.
- Modifying the overlap resolution logic to skip overlaps if a prioritized event is involved.
- Updating email templates to include information about prioritization and overlap events.
- Adding a check for priority tokens in event descriptions.
2025-07-14 15:41:04 +02:00
6 changed files with 395 additions and 106 deletions

View File

@ -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

View File

@ -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,25 +79,33 @@ class EmailTemplate(models.Model):
If not set, no email will be sent.<br>
Jinja2 template syntax can be used in the subject and body.<br>
Available variables:<br>
- event_name: The name of the event<br>
- event_start: The start time (datetime) of the event<br>
- event_end: The end time (datetime) of the event<br>
- event_created: The datetime the event was created<br>
- event_is_deleted: Whether the event was deleted<br>
- event_is_cancelled: Whether the event was cancelled<br>
- event_is_recurring: Whether the event is recurring<br>
- event_organizer: The organizer of the event<br>
- overlap_event_name: The name of the overlap event<br>
- overlap_event_start: The start time (datetime) of the overlap event<br>
- overlap_event_end: The end time (datetime) of the overlap event<br>
- overlap_event_created: The datetime the overlap event was created<br>
- overlap_event_organizer: The organizer of the overlap event<br>
- name: The name of the event<br>
- start: The start time (datetime) of the event<br>
- end: The end time (datetime) of the event<br>
- created: The datetime the event was created<br>
- is_deleted: Whether the event was deleted<br>
- is_cancelled: Whether the event was cancelled<br>
- is_recurring: Whether the event is recurring<br>
- organizer: The organizer of the event<br>
- is_prioritized: Whether the event is prioritized<br>
- is_deprioritized: Whether the event was deprioritized in favor of another event<br>
- overlap_events: List of overlap events, each with:<br>
- name: The name of the overlap event<br>
- start: The start time (datetime) of the overlap event<br>
- end: The end time (datetime) of the overlap event<br>
- created: The datetime the overlap event was created<br>
- organizer: The organizer of the overlap event<br>
- is_prioritized: Whether the overlap event is prioritized<br>
- calendar_name: The name of the calendar""")
def __str__(self):
return self.name
@app.admin
@app.admin(
list_display=("name", "all_users", "auto_clear_bookings",
"auto_clear_overlap_horizon_days", "email_template"),
list_filter=("auto_clear_bookings",))
class Calendar(models.Model):
"""
A calendar model to store events.
@ -130,8 +140,19 @@ class Calendar(models.Model):
def __str__(self):
return self.name
def all_users(self) -> str:
"""
Get the users of the calendar.
It's kind of inefficient, but in small databases it should be fine.
:return: A list of users.
"""
return ", ".join([u.username for u in self.users.all()])
@app.admin(ordering=("start", "end", "name"), list_filter=("cancelled",))
@app.admin(ordering=("start", "end", "calendar", "name"),
list_filter=("cancelled",),
list_display=("name", "calendar", "created", "duration", "start",
"end"))
class Event(models.Model):
"""
Event model to store events in a calendar and send them to a CalDAV server.
@ -222,9 +243,105 @@ class Event(models.Model):
def uuid(self) -> UUID:
return shortuuid.decode(self.id.__str__())
def duration(self) -> str:
"""
Calculate the duration of the event.
:return: The duration in minutes.
"""
duration = (self.end - self.start).total_seconds() // 60
return f"{duration:.0f}m"
def __str__(self):
string = f"{self.id} - {self.name} - ({self.start:%Y-%m-%d %H:%M} - {self.end:%Y-%m-%d %H:%M})"
return string if not self.cancelled else f"{string} - CANCELLED"
return self.name
@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',))
@ -341,7 +458,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 +468,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 +481,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):

View File

@ -1,14 +1,16 @@
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 typing import NamedTuple
import pytz
from email.utils import format_datetime
from pprint import pprint
from typing import NamedTuple
import caldav
import pytz
from caldav import CalendarObjectResource
from jinja2 import Template
@ -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,7 @@ def clear_overlaps(target_calendars: list, is_test: bool=False) -> dict:
end=date.today() + timedelta(days=horizon),
event=True,
expand=True,
#split_expanded=False,
split_expanded=True,
)
events = []
@ -129,6 +134,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 +148,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 +158,72 @@ 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,
},
"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
result["cancellation_type"] = "deleted"
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
event.obj.icalendar_component["status"] = "CANCELLED"
event.obj.save()
is_cancelled = True
result["cancellation_type"] = "cancelled"
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 +241,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 +258,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 +321,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 +402,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 +432,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 +443,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:
"""

View File

@ -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.<br>\n If not set, no email will be sent.<br>\n Jinja2 template syntax can be used in the subject and body.<br>\n Available variables:<br>\n - name: The name of the event<br>\n - start: The start time (datetime) of the event<br>\n - end: The end time (datetime) of the event<br>\n - created: The datetime the event was created<br>\n - is_deleted: Whether the event was deleted<br>\n - is_cancelled: Whether the event was cancelled<br>\n - is_recurring: Whether the event is recurring<br>\n - organizer: The organizer of the event<br>\n - is_prioritized: Whether the event is prioritized<br>\n - overlap_events: List of overlap events, each with:<br>\n - name: The name of the overlap event<br>\n - start: The start time (datetime) of the overlap event<br>\n - end: The end time (datetime) of the overlap event<br>\n - created: The datetime the overlap event was created<br>\n - organizer: The organizer of the overlap event<br>\n - is_prioritized: Whether the overlap event is prioritized<br>\n - calendar_name: The name of the calendar"
),
),
]

View File

@ -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.<br>\n If not set, no email will be sent.<br>\n Jinja2 template syntax can be used in the subject and body.<br>\n Available variables:<br>\n - name: The name of the event<br>\n - start: The start time (datetime) of the event<br>\n - end: The end time (datetime) of the event<br>\n - created: The datetime the event was created<br>\n - is_deleted: Whether the event was deleted<br>\n - is_cancelled: Whether the event was cancelled<br>\n - is_recurring: Whether the event is recurring<br>\n - organizer: The organizer of the event<br>\n - is_prioritized: Whether the event is prioritized<br>\n - is_deprioritized: Whether the event was deprioritized in favor of another event<br>\n - overlap_events: List of overlap events, each with:<br>\n - name: The name of the overlap event<br>\n - start: The start time (datetime) of the overlap event<br>\n - end: The end time (datetime) of the overlap event<br>\n - created: The datetime the overlap event was created<br>\n - organizer: The organizer of the overlap event<br>\n - is_prioritized: Whether the overlap event is prioritized<br>\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),
),
]

View File

@ -1 +1 @@
1.1.0
1.2.0