diff --git a/backend/core/views.py b/backend/core/views.py index ba1358b78..501f584dd 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -15,6 +15,12 @@ BUILD, VERSION, ) + +from django.utils.decorators import method_decorator +from django.views.decorators.cache import cache_page +from django.views.decorators.vary import vary_on_cookie, vary_on_headers +from django.core.cache import cache + from django.contrib.auth.models import Permission from django.core.files.storage import default_storage from django.db import models @@ -57,6 +63,10 @@ User = get_user_model() +SHORT_CACHE_TTL = 2 # mn +MED_CACHE_TTL = 5 # mn +LONG_CACHE_TTL = 60 # mn + class BaseModelViewSet(viewsets.ModelViewSet): filter_backends = [ @@ -133,6 +143,10 @@ def partial_update(self, request: Request, *args, **kwargs) -> Response: self._process_request_data(request) return super().partial_update(request, *args, **kwargs) + def destroy(self, request: Request, *args, **kwargs) -> Response: + self._process_request_data(request) + return super().destroy(request, *args, **kwargs) + class Meta: abstract = True @@ -248,6 +262,12 @@ class ThreatViewSet(BaseModelViewSet): filterset_fields = ["folder", "risk_scenarios"] search_fields = ["name", "provider", "description"] + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + + def retrieve(self, request, *args, **kwargs): + return super().retrieve(request, *args, **kwargs) + @action(detail=False, name="Get threats count") def threats_count(self, request): return Response({"results": threats_count_per_name(request.user)}) @@ -276,10 +296,12 @@ class ReferenceControlViewSet(BaseModelViewSet): filterset_fields = ["folder", "category", "csf_function"] search_fields = ["name", "description", "provider"] + @method_decorator(cache_page(60 * LONG_CACHE_TTL)) @action(detail=False, name="Get category choices") def category(self, request): return Response(dict(ReferenceControl.CATEGORY)) + @method_decorator(cache_page(60 * LONG_CACHE_TTL)) @action(detail=False, name="Get function choices") def csf_function(self, request): return Response(dict(ReferenceControl.CSF_FUNCTION)) @@ -340,6 +362,7 @@ def per_status(self, request): data = assessment_per_status(request.user, RiskAssessment) return Response({"results": data}) + @method_decorator(cache_page(60 * LONG_CACHE_TTL)) @action(detail=False, name="Get status choices") def status(self, request): return Response(dict(RiskAssessment.Status.choices)) @@ -624,18 +647,22 @@ class AppliedControlViewSet(BaseModelViewSet): ] search_fields = ["name", "description", "risk_scenarios", "requirement_assessments"] + @method_decorator(cache_page(60 * LONG_CACHE_TTL)) @action(detail=False, name="Get status choices") def status(self, request): return Response(dict(AppliedControl.Status.choices)) + @method_decorator(cache_page(60 * LONG_CACHE_TTL)) @action(detail=False, name="Get category choices") def category(self, request): return Response(dict(AppliedControl.CATEGORY)) + @method_decorator(cache_page(60 * LONG_CACHE_TTL)) @action(detail=False, name="Get csf_function choices") def csf_function(self, request): return Response(dict(AppliedControl.CSF_FUNCTION)) + @method_decorator(cache_page(60 * LONG_CACHE_TTL)) @action(detail=False, name="Get effort choices") def effort(self, request): return Response(dict(AppliedControl.EFFORT)) @@ -761,6 +788,7 @@ class PolicyViewSet(AppliedControlViewSet): ] search_fields = ["name", "description", "risk_scenarios", "requirement_assessments"] + @method_decorator(cache_page(60 * LONG_CACHE_TTL)) @action(detail=False, name="Get csf_function choices") def csf_function(self, request): return Response(dict(AppliedControl.CSF_FUNCTION)) @@ -782,14 +810,17 @@ class RiskScenarioViewSet(BaseModelViewSet): "applied_controls", ] + @method_decorator(cache_page(60 * LONG_CACHE_TTL)) @action(detail=False, name="Get treatment choices") def treatment(self, request): return Response(dict(RiskScenario.TREATMENT_OPTIONS)) + @method_decorator(cache_page(60 * LONG_CACHE_TTL)) @action(detail=False, name="Get qualification choices") def qualifications(self, request): return Response(dict(RiskScenario.QUALIFICATIONS)) + @method_decorator(cache_page(60 * LONG_CACHE_TTL)) @action(detail=True, name="Get probability choices") def probability(self, request, pk): undefined = {-1: "--"} @@ -802,6 +833,7 @@ def probability(self, request, pk): choices = undefined | _choices return Response(choices) + @method_decorator(cache_page(60 * LONG_CACHE_TTL)) @action(detail=True, name="Get impact choices") def impact(self, request, pk): undefined = dict([(-1, "--")]) @@ -814,6 +846,7 @@ def impact(self, request, pk): choices = undefined | _choices return Response(choices) + @method_decorator(cache_page(60 * LONG_CACHE_TTL)) @action(detail=True, name="Get strength of knowledge choices") def strength_of_knowledge(self, request, pk): undefined = {-1: RiskScenario.DEFAULT_SOK_OPTIONS[-1]} @@ -1104,7 +1137,16 @@ def perform_create(self, serializer): is_recursive=True, ) ra4.perimeter_folders.add(folder) + # Clear the cache after a new folder is created - purposely clearing everything + + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + def retrieve(self, request, *args, **kwargs): + return super().retrieve(request, *args, **kwargs) + + @method_decorator(cache_page(60 * MED_CACHE_TTL)) + @method_decorator(vary_on_cookie) @action(detail=False, methods=["get"]) def org_tree(self, request): """ @@ -1150,6 +1192,8 @@ def org_tree(self, request): return Response(tree) +@cache_page(60 * SHORT_CACHE_TTL) +@vary_on_cookie @api_view(["GET"]) @permission_classes([permissions.IsAuthenticated]) def get_counters_view(request): @@ -1159,6 +1203,8 @@ def get_counters_view(request): return Response({"results": get_counters(request.user)}) +@cache_page(60 * SHORT_CACHE_TTL) +@vary_on_cookie @api_view(["GET"]) @permission_classes([permissions.IsAuthenticated]) def get_metrics_view(request): @@ -1171,6 +1217,8 @@ def get_metrics_view(request): # TODO: Add all the proper docstrings for the following list of functions +@cache_page(60 * SHORT_CACHE_TTL) +@vary_on_cookie @api_view(["GET"]) @permission_classes([permissions.IsAuthenticated]) def get_agg_data(request): @@ -1254,6 +1302,8 @@ class FrameworkViewSet(BaseModelViewSet): search_fields = ["name", "description"] ordering_fields = ["name", "description"] + @method_decorator(cache_page(60 * LONG_CACHE_TTL)) + @method_decorator(vary_on_cookie) @action(detail=False, methods=["get"]) def names(self, request): uuid_list = request.query_params.getlist("id[]", []) @@ -1266,6 +1316,12 @@ def names(self, request): } ) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + + def retrieve(self, request, *args, **kwargs): + return super().retrieve(request, *args, **kwargs) + @action(detail=True, methods=["get"]) def tree(self, request, pk): _framework = Framework.objects.get(id=pk) @@ -1322,6 +1378,9 @@ class RequirementNodeViewSet(BaseModelViewSet): filterset_fields = ["framework", "urn"] search_fields = ["name", "description"] + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + class RequirementViewSet(BaseModelViewSet): """ @@ -1332,6 +1391,9 @@ class RequirementViewSet(BaseModelViewSet): filterset_fields = ["framework", "urn"] search_fields = ["name"] + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + class EvidenceViewSet(BaseModelViewSet): """ @@ -1415,10 +1477,13 @@ class ComplianceAssessmentViewSet(BaseModelViewSet): search_fields = ["name", "description"] ordering_fields = ["name", "description"] + @method_decorator(cache_page(60 * LONG_CACHE_TTL)) @action(detail=False, name="Get status choices") def status(self, request): return Response(dict(ComplianceAssessment.Status.choices)) + @method_decorator(cache_page(60 * MED_CACHE_TTL)) + @method_decorator(vary_on_cookie) @action(detail=True, name="Get implementation group choices") def selected_implementation_groups(self, request, pk): compliance_assessment = self.get_object() @@ -1648,6 +1713,8 @@ def quality_check(self, request): ] return Response({"results": res}) + @method_decorator(cache_page(60 * SHORT_CACHE_TTL)) + @method_decorator(vary_on_cookie) @action(detail=True, methods=["get"]) def global_score(self, request, pk): """Returns the global score of the compliance assessment""" @@ -1745,6 +1812,8 @@ def export(self, request, pk): else: return Response({"error": "Permission denied"}) + @method_decorator(cache_page(60 * SHORT_CACHE_TTL)) + @method_decorator(vary_on_cookie) @action(detail=True, methods=["get"]) def donut_data(self, request, pk): compliance_assessment = ComplianceAssessment.objects.get(id=pk) @@ -1760,6 +1829,11 @@ class RequirementAssessmentViewSet(BaseModelViewSet): filterset_fields = ["folder", "evidences"] search_fields = ["name", "description"] + def update(self, request, *args, **kwargs): + response = super().update(request, *args, **kwargs) + cache.clear() + return response + @action(detail=False, name="Get updatable measures") def updatables(self, request): (_, object_ids_change, _) = RoleAssignment.get_accessible_object_ids( @@ -1825,10 +1899,12 @@ def to_review(self, request): return Response({"results": measures}) + @method_decorator(cache_page(60 * LONG_CACHE_TTL)) @action(detail=False, name="Get status choices") def status(self, request): return Response(dict(RequirementAssessment.Status.choices)) + @method_decorator(cache_page(60 * LONG_CACHE_TTL)) @action(detail=False, name="Get result choices") def result(self, request): return Response(dict(RequirementAssessment.Result.choices)) diff --git a/frontend/src/lib/utils/crud.ts b/frontend/src/lib/utils/crud.ts index 263aac2df..91c10f31e 100644 --- a/frontend/src/lib/utils/crud.ts +++ b/frontend/src/lib/utils/crud.ts @@ -389,8 +389,7 @@ export const URL_MODEL_MAP: ModelMap = { fileFields: ['attachment'], foreignKeyFields: [ { field: 'folder', urlModel: 'folders', urlParams: 'content_type=DO&content_type=GL' }, - { field: 'applied_controls', urlModel: 'applied-controls' }, - { field: 'requirement_assessments', urlModel: 'requirement-assessments' } + { field: 'applied_controls', urlModel: 'applied-controls' } ] }, 'compliance-assessments': { diff --git a/frontend/src/routes/(app)/(internal)/[model=urlmodel]/[id=uuid]/+layout.server.ts b/frontend/src/routes/(app)/(internal)/[model=urlmodel]/[id=uuid]/+layout.server.ts deleted file mode 100644 index 6e991e2a1..000000000 --- a/frontend/src/routes/(app)/(internal)/[model=urlmodel]/[id=uuid]/+layout.server.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { getModelInfo } from '$lib/utils/crud'; -import { loadDetail } from '$lib/utils/load'; -import type { LayoutServerLoad } from './$types'; - -export const load: LayoutServerLoad = async (event) => { - return await loadDetail({ event, model: getModelInfo(event.params.model), id: event.params.id }); -}; diff --git a/frontend/src/routes/(app)/(internal)/[model=urlmodel]/[id=uuid]/+page.server.ts b/frontend/src/routes/(app)/(internal)/[model=urlmodel]/[id=uuid]/+page.server.ts index aa69cf586..23d21aba6 100644 --- a/frontend/src/routes/(app)/(internal)/[model=urlmodel]/[id=uuid]/+page.server.ts +++ b/frontend/src/routes/(app)/(internal)/[model=urlmodel]/[id=uuid]/+page.server.ts @@ -12,6 +12,13 @@ import { zod } from 'sveltekit-superforms/adapters'; import { z } from 'zod'; import { safeTranslate } from '$lib/utils/i18n'; +import { loadDetail } from '$lib/utils/load'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async (event) => { + return await loadDetail({ event, model: getModelInfo(event.params.model), id: event.params.id }); +}; + export const actions: Actions = { create: async (event) => { const formData = await event.request.formData(); @@ -70,7 +77,7 @@ export const actions: Actions = { if (fileFields) { for (const [, file] of Object.entries(fileFields)) { - if (file.size <= 0) { + if (!file || file.size <= 0) { continue; } const fileUploadEndpoint = `${BASE_API_URL}/${urlModel}/${createdObject.id}/upload/`; diff --git a/frontend/src/routes/(app)/(internal)/[model=urlmodel]/[id=uuid]/edit/+page.svelte b/frontend/src/routes/(app)/(internal)/[model=urlmodel]/[id=uuid]/edit/+page.svelte index 4ff531ba0..8658525a0 100644 --- a/frontend/src/routes/(app)/(internal)/[model=urlmodel]/[id=uuid]/edit/+page.svelte +++ b/frontend/src/routes/(app)/(internal)/[model=urlmodel]/[id=uuid]/edit/+page.svelte @@ -5,7 +5,7 @@ export let data: PageData; - breadcrumbObject.set(data.data); + breadcrumbObject.set(data.object);