DefectDojo is a Django application (dojo app) for vulnerability management. The codebase is undergoing a modular reorganization to move from monolithic files toward self-contained domain modules.
All domain modules should match the structure of dojo/url/. This is the canonical example of a fully reorganized module.
dojo/{module}/
├── __init__.py # import dojo.{module}.admin # noqa: F401
├── models.py # Domain models, constants, factory methods
├── admin.py # @admin.register() for the module's models
├── services.py # Business logic (no HTTP concerns)
├── queries.py # Complex DB aggregations/annotations
├── signals.py # Django signal handlers
├── [manager.py] # Custom QuerySet/Manager if needed
├── [validators.py] # Field-level validators if needed
├── [helpers.py] # Async task wrappers, tag propagation, etc.
├── ui/
│ ├── __init__.py # Empty
│ ├── forms.py # Django ModelForms
│ ├── filters.py # UI-layer django-filter classes
│ ├── views.py # Thin view functions — delegates to services.py
│ └── urls.py # URL routing
└── api/
├── __init__.py # path = "{module}"
├── serializer.py # DRF serializers
├── views.py # API ViewSets — delegates to services.py
├── filters.py # API-layer filters
└── urls.py # add_{module}_urls(router) registration
services.py is the critical layer: Both ui/views.py and api/views.py call services.py for business logic. Services accept domain objects and primitives — never request/response objects, forms, or serializers.
Backward-compatible re-exports: When moving code out of monolithic files (dojo/models.py, dojo/forms.py, dojo/filters.py, dojo/api_v2/serializers.py, dojo/api_v2/views.py), always leave a re-export at the original location:
from dojo.{module}.models import {Model} # noqa: F401 -- backward compatNever remove re-exports until all consumers are updated in a dedicated cleanup pass.
Modules in various stages of reorganization:
| Module | models.py | services.py | ui/ | api/ | Status |
|---|---|---|---|---|---|
| url | In module | N/A | Done | Done | Complete |
| location | In module | N/A | N/A | Done | Complete |
| product_type | In module | N/A | Done | Done | Complete (#14970) |
| 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, 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) |
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(~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 formsdojo/filters.py(~1,376 lines) — remaining/shared filter classes + shared basesdojo/api_v2/serializers.py(~1,101 lines) — remaining serializers + re-exports for prefetcher discoverydojo/api_v2/views.py(~901 lines) — remaining viewsets + shared base classes/helpers
When asked to reorganize a module, follow these phases in order. Each phase should be independently verifiable.
Before any changes, identify all code to extract:
# 1. Model classes and line ranges in dojo/models.py
grep -n "class {Model}" dojo/models.py
# 2. Form classes in dojo/forms.py
grep -n "class.*{Module}" dojo/forms.py
grep -n "model = {Model}" dojo/forms.py
# 3. Filter classes in dojo/filters.py
grep -n "class.*{Module}\|class.*{Model}" dojo/filters.py
# 4. Serializer classes
grep -n "class.*{Model}" dojo/api_v2/serializers.py
# 5. ViewSet classes
grep -n "class.*{Model}\|class.*{Module}" dojo/api_v2/views.py
# 6. Admin registrations
grep -n "admin.site.register({Model}" dojo/models.py
# 7. All import sites (to verify backward compat)
grep -rn "from dojo.models import.*{Model}" dojo/ unittests/
# 8. Business logic in current views
# Scan dojo/{module}/views.py for: .save(), .delete(), create_notification(),
# jira_helper.*, dojo_dispatch_task(), multi-model workflows- Create
dojo/{module}/models.pywith the model class(es) and associated constants - Create
dojo/{module}/admin.pywithadmin.site.register()calls (remove fromdojo/models.py) - Update
dojo/{module}/__init__.pytoimport dojo.{module}.admin # noqa: F401 - Add re-exports in
dojo/models.py - Remove original model code (keep re-export line)
Import rules for models.py:
- Prefer string FK refs to break circular imports. Convert EVERY ForeignKey/ManyToMany/OneToOne whose target is NOT a class being moved into a string ref
"dojo.<Model>"(e.g.models.ForeignKey(Engagement, ...)→models.ForeignKey("dojo.Engagement", ...)). This lets the extractedmodels.pycarry ZERO top-levelfrom dojo.models import ..., which is what actually prevents circular imports. String refs produce identical migrations (Django resolves via the app registry) —makemigrations --checkmust still say "No changes detected". - References AMONG the classes being moved together also use string refs, for uniformity and to avoid in-file ordering issues.
- Downward/other dojo references inside METHOD bodies: lazy imports inside the method.
- Shared utilities (
copy_model_util,_manage_inherited_tags,get_current_date,tomorrow, etc.): import fromdojo.models. CAVEAT: if a utility is used as a class-body field default (e.g.default=get_current_date), it must be imported (not redefined locally) so its__module__staysdojo.models— otherwise migration serialization changes andmakemigrationsflags a diff. These utils are defined early indojo.models(before the re-export that loads your module), so a top-levelfrom dojo.models import get_current_date, tomorrow, copy_model_utilresolves correctly despite the partial circular load. - Do NOT set
app_labelin Meta — all models inheritdojoapp_label automatically
Lint conventions (the repo pre-commit ruff is strict — match exactly):
- Method-body lazy imports need
# noqa: PLC0415 -- lazy import, avoids circular dependency. - Mid-file / non-top re-exports in
dojo/models.pyneed# noqa: E402, plus# noqa: F401ONLY on names not referenced elsewhere indojo/models.py(a name still used by a remaining class body must NOT get F401). - Self-check before committing:
/home/valentijn/.local/bin/ruff check --config ruff.toml <files>(ruff is a host binary, NOT in the uwsgi container). Never letruff --fixwrap a re-export into a parenthesized multiline — shorten the comment instead.
Re-export placement: use ONE consolidated re-export block per module, placed at the earliest moved class's original position. A name referenced in a class-body FK at load-time must be re-exported BEFORE that line.
Constants: single-source module-level constants in the extracted module and re-export from dojo/models.py (done for IMPORT_ACTIONS, ENGAGEMENT_STATUS_CHOICES). Do not duplicate.
Watch for load-bearing imports: some imports in dojo/models.py exist for side effects, not the imported name (e.g. from dojo.utils import parse_cvss_data transitively registers dojo.location models for apps.py:ready()). If you remove the last consumer of such an import, keep it as a re-export or apps.py breaks.
Verify (runs in docker; model imports need manage.py shell -c, not bare python -c):
docker compose exec -T uwsgi python manage.py check
docker compose exec -T uwsgi python manage.py makemigrations --check --dry-run # must say "No changes detected"
docker compose exec -T uwsgi python manage.py shell -c "from dojo.{module}.models import {Model}; print('ok')"
docker compose exec -T uwsgi python manage.py shell -c "from dojo.models import {Model}; print('ok')"This phase is conditional. If the module's views are pure CRUD (form save/delete, simple field add/remove) with none of the "belongs in services" items below, there is NO services.py — skip the phase (the url/location reference modules have none). Don't invent a service just to have one.
Create dojo/{module}/services.py with business logic extracted from UI views.
What belongs in services.py:
- State transitions (close, reopen, status changes)
- Multi-step creation/update workflows
- External integration calls (JIRA, GitHub)
- Notification dispatching
- Copy/clone operations
- Bulk operations
- Merge operations
What stays in views:
- HTTP request/response handling
- Form instantiation and validation
- Serialization/deserialization
- Authorization checks (
@user_is_authorized,user_has_permission_or_403) - Template rendering, redirects
- Pagination, breadcrumbs
Service function pattern:
def close_engagement(engagement: Engagement, user: User) -> Engagement:
engagement.active = False
engagement.status = "Completed"
engagement.save()
if jira_helper.get_jira_project(engagement):
dojo_dispatch_task(jira_helper.close_epic, engagement.id, push_to_jira=True)
return engagementUpdate UI views and API viewsets to call the service instead of containing logic inline.
- Create
dojo/{module}/ui/__init__.py(empty) - Create
dojo/{module}/ui/forms.py— move form classes fromdojo/forms.py - Add re-exports in
dojo/forms.py
- Create
dojo/{module}/ui/filters.py— move module-specific filters fromdojo/filters.py - Shared base classes (
DojoFilter,DateRangeFilter,ReportBooleanFilter) stay indojo/filters.py. Keep the original base class (class XFilter(DojoFilter)) — do NOT switch toFilterSetto dodge an import. - Circular-import caveat: a re-export in
dojo/filters.py(from dojo.{module}.ui.filters import XFilter) whileui/filters.pyimportsDojoFilterback fromdojo.filterscreates a real cycle (fails whenui/filters.pyloads first). Resolve per the re-export rule below — usually: drop thedojo/filters.pyre-export when the filter's only consumer is the module's own view, and import the filter directly fromdojo.{module}.ui.filtersin that view (matches theurlmodule).
Re-export decisions (Phases 3,4,6,8) — decide per symbol, by actual remaining consumers:
grep -rnthe symbol acrossdojo/andunittests/first. Account for multi-linefrom x import (\n ...\n)blocks — a one-line grep misses them.- If a symbol is still referenced by code that REMAINS in the monolith (e.g.
ProductTypeSerializerused byReportGenerateSerializerinapi_v2/serializers.py) → keep the re-export (# noqa: E402+F401as needed).- If the ONLY consumers are code you are moving/updating anyway (the module's own views/tests) → omit the re-export and point those consumers at the new path. This is required when a re-export would cycle (filter↔
dojo.filters,api_v2.views↔{module}.api.views).- After dropping any re-export, run the module's real unit tests (not just
manage.py check) —checkwon't catch a broken import in a test module.
- Move
dojo/{module}/views.py->dojo/{module}/ui/views.py - Move
dojo/{module}/urls.py->dojo/{module}/ui/urls.py - Update URL imports:
- product: update
dojo/asset/urls.py - product_type: update
dojo/organization/urls.py - others: update the include in
dojo/urls.py
- product: update
-
Create
dojo/{module}/api/__init__.pywithpath = "{module}" -
Create
dojo/{module}/api/serializer.py— move fromdojo/api_v2/serializers.py -
Re-export ONLY the serializers still referenced by code REMAINING in
api_v2/serializers.py(e.g. one nested byReportGenerateSerializer/ used in aRiskAcceptancerepresentation). Serializers consumed only by the viewset are imported by their new path in Phase 8, so omit those re-exports.EXCEPTION — prefetcher discovery (re-export the FULL moved ModelSerializer set):
dojo/api_v2/prefetch/prefetcher.pybuilds its model→serializer map viainspect.getmembers(sys.modules["dojo.api_v2.serializers"], ...). Any movedModelSerializerthat drops out ofapi_v2/serializers.py's module members disappears from that map, so prefetch breaks (e.g.test_detail_prefetch/test_list_prefetchfail with'<field>' not found) — andmanage.py checkdoes NOT catch it; only thetest_rest_frameworkprefetch tests do. So re-export the ENTIRE set of movedModelSerializers (not just the ReportGenerate-nested ones), even nested/sub serializers with no other consumer. This re-export block is byte-identical module membership → zero behavior change. (Pureserializers.Serializersubclasses that aren't tied to a model and aren't referenced elsewhere can still be omitted.) This bit the finding module (18 serializers); revisit earlier modules if their prefetch tests ever regress.
Cycle-break for serializers that reference api_v2 serializers (matches dojo/test/api/serializer.py, dojo/engagement/api/serializer.py): a moved serializer cannot import NoteSerializer/FileSerializer/TagListSerializerField etc. from dojo.api_v2.serializers at module level — that cycles once api_v2/serializers.py re-imports your serializer. Convert class-body field assignments (tags = TagListSerializerField(...), notes = NoteSerializer(many=True)) into a lazy get_fields() override that imports inside the method (# noqa: PLC0415); build_relational_field lazy-imports the same way. The extracted module then carries ZERO top-level dojo.api_v2.serializers import.
@extend_schema_field decorators referencing api_v2 serializers also cycle (their argument is evaluated eagerly at class-body load). A class-body @extend_schema_field(RiskAcceptanceSerializer) / @extend_schema_field(BurpRawRequestResponseSerializer) cannot stay. Drop the decorator and reapply the override at the bottom of the module via drf_spectacular.utils.set_override(Cls.method, "field", LazyImportedSerializer) inside a small _apply_schema_overrides() that lazy-imports the api_v2 serializer (# noqa: PLC0415). This preserves the generated schema with no top-level api_v2 reference. (Decorators whose argument is one of the MOVED serializers in the same file are fine as-is.)
- Create
dojo/{module}/api/filters.py— moveApi{Model}Filterfromdojo/filters.py - Add re-exports
- Create
dojo/{module}/api/views.py— move fromdojo/api_v2/views.py - Do NOT re-export the viewset in
dojo/api_v2/views.py— it would cycle (api_v2.views↔{module}.api.views, because the viewset imports its base classes back fromapi_v2.views). Update the consumers instead: thedojo/urls.pyregistration (Phase 9) andunittests/test_rest_framework.py, which imports viewsets by name (a dropped re-export there is an ImportError thatmanage.py checkwon't catch — only the test run does).
Viewset import pattern (matches dojo/test/api/views.py, dojo/engagement/api/views.py): from dojo.api_v2.views import DojoModelViewSet, PrefetchDojoModelViewSet, report_generate, schema_with_prefetch — base classes and helpers stay in the monolith. Requalify every serializers.X reference that stays in api_v2 to api_v2_serializers.X via from dojo.api_v2 import serializers as api_v2_serializers; import the MOVED serializers by name from dojo.{module}.api.serializer. PRESERVE active class decorators such as @extend_schema_view(**schema_with_prefetch()) — they are easy to drop when copying a viewset and silently change the generated schema. After moving, prune the now-unused engagement-specific imports left behind in api_v2/views.py (filter, services, queries, models) — ruff flags them.
- Create
dojo/{module}/api/urls.py:from dojo.{module}.api import path from dojo.{module}.api.views import {ViewSet} def add_{module}_urls(router): router.register(path, {ViewSet}, path) return router
- Update
dojo/urls.py— replacev2_api.register(...)withadd_{module}_urls(v2_api)
Preserve the exact route and basename from the original v2_api.register(...) call. They often differ (e.g. route product_types, basename="product_type"); path in api/__init__.py should be the route string, and pass basename= explicitly if the original did. Changing either breaks DRF URL reversing and the API tests. Verify with reverse('{basename}-list').
When copying a class/function out, capture through to the next top-level class/dedent. A fixed-line-window read can silently truncate a long class (trailing fields + Meta + __init__), yielding a partial copy that still imports cleanly but drops behavior. Confirm the last line of the source class before deleting it from the monolith.
docker compose exec -T uwsgi python manage.py check
docker compose exec -T uwsgi python manage.py makemigrations --check --dry-run
# Tests run via the wrapper (NOT pytest/manage.py test directly); tee to capture output:
./run-unittest.sh --test-case unittests.{relevant_test_module} 2>&1 | tee /tmp/test.logThe model hierarchy is: Product_Type -> Product -> Engagement -> Test -> Finding
Extract in this order (top to bottom) so that upward FKs can import from already-extracted modules. The recommended order is: product_type, test, engagement, product, finding.
For downward references (e.g., Product_Type's cached properties querying Finding), always use lazy imports:
@cached_property
def critical_present(self):
from dojo.models import Finding # lazy import
return Finding.objects.filter(test__engagement__product__prod_type=self, severity="Critical").exists()- Single Django app: Everything is under
app_label = "dojo". Moving models to subdirectories does NOT require migration changes. - Model discovery: Triggered by
__init__.pyimportingadmin.py, which importsmodels.py. This is the same chaindojo/url/uses. - Signal registration: Handled in
dojo/apps.pyviaimport dojo.{module}.signals. Already set up for test, engagement, product, product_type. - Watson search: Uses
self.get_model("Product")inapps.py— works via Django's model registry regardless of file location. - Admin registration: Currently at the bottom of
dojo/models.py(lines 4888-4973). Must be moved to{module}/admin.pyand removed fromdojo/models.pyto avoidAlreadyRegisterederrors.
This section is the complete, self-contained brief for a fresh agent session (auto mode) to finish the reorganization. The 5 core hierarchy modules (
product_type,test,engagement,product,finding) are DONE — they are the templates. What remains is moving the ~45 peripheral model classes still defined indojo/models.pyinto their domain modules, each as a full vertical slice (all 9 phases), reusing the playbook above.
dojo/models.py is now ~2,254 lines and still defines these peripheral model classes. Move each into its module (most module dirs already exist with views.py/urls.py/helpers but NO models.py/admin.py — only dojo/url/ and dojo/location/ are complete-with-models templates). Leave backward-compat re-exports in every monolith (dojo/models.py, forms.py, filters.py, api_v2/serializers.py, api_v2/views.py) per the rules above.
Decisions already locked with the user (do NOT relitigate):
- Full vertical slice per module (Phases 1–9), not models-only. Skip a phase only when the module genuinely has no code for it (e.g. no API serializer/viewset exists → no
api/layer; no module-specific form → noui/forms.py). Follow the "Phase 2 is conditional" / re-export-by-actual-consumer rules above. - These models STAY in
dojo/models.py(no module worth creating — do NOT extract):DojoMeta,Network_Locations,Sonarqube_Issue,Sonarqube_Issue_Transition,Check_List,Testing_Guide_Category,Testing_Guide,Language_Type,Languages,App_Analysis. Leave them untouched. CWE+BurpRawRequestResponsefold intofinding(they are finding-domain), and are done FIRST on the EXISTING finding PR (#14974), not a new PR.
The 5 core PRs already exist (stacked, merge bottom-up): dev ← #14970 product_type ← #14971 test ← #14972 engagement ← #14973 product ← #14974 finding. The new work CONTINUES this stack on top of #14974. All branches and PRs follow the same conventions as the existing 5.
| PR | Branch (head) | Base | Contents |
|---|---|---|---|
| 1–5 | existing | existing | DONE: product_type, test, engagement, product, finding |
| 5 (#14974) | reorg/finding-models |
reorg/product-models |
ADD CWE + BurpRawRequestResponse to dojo/finding/ (full slice). Existing PR — do NOT create a new one. |
| 6 | reorg/peripheral-user |
reorg/finding-models |
Bundle A: user (Dojo_User, UserContactInfo, Contact) + system_settings (System_Settings) |
| 7 | reorg/peripheral-tools-endpoint |
reorg/peripheral-user |
Bundle B: endpoint (Endpoint_Params, Endpoint_Status, Endpoint) + tool_type (Tool_Type) + tool_config (Tool_Configuration, + admin classes ToolConfigForm_Admin/Tool_Configuration_Admin) + tool_product (Tool_Product_Settings, Tool_Product_History) |
| 8 | reorg/peripheral-survey-benchmark |
reorg/peripheral-tools-endpoint |
Bundle C: survey (Question, TextQuestion, Choice, ChoiceQuestion, Engagement_Survey, Answered_Survey, General_Survey, Answer, TextAnswer, ChoiceAnswer) + benchmark (Benchmark_Type, Benchmark_Category, Benchmark_Requirement, Benchmark_Product, Benchmark_Product_Summary) |
| 9 | reorg/peripheral-notes-files |
reorg/peripheral-survey-benchmark |
Bundle D: notes (NoteHistory, Notes) + note_type (Note_Type) + file_uploads (UniqueUploadNameProvider, FileUpload, FileAccessToken) + reports (Report_Type) + risk_acceptance (Risk_Acceptance) |
| 10 | reorg/peripheral-misc |
reorg/peripheral-notes-files |
Bundle E: regulations (Regulation) + banner (BannerConf) + announcement (Announcement, UserAnnouncement) + development_environment (Development_Environment) + object (Objects_Review, Objects_Product) |
Bundle order is by FK direction: user first (Dojo_User is an FK target almost everywhere); everything else references already-moved or string-ref'd models. Inside a bundle, FKs between same-bundle models are real class refs; FKs to anything OUTSIDE the bundle become string refs "dojo.<Model>" (per the string-FK rule above — this keeps the extracted models.py free of top-level from dojo.models import).
- Branches live on the
upstreamremote (git@github.com:DefectDojo/django-DefectDojo.git), exactly like the existing 5 (their head branches are on upstream, e.g.upstream/reorg/finding-models). Push each new branch toupstream, and force-push with--force-with-leaseon cascade (git push --force-with-lease upstream <branch>:<branch>). - The 5 new PRs are DRAFT PRs. Create with
gh pr create --draft --repo DefectDojo/django-DefectDojo --base <prev-branch> --head <this-branch>. - Each new branch is created from its predecessor's tip:
git checkout -b reorg/peripheral-user reorg/finding-models, etc. Merge bottom-up. - PR descriptions: every PR in the stack (all 10) must include a stack map listing all 10 PRs in order with checkboxes and the bottom-up merge note, so reviewers see the whole picture. Summary section only — NO test-plan section (see CLAUDE.local.md / PR rules). Format PR URLs as markdown links. Read an existing body with
gh pr view <N> --json body -q '.body'before editing; edit via--body-fileor the RESTgh api -X PATCHpath (inline--bodysilently fails on this repo). - Cascade after editing a lower branch (e.g. this AGENTS.md commit on #14970):
git rebase --onto <new-parent> <old-parent-sha> <branch>up the chain, then force-push all with--force-with-lease. AGENTS.md edits always land on the bottom branch (#14970) and cascade.
For EACH module in a bundle, run Phase 0 pre-flight first (the grep block above) to discover its exact forms/filters/serializers/viewsets/urls/admin/signals/consumers — do NOT trust a memorized list. Then Phases 1–9. Reference complete templates: dojo/url/, dojo/location/ (models), and dojo/finding/, dojo/product/, dojo/test/, dojo/engagement/ (full API+UI slices). Verify gates after each phase (manage.py check, makemigrations --check --dry-run, ./run-unittest.sh --test-case unittests.<module> 2>&1 | tee /tmp/test.log). All gates run in docker (docker compose exec -T uwsgi ...); model imports need manage.py shell -c.
Model line ranges in dojo/models.py (snapshot — re-grep before editing; line numbers shift as you extract)
- CWE 1027–1031 · BurpRawRequestResponse 1563–1575 →
finding(PR #14974) - Dojo_User 174–209 · UserContactInfo 211–234 · Contact 605–612 · System_Settings 236–595
- Tool_Type 940–949 · Tool_Configuration 951–979 · ToolConfigForm_Admin/Tool_Configuration_Admin 981–1010 · Endpoint_Params 1033–1039 · Endpoint_Status 1041–1093 · Endpoint 1095–1470 · Tool_Product_Settings 1765–1777 · Tool_Product_History 1779–1785
- Benchmark_Type 1890–1905 · Benchmark_Category 1907–1921 · Benchmark_Requirement 1923–1939 · Benchmark_Product 1941–1957 · Benchmark_Product_Summary 1959–1989 · Question 1992–2012 · TextQuestion 2014–2024 · Choice 2026–2039 · ChoiceQuestion 2041–2058 · Engagement_Survey 2060–2076 · Answered_Survey 2078–2101 · General_Survey 2107–2123 · Answer 2126–2138 · TextAnswer 2140–2149 · ChoiceAnswer 2151–2253
- Note_Type 614–623 · NoteHistory 625–636 · Notes 638–669 · UniqueUploadNameProvider 108–135 · FileUpload 671–749 · FileAccessToken 1679–1703 · Report_Type 751–753 · Risk_Acceptance 1577–1677
- Regulation 136–168 · Announcement 1713–1725 · UserAnnouncement 1727–1730 · BannerConf 1732–1763 · Development_Environment 1472–1481 · Objects_Review 1829–1835 · Objects_Product 1837–1861
Question/Answer(survey): base classes are defined inside awith warnings.catch_warnings(): ...block (polymorphic-model deprecation suppression). PRESERVE that block structure when moving todojo/survey/models.py— don't flatten it.- survey & benchmark have NO serializers/viewsets in
api_v2(verified). So Bundle C likely has noapi/layer — skip Phases 6–9 for those modules (confirm with Phase 0). They DO have UI views/urls/forms/filters. Benchmark_Requirement→ M2MCWE:CWEmoves tofindingin PR #14974 (lands lower in the stack), so by the time Bundle C runs, use string ref"dojo.CWE"(thedojo.modelsre-export stays valid). Same for any otherCWEreference.Risk_Acceptance: M2Maccepted_findings→Finding, FKowner→Dojo_User, M2Mnotes→Notes — all cross-bundle → string refs.dojo/risk_acceptance/already hasapi.py/helper.py/queries.py/signals.pybut nomodels.py; reconcileapi.pyvs the playbook'sapi/dir layout.Endpoint: referencesDojo_User,Finding,Product,Endpoint_Status— string-ref everything except same-bundleEndpoint_Params/Endpoint_Status.dojo/endpoint/already hasqueries.py/utils.py/signals.py.tool_configadmin:ToolConfigForm_Admin(aforms.ModelForm) andTool_Configuration_Admin(anadmin.ModelAdmin) currently sit indojo/models.py— move them todojo/tool_config/admin.py(form + admin), notmodels.py.CWE/BurpRawRequestResponseare heavily imported (20+ files acrossdojo/andunittests/, including tool parsers for CWE and importers for Burp). Run the Phase 0 consumer grep (grep -rn "import.*\bCWE\b" dojo/ unittests/, same forBurpRawRequestResponse) and rely on thedojo.modelsre-export for external consumers — only repoint finding's own code.- Shared bases (the
FindingTagStringFiltertrap): before moving any form/filter, grep for subclasses/consumers OUTSIDE the module. If a base form/filter is also used by a model staying indojo/models.pyor another module, KEEP it in the monolith and import it, rather than moving + back-importing (which cycles). The prefetcher full-re-export rule (Phase 6) applies to any movedModelSerializer.
Update the Current State table above (mark the newly-completed modules Complete), and update the monolith line counts in "Monolithic Files Being Decomposed" (they are stale — dojo/models.py is ~2,254 lines now, not 4,973).