Skip to content
Draft
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")
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)
10 changes: 10 additions & 0 deletions backend/apps/owasp/api/internal/mutations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""OWASP app mutations."""

import strawberry

from .snapshot_subscription import SnapshotSubscriptionMutations


@strawberry.type
class OwaspMutations(SnapshotSubscriptionMutations):
"""OWASP mutations."""
188 changes: 188 additions & 0 deletions backend/apps/owasp/api/internal/mutations/snapshot_subscription.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
"""OWASP snapshot subscription GraphQL mutations."""

import strawberry
from strawberry.types import Info

from apps.nest.api.internal.permissions import IsAuthenticated
from apps.owasp.api.internal.nodes.snapshot_subscription import SnapshotSubscriptionNode
from apps.owasp.models.snapshot_subscription import SnapshotSubscription


@strawberry.input
class CreateSnapshotSubscriptionInput:
"""Input for creating a snapshot subscription."""

frequency: str = "weekly"
include_chapters: bool = True
include_events: bool = True
include_issues: bool = True
include_posts: bool = True
include_projects: bool = True
include_pull_requests: bool = True
include_releases: bool = True
include_users: bool = True


@strawberry.input
class UpdateSnapshotSubscriptionInput:
"""Input for updating a snapshot subscription."""

frequency: str | None = None
include_chapters: bool | None = None
include_events: bool | None = None
include_issues: bool | None = None
include_posts: bool | None = None
include_projects: bool | None = None
include_pull_requests: bool | None = None
include_releases: bool | None = None
include_users: bool | None = None


@strawberry.type
class SnapshotSubscriptionResult:
"""Result payload for snapshot subscription mutations."""

ok: bool
message: str
subscription: SnapshotSubscriptionNode | None = None


@strawberry.type
class SnapshotSubscriptionMutations:
"""GraphQL mutations for snapshot subscription management."""

@strawberry.mutation(permission_classes=[IsAuthenticated])
def create_snapshot_subscription(
self,
info: Info,
input_data: CreateSnapshotSubscriptionInput,
) -> SnapshotSubscriptionResult:
"""Create a new snapshot subscription for the logged-in user."""
user = info.context.request.user

if SnapshotSubscription.objects.filter(user=user).exists():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Canceled users cannot resubscribe because create checks existence instead of active state/reactivation logic. This leaves users permanently inactive through GraphQL once they cancel.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/apps/owasp/api/internal/mutations/snapshot_subscription.py, line 63:

<comment>Canceled users cannot resubscribe because create checks existence instead of active state/reactivation logic. This leaves users permanently inactive through GraphQL once they cancel.</comment>

<file context>
@@ -0,0 +1,188 @@
+        """Create a new snapshot subscription for the logged-in user."""
+        user = info.context.request.user
+
+        if SnapshotSubscription.objects.filter(user=user).exists():
+            return SnapshotSubscriptionResult(
+                ok=False,
</file context>

return SnapshotSubscriptionResult(
ok=False,
message="Subscription already exists.",
)

if input_data.frequency not in dict(SnapshotSubscription.Frequency.choices):
return SnapshotSubscriptionResult(
ok=False,
message="Invalid frequency. Must be 'weekly' or 'monthly'.",
)

subscription = SnapshotSubscription.objects.create(
user=user,
frequency=input_data.frequency,
include_chapters=input_data.include_chapters,
include_events=input_data.include_events,
include_issues=input_data.include_issues,
include_posts=input_data.include_posts,
include_projects=input_data.include_projects,
include_pull_requests=input_data.include_pull_requests,
include_releases=input_data.include_releases,
include_users=input_data.include_users,
)
Comment on lines +63 to +86

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Canceled users cannot subscribe again, and concurrent creates can still 500.

Because cancel/unsubscribe only flips is_active to False, this exists() check permanently blocks re-subscription for that user. It also leaves a race window before create() against the model’s one-to-one constraint, so two overlapping requests can both pass Line 63 and one will fail with an unhandled integrity error.

Proposed fix
+from django.db import IntegrityError, transaction
+
     `@strawberry.mutation`(permission_classes=[IsAuthenticated])
     def create_snapshot_subscription(
         self,
         info: Info,
         input_data: CreateSnapshotSubscriptionInput,
     ) -> SnapshotSubscriptionResult:
         """Create a new snapshot subscription for the logged-in user."""
         user = info.context.request.user
-
-        if SnapshotSubscription.objects.filter(user=user).exists():
-            return SnapshotSubscriptionResult(
-                ok=False,
-                message="Subscription already exists.",
-            )
 
         if input_data.frequency not in dict(SnapshotSubscription.Frequency.choices):
             return SnapshotSubscriptionResult(
                 ok=False,
                 message="Invalid frequency. Must be 'weekly' or 'monthly'.",
             )
 
-        subscription = SnapshotSubscription.objects.create(
-            user=user,
-            frequency=input_data.frequency,
-            include_chapters=input_data.include_chapters,
-            include_events=input_data.include_events,
-            include_issues=input_data.include_issues,
-            include_posts=input_data.include_posts,
-            include_projects=input_data.include_projects,
-            include_pull_requests=input_data.include_pull_requests,
-            include_releases=input_data.include_releases,
-            include_users=input_data.include_users,
-        )
+        defaults = {
+            "frequency": input_data.frequency,
+            "include_chapters": input_data.include_chapters,
+            "include_events": input_data.include_events,
+            "include_issues": input_data.include_issues,
+            "include_posts": input_data.include_posts,
+            "include_projects": input_data.include_projects,
+            "include_pull_requests": input_data.include_pull_requests,
+            "include_releases": input_data.include_releases,
+            "include_users": input_data.include_users,
+            "is_active": True,
+        }
+
+        try:
+            with transaction.atomic():
+                subscription, created = SnapshotSubscription.objects.get_or_create(
+                    user=user,
+                    defaults=defaults,
+                )
+        except IntegrityError:
+            return SnapshotSubscriptionResult(
+                ok=False,
+                message="Subscription already exists.",
+            )
+
+        if not created:
+            for field_name, value in defaults.items():
+                setattr(subscription, field_name, value)
+            subscription.save()
 
         return SnapshotSubscriptionResult(
             ok=True,
             message="Subscription created successfully.",
             subscription=subscription,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if SnapshotSubscription.objects.filter(user=user).exists():
return SnapshotSubscriptionResult(
ok=False,
message="Subscription already exists.",
)
if input_data.frequency not in dict(SnapshotSubscription.Frequency.choices):
return SnapshotSubscriptionResult(
ok=False,
message="Invalid frequency. Must be 'weekly' or 'monthly'.",
)
subscription = SnapshotSubscription.objects.create(
user=user,
frequency=input_data.frequency,
include_chapters=input_data.include_chapters,
include_events=input_data.include_events,
include_issues=input_data.include_issues,
include_posts=input_data.include_posts,
include_projects=input_data.include_projects,
include_pull_requests=input_data.include_pull_requests,
include_releases=input_data.include_releases,
include_users=input_data.include_users,
)
user = info.context.request.user
if input_data.frequency not in dict(SnapshotSubscription.Frequency.choices):
return SnapshotSubscriptionResult(
ok=False,
message="Invalid frequency. Must be 'weekly' or 'monthly'.",
)
defaults = {
"frequency": input_data.frequency,
"include_chapters": input_data.include_chapters,
"include_events": input_data.include_events,
"include_issues": input_data.include_issues,
"include_posts": input_data.include_posts,
"include_projects": input_data.include_projects,
"include_pull_requests": input_data.include_pull_requests,
"include_releases": input_data.include_releases,
"include_users": input_data.include_users,
"is_active": True,
}
try:
with transaction.atomic():
subscription, created = SnapshotSubscription.objects.get_or_create(
user=user,
defaults=defaults,
)
except IntegrityError:
return SnapshotSubscriptionResult(
ok=False,
message="Subscription already exists.",
)
if not created:
for field_name, value in defaults.items():
setattr(subscription, field_name, value)
subscription.save()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/apps/owasp/api/internal/mutations/snapshot_subscription.py` around
lines 63 - 86, The subscription creation logic in SnapshotSubscription mutation
is checking only SnapshotSubscription.objects.filter(user=user).exists(), which
blocks re-subscription after cancel and still allows a race before create().
Update the check to distinguish active vs inactive subscriptions (for example,
based on is_active in the SnapshotSubscription model) so canceled users can
subscribe again, and make the create path resilient to concurrent requests by
handling the one-to-one constraint failure around the subscription creation in
the mutation that builds the SnapshotSubscriptionResult.


return SnapshotSubscriptionResult(
ok=True,
message="Subscription created successfully.",
subscription=subscription,
)

@strawberry.mutation(permission_classes=[IsAuthenticated])
def update_snapshot_subscription(
self,
info: Info,
input_data: UpdateSnapshotSubscriptionInput,
) -> SnapshotSubscriptionResult:
"""Update the logged-in user's snapshot subscription."""
user = info.context.request.user

