Skip to content

Commit 4791b8a

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 the settings-loading import chain. - 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, async_create_notification, send_{slack,msteams,mail,webhooks}_notification, webhook_reactivation, webhook_status_cleanup) consolidated into dojo/notifications/tasks.py. async_create_notification calls into helper.create_notification via lazy import so test patches still apply. - 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, the notification + cleanup-alerts + jira-webhook + REST framework test suites pass, and ruff check . is clean for new code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5178368 commit 4791b8a

101 files changed

Lines changed: 1037 additions & 854 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: 3 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from rest_framework import serializers
2222
from rest_framework.exceptions import NotFound
2323
from rest_framework.exceptions import ValidationError as RestFrameworkValidationError
24-
from rest_framework.fields import DictField, MultipleChoiceField
24+
from rest_framework.fields import DictField
2525

2626
import dojo.finding.helper as finding_helper
2727
import dojo.risk_acceptance.helper as ra_helper
@@ -43,9 +43,7 @@
4343
from dojo.jira import services as jira_services
4444
from dojo.location.models import Location, LocationFindingReference
4545
from dojo.models import (
46-
DEFAULT_NOTIFICATION,
4746
IMPORT_ACTIONS,
48-
NOTIFICATION_CHOICES,
4947
SEVERITIES,
5048
SEVERITY_CHOICES,
5149
STATS_FIELDS,
@@ -82,8 +80,6 @@
8280
Note_Type,
8381
NoteHistory,
8482
Notes,
85-
Notification_Webhooks,
86-
Notifications,
8783
Product,
8884
Product_API_Scan_Configuration,
8985
Product_Group,
@@ -3069,110 +3065,7 @@ class FindingNoteSerializer(serializers.Serializer):
30693065
note_id = serializers.IntegerField()
30703066

30713067

3072-
class NotificationsSerializer(serializers.ModelSerializer):
3073-
product = serializers.PrimaryKeyRelatedField(
3074-
queryset=Product.objects.all(),
3075-
required=False,
3076-
default=None,
3077-
allow_null=True,
3078-
)
3079-
user = serializers.PrimaryKeyRelatedField(
3080-
queryset=Dojo_User.objects.all(),
3081-
required=False,
3082-
default=None,
3083-
allow_null=True,
3084-
)
3085-
product_type_added = MultipleChoiceField(
3086-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3087-
)
3088-
product_added = MultipleChoiceField(
3089-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3090-
)
3091-
engagement_added = MultipleChoiceField(
3092-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3093-
)
3094-
test_added = MultipleChoiceField(
3095-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3096-
)
3097-
scan_added = MultipleChoiceField(
3098-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3099-
)
3100-
jira_update = MultipleChoiceField(
3101-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3102-
)
3103-
upcoming_engagement = MultipleChoiceField(
3104-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3105-
)
3106-
stale_engagement = MultipleChoiceField(
3107-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3108-
)
3109-
auto_close_engagement = MultipleChoiceField(
3110-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3111-
)
3112-
close_engagement = MultipleChoiceField(
3113-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3114-
)
3115-
user_mentioned = MultipleChoiceField(
3116-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3117-
)
3118-
code_review = MultipleChoiceField(
3119-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3120-
)
3121-
review_requested = MultipleChoiceField(
3122-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3123-
)
3124-
other = MultipleChoiceField(
3125-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3126-
)
3127-
sla_breach = MultipleChoiceField(
3128-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3129-
)
3130-
sla_breach_combined = MultipleChoiceField(
3131-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3132-
)
3133-
risk_acceptance_expiration = MultipleChoiceField(
3134-
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION,
3135-
)
3136-
template = serializers.BooleanField(default=False)
3137-
3138-
class Meta:
3139-
model = Notifications
3140-
fields = "__all__"
3141-
3142-
def validate(self, data):
3143-
user = None
3144-
product = None
3145-
template = False
3146-
3147-
if self.instance is not None:
3148-
user = self.instance.user
3149-
product = self.instance.product
3150-
3151-
if "user" in data:
3152-
user = data.get("user")
3153-
if "product" in data:
3154-
product = data.get("product")
3155-
if "template" in data:
3156-
template = data.get("template")
3157-
3158-
if (
3159-
template
3160-
and Notifications.objects.filter(template=True).count() > 0
3161-
):
3162-
msg = "Notification template already exists"
3163-
raise ValidationError(msg)
3164-
if (
3165-
self.instance is None
3166-
or user != self.instance.user
3167-
or product != self.instance.product
3168-
):
3169-
notifications = Notifications.objects.filter(
3170-
user=user, product=product, template=template,
3171-
).count()
3172-
if notifications > 0:
3173-
msg = "Notification for user and product already exists"
3174-
raise ValidationError(msg)
3175-
return data
3068+
from dojo.notifications.api.serializer import NotificationsSerializer # noqa: E402, F401 -- backward compat
31763069

31773070

31783071
class EngagementPresetsSerializer(serializers.ModelSerializer):
@@ -3349,7 +3242,4 @@ def create(self, validated_data):
33493242
raise
33503243

33513244

3352-
class NotificationWebhooksSerializer(serializers.ModelSerializer):
3353-
class Meta:
3354-
model = Notification_Webhooks
3355-
fields = "__all__"
3245+
from dojo.notifications.api.serializer import NotificationWebhooksSerializer # noqa: E402, F401 -- backward compat

dojo/api_v2/views.py

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,6 @@
119119
Note_Type,
120120
NoteHistory,
121121
Notes,
122-
Notification_Webhooks,
123-
Notifications,
124122
Product,
125123
Product_API_Scan_Configuration,
126124
Product_Group,
@@ -3406,21 +3404,6 @@ def queue_task_purge(self, request):
34063404
return Response({"purged": purged})
34073405

34083406

3409-
# Authorization: superuser
3410-
@extend_schema_view(**schema_with_prefetch())
3411-
class NotificationsViewSet(
3412-
PrefetchDojoModelViewSet,
3413-
):
3414-
serializer_class = serializers.NotificationsSerializer
3415-
queryset = Notifications.objects.none()
3416-
filter_backends = (DjangoFilterBackend,)
3417-
filterset_fields = ["id", "user", "product", "template"]
3418-
permission_classes = (permissions.IsSuperUser, DjangoModelPermissions)
3419-
3420-
def get_queryset(self):
3421-
return Notifications.objects.all().order_by("id")
3422-
3423-
34243407
@extend_schema_view(**schema_with_prefetch())
34253408
class EngagementPresetsViewset(
34263409
PrefetchDojoModelViewSet,
@@ -3683,13 +3666,3 @@ class AnnouncementViewSet(
36833666

36843667
def get_queryset(self):
36853668
return Announcement.objects.all().order_by("id")
3686-
3687-
3688-
class NotificationWebhooksViewSet(
3689-
PrefetchDojoModelViewSet,
3690-
):
3691-
serializer_class = serializers.NotificationWebhooksSerializer
3692-
queryset = Notification_Webhooks.objects.all()
3693-
filter_backends = (DjangoFilterBackend,)
3694-
filterset_fields = "__all__"
3695-
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: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import contextlib
2-
import time
32

43
# import the settings file
54
from django.conf import settings
65
from django.contrib import messages
76

87
from dojo.announcement.os_message import get_os_banner
98
from dojo.labels import get_labels
10-
from dojo.models import Alerts, System_Settings, UserAnnouncement
9+
from dojo.models import System_Settings, UserAnnouncement
1110

1211

1312
def globalize_vars(request):
@@ -86,14 +85,6 @@ def bind_system_settings(request):
8685
return {"system_settings": system_settings}
8786

8887

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-
9788
def bind_announcement(request):
9889
with contextlib.suppress(Exception): # TODO: this should be replaced with more meaningful exception
9990
if request.user.is_authenticated:
@@ -104,21 +95,10 @@ def bind_announcement(request):
10495
return {}
10596

10697

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-
}
98+
from dojo.notifications.context_processors import ( # noqa: E402, F401 -- backward compat
99+
bind_alert_count,
100+
session_expiry_notification,
101+
)
122102

123103

124104
def labels(request):

dojo/forms.py

Lines changed: 6 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,6 @@
8585
Global_Role,
8686
Note_Type,
8787
Notes,
88-
Notification_Webhooks,
89-
Notifications,
9088
Objects_Product,
9189
Product,
9290
Product_API_Scan_Configuration,
@@ -3155,55 +3153,12 @@ class Meta:
31553153
exclude = [""]
31563154

31573155

3158-
class NotificationsForm(forms.ModelForm):
3159-
3160-
class Meta:
3161-
model = Notifications
3162-
exclude = ["template"]
3163-
3164-
3165-
class NotificationsWebhookForm(forms.ModelForm):
3166-
class Meta:
3167-
model = Notification_Webhooks
3168-
exclude = []
3169-
3170-
def __init__(self, *args, **kwargs):
3171-
is_superuser = kwargs.pop("is_superuser", False)
3172-
super().__init__(*args, **kwargs)
3173-
if not is_superuser: # Only superadmins can edit owner
3174-
self.fields["owner"].disabled = True # TODO: needs to be tested
3175-
3176-
3177-
class DeleteNotificationsWebhookForm(forms.ModelForm):
3178-
id = forms.IntegerField(required=True,
3179-
widget=forms.widgets.HiddenInput())
3180-
3181-
def __init__(self, *args, **kwargs):
3182-
super().__init__(*args, **kwargs)
3183-
self.fields["name"].disabled = True
3184-
self.fields["url"].disabled = True
3185-
3186-
class Meta:
3187-
model = Notification_Webhooks
3188-
fields = ["id", "name", "url"]
3189-
3190-
3191-
class ProductNotificationsForm(forms.ModelForm):
3192-
3193-
def __init__(self, *args, **kwargs):
3194-
super().__init__(*args, **kwargs)
3195-
if not self.instance.id:
3196-
self.initial["engagement_added"] = ""
3197-
self.initial["close_engagement"] = ""
3198-
self.initial["test_added"] = ""
3199-
self.initial["scan_added"] = ""
3200-
self.initial["sla_breach"] = ""
3201-
self.initial["sla_breach_combined"] = ""
3202-
self.initial["risk_acceptance_expiration"] = ""
3203-
3204-
class Meta:
3205-
model = Notifications
3206-
fields = ["engagement_added", "close_engagement", "test_added", "scan_added", "sla_breach", "sla_breach_combined", "risk_acceptance_expiration"]
3156+
from dojo.notifications.ui.forms import ( # noqa: E402, F401 -- backward compat
3157+
DeleteNotificationsWebhookForm,
3158+
NotificationsForm,
3159+
NotificationsWebhookForm,
3160+
ProductNotificationsForm,
3161+
)
32073162

32083163

32093164
class AjaxChoiceField(forms.ChoiceField):

0 commit comments

Comments
 (0)