Skip to content

Commit 60d786c

Browse files
Maffoochclaude
andcommitted
Consolidate notification code into dojo/notifications/ package
Move notification models, admin, services (helper.py), signals, tasks, settings, context processors, forms, UI views/urls/templates, and API serializers/viewsets/urls into dojo/notifications/, matching the canonical dojo/url/ shape from CLAUDE.md. - Notifications, Notification_Webhooks, Alerts (and constants) extracted from dojo/models.py into dojo/notifications/models.py; admin moved to dojo/notifications/admin.py using @admin.register(); registered from dojo/apps.py ready() to keep model class definition out of settings load. - helper.py absorbs process_tag_notifications and sla_compute_and_notify from dojo/utils.py; all @app.task wrappers (log_generic_alert, add_alerts, cleanup_alerts, async_sla_compute_and_notify_task, send_{slack,msteams,mail,webhooks}_notification, webhook_reactivation, webhook_status_cleanup) consolidated into dojo/notifications/tasks.py. - Default-Notifications-on-User-create logic moved out of dojo/utils.py into dojo/notifications/signals.py and registered via apps.py ready(). - 7 UI .html templates and 62 channel .tpl templates moved under dojo/notifications/templates/notifications/. New TEMPLATES DIRS entry added so notifications/{channel}/{event}.tpl lookups still resolve; view template_name strings updated where the namespace changed. - Env-var schema extracted into dojo/notifications/settings.py (NOTIFICATIONS_ENV_DEFAULTS dict + populate_settings); settings.dist.py unpacks it into env() and applies values to globals(). - bind_alert_count and session_expiry_notification moved to dojo/notifications/context_processors.py; TEMPLATES paths updated. - API NotificationsSerializer/NotificationWebhooksSerializer and Viewsets relocated under dojo/notifications/api/, exposed via add_notifications_urls(router) (CLAUDE.md API pattern). - Celery beat task paths updated to dojo.notifications.tasks.*. - Backward-compatible re-exports preserved at every original location (dojo/models.py, dojo/forms.py, dojo/utils.py, dojo/tasks.py, dojo/context_processors.py, dojo/api_v2/{serializers,views}.py, dojo/notifications/helper.py). - No new migrations (string FK references are migration-equivalent). Verified: manage.py check, makemigrations --check (no diff), spectacular --fail-on-warn, and the full test_notifications + test_apiv2_notifications + test_jira_webhook + test_cleanup_alerts + test_rest_framework suite (1325 tests) pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3adf4cd commit 60d786c

101 files changed

Lines changed: 960 additions & 767 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

dojo/api_v2/serializers.py

Lines changed: 2 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -3138,110 +3138,7 @@ class FindingNoteSerializer(serializers.Serializer):
31383138
note_id = serializers.IntegerField()
31393139

31403140

