Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/apps/owasp/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from .project import ProjectAdmin
from .project_health_metrics import ProjectHealthMetricsAdmin
from .snapshot import SnapshotAdmin
from .snapshot_subscription import SnapshotSubscriptionAdmin
from .sponsor import SponsorAdmin

admin.site.register(ProjectHealthRequirements)
44 changes: 44 additions & 0 deletions backend/apps/owasp/admin/snapshot_subscription.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Admin registration for SnapshotSubscription model."""

from django.contrib import admin

from apps.owasp.models.snapshot_subscription import SnapshotSubscription


class SnapshotSubscriptionAdmin(admin.ModelAdmin):
"""Admin for SnapshotSubscription model."""

list_display = ("user", "frequency", "is_active", "created_at", "updated_at")
Comment thread
HarshitVerma109 marked this conversation as resolved.
list_filter = ("frequency", "is_active", "created_at")
search_fields = ("user__email", "user__username")
raw_id_fields = ("user",)
readonly_fields = ("unsubscribe_token", "created_at", "updated_at")

fieldsets = (
(None, {"fields": ("user", "frequency", "is_active")}),
(
"Content Preferences",
{
"fields": (
"include_chapters",
"include_events",
"include_issues",
"include_posts",
"include_projects",
"include_pull_requests",
"include_releases",
"include_users",
),
},
),
(
"System",
{
"fields": ("unsubscribe_token", "created_at", "updated_at"),
"classes": ("collapse",),
},
),
)


admin.site.register(SnapshotSubscription, SnapshotSubscriptionAdmin)
66 changes: 66 additions & 0 deletions backend/apps/owasp/migrations/0075_snapshotsubscription.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Generated by Django 6.0.6 on 2026-06-16 14:45

import uuid

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("owasp", "0074_rename_snapshot_m2m_fields"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="SnapshotSubscription",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
(
"frequency",
models.CharField(
choices=[("weekly", "Weekly"), ("monthly", "Monthly")],
default="weekly",
max_length=10,
),
),
("is_active", models.BooleanField(default=True)),
(
"unsubscribe_token",
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
),
("include_chapters", models.BooleanField(default=True)),
("include_events", models.BooleanField(default=True)),
("include_issues", models.BooleanField(default=True)),
("include_posts", models.BooleanField(default=True)),
("include_projects", models.BooleanField(default=True)),
("include_pull_requests", models.BooleanField(default=True)),
("include_releases", models.BooleanField(default=True)),
("include_users", models.BooleanField(default=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="snapshot_subscription",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name_plural": "Snapshot Subscriptions",
"db_table": "owasp_snapshot_subscriptions",
"indexes": [
models.Index(fields=["is_active"], name="owasp_sub_active_idx"),
],
},
),
]
1 change: 1 addition & 0 deletions backend/apps/owasp/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@
from .project_health_metrics import ProjectHealthMetrics
from .project_health_requirements import ProjectHealthRequirements
from .snapshot import Snapshot
from .snapshot_subscription import SnapshotSubscription
from .sponsor import Sponsor
85 changes: 85 additions & 0 deletions backend/apps/owasp/models/snapshot_subscription.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""OWASP app snapshot subscription model."""

import uuid

from django.db import models

from apps.nest.models import User


class SnapshotSubscription(models.Model):
"""Model representing a user's subscription to snapshot digest emails."""

class Meta:
"""Model options."""

db_table = "owasp_snapshot_subscriptions"
verbose_name_plural = "Snapshot Subscriptions"
indexes = [
models.Index(fields=["is_active"], name="owasp_sub_active_idx"),
]
Comment thread
coderabbitai[bot] marked this conversation as resolved.

class Frequency(models.TextChoices):
"""Subscription frequency choices."""

WEEKLY = "weekly", "Weekly"
MONTHLY = "monthly", "Monthly"

class Status(models.TextChoices):
Comment thread
HarshitVerma109 marked this conversation as resolved.
"""Subscription status choices."""

ACTIVE = "active", "Active"
INACTIVE = "inactive", "Inactive"

user = models.OneToOneField(
User,
on_delete=models.CASCADE,
related_name="snapshot_subscription",
)
frequency = models.CharField(
max_length=10,
choices=Frequency.choices,
default=Frequency.WEEKLY,
)
is_active = models.BooleanField(default=True)
unsubscribe_token = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
)

# Content preferences
include_chapters = models.BooleanField(default=True)
include_events = models.BooleanField(default=True)
include_issues = models.BooleanField(default=True)
include_posts = models.BooleanField(default=True)
include_projects = models.BooleanField(default=True)
include_pull_requests = models.BooleanField(default=True)
include_releases = models.BooleanField(default=True)
include_users = models.BooleanField(default=True)

