Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
81 changes: 81 additions & 0 deletions backend/apps/owasp/models/snapshot_subscription.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""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 = "active" if self.is_active else "inactive"
Comment thread
HarshitVerma109 marked this conversation as resolved.
Outdated
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,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""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"

preferences_fields = preferences_fieldset[1]["fields"]
assert "include_chapters" in preferences_fields
assert "include_events" in preferences_fields
assert "include_issues" in preferences_fields
assert "include_posts" in preferences_fields
assert "include_projects" in preferences_fields
assert "include_pull_requests" in preferences_fields
assert "include_releases" in preferences_fields
assert "include_users" in preferences_fields
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Tests for snapshot subscription model."""

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.
Loading