3141-
class NotificationsSerializer(serializers.ModelSerializer):
3142-
product = serializers.PrimaryKeyRelatedField(
3143-
queryset=Product.objects.all(),
3144-
required=False,
3145-
default=None,
3146-
allow_null=True,
3147-
)
3148-
user = serializers.PrimaryKeyRelatedField(
3149-
queryset=Dojo_User.objects.all(),
3150-
required=False,
3151-
default=None,
3152-
allow_null=True,
3153-
)
3154-
product_type_added = MultipleChoiceField(
3155-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3156-
)
3157-
product_added = MultipleChoiceField(
3158-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3159-
)
3160-
engagement_added = MultipleChoiceField(
3161-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3162-
)
3163-
test_added = MultipleChoiceField(
3164-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3165-
)
3166-
scan_added = MultipleChoiceField(
3167-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3168-
)
3169-
jira_update = MultipleChoiceField(
3170-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3171-
)
3172-
upcoming_engagement = MultipleChoiceField(
3173-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3174-
)
3175-
stale_engagement = MultipleChoiceField(
3176-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3177-
)
3178-
auto_close_engagement = MultipleChoiceField(
3179-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3180-
)
3181-
close_engagement = MultipleChoiceField(
3182-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3183-
)
3184-
user_mentioned = MultipleChoiceField(
3185-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3186-
)
3187-
code_review = MultipleChoiceField(
3188-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3189-
)
3190-
review_requested = MultipleChoiceField(
3191-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3192-
)
3193-
other = MultipleChoiceField(
3194-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3195-
)
3196-
sla_breach = MultipleChoiceField(
3197-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3198-
)
3199-
sla_breach_combined = MultipleChoiceField(
3200-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3201-
)
3202-
risk_acceptance_expiration = MultipleChoiceField(
3203-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3204-
)
3205-
template = serializers.BooleanField(default=False)
3206-
3207-
class Meta:
3208-
model = Notifications
3209-
fields = "__all__"
3210-
3211-
def validate(self, data):
3212-
user = None
3213-
product = None
3214-
template = False
3215-
3216-
if self.instance is not None:
3217-
user = self.instance.user
3218-
product = self.instance.product
3219-
3220-
if "user" in data:
3221-
user = data.get("user")
3222-
if "product" in data:
3223-
product = data.get("product")
3224-
if "template" in data:
3225-
template = data.get("template")
3226-
3227-
if (
3228-
template
3229-
and Notifications.objects.filter(template=True).count() > 0
3230-
):
3231-
msg = "Notification template already exists"
3232-
raise ValidationError(msg)
3233-
if (
3234-
self.instance is None
3235-
or user != self.instance.user
3236-
or product != self.instance.product
3237-
):
3238-
notifications = Notifications.objects.filter(
3239-
user=user, product=product, template=template,
3240-
).count()
3241-
if notifications > 0:
3242-
msg = "Notification for user and product already exists"
3243-
raise ValidationError(msg)
3244-
return data
3141+
from dojo.notifications.api.serializer import NotificationsSerializer # noqa: E402, F401 -- backward compat
32453142

32463143

32473144
class EngagementPresetsSerializer(serializers.ModelSerializer):
@@ -3418,7 +3315,4 @@ def create(self, validated_data):
34183315
raise
34193316

34203317

3421-
class NotificationWebhooksSerializer(serializers.ModelSerializer):
3422-
class Meta:
3423-
model = Notification_Webhooks
3424-
fields = "__all__"
3318+
from dojo.notifications.api.serializer import NotificationWebhooksSerializer # noqa: E402, F401 -- backward compat

dojo/api_v2/views.py

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3473,21 +3473,6 @@ def queue_task_purge(self, request):
34733473
return Response({"purged": purged})
34743474

34753475