created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

def __str__(self):
"""Return a string representation."""
status = (
SnapshotSubscription.Status.ACTIVE
if self.is_active
else SnapshotSubscription.Status.INACTIVE
)
return f"{self.user} ({self.frequency}, {status})"

@property
def content_preferences(self):
"""Return a dictionary of content preference settings."""
return {
"chapters": self.include_chapters,
"events": self.include_events,
"issues": self.include_issues,
"posts": self.include_posts,
"projects": self.include_projects,
"pull_requests": self.include_pull_requests,
"releases": self.include_releases,
"users": self.include_users,
}
Binary file modified backend/data/nest.dump
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Tests for snapshot subscription admin."""

from django.contrib.admin.sites import AdminSite

from apps.owasp.admin.snapshot_subscription import SnapshotSubscriptionAdmin
from apps.owasp.models.snapshot_subscription import SnapshotSubscription


class TestSnapshotSubscriptionAdmin:
"""Test SnapshotSubscriptionAdmin configuration."""

def test_admin_configuration(self):
"""Test admin configuration matches expected setup."""
site = AdminSite()
admin = SnapshotSubscriptionAdmin(SnapshotSubscription, site)

assert admin.list_display == (
"user",
"frequency",
"is_active",
"created_at",
"updated_at",
)
assert admin.list_filter == ("frequency", "is_active", "created_at")
assert admin.search_fields == ("user__email", "user__username")
assert admin.raw_id_fields == ("user",)
assert admin.readonly_fields == ("unsubscribe_token", "created_at", "updated_at")

# Check fieldsets structure
assert len(admin.fieldsets) == 3

# Check Content Preferences fieldset
preferences_fieldset = admin.fieldsets[1]
assert preferences_fieldset[0] == "Content Preferences"

assert preferences_fieldset[1]["fields"] == (
"include_chapters",
"include_events",
"include_issues",
"include_posts",
"include_projects",
"include_pull_requests",
"include_releases",
"include_users",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Tests for snapshot subscription model."""

import uuid
from unittest.mock import MagicMock

from django.test import SimpleTestCase

from apps.owasp.models.snapshot_subscription import SnapshotSubscription


class TestSnapshotSubscription(SimpleTestCase):
"""Test SnapshotSubscription model."""

def test_str_representation_active(self):
"""Test string representation for active subscription."""
sub = MagicMock(spec=SnapshotSubscription)
sub.user = MagicMock()
sub.frequency = SnapshotSubscription.Frequency.WEEKLY
sub.is_active = True

result = SnapshotSubscription.__str__(sub)
assert result == f"{sub.user} (weekly, active)"

def test_str_representation_inactive(self):
"""Test string representation for inactive subscription."""
sub = MagicMock(spec=SnapshotSubscription)
sub.user = MagicMock()
sub.frequency = SnapshotSubscription.Frequency.MONTHLY
sub.is_active = False

result = SnapshotSubscription.__str__(sub)
assert result == f"{sub.user} (monthly, inactive)"

def test_content_preferences_all_defaults(self):
"""Test that content_preferences returns all True by default."""
sub = SnapshotSubscription()
prefs = sub.content_preferences
assert prefs == {
"chapters": True,
"events": True,
"issues": True,
"posts": True,
Comment thread
HarshitVerma109 marked this conversation as resolved.
"projects": True,
"pull_requests": True,
"releases": True,
"users": True,
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

def test_content_preferences_custom(self):
"""Test content_preferences with custom values."""
sub = MagicMock(spec=SnapshotSubscription)
sub.include_chapters = False
sub.include_events = True
sub.include_issues = True
sub.include_posts = False
sub.include_projects = False
sub.include_pull_requests = True
sub.include_releases = True
sub.include_users = True

prefs = SnapshotSubscription.content_preferences.fget(sub)
assert prefs == {
"chapters": False,
"events": True,
"issues": True,
"posts": False,
"projects": False,
"pull_requests": True,
"releases": True,
"users": True,
}

def test_frequency_choices(self):
"""Test frequency choices are correctly defined."""
assert SnapshotSubscription.Frequency.WEEKLY == "weekly"
assert SnapshotSubscription.Frequency.MONTHLY == "monthly"
Comment thread
HarshitVerma109 marked this conversation as resolved.

def test_unsubscribe_token_defaults(self):
"""Test that unsubscribe_token is a unique UUID for each instance."""
first = SnapshotSubscription()
second = SnapshotSubscription()

assert isinstance(first.unsubscribe_token, uuid.UUID)
assert isinstance(second.unsubscribe_token, uuid.UUID)
assert first.unsubscribe_token != second.unsubscribe_token
Loading