✨(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:
@@ -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",)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user