✨(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",
|
"updated_at",
|
||||||
)
|
)
|
||||||
search_fields = ("id", "sub", "admin_email", "email", "full_name")
|
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")
|
short_name = factory.Faker("first_name")
|
||||||
language = factory.fuzzy.FuzzyChoice([lang[0] for lang in settings.LANGUAGES])
|
language = factory.fuzzy.FuzzyChoice([lang[0] for lang in settings.LANGUAGES])
|
||||||
password = make_password("password")
|
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):
|
def __str__(self):
|
||||||
return f"{self.calendar.name} shared with {self.shared_with.email}"
|
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