try:
subscription = SnapshotSubscription.objects.get(user=user)
except SnapshotSubscription.DoesNotExist:
return SnapshotSubscriptionResult(
ok=False,
message="Subscription not found.",
)

if input_data.frequency is not None:
if input_data.frequency not in dict(SnapshotSubscription.Frequency.choices):
return SnapshotSubscriptionResult(
ok=False,
message="Invalid frequency. Must be 'weekly' or 'monthly'.",
)
subscription.frequency = input_data.frequency

fields = {
"include_chapters": input_data.include_chapters,
"include_events": input_data.include_events,
"include_issues": input_data.include_issues,
"include_posts": input_data.include_posts,
"include_projects": input_data.include_projects,
"include_pull_requests": input_data.include_pull_requests,
"include_releases": input_data.include_releases,
"include_users": input_data.include_users,
}

for field_name, value in fields.items():
if value is not None:
setattr(subscription, field_name, value)

subscription.save()

return SnapshotSubscriptionResult(
ok=True,
message="Subscription updated successfully.",
subscription=subscription,
)

@strawberry.mutation(permission_classes=[IsAuthenticated])
def cancel_snapshot_subscription(self, info: Info) -> SnapshotSubscriptionResult:
"""Cancel the logged-in user's snapshot subscription."""
user = info.context.request.user

