diff --git a/AGENTS.md b/AGENTS.md
index e53dcd7cadf..ed261984c8a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -58,18 +58,22 @@ Modules in various stages of reorganization:
| **test** | In module | N/A | Done | Done | **Complete** (#14971) |
| **engagement** | In module | In module | Done | Done | **Complete** (#14972) |
| **product** | In module | N/A | Done | Done | **Complete** (#14973) |
-| **finding** | In module | N/A (helper.py) | Done | Done | **Complete** (#14974); CWE+Burp pending |
-| **peripheral (×18)** | In dojo/models.py | — | Partial/none | Partial/none | **Phase 10** (PRs #6–10, see below) |
+| **finding** | In module | N/A (helper.py) | Done | Done | **Complete** (#14974, incl. CWE + BurpRawRequestResponse) |
+| **user / system_settings** | In module | N/A | Done | Done | **Complete** (#14981) |
+| **endpoint / tool_type / tool_config / tool_product** | In module | N/A | Done | Done | **Complete** (#14982) |
+| **survey / benchmark** | In module | N/A | Done | N/A (no API) | **Complete** (#14983) |
+| **notes / note_type / file_uploads / reports / risk_acceptance** | In module | N/A | Done | Done | **Complete** (#14986) |
+| **regulations / banner / announcement / development_environment / object** | In module | N/A | Done | Partial (API where one exists) | **Complete** (#14987) |
### Monolithic Files Being Decomposed
-These files still contain code for multiple modules. Extract code to the target module's subdirectory and leave a re-export stub.
+These files still contain code for multiple modules. Extract code to the target module's subdirectory and leave a re-export stub. (Counts reflect the completed Phase 10 stack; they shrink as branches merge to `dev`.)
-- `dojo/models.py` (4,973 lines) — All model definitions
-- `dojo/forms.py` (4,127 lines) — All Django forms
-- `dojo/filters.py` (4,016 lines) — All UI and API filter classes
-- `dojo/api_v2/serializers.py` (3,387 lines) — All DRF serializers
-- `dojo/api_v2/views.py` (3,519 lines) — All API viewsets
+- `dojo/models.py` (~645 lines) — re-export hub + the few models intentionally left here (`DojoMeta`, `Network_Locations`, `Sonarqube_Issue`/`Sonarqube_Issue_Transition`, `Check_List`, `Testing_Guide_Category`/`Testing_Guide`, `Language_Type`/`Languages`, `App_Analysis`, `SLA_Configuration`) plus shared utilities (`copy_model_util`, `get_current_date`, `tomorrow`, `UniqueUploadNameProvider`)
+- `dojo/forms.py` (~914 lines) — remaining/shared Django forms
+- `dojo/filters.py` (~1,376 lines) — remaining/shared filter classes + shared bases
+- `dojo/api_v2/serializers.py` (~1,101 lines) — remaining serializers + re-exports for prefetcher discovery
+- `dojo/api_v2/views.py` (~901 lines) — remaining viewsets + shared base classes/helpers
---
diff --git a/dojo/announcement/__init__.py b/dojo/announcement/__init__.py
index e69de29bb2d..d22235e108d 100644
--- a/dojo/announcement/__init__.py
+++ b/dojo/announcement/__init__.py
@@ -0,0 +1 @@
+import dojo.announcement.admin # noqa: F401
diff --git a/dojo/announcement/admin.py b/dojo/announcement/admin.py
new file mode 100644
index 00000000000..23507595678
--- /dev/null
+++ b/dojo/announcement/admin.py
@@ -0,0 +1,6 @@
+from django.contrib import admin
+
+from dojo.announcement.models import Announcement, UserAnnouncement
+
+admin.site.register(Announcement)
+admin.site.register(UserAnnouncement)
diff --git a/dojo/announcement/api/__init__.py b/dojo/announcement/api/__init__.py
new file mode 100644
index 00000000000..0e64dd504ef
--- /dev/null
+++ b/dojo/announcement/api/__init__.py
@@ -0,0 +1 @@
+path = "announcements" # noqa: RUF067
diff --git a/dojo/announcement/api/serializer.py b/dojo/announcement/api/serializer.py
new file mode 100644
index 00000000000..0022e2f22ef
--- /dev/null
+++ b/dojo/announcement/api/serializer.py
@@ -0,0 +1,21 @@
+from django.db import IntegrityError
+from rest_framework import serializers
+
+from dojo.announcement.models import Announcement
+
+
+class AnnouncementSerializer(serializers.ModelSerializer):
+
+ class Meta:
+ model = Announcement
+ fields = "__all__"
+
+ def create(self, validated_data):
+ validated_data["id"] = 1
+ try:
+ return super().create(validated_data)
+ except IntegrityError as e:
+ if 'duplicate key value violates unique constraint "dojo_announcement_pkey"' in str(e):
+ msg = "No more than one Announcement is allowed"
+ raise serializers.ValidationError(msg)
+ raise
diff --git a/dojo/announcement/api/urls.py b/dojo/announcement/api/urls.py
new file mode 100644
index 00000000000..1da04620311
--- /dev/null
+++ b/dojo/announcement/api/urls.py
@@ -0,0 +1,7 @@
+from dojo.announcement.api import path
+from dojo.announcement.api.views import AnnouncementViewSet
+
+
+def add_announcement_urls(router):
+ router.register(path, AnnouncementViewSet, basename="announcement")
+ return router
diff --git a/dojo/announcement/api/views.py b/dojo/announcement/api/views.py
new file mode 100644
index 00000000000..cf6a411adc9
--- /dev/null
+++ b/dojo/announcement/api/views.py
@@ -0,0 +1,20 @@
+from django_filters.rest_framework import DjangoFilterBackend
+
+from dojo.announcement.api.serializer import AnnouncementSerializer
+from dojo.announcement.models import Announcement
+from dojo.api_v2.views import DojoModelViewSet
+from dojo.authorization import api_permissions as permissions
+
+
+# Authorization: configuration
+class AnnouncementViewSet(
+ DojoModelViewSet,
+):
+ serializer_class = AnnouncementSerializer
+ queryset = Announcement.objects.none()
+ filter_backends = (DjangoFilterBackend,)
+ filterset_fields = "__all__"
+ permission_classes = (permissions.UserHasConfigurationPermissionStaff,)
+
+ def get_queryset(self):
+ return Announcement.objects.all().order_by("id")
diff --git a/dojo/announcement/models.py b/dojo/announcement/models.py
new file mode 100644
index 00000000000..9266b96c019
--- /dev/null
+++ b/dojo/announcement/models.py
@@ -0,0 +1,28 @@
+from django.db import models
+from django.utils.translation import gettext as _
+
+ANNOUNCEMENT_STYLE_CHOICES = (
+ ("info", "Info"),
+ ("success", "Success"),
+ ("warning", "Warning"),
+ ("danger", "Danger"),
+)
+
+
+class Announcement(models.Model):
+ message = models.CharField(max_length=500,
+ help_text=_("This dismissable message will be displayed on all pages for authenticated users. It can contain basic html tags, for example https://example.com"),
+ default="")
+ style = models.CharField(max_length=64, choices=ANNOUNCEMENT_STYLE_CHOICES, default="info",
+ help_text=_("The style of banner to display. (info, success, warning, danger)"))
+ dismissable = models.BooleanField(default=False,
+ null=False,
+ blank=True,
+ verbose_name=_("Dismissable?"),
+ help_text=_("Ticking this box allows users to dismiss the current announcement"),
+ )
+
+
+class UserAnnouncement(models.Model):
+ announcement = models.ForeignKey("dojo.Announcement", null=True, editable=False, on_delete=models.CASCADE, related_name="user_announcement")
+ user = models.ForeignKey("dojo.Dojo_User", null=True, editable=False, on_delete=models.CASCADE)
diff --git a/dojo/announcement/signals.py b/dojo/announcement/signals.py
index c74fd0e5d50..677fafe93c2 100644
--- a/dojo/announcement/signals.py
+++ b/dojo/announcement/signals.py
@@ -1,7 +1,8 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
-from dojo.models import Announcement, Dojo_User, UserAnnouncement
+from dojo.announcement.models import Announcement, UserAnnouncement
+from dojo.user.models import Dojo_User
@receiver(post_save, sender=Dojo_User)
diff --git a/dojo/announcement/ui/__init__.py b/dojo/announcement/ui/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/dojo/announcement/ui/forms.py b/dojo/announcement/ui/forms.py
new file mode 100644
index 00000000000..47e4f97721d
--- /dev/null
+++ b/dojo/announcement/ui/forms.py
@@ -0,0 +1,17 @@
+from django import forms
+
+from dojo.announcement.models import Announcement
+
+
+class AnnouncementCreateForm(forms.ModelForm):
+ class Meta:
+ model = Announcement
+ fields = "__all__"
+
+
+class AnnouncementRemoveForm(AnnouncementCreateForm):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields["dismissable"].disabled = True
+ self.fields["message"].disabled = True
+ self.fields["style"].disabled = True
diff --git a/dojo/announcement/urls.py b/dojo/announcement/ui/urls.py
similarity index 88%
rename from dojo/announcement/urls.py
rename to dojo/announcement/ui/urls.py
index 9dc91187653..2ac2f2b8af0 100644
--- a/dojo/announcement/urls.py
+++ b/dojo/announcement/ui/urls.py
@@ -1,6 +1,6 @@
from django.urls import re_path
-from dojo.announcement import views
+from dojo.announcement.ui import views
urlpatterns = [
re_path(
diff --git a/dojo/announcement/views.py b/dojo/announcement/ui/views.py
similarity index 95%
rename from dojo/announcement/views.py
rename to dojo/announcement/ui/views.py
index 7afe915210b..f668901e86f 100644
--- a/dojo/announcement/views.py
+++ b/dojo/announcement/ui/views.py
@@ -7,8 +7,8 @@
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
-from dojo.forms import AnnouncementCreateForm, AnnouncementRemoveForm
-from dojo.models import Announcement, UserAnnouncement
+from dojo.announcement.models import Announcement, UserAnnouncement
+from dojo.announcement.ui.forms import AnnouncementCreateForm, AnnouncementRemoveForm
from dojo.utils import add_breadcrumb
logger = logging.getLogger(__name__)
diff --git a/dojo/api_v2/serializers.py b/dojo/api_v2/serializers.py
index e52b1a3a04a..7714214c49a 100644
--- a/dojo/api_v2/serializers.py
+++ b/dojo/api_v2/serializers.py
@@ -28,7 +28,6 @@
SEVERITIES,
SEVERITY_CHOICES,
STATS_FIELDS,
- Announcement,
App_Analysis,
Development_Environment,
DojoMeta,
@@ -43,7 +42,6 @@
Notes,
Product,
Product_API_Scan_Configuration,
- Regulation,
SLA_Configuration,
Sonarqube_Issue,
Sonarqube_Issue_Transition,
@@ -327,15 +325,6 @@ class Meta:
fields = "__all__"
-from dojo.tool_type.api.serializer import ToolTypeSerializer # noqa: E402, F401 -- re-export
-
-
-class RegulationSerializer(serializers.ModelSerializer):
- class Meta:
- model = Regulation
- fields = "__all__"
-
-
from dojo.endpoint.api.serializer import ( # noqa: E402, F401 -- re-export; prefetcher discovery requires all moved ModelSerializers here
EndpointParamsSerializer,
EndpointSerializer,
@@ -346,8 +335,10 @@ class Meta:
JIRAIssueSerializer,
JIRAProjectSerializer,
)
+from dojo.regulations.api.serializer import RegulationSerializer # noqa: E402, F401 -- re-export; prefetcher discovery
from dojo.tool_config.api.serializer import ToolConfigurationSerializer # noqa: E402, F401 -- re-export
from dojo.tool_product.api.serializer import ToolProductSettingsSerializer # noqa: E402, F401 -- re-export
+from dojo.tool_type.api.serializer import ToolTypeSerializer # noqa: E402, F401 -- re-export; prefetcher discovery
class SonarqubeIssueSerializer(serializers.ModelSerializer):
@@ -362,11 +353,9 @@ class Meta:
fields = "__all__"
-class DevelopmentEnvironmentSerializer(serializers.ModelSerializer):
- class Meta:
- model = Development_Environment
- fields = "__all__"
-
+from dojo.development_environment.api.serializer import ( # noqa: E402 -- re-export; prefetcher discovery
+ DevelopmentEnvironmentSerializer, # noqa: F401 -- re-export; prefetcher discovery
+)
# Risk acceptance serializers live in dojo/risk_acceptance/api/serializer.py. Re-exported here
# for backward compat: RiskAcceptanceSerializer is lazy-imported by dojo/finding/api/serializer.py
@@ -1106,21 +1095,7 @@ class Meta:
exclude = ("content_type",)
-class AnnouncementSerializer(serializers.ModelSerializer):
-
- class Meta:
- model = Announcement
- fields = "__all__"
-
- def create(self, validated_data):
- validated_data["id"] = 1
- try:
- return super().create(validated_data)
- except IntegrityError as e:
- if 'duplicate key value violates unique constraint "dojo_announcement_pkey"' in str(e):
- msg = "No more than one Announcement is allowed"
- raise serializers.ValidationError(msg)
- raise
-
-
+from dojo.announcement.api.serializer import ( # noqa: E402 -- re-export; prefetcher discovery
+ AnnouncementSerializer, # noqa: F401 -- re-export; prefetcher discovery
+)
from dojo.notifications.api.serializer import NotificationWebhooksSerializer # noqa: E402, F401 -- backward compat
diff --git a/dojo/api_v2/views.py b/dojo/api_v2/views.py
index d4b763ea167..a90210d8b89 100644
--- a/dojo/api_v2/views.py
+++ b/dojo/api_v2/views.py
@@ -46,9 +46,7 @@
from dojo.jira import services as jira_services
from dojo.labels import get_labels
from dojo.models import (
- Announcement,
App_Analysis,
- Development_Environment,
Dojo_User,
DojoMeta,
Endpoint,
@@ -57,7 +55,6 @@
Languages,
Network_Locations,
Product,
- Regulation,
SLA_Configuration,
Sonarqube_Issue,
Sonarqube_Issue_Transition,
@@ -316,31 +313,8 @@ def process_patch(self, request):
raise ValidationError(msg)
-# Authorization: authenticated, configuration
-class DevelopmentEnvironmentViewSet(
- DojoModelViewSet,
-):
- serializer_class = serializers.DevelopmentEnvironmentSerializer
- queryset = Development_Environment.objects.none()
- filter_backends = (DjangoFilterBackend,)
- permission_classes = (IsAuthenticated, permissions.UserHasDevelopmentEnvironmentPermission)
-
- def get_queryset(self):
- return Development_Environment.objects.all().order_by("id")
-
-
-# Authorization: authenticated, configuration
-class RegulationsViewSet(
- DojoModelViewSet,
-):
- serializer_class = serializers.RegulationSerializer
- queryset = Regulation.objects.none()
- filter_backends = (DjangoFilterBackend,)
- filterset_fields = ["id", "name", "description"]
- permission_classes = (IsAuthenticated, permissions.UserHasRegulationPermission)
-
- def get_queryset(self):
- return Regulation.objects.all().order_by("id")
+# DevelopmentEnvironmentViewSet moved to dojo/development_environment/api/views.py
+# RegulationsViewSet moved to dojo/regulations/api/views.py
# Authorization: authenticated users, DjangoModelPermissions
@@ -924,15 +898,4 @@ def get_queryset(self):
return SLA_Configuration.objects.all().order_by("id")
-# Authorization: configuration
-class AnnouncementViewSet(
- DojoModelViewSet,
-):
- serializer_class = serializers.AnnouncementSerializer
- queryset = Announcement.objects.none()
- filter_backends = (DjangoFilterBackend,)
- filterset_fields = "__all__"
- permission_classes = (permissions.UserHasConfigurationPermissionStaff,)
-
- def get_queryset(self):
- return Announcement.objects.all().order_by("id")
+# AnnouncementViewSet moved to dojo/announcement/api/views.py
diff --git a/dojo/banner/__init__.py b/dojo/banner/__init__.py
index e69de29bb2d..b0433151e3a 100644
--- a/dojo/banner/__init__.py
+++ b/dojo/banner/__init__.py
@@ -0,0 +1 @@
+import dojo.banner.admin # noqa: F401
diff --git a/dojo/banner/admin.py b/dojo/banner/admin.py
new file mode 100644
index 00000000000..02dc5f7d321
--- /dev/null
+++ b/dojo/banner/admin.py
@@ -0,0 +1,5 @@
+from django.contrib import admin
+
+from dojo.banner.models import BannerConf
+
+admin.site.register(BannerConf)
diff --git a/dojo/banner/models.py b/dojo/banner/models.py
new file mode 100644
index 00000000000..7c885b8ea34
--- /dev/null
+++ b/dojo/banner/models.py
@@ -0,0 +1,7 @@
+from django.db import models
+from django.utils.translation import gettext as _
+
+
+class BannerConf(models.Model):
+ banner_enable = models.BooleanField(default=False, null=True, blank=True)
+ banner_message = models.CharField(max_length=500, help_text=_("This message will be displayed on the login page. It can contain basic html tags, for example https://example.com"), default="")
diff --git a/dojo/banner/ui/__init__.py b/dojo/banner/ui/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/dojo/banner/ui/forms.py b/dojo/banner/ui/forms.py
new file mode 100644
index 00000000000..78a1fbbcf4b
--- /dev/null
+++ b/dojo/banner/ui/forms.py
@@ -0,0 +1,18 @@
+from django import forms
+
+
+class LoginBanner(forms.Form):
+ banner_enable = forms.BooleanField(
+ label="Enable login banner",
+ initial=False,
+ required=False,
+ help_text="Tick this box to enable a text banner on the login page",
+ )
+
+ banner_message = forms.CharField(
+ required=False,
+ label="Message to display on the login page",
+ )
+
+ def clean(self):
+ return super().clean()
diff --git a/dojo/banner/urls.py b/dojo/banner/ui/urls.py
similarity index 82%
rename from dojo/banner/urls.py
rename to dojo/banner/ui/urls.py
index c0b75f1ff77..3751ac59d63 100644
--- a/dojo/banner/urls.py
+++ b/dojo/banner/ui/urls.py
@@ -1,6 +1,6 @@
from django.urls import re_path
-from dojo.banner import views
+from dojo.banner.ui import views
urlpatterns = [
re_path(
diff --git a/dojo/banner/views.py b/dojo/banner/ui/views.py
similarity index 94%
rename from dojo/banner/views.py
rename to dojo/banner/ui/views.py
index 1bdf8ce2e68..03646c7f914 100644
--- a/dojo/banner/views.py
+++ b/dojo/banner/ui/views.py
@@ -5,8 +5,8 @@
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
-from dojo.forms import LoginBanner
-from dojo.models import BannerConf
+from dojo.banner.models import BannerConf
+from dojo.banner.ui.forms import LoginBanner
from dojo.utils import add_breadcrumb
logger = logging.getLogger(__name__)
diff --git a/dojo/development_environment/__init__.py b/dojo/development_environment/__init__.py
index e69de29bb2d..adc8d51562b 100644
--- a/dojo/development_environment/__init__.py
+++ b/dojo/development_environment/__init__.py
@@ -0,0 +1 @@
+import dojo.development_environment.admin # noqa: F401
diff --git a/dojo/development_environment/admin.py b/dojo/development_environment/admin.py
new file mode 100644
index 00000000000..a6ea8885e39
--- /dev/null
+++ b/dojo/development_environment/admin.py
@@ -0,0 +1,5 @@
+from django.contrib import admin
+
+from dojo.development_environment.models import Development_Environment
+
+admin.site.register(Development_Environment)
diff --git a/dojo/development_environment/api/__init__.py b/dojo/development_environment/api/__init__.py
new file mode 100644
index 00000000000..d48867a443d
--- /dev/null
+++ b/dojo/development_environment/api/__init__.py
@@ -0,0 +1 @@
+path = "development_environments" # noqa: RUF067
diff --git a/dojo/development_environment/api/serializer.py b/dojo/development_environment/api/serializer.py
new file mode 100644
index 00000000000..393c6ac2e98
--- /dev/null
+++ b/dojo/development_environment/api/serializer.py
@@ -0,0 +1,9 @@
+from rest_framework import serializers
+
+from dojo.development_environment.models import Development_Environment
+
+
+class DevelopmentEnvironmentSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Development_Environment
+ fields = "__all__"
diff --git a/dojo/development_environment/api/urls.py b/dojo/development_environment/api/urls.py
new file mode 100644
index 00000000000..6d4937f37f7
--- /dev/null
+++ b/dojo/development_environment/api/urls.py
@@ -0,0 +1,7 @@
+from dojo.development_environment.api import path
+from dojo.development_environment.api.views import DevelopmentEnvironmentViewSet
+
+
+def add_development_environment_urls(router):
+ router.register(path, DevelopmentEnvironmentViewSet, basename="development_environment")
+ return router
diff --git a/dojo/development_environment/api/views.py b/dojo/development_environment/api/views.py
new file mode 100644
index 00000000000..e79748e89be
--- /dev/null
+++ b/dojo/development_environment/api/views.py
@@ -0,0 +1,20 @@
+from django_filters.rest_framework import DjangoFilterBackend
+from rest_framework.permissions import IsAuthenticated
+
+from dojo.api_v2.views import DojoModelViewSet
+from dojo.authorization import api_permissions as permissions
+from dojo.development_environment.api.serializer import DevelopmentEnvironmentSerializer
+from dojo.development_environment.models import Development_Environment
+
+
+# Authorization: authenticated, configuration
+class DevelopmentEnvironmentViewSet(
+ DojoModelViewSet,
+):
+ serializer_class = DevelopmentEnvironmentSerializer
+ queryset = Development_Environment.objects.none()
+ filter_backends = (DjangoFilterBackend,)
+ permission_classes = (IsAuthenticated, permissions.UserHasDevelopmentEnvironmentPermission)
+
+ def get_queryset(self):
+ return Development_Environment.objects.all().order_by("id")
diff --git a/dojo/development_environment/models.py b/dojo/development_environment/models.py
new file mode 100644
index 00000000000..d8803d6e576
--- /dev/null
+++ b/dojo/development_environment/models.py
@@ -0,0 +1,13 @@
+from django.db import models
+from django.urls import reverse
+
+
+class Development_Environment(models.Model):
+ name = models.CharField(max_length=200)
+
+ def __str__(self):
+ return self.name
+
+ def get_breadcrumbs(self):
+ return [{"title": str(self),
+ "url": reverse("edit_dev_env", args=(self.id,))}]
diff --git a/dojo/development_environment/ui/__init__.py b/dojo/development_environment/ui/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/dojo/development_environment/ui/forms.py b/dojo/development_environment/ui/forms.py
new file mode 100644
index 00000000000..4df9ceca798
--- /dev/null
+++ b/dojo/development_environment/ui/forms.py
@@ -0,0 +1,15 @@
+from django import forms
+
+from dojo.development_environment.models import Development_Environment
+
+
+class Development_EnvironmentForm(forms.ModelForm):
+ class Meta:
+ model = Development_Environment
+ fields = ["name"]
+
+
+class Delete_Dev_EnvironmentForm(forms.ModelForm):
+ class Meta:
+ model = Development_Environment
+ fields = ["id"]
diff --git a/dojo/development_environment/urls.py b/dojo/development_environment/ui/urls.py
similarity index 85%
rename from dojo/development_environment/urls.py
rename to dojo/development_environment/ui/urls.py
index 1c1c60393d7..918789fcf79 100644
--- a/dojo/development_environment/urls.py
+++ b/dojo/development_environment/ui/urls.py
@@ -1,6 +1,6 @@
from django.urls import re_path
-from dojo.development_environment import views
+from dojo.development_environment.ui import views
urlpatterns = [
# dev envs
diff --git a/dojo/development_environment/views.py b/dojo/development_environment/ui/views.py
similarity index 95%
rename from dojo/development_environment/views.py
rename to dojo/development_environment/ui/views.py
index 8705fdd4c7c..8429c73bff0 100644
--- a/dojo/development_environment/views.py
+++ b/dojo/development_environment/ui/views.py
@@ -9,9 +9,9 @@
from django.urls import reverse
from dojo.authorization.authorization import user_has_configuration_permission_or_403
+from dojo.development_environment.models import Development_Environment
+from dojo.development_environment.ui.forms import Delete_Dev_EnvironmentForm, Development_EnvironmentForm
from dojo.filters import DevelopmentEnvironmentFilter
-from dojo.forms import Delete_Dev_EnvironmentForm, Development_EnvironmentForm
-from dojo.models import Development_Environment
from dojo.utils import add_breadcrumb, get_page_items
logger = logging.getLogger(__name__)
diff --git a/dojo/forms.py b/dojo/forms.py
index 0a349d0c481..0ef434a0fcc 100644
--- a/dojo/forms.py
+++ b/dojo/forms.py
@@ -45,7 +45,6 @@
from dojo.location.utils import validate_locations_to_add
from dojo.models import (
SEVERITY_CHOICES,
- Announcement,
App_Analysis,
Check_List,
Development_Environment,
@@ -54,10 +53,8 @@
Endpoint,
FileUpload,
Finding_Group,
- Objects_Product,
Product_API_Scan_Configuration,
Product_Type,
- Regulation,
SLA_Configuration,
Test_Type,
User,
@@ -178,17 +175,10 @@ def clean_name(self):
return self.cleaned_data["name"]
-class Development_EnvironmentForm(forms.ModelForm):
- class Meta:
- model = Development_Environment
- fields = ["name"]
-
-
-class Delete_Dev_EnvironmentForm(forms.ModelForm):
- class Meta:
- model = Development_Environment
- fields = ["id"]
-
+from dojo.development_environment.ui.forms import ( # noqa: E402, F401 -- re-export
+ Delete_Dev_EnvironmentForm,
+ Development_EnvironmentForm,
+)
# Re-exported for external consumers (finding_group/test/engagement/product views + unittests).
# The remaining finding forms live only in dojo.finding.ui.forms and are imported there by finding's own views.
@@ -788,12 +778,7 @@ class Meta:
BenchmarkForm,
DeleteBenchmarkForm,
)
-
-
-class RegulationForm(forms.ModelForm):
- class Meta:
- model = Regulation
- exclude = ["product"]
+from dojo.regulations.ui.forms import RegulationForm # noqa: E402, F401 -- re-export
class AppAnalysisForm(forms.ModelForm):
@@ -859,40 +844,16 @@ class Meta:
fields = ["id"]
-class DeleteObjectsSettingsForm(forms.ModelForm):
- id = forms.IntegerField(required=True,
- widget=forms.widgets.HiddenInput())
-
- class Meta:
- model = Objects_Product
- fields = ["id"]
-
-
-class ObjectSettingsForm(forms.ModelForm):
-
- # tags = forms.CharField(widget=forms.SelectMultiple(choices=[]),
- # required=False,
- # help_text="Add tags that help describe this object. "
- # "Choose from the list or add new tags. Press TAB key to add.")
-
- class Meta:
- model = Objects_Product
- fields = ["path", "folder", "artifact", "name", "review_status", "tags"]
- exclude = ["product"]
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- def clean(self):
- return self.cleaned_data
-
-
from dojo.notifications.ui.forms import ( # noqa: E402, F401 -- backward compat
DeleteNotificationsWebhookForm,
NotificationsForm,
NotificationsWebhookForm,
ProductNotificationsForm,
)
+from dojo.object.ui.forms import ( # noqa: E402, F401 -- re-export
+ DeleteObjectsSettingsForm,
+ ObjectSettingsForm,
+)
class AjaxChoiceField(forms.ChoiceField):
@@ -900,35 +861,11 @@ def valid_value(self, value):
return True
-class LoginBanner(forms.Form):
- banner_enable = forms.BooleanField(
- label="Enable login banner",
- initial=False,
- required=False,
- help_text="Tick this box to enable a text banner on the login page",
- )
-
- banner_message = forms.CharField(
- required=False,
- label="Message to display on the login page",
- )
-
- def clean(self):
- return super().clean()
-
-
-class AnnouncementCreateForm(forms.ModelForm):
- class Meta:
- model = Announcement
- fields = "__all__"
-
-
-class AnnouncementRemoveForm(AnnouncementCreateForm):
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.fields["dismissable"].disabled = True
- self.fields["message"].disabled = True
- self.fields["style"].disabled = True
+from dojo.announcement.ui.forms import ( # noqa: E402, F401 -- re-export
+ AnnouncementCreateForm,
+ AnnouncementRemoveForm,
+)
+from dojo.banner.ui.forms import LoginBanner # noqa: E402, F401 -- re-export
class ConfigurationPermissionsForm(forms.Form):
diff --git a/dojo/models.py b/dojo/models.py
index c5eb30b0d15..c4d3d0aaa68 100644
--- a/dojo/models.py
+++ b/dojo/models.py
@@ -119,44 +119,11 @@ def __call__(self, model_instance, filename):
return Path(now().strftime(self.directory)) / filename
-class Regulation(models.Model):
- PRIVACY_CATEGORY = "privacy"
- FINANCE_CATEGORY = "finance"
- EDUCATION_CATEGORY = "education"
- MEDICAL_CATEGORY = "medical"
- CORPORATE_CATEGORY = "corporate"
- SECURITY_CATEGORY = "security"
- GOVERNMENT_CATEGORY = "government"
- OTHER_CATEGORY = "other"
- CATEGORY_CHOICES = (
- (PRIVACY_CATEGORY, _("Privacy")),
- (FINANCE_CATEGORY, _("Finance")),
- (EDUCATION_CATEGORY, _("Education")),
- (MEDICAL_CATEGORY, _("Medical")),
- (CORPORATE_CATEGORY, _("Corporate")),
- (SECURITY_CATEGORY, _("Security")),
- (GOVERNMENT_CATEGORY, _("Government")),
- (OTHER_CATEGORY, _("Other")),
- )
-
- name = models.CharField(max_length=128, unique=True, help_text=_("The name of the regulation."))
- acronym = models.CharField(max_length=20, unique=True, help_text=_("A shortened representation of the name."))
- category = models.CharField(max_length=16, choices=CATEGORY_CHOICES, help_text=_("The subject of the regulation."))
- jurisdiction = models.CharField(max_length=64, help_text=_("The territory over which the regulation applies."))
- description = models.TextField(blank=True, help_text=_("Information about the regulation's purpose."))
- reference = models.URLField(blank=True, help_text=_("An external URL for more information."))
-
- class Meta:
- ordering = ["name"]
-
- def __str__(self):
- return self.acronym + " (" + self.jurisdiction + ")"
-
-
User = get_user_model()
-from dojo.user.models import Contact, Dojo_User, UserContactInfo # noqa: E402, F401, I001 -- must precede system_settings (middleware load-order)
+from dojo.regulations.models import Regulation # noqa: E402, F401, I001 -- re-export; user/system_settings block intentionally out-of-order (load-order)
+from dojo.user.models import Contact, Dojo_User, UserContactInfo # noqa: E402, F401 -- must precede system_settings (middleware load-order)
from dojo.system_settings.models import System_Settings # noqa: E402, F401 -- re-export
@@ -390,6 +357,7 @@ def __str__(self):
return self.location
+from dojo.development_environment.models import Development_Environment # noqa: E402, F401 -- re-export
from dojo.endpoint.models import Endpoint, Endpoint_Params, Endpoint_Status # noqa: E402, F401 -- re-export
from dojo.engagement.models import ( # noqa: E402 -- re-export; class-body FKs below reference these
ENGAGEMENT_STATUS_CHOICES, # noqa: F401 -- re-export
@@ -398,17 +366,6 @@ def __str__(self):
)
-class Development_Environment(models.Model):
- name = models.CharField(max_length=200)
-
- def __str__(self):
- return self.name
-
- def get_breadcrumbs(self):
- return [{"title": str(self),
- "url": reverse("edit_dev_env", args=(self.id,))}]
-
-
class Sonarqube_Issue(models.Model):
key = models.CharField(max_length=60, unique=True, help_text=_("SonarQube issue key"))
status = models.CharField(max_length=20, help_text=_("SonarQube issue status"))
@@ -491,40 +448,12 @@ def get_breadcrumb(self):
return bc
-from dojo.risk_acceptance.models import Risk_Acceptance # noqa: E402, F401 -- re-export
-
-ANNOUNCEMENT_STYLE_CHOICES = (
- ("info", "Info"),
- ("success", "Success"),
- ("warning", "Warning"),
- ("danger", "Danger"),
+from dojo.announcement.models import ( # noqa: E402 -- re-export
+ ANNOUNCEMENT_STYLE_CHOICES, # noqa: F401 -- re-export
+ Announcement, # noqa: F401 -- re-export
+ UserAnnouncement, # noqa: F401 -- re-export
)
-
-
-class Announcement(models.Model):
- message = models.CharField(max_length=500,
- help_text=_("This dismissable message will be displayed on all pages for authenticated users. It can contain basic html tags, for example https://example.com"),
- default="")
- style = models.CharField(max_length=64, choices=ANNOUNCEMENT_STYLE_CHOICES, default="info",
- help_text=_("The style of banner to display. (info, success, warning, danger)"))
- dismissable = models.BooleanField(default=False,
- null=False,
- blank=True,
- verbose_name=_("Dismissable?"),
- help_text=_("Ticking this box allows users to dismiss the current announcement"),
- )
-
-
-class UserAnnouncement(models.Model):
- announcement = models.ForeignKey(Announcement, null=True, editable=False, on_delete=models.CASCADE, related_name="user_announcement")
- user = models.ForeignKey(Dojo_User, null=True, editable=False, on_delete=models.CASCADE)
-
-
-class BannerConf(models.Model):
- banner_enable = models.BooleanField(default=False, null=True, blank=True)
- banner_message = models.CharField(max_length=500, help_text=_("This message will be displayed on the login page. It can contain basic html tags, for example https://example.com"), default="")
-
-
+from dojo.banner.models import BannerConf # noqa: E402, F401 -- re-export
from dojo.github.models import ( # noqa: E402, F401 -- backward compat
GITHUB_Clone,
GITHUB_Conf,
@@ -551,6 +480,7 @@ class BannerConf(models.Model):
Notification_Webhooks,
Notifications,
)
+from dojo.risk_acceptance.models import Risk_Acceptance # noqa: E402, F401 -- re-export
from dojo.tool_product.models import Tool_Product_History, Tool_Product_Settings # noqa: E402, F401 -- re-export
@@ -596,38 +526,7 @@ def __str__(self):
return self.name + " | " + self.product.name
-class Objects_Review(models.Model):
- name = models.CharField(max_length=100, null=True, blank=True)
- created = models.DateTimeField(auto_now_add=True, null=False)
-
- def __str__(self):
- return self.name
-
-
-class Objects_Product(models.Model):
- product = models.ForeignKey(Product, on_delete=models.CASCADE)
- name = models.CharField(max_length=100, null=True, blank=True)
- path = models.CharField(max_length=600, verbose_name=_("Full file path"),
- null=True, blank=True)
- folder = models.CharField(max_length=400, verbose_name=_("Folder"),
- null=True, blank=True)
- artifact = models.CharField(max_length=400, verbose_name=_("Artifact"),
- null=True, blank=True)
- review_status = models.ForeignKey(Objects_Review, on_delete=models.CASCADE)
- created = models.DateTimeField(auto_now_add=True, null=False)
-
- tags = TagField(blank=True, force_lowercase=True, help_text=_("Add tags that help describe this object. Choose from the list or add new tags. Press Enter key to add."))
-
- def __str__(self):
- name = None
- if self.path is not None:
- name = self.path
- elif self.folder is not None:
- name = self.folder
- elif self.artifact is not None:
- name = self.artifact
-
- return name
+from dojo.object.models import Objects_Product, Objects_Review # noqa: E402, F401 -- re-export
class Testing_Guide_Category(models.Model):
@@ -696,15 +595,14 @@ def __str__(self):
tagulous.admin.register(Engagement.inherited_tags)
tagulous.admin.register(Finding_Template.tags)
tagulous.admin.register(App_Analysis.tags)
-tagulous.admin.register(Objects_Product.tags)
+# Objects_Product.tags registered in dojo/object/admin.py
# Testing
admin.site.register(Testing_Guide_Category)
admin.site.register(Testing_Guide)
admin.site.register(Network_Locations)
-admin.site.register(Objects_Product)
-admin.site.register(Objects_Review)
+# Objects_Product + Objects_Review admin registered in dojo/object/admin.py
admin.site.register(Languages)
admin.site.register(Language_Type)
admin.site.register(App_Analysis)
@@ -713,7 +611,7 @@ def __str__(self):
# Notes + NoteHistory admin registered in dojo/notes/admin.py
# Note_Type admin registered in dojo/note_type/admin.py
admin.site.register(SLA_Configuration)
-admin.site.register(Regulation)
+# Regulation admin registered in dojo/regulations/admin.py
from dojo.authorization.models import ( # noqa: E402
Dojo_Group,
Dojo_Group_Member,
@@ -742,7 +640,6 @@ def __str__(self):
# NoteHistory admin registered in dojo/notes/admin.py
# Report_Type admin registered in dojo/reports/admin.py
admin.site.register(DojoMeta)
-admin.site.register(Development_Environment)
-admin.site.register(Announcement)
-admin.site.register(UserAnnouncement)
-admin.site.register(BannerConf)
+# Development_Environment admin registered in dojo/development_environment/admin.py
+# Announcement + UserAnnouncement admin registered in dojo/announcement/admin.py
+# BannerConf admin registered in dojo/banner/admin.py
diff --git a/dojo/object/__init__.py b/dojo/object/__init__.py
index e69de29bb2d..2a0769e5c0e 100644
--- a/dojo/object/__init__.py
+++ b/dojo/object/__init__.py
@@ -0,0 +1 @@
+import dojo.object.admin # noqa: F401
diff --git a/dojo/object/admin.py b/dojo/object/admin.py
new file mode 100644
index 00000000000..5782d037789
--- /dev/null
+++ b/dojo/object/admin.py
@@ -0,0 +1,8 @@
+import tagulous.admin
+from django.contrib import admin
+
+from dojo.object.models import Objects_Product, Objects_Review
+
+admin.site.register(Objects_Product)
+admin.site.register(Objects_Review)
+tagulous.admin.register(Objects_Product.tags)
diff --git a/dojo/object/models.py b/dojo/object/models.py
new file mode 100644
index 00000000000..37f99568d40
--- /dev/null
+++ b/dojo/object/models.py
@@ -0,0 +1,37 @@
+from django.db import models
+from django.utils.translation import gettext as _
+from tagulous.models import TagField
+
+
+class Objects_Review(models.Model):
+ name = models.CharField(max_length=100, null=True, blank=True)
+ created = models.DateTimeField(auto_now_add=True, null=False)
+
+ def __str__(self):
+ return self.name
+
+
+class Objects_Product(models.Model):
+ product = models.ForeignKey("dojo.Product", on_delete=models.CASCADE)
+ name = models.CharField(max_length=100, null=True, blank=True)
+ path = models.CharField(max_length=600, verbose_name=_("Full file path"),
+ null=True, blank=True)
+ folder = models.CharField(max_length=400, verbose_name=_("Folder"),
+ null=True, blank=True)
+ artifact = models.CharField(max_length=400, verbose_name=_("Artifact"),
+ null=True, blank=True)
+ review_status = models.ForeignKey("dojo.Objects_Review", on_delete=models.CASCADE)
+ created = models.DateTimeField(auto_now_add=True, null=False)
+
+ tags = TagField(blank=True, force_lowercase=True, help_text=_("Add tags that help describe this object. Choose from the list or add new tags. Press Enter key to add."))
+
+ def __str__(self):
+ name = None
+ if self.path is not None:
+ name = self.path
+ elif self.folder is not None:
+ name = self.folder
+ elif self.artifact is not None:
+ name = self.artifact
+
+ return name
diff --git a/dojo/object/ui/__init__.py b/dojo/object/ui/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/dojo/object/ui/forms.py b/dojo/object/ui/forms.py
new file mode 100644
index 00000000000..1c94c6bd9e8
--- /dev/null
+++ b/dojo/object/ui/forms.py
@@ -0,0 +1,26 @@
+from django import forms
+
+from dojo.object.models import Objects_Product
+
+
+class DeleteObjectsSettingsForm(forms.ModelForm):
+ id = forms.IntegerField(required=True,
+ widget=forms.widgets.HiddenInput())
+
+ class Meta:
+ model = Objects_Product
+ fields = ["id"]
+
+
+class ObjectSettingsForm(forms.ModelForm):
+
+ class Meta:
+ model = Objects_Product
+ fields = ["path", "folder", "artifact", "name", "review_status", "tags"]
+ exclude = ["product"]
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ def clean(self):
+ return self.cleaned_data
diff --git a/dojo/object/urls.py b/dojo/object/ui/urls.py
similarity index 93%
rename from dojo/object/urls.py
rename to dojo/object/ui/urls.py
index b31e9350648..9aa1d7cbbdc 100644
--- a/dojo/object/urls.py
+++ b/dojo/object/ui/urls.py
@@ -1,6 +1,6 @@
from django.urls import re_path
-from . import views
+from dojo.object.ui import views
urlpatterns = [
re_path(r"^product/(?P\d+)/object/add$", views.new_object, name="new_object"),
diff --git a/dojo/object/views.py b/dojo/object/ui/views.py
similarity index 88%
rename from dojo/object/views.py
rename to dojo/object/ui/views.py
index 40cc57a45b2..c0f9342c6c1 100644
--- a/dojo/object/views.py
+++ b/dojo/object/ui/views.py
@@ -6,9 +6,9 @@
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
-from dojo.forms import DeleteObjectsSettingsForm, ObjectSettingsForm
from dojo.labels import get_labels
-from dojo.models import Objects_Product, Product
+from dojo.object.models import Objects_Product
+from dojo.object.ui.forms import DeleteObjectsSettingsForm, ObjectSettingsForm
from dojo.utils import Product_Tab
logger = logging.getLogger(__name__)
@@ -18,6 +18,7 @@
def new_object(request, pid):
page_name = labels.ASSET_TRACKED_FILES_ADD_LABEL
+ from dojo.models import Product # noqa: PLC0415 -- lazy import, avoids circular dependency
prod = get_object_or_404(Product, id=pid)
if request.method == "POST":
tform = ObjectSettingsForm(request.POST)
@@ -44,6 +45,7 @@ def new_object(request, pid):
def view_objects(request, pid):
+ from dojo.models import Product # noqa: PLC0415 -- lazy import, avoids circular dependency
product = get_object_or_404(Product, id=pid)
object_queryset = Objects_Product.objects.filter(product=pid).order_by("path", "folder", "artifact")
@@ -59,6 +61,7 @@ def view_objects(request, pid):
def edit_object(request, pid, ttid):
object_prod = get_object_or_404(Objects_Product, pk=ttid)
+ from dojo.models import Product # noqa: PLC0415 -- lazy import, avoids circular dependency
product = get_object_or_404(Product, id=pid)
if object_prod.product != product:
raise PermissionDenied
@@ -87,6 +90,7 @@ def edit_object(request, pid, ttid):
def delete_object(request, pid, ttid):
object_prod = get_object_or_404(Objects_Product, pk=ttid)
+ from dojo.models import Product # noqa: PLC0415 -- lazy import, avoids circular dependency
product = get_object_or_404(Product, id=pid)
if object_prod.product != product:
raise PermissionDenied
diff --git a/dojo/regulations/__init__.py b/dojo/regulations/__init__.py
index e69de29bb2d..a6ad8a993aa 100644
--- a/dojo/regulations/__init__.py
+++ b/dojo/regulations/__init__.py
@@ -0,0 +1 @@
+import dojo.regulations.admin # noqa: F401
diff --git a/dojo/regulations/admin.py b/dojo/regulations/admin.py
new file mode 100644
index 00000000000..6d5961769f5
--- /dev/null
+++ b/dojo/regulations/admin.py
@@ -0,0 +1,5 @@
+from django.contrib import admin
+
+from dojo.regulations.models import Regulation
+
+admin.site.register(Regulation)
diff --git a/dojo/regulations/api/__init__.py b/dojo/regulations/api/__init__.py
new file mode 100644
index 00000000000..de5e580ef42
--- /dev/null
+++ b/dojo/regulations/api/__init__.py
@@ -0,0 +1 @@
+path = "regulations" # noqa: RUF067
diff --git a/dojo/regulations/api/serializer.py b/dojo/regulations/api/serializer.py
new file mode 100644
index 00000000000..519d5c0ef10
--- /dev/null
+++ b/dojo/regulations/api/serializer.py
@@ -0,0 +1,9 @@
+from rest_framework import serializers
+
+from dojo.regulations.models import Regulation
+
+
+class RegulationSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Regulation
+ fields = "__all__"
diff --git a/dojo/regulations/api/urls.py b/dojo/regulations/api/urls.py
new file mode 100644
index 00000000000..fa88fd0086f
--- /dev/null
+++ b/dojo/regulations/api/urls.py
@@ -0,0 +1,7 @@
+from dojo.regulations.api import path
+from dojo.regulations.api.views import RegulationsViewSet
+
+
+def add_regulations_urls(router):
+ router.register(path, RegulationsViewSet, basename="regulations")
+ return router
diff --git a/dojo/regulations/api/views.py b/dojo/regulations/api/views.py
new file mode 100644
index 00000000000..8d0574afc89
--- /dev/null
+++ b/dojo/regulations/api/views.py
@@ -0,0 +1,21 @@
+from django_filters.rest_framework import DjangoFilterBackend
+from rest_framework.permissions import IsAuthenticated
+
+from dojo.api_v2.views import DojoModelViewSet
+from dojo.authorization import api_permissions as permissions
+from dojo.regulations.api.serializer import RegulationSerializer
+from dojo.regulations.models import Regulation
+
+
+# Authorization: authenticated, configuration
+class RegulationsViewSet(
+ DojoModelViewSet,
+):
+ serializer_class = RegulationSerializer
+ queryset = Regulation.objects.none()
+ filter_backends = (DjangoFilterBackend,)
+ filterset_fields = ["id", "name", "description"]
+ permission_classes = (IsAuthenticated, permissions.UserHasRegulationPermission)
+
+ def get_queryset(self):
+ return Regulation.objects.all().order_by("id")
diff --git a/dojo/regulations/models.py b/dojo/regulations/models.py
new file mode 100644
index 00000000000..4910f32f481
--- /dev/null
+++ b/dojo/regulations/models.py
@@ -0,0 +1,36 @@
+from django.db import models
+from django.utils.translation import gettext as _
+
+
+class Regulation(models.Model):
+ PRIVACY_CATEGORY = "privacy"
+ FINANCE_CATEGORY = "finance"
+ EDUCATION_CATEGORY = "education"
+ MEDICAL_CATEGORY = "medical"
+ CORPORATE_CATEGORY = "corporate"
+ SECURITY_CATEGORY = "security"
+ GOVERNMENT_CATEGORY = "government"
+ OTHER_CATEGORY = "other"
+ CATEGORY_CHOICES = (
+ (PRIVACY_CATEGORY, _("Privacy")),
+ (FINANCE_CATEGORY, _("Finance")),
+ (EDUCATION_CATEGORY, _("Education")),
+ (MEDICAL_CATEGORY, _("Medical")),
+ (CORPORATE_CATEGORY, _("Corporate")),
+ (SECURITY_CATEGORY, _("Security")),
+ (GOVERNMENT_CATEGORY, _("Government")),
+ (OTHER_CATEGORY, _("Other")),
+ )
+
+ name = models.CharField(max_length=128, unique=True, help_text=_("The name of the regulation."))
+ acronym = models.CharField(max_length=20, unique=True, help_text=_("A shortened representation of the name."))
+ category = models.CharField(max_length=16, choices=CATEGORY_CHOICES, help_text=_("The subject of the regulation."))
+ jurisdiction = models.CharField(max_length=64, help_text=_("The territory over which the regulation applies."))
+ description = models.TextField(blank=True, help_text=_("Information about the regulation's purpose."))
+ reference = models.URLField(blank=True, help_text=_("An external URL for more information."))
+
+ class Meta:
+ ordering = ["name"]
+
+ def __str__(self):
+ return self.acronym + " (" + self.jurisdiction + ")"
diff --git a/dojo/regulations/ui/__init__.py b/dojo/regulations/ui/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/dojo/regulations/ui/forms.py b/dojo/regulations/ui/forms.py
new file mode 100644
index 00000000000..8e3a7c5f89b
--- /dev/null
+++ b/dojo/regulations/ui/forms.py
@@ -0,0 +1,9 @@
+from django import forms
+
+from dojo.regulations.models import Regulation
+
+
+class RegulationForm(forms.ModelForm):
+ class Meta:
+ model = Regulation
+ exclude = ["product"]
diff --git a/dojo/regulations/urls.py b/dojo/regulations/ui/urls.py
similarity index 88%
rename from dojo/regulations/urls.py
rename to dojo/regulations/ui/urls.py
index 324669f6759..21acf979edf 100644
--- a/dojo/regulations/urls.py
+++ b/dojo/regulations/ui/urls.py
@@ -1,6 +1,6 @@
from django.urls import re_path
-from . import views
+from dojo.regulations.ui import views
urlpatterns = [
re_path(r"^regulations/add", views.new_regulation, name="new_regulation"),
diff --git a/dojo/regulations/views.py b/dojo/regulations/ui/views.py
similarity index 96%
rename from dojo/regulations/views.py
rename to dojo/regulations/ui/views.py
index 9bbb3296190..6fd127921a0 100644
--- a/dojo/regulations/views.py
+++ b/dojo/regulations/ui/views.py
@@ -8,8 +8,8 @@
from django.urls import reverse
from dojo.authorization.authorization import user_has_configuration_permission_or_403
-from dojo.forms import RegulationForm
-from dojo.models import Regulation
+from dojo.regulations.models import Regulation
+from dojo.regulations.ui.forms import RegulationForm
from dojo.utils import add_breadcrumb
logger = logging.getLogger(__name__)
diff --git a/dojo/urls.py b/dojo/urls.py
index be79a7f8bd0..063d65ef220 100644
--- a/dojo/urls.py
+++ b/dojo/urls.py
@@ -10,13 +10,12 @@
from rest_framework.routers import DefaultRouter
from dojo import views
-from dojo.announcement.urls import urlpatterns as announcement_urls
+from dojo.announcement.api.urls import add_announcement_urls
+from dojo.announcement.ui.urls import urlpatterns as announcement_urls
from dojo.api_v2.views import (
- AnnouncementViewSet,
AppAnalysisViewSet,
CeleryViewSet,
ConfigurationPermissionViewSet,
- DevelopmentEnvironmentViewSet,
DojoMetaViewSet,
ImportLanguagesView,
ImportScanView,
@@ -26,7 +25,6 @@
LanguageTypeViewSet,
LanguageViewSet,
NetworkLocationsViewset,
- RegulationsViewSet,
ReImportScanView,
SLAConfigurationViewset,
SonarqubeIssueTransitionViewSet,
@@ -35,10 +33,11 @@
from dojo.api_v2.views import DojoSpectacularAPIView as SpectacularAPIView
from dojo.asset.api.urls import add_asset_urls
from dojo.asset.urls import urlpatterns as asset_urls
-from dojo.banner.urls import urlpatterns as banner_urls
+from dojo.banner.ui.urls import urlpatterns as banner_urls
from dojo.benchmark.ui.urls import urlpatterns as benchmark_urls
from dojo.components.urls import urlpatterns as component_urls
-from dojo.development_environment.urls import urlpatterns as dev_env_urls
+from dojo.development_environment.api.urls import add_development_environment_urls
+from dojo.development_environment.ui.urls import urlpatterns as dev_env_urls
from dojo.endpoint.api.urls import add_endpoint_urls, register_endpoint_meta_import
from dojo.endpoint.ui.urls import urlpatterns as endpoint_urls
from dojo.engagement.api.urls import add_engagement_urls
@@ -58,12 +57,13 @@
from dojo.notes.ui.urls import urlpatterns as notes_urls
from dojo.notifications.api.urls import add_notifications_urls
from dojo.notifications.ui.urls import urlpatterns as notifications_urls
-from dojo.object.urls import urlpatterns as object_urls
+from dojo.object.ui.urls import urlpatterns as object_urls
from dojo.organization.api.urls import add_organization_urls
from dojo.organization.urls import urlpatterns as organization_urls
from dojo.product.api.urls import add_product_urls
from dojo.product_type.api.urls import add_product_type_urls
-from dojo.regulations.urls import urlpatterns as regulations
+from dojo.regulations.api.urls import add_regulations_urls
+from dojo.regulations.ui.urls import urlpatterns as regulations
from dojo.reports.ui.urls import urlpatterns as reports_urls
from dojo.risk_acceptance.api.urls import add_risk_acceptance_urls
from dojo.search.urls import urlpatterns as search_urls
@@ -98,9 +98,9 @@
# v2 api written in django-rest-framework
v2_api = DefaultRouter()
-v2_api.register(r"announcements", AnnouncementViewSet, basename="announcement")
+v2_api = add_announcement_urls(v2_api)
v2_api.register(r"configuration_permissions", ConfigurationPermissionViewSet, basename="permission")
-v2_api.register(r"development_environments", DevelopmentEnvironmentViewSet, basename="development_environment")
+v2_api = add_development_environment_urls(v2_api)
# RBAC endpoints moved to Pro under legacy authorization:
# dojo_groups, dojo_group_members → pro/groups, pro/group_members
v2_api = register_endpoint_meta_import(v2_api)
@@ -127,7 +127,7 @@
v2_api = add_finding_urls(v2_api)
# RBAC endpoints moved to Pro under legacy authorization:
# product_type_members, product_type_groups → pro/product_type_members, pro/product_type_groups
-v2_api.register(r"regulations", RegulationsViewSet, basename="regulations")
+v2_api = add_regulations_urls(v2_api)
v2_api.register(r"reimport-scan", ReImportScanView, basename="reimportscan")
v2_api = add_risk_acceptance_urls(v2_api)
# RBAC endpoint moved to Pro under legacy authorization: roles → pro/roles
diff --git a/unittests/test_rest_framework.py b/unittests/test_rest_framework.py
index c661d4678b3..f82fe0a10fc 100644
--- a/unittests/test_rest_framework.py
+++ b/unittests/test_rest_framework.py
@@ -33,14 +33,13 @@
)
from rest_framework.test import APIClient
+from dojo.announcement.api.views import AnnouncementViewSet
from dojo.api_v2.mixins import DeletePreviewModelMixin
from dojo.api_v2.prefetch import PrefetchListMixin, PrefetchRetrieveMixin
from dojo.api_v2.prefetch.utils import get_prefetchable_fields
from dojo.api_v2.views import (
- AnnouncementViewSet,
AppAnalysisViewSet,
ConfigurationPermissionViewSet,
- DevelopmentEnvironmentViewSet,
ImportLanguagesView,
ImportScanView,
JiraInstanceViewSet,
@@ -57,6 +56,7 @@
AssetViewSet,
)
from dojo.authorization.roles_permissions import Permissions, permission_to_action
+from dojo.development_environment.api.views import DevelopmentEnvironmentViewSet
from dojo.endpoint.api.views import EndpointStatusViewSet, EndPointViewSet
from dojo.engagement.api.views import EngagementViewSet
from dojo.finding.api.views import (