3476-
# Authorization: superuser
3477-
@extend_schema_view(**schema_with_prefetch())
3478-
class NotificationsViewSet(
3479-
PrefetchDojoModelViewSet,
3480-
):
3481-
serializer_class = serializers.NotificationsSerializer
3482-
queryset = Notifications.objects.none()
3483-
filter_backends = (DjangoFilterBackend,)
3484-
filterset_fields = ["id", "user", "product", "template"]
3485-
permission_classes = (permissions.IsSuperUser, DjangoModelPermissions)
3486-
3487-
def get_queryset(self):
3488-
return Notifications.objects.all().order_by("id")
3489-
3490-
34913476
@extend_schema_view(**schema_with_prefetch())
34923477
class EngagementPresetsViewset(
34933478
PrefetchDojoModelViewSet,
@@ -3752,11 +3737,3 @@ def get_queryset(self):
37523737
return Announcement.objects.all().order_by("id")
37533738

37543739

3755-
class NotificationWebhooksViewSet(
3756-
PrefetchDojoModelViewSet,
3757-
):
3758-
serializer_class = serializers.NotificationWebhooksSerializer
3759-
queryset = Notification_Webhooks.objects.all()
3760-
filter_backends = (DjangoFilterBackend,)
3761-
filterset_fields = "__all__"
3762-
permission_classes = (permissions.IsSuperUser, DjangoModelPermissions) # TODO: add permission also for other users

dojo/apps.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ def ready(self):
8484
import dojo.file_uploads.signals # noqa: PLC0415, F401 raised: AppRegistryNotReady
8585
import dojo.finding_group.signals # noqa: PLC0415, F401 raised: AppRegistryNotReady
8686
import dojo.notes.signals # noqa: PLC0415, F401 raised: AppRegistryNotReady
87+
import dojo.notifications.admin # noqa: PLC0415, F401 raised: AppRegistryNotReady
88+
import dojo.notifications.signals # noqa: PLC0415, F401 raised: AppRegistryNotReady
8789
import dojo.product.signals # noqa: PLC0415, F401 raised: AppRegistryNotReady
8890
import dojo.product_type.signals # noqa: PLC0415, F401 raised: AppRegistryNotReady
8991
import dojo.risk_acceptance.signals # noqa: PLC0415, F401 raised: AppRegistryNotReady

dojo/context_processors.py

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,6 @@ def bind_system_settings(request):
8686
return {"system_settings": system_settings}
8787

8888

89-
def bind_alert_count(request):
90-
if not settings.DISABLE_ALERT_COUNTER:
91-
92-
if hasattr(request, "user") and request.user.is_authenticated:
93-
return {"alert_count": Alerts.objects.filter(user_id=request.user).count()}
94-
return {}
95-
96-
9789
def bind_announcement(request):
9890
with contextlib.suppress(Exception): # TODO: this should be replaced with more meaningful exception
9991
if request.user.is_authenticated:
@@ -104,21 +96,10 @@ def bind_announcement(request):
10496
return {}
10597

10698

107-
def session_expiry_notification(request):
108-
try:
109-
if request.user.is_authenticated:
110-
last_activity = request.session.get("_last_activity", time.time())
111-
expiry_time = last_activity + settings.SESSION_COOKIE_AGE # When the session will expire
112-
warning_time = settings.SESSION_EXPIRE_WARNING # Show warning X seconds before expiry
113-
notify_time = expiry_time - warning_time
114-
else:
115-
notify_time = None
116-
except Exception:
117-
return {}
118-
else:
119-
return {
120-
"session_notify_time": notify_time,
121-
}
99+
from dojo.notifications.context_processors import ( # noqa: E402, F401 -- backward compat
100+
bind_alert_count,
101+
session_expiry_notification,
102+
)
122103

123104

124105
def labels(request):

dojo/forms.py

Lines changed: 6 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3240,55 +3240,12 @@ class Meta:
32403240
exclude = [""]
32413241

32423242

3243-
class NotificationsForm(forms.ModelForm):
3244-
3245-
class Meta:
3246-
model = Notifications
3247-
exclude = ["template"]
3248-
3249-
3250-
class NotificationsWebhookForm(forms.ModelForm):
3251-
class Meta:
3252-
model = Notification_Webhooks
3253-
exclude = []
3254-
3255-
def __init__(self, *args, **kwargs):
3256-
is_superuser = kwargs.pop("is_superuser", False)
3257-
super().__init__(*args, **kwargs)
3258-
if not is_superuser: # Only superadmins can edit owner
3259-
self.fields["owner"].disabled = True # TODO: needs to be tested
3260-
3261-
3262-
class DeleteNotificationsWebhookForm(forms.ModelForm):
3263-
id = forms.IntegerField(required=True,
3264-
widget=forms.widgets.HiddenInput())
3265-
3266-
def __init__(self, *args, **kwargs):
3267-
super().__init__(*args, **kwargs)
3268-
self.fields["name"].disabled = True
3269-
self.fields["url"].disabled = True
3270-
3271-
class Meta:
3272-
model = Notification_Webhooks
3273-
fields = ["id", "name", "url"]
3274-
3275-
3276-
class ProductNotificationsForm(forms.ModelForm):
3277-
3278-
def __init__(self, *args, **kwargs):
3279-
super().__init__(*args, **kwargs)
3280-
if not self.instance.id:
3281-
self.initial["engagement_added"] = ""
3282-
self.initial["close_engagement"] = ""
3283-
self.initial["test_added"] = ""
3284-
self.initial["scan_added"] = ""
3285-
self.initial["sla_breach"] = ""
3286-
self.initial["sla_breach_combined"] = ""
3287-
self.initial["risk_acceptance_expiration"] = ""
3288-
3289-
class Meta:
3290-
model = Notifications
3291-
fields = ["engagement_added", "close_engagement", "test_added", "scan_added", "sla_breach", "sla_breach_combined", "risk_acceptance_expiration"]
3243+
from dojo.notifications.ui.forms import ( # noqa: E402, F401 -- backward compat
3244+
DeleteNotificationsWebhookForm,
3245+
NotificationsForm,
3246+
NotificationsWebhookForm,
3247+
ProductNotificationsForm,
3248+
)
32923249

32933250

32943251
class AjaxChoiceField(forms.ChoiceField):

0 commit comments

Comments
 (0)