(back) add CalendarSubscriptionToken model

Add model for storing calendar subscription tokens with
secure token generation and expiration handling for
iCal/CalDAV subscription URLs.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Nathan Panchout
2026-01-25 20:32:48 +01:00
parent 08fbcc8cc5
commit 0cb9b40530
3 changed files with 129 additions and 0 deletions

View File

@@ -91,3 +91,32 @@ class UserAdmin(auth_admin.UserAdmin):
"updated_at",
)
search_fields = ("id", "sub", "admin_email", "email", "full_name")
@admin.register(models.Calendar)
class CalendarAdmin(admin.ModelAdmin):
"""Admin class for Calendar model."""
list_display = ("name", "owner", "is_default", "is_visible", "created_at")
list_filter = ("is_default", "is_visible")
search_fields = ("name", "owner__email", "caldav_path")
readonly_fields = ("id", "created_at", "updated_at")
@admin.register(models.CalendarSubscriptionToken)
class CalendarSubscriptionTokenAdmin(admin.ModelAdmin):
"""Admin class for CalendarSubscriptionToken model."""
list_display = (
"calendar_name",
"owner",
"caldav_path",
"token",
"is_active",
"last_accessed_at",
"created_at",
)
list_filter = ("is_active",)
search_fields = ("calendar_name", "owner__email", "caldav_path", "token")
readonly_fields = ("id", "token", "created_at", "last_accessed_at")
raw_id_fields = ("owner",)

View File

@@ -26,3 +26,34 @@ class UserFactory(factory.django.DjangoModelFactory):
short_name = factory.Faker("first_name")
language = factory.fuzzy.FuzzyChoice([lang[0] for lang in settings.LANGUAGES])
password = make_password("password")
class CalendarFactory(factory.django.DjangoModelFactory):
"""A factory to create calendars for testing purposes."""
class Meta:
model = models.Calendar
owner = factory.SubFactory(UserFactory)
name = factory.Faker("sentence", nb_words=3)
color = factory.Faker("hex_color")
description = factory.Faker("paragraph")
is_default = False
is_visible = True
caldav_path = factory.LazyAttribute(
lambda obj: f"/calendars/{obj.owner.email}/{fake.uuid4()}"
)
class CalendarSubscriptionTokenFactory(factory.django.DjangoModelFactory):
"""A factory to create calendar subscription tokens for testing purposes."""
class Meta:
model = models.CalendarSubscriptionToken
owner = factory.SubFactory(UserFactory)
caldav_path = factory.LazyAttribute(
lambda obj: f"/calendars/{obj.owner.email}/{fake.uuid4()}/"
)
calendar_name = factory.Faker("sentence", nb_words=3)
is_active = True

View File

@@ -400,3 +400,72 @@ class CalendarShare(models.Model):
def __str__(self):
return f"{self.calendar.name} shared with {self.shared_with.email}"
class CalendarSubscriptionToken(models.Model):
"""
Stores subscription tokens for iCal export.
Each calendar can have one token that allows unauthenticated read-only access
via a public URL for use in external calendar applications.
This model is standalone and stores the CalDAV path directly,
without requiring a foreign key to the Calendar model.
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
# Owner of the calendar (for permission verification)
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="subscription_tokens",
)
# CalDAV path stored directly (e.g., /calendars/user@example.com/uuid/)
caldav_path = models.CharField(
max_length=512,
help_text=_("CalDAV path of the calendar"),
)
# Calendar display name (for UI display)
calendar_name = models.CharField(
max_length=255,
blank=True,
default="",
help_text=_("Display name of the calendar"),
)
token = models.UUIDField(
unique=True,
db_index=True,
default=uuid.uuid4,
help_text=_("Secret token used in the subscription URL"),
)
is_active = models.BooleanField(
default=True,
help_text=_("Whether this subscription token is active"),
)
last_accessed_at = models.DateTimeField(
null=True,
blank=True,
help_text=_("Last time this subscription URL was accessed"),
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = _("calendar subscription token")
verbose_name_plural = _("calendar subscription tokens")
constraints = [
models.UniqueConstraint(
fields=["owner", "caldav_path"],
name="unique_token_per_owner_calendar",
)
]
indexes = [
# Composite index for the public iCal endpoint query:
# CalendarSubscriptionToken.objects.filter(token=..., is_active=True)
models.Index(fields=["token", "is_active"], name="token_active_idx"),
]
def __str__(self):
return f"Subscription token for {self.calendar_name or self.caldav_path}"