try:
subscription = SnapshotSubscription.objects.get(user=user)
except SnapshotSubscription.DoesNotExist:
return SnapshotSubscriptionResult(
ok=False,
message="Subscription not found.",
)

subscription.is_active = False
subscription.save()

return SnapshotSubscriptionResult(
ok=True,
message="Subscription cancelled successfully.",
subscription=subscription,
)

@strawberry.mutation
def unsubscribe_by_token(self, token: str) -> SnapshotSubscriptionResult:
"""Unsubscribe using a token from an email link. No auth required."""
try:
subscription = SnapshotSubscription.objects.get(unsubscribe_token=token)
except (SnapshotSubscription.DoesNotExist, ValueError):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: unsubscribe_by_token misses ValidationError handling for malformed UUID tokens. Invalid token format can raise an unhandled exception and return a GraphQL error.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/apps/owasp/api/internal/mutations/snapshot_subscription.py, line 169:

<comment>`unsubscribe_by_token` misses `ValidationError` handling for malformed UUID tokens. Invalid token format can raise an unhandled exception and return a GraphQL error.</comment>

<file context>
@@ -0,0 +1,188 @@
+        """Unsubscribe using a token from an email link. No auth required."""
+        try:
+            subscription = SnapshotSubscription.objects.get(unsubscribe_token=token)
+        except (SnapshotSubscription.DoesNotExist, ValueError):
+            return SnapshotSubscriptionResult(
+                ok=False,
</file context>

return SnapshotSubscriptionResult(
ok=False,
message="Invalid unsubscribe token.",
)
Comment on lines +167 to +173

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

python3 - <<'PY'
import os
import sys
import django
from django.conf import settings

# Minimal Django setup
if not settings.configured:
    settings.configure(
        DEBUG=True,
        DATABASES={
            'default': {
                'ENGINE': 'django.db.backends.sqlite3',
                'NAME': ':memory:',
            }
        },
        INSTALLED_APPS=[
            'django.contrib.contenttypes',
            'django.contrib.auth',
        ],
        USE_TZ=True,
        SECRET_KEY='test-secret',
    )
    django.setup()

from uuid import UUID
from django.core.exceptions import ValidationError
from django.db import models

class TestModel(models.Model):
    uuid_field = models.UUIDField()
    class Meta:
        app_label = 'test'

# Test the to_python method of UUIDField
field = TestModel._meta.get_field('uuid_field')

# Valid UUID
try:
    result = field.to_python('550e8400-e29b-41d4-a716-446655440000')
    print(f"Valid UUID parsed: {result}")
except Exception as e:
    print(f"Valid UUID raised: {type(e).__name__}: {e}")

# Invalid UUID (malformed)
try:
    result = field.to_python('not-a-uuid')
    print(f"Invalid UUID parsed: {result}")
except Exception as e:
    print(f"Invalid UUID raised: {type(e).__name__}: {e}")

# Test with query simulation
print("\n--- Testing actual ORM lookup simulation ---")
from django.db.models.lookups import Lookup
from django.db.models.sql.query import Query

# Try to see what happens when we try to create a lookup with invalid UUID
try:
    # This simulates what Django does internally when you do .get(uuid_field='bad-uuid')
    # The field's to_python is called before the query
    converted = field.to_python('bad-uuid')
    print(f"Converted: {converted}")
except Exception as e:
    print(f"Conversion raised: {type(e).__name__}: {e}")
    print(f"Is ValidationError? {isinstance(e, ValidationError)}")
PY

Repository: OWASP/Nest

Length of output: 267


🏁 Script executed:

cat -n backend/apps/owasp/api/internal/mutations/snapshot_subscription.py | head -20

Repository: OWASP/Nest

Length of output: 927


🏁 Script executed:

cat -n backend/apps/owasp/api/internal/mutations/snapshot_subscription.py | sed -n '160,180p'

Repository: OWASP/Nest

Length of output: 1078


🏁 Script executed:

cat backend/requirements.txt backend/requirements-dev.txt 2>/dev/null | grep -i django || cat requirements.txt requirements-dev.txt 2>/dev/null | grep -i django || find . -name "pyproject.toml" -o -name "requirements*.txt" | head -5 | xargs grep -i django 2>/dev/null | head -10

Repository: OWASP/Nest

Length of output: 891


🏁 Script executed:

python3 - <<'PY'
# Simulate Django UUIDField behavior without installing Django
# Based on Django source code: django/db/models/fields/__init__.py - UUIDField.to_python()

from uuid import UUID
from dataclasses import dataclass
from typing import Any

# Simulate Django's ValidationError
class ValidationError(Exception):
    pass

def simulate_uuid_field_to_python(value: Any) -> UUID:
    """
    Simulates Django's UUIDField.to_python() method behavior.
    Source: https://github.com/django/django/blob/main/django/db/models/fields/__init__.py
    """
    if value is None:
        return None
    
    if isinstance(value, UUID):
        return value
    
    try:
        return UUID(str(value))
    except (ValueError, AttributeError):
        # Django raises ValidationError here if the value is not a valid UUID string
        raise ValidationError("Invalid UUID format.")

# Test cases
test_cases = [
    ("550e8400-e29b-41d4-a716-446655440000", True),  # Valid UUID
    ("not-a-uuid", False),  # Invalid UUID
    ("", False),  # Empty string
    ("123", False),  # Too short
    (None, True),  # None should return None
]

print("Testing UUID field validation behavior:\n")
for value, should_pass in test_cases:
    try:
        result = simulate_uuid_field_to_python(value)
        status = "PASS" if should_pass else "FAIL (should have raised)"
        print(f"Value: {repr(value):30} -> Parsed: {result} [{status}]")
    except ValidationError as e:
        status = "PASS" if not should_pass else "FAIL (should have parsed)"
        print(f"Value: {repr(value):30} -> ValidationError [{status}]")
    except ValueError as e:
        status = "FAIL (raises ValueError instead of ValidationError)"
        print(f"Value: {repr(value):30} -> ValueError [{status}]")
    except Exception as e:
        print(f"Value: {repr(value):30} -> {type(e).__name__}: {e}")

print("\n" + "="*60)
print("Conclusion:")
print("Django's UUIDField.to_python() raises ValidationError for invalid UUIDs.")
print("The ORM's get() call will trigger this during field conversion.")
print("Therefore, catching ValidationError is necessary to prevent 500 errors.")
PY

Repository: OWASP/Nest

Length of output: 826


🏁 Script executed:

find . -name "snapshot_subscription.py" -path "*/models/*" | head -1 | xargs cat -n | grep -A 5 -B 5 "unsubscribe_token"

Repository: OWASP/Nest

Length of output: 495


Catch the ValidationError Django raises for invalid UUIDs.

An invalid UUID string passed to a UUIDField lookup is coerced by Django's ORM before the query runs. This coercion fails by raising ValidationError, not ValueError. Since the current exception handler only catches ValueError, a malformed unsubscribe token will propagate as an unhandled ValidationError, resulting in a 500 error.

Update the exception block to catch ValidationError:

from django.core.exceptions import ValidationError

try:
    subscription = SnapshotSubscription.objects.get(unsubscribe_token=token)
except (SnapshotSubscription.DoesNotExist, ValidationError):
    return SnapshotSubscriptionResult(
        ok=False,
        message="Invalid unsubscribe token.",
    )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/apps/owasp/api/internal/mutations/snapshot_subscription.py` around
lines 167 - 173, The unsubscribe-token lookup in SnapshotSubscription mutation
only handles SnapshotSubscription.DoesNotExist and ValueError, but invalid UUID
input raises Django’s ValidationError before the query completes. Update the
exception handling in the mutation that calls
SnapshotSubscription.objects.get(unsubscribe_token=token) to also catch
ValidationError, and make sure the needed import from django.core.exceptions is
added so malformed tokens return the existing invalid-token result instead of an
unhandled error.


if not subscription.is_active:
return SnapshotSubscriptionResult(
ok=False,
message="Subscription is already inactive.",
)

subscription.is_active = False
subscription.save()

return SnapshotSubscriptionResult(
ok=True,
message="Successfully unsubscribed.",
subscription=subscription,
)
27 changes: 27 additions & 0 deletions backend/apps/owasp/api/internal/nodes/snapshot_subscription.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""OWASP snapshot subscription GraphQL node."""

import strawberry
import strawberry_django

from apps.owasp.models.snapshot_subscription import SnapshotSubscription


@strawberry_django.type(
SnapshotSubscription,
fields=[
"frequency",
"is_active",
"include_chapters",
"include_events",
"include_issues",
"include_posts",
"include_projects",
"include_pull_requests",
"include_releases",
"include_users",
"created_at",
"updated_at",
],
)
class SnapshotSubscriptionNode(strawberry.relay.Node):
"""Snapshot subscription node."""
2 changes: 2 additions & 0 deletions backend/apps/owasp/api/internal/queries/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .project import ProjectQuery
from .project_health_metrics import ProjectHealthMetricsQuery
from .snapshot import SnapshotQuery
from .snapshot_subscription import SnapshotSubscriptionQuery
from .sponsor import SponsorQuery
from .stats import StatsQuery

Expand All @@ -23,6 +24,7 @@ class OwaspQuery(
ProjectHealthMetricsQuery,
ProjectQuery,
SnapshotQuery,
SnapshotSubscriptionQuery,
SponsorQuery,
StatsQuery,
):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""OWASP snapshot subscription GraphQL queries."""

import strawberry
import strawberry_django
from strawberry.types import Info

from apps.owasp.api.internal.nodes.snapshot_subscription import SnapshotSubscriptionNode
from apps.owasp.models.snapshot_subscription import SnapshotSubscription


@strawberry.type
class SnapshotSubscriptionQuery:
"""Snapshot subscription queries."""

@strawberry_django.field
def my_subscription(self, info: Info) -> SnapshotSubscriptionNode | None:
"""Resolve the current user's snapshot subscription."""
user = info.context.request.user
if not user.is_authenticated:
return None

try:
return SnapshotSubscription.objects.get(user=user)
except SnapshotSubscription.DoesNotExist:
return None
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
Loading
Loading