-
-
Notifications
You must be signed in to change notification settings - Fork 648
Feature/snapshot subscription graphql #5058
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/community-snapshots
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) |
| 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.""" |
| 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(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Prompt for AI agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return SnapshotSubscriptionResult( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ok=False, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message="Invalid unsubscribe token.", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+167
to
+173
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)}")
PYRepository: OWASP/Nest Length of output: 267 🏁 Script executed: cat -n backend/apps/owasp/api/internal/mutations/snapshot_subscription.py | head -20Repository: 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 -10Repository: 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.")
PYRepository: 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 An invalid UUID string passed to a Update the exception block to catch 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 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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.""" |
| 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 |
| 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"), | ||
| ], | ||
| }, | ||
| ), | ||
| ] |
There was a problem hiding this comment.
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