Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 73 additions & 80 deletions backend/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@
AttackPath,
)

from tprm.models import Entity
from tprm.models import Entity, Representative

from .models import *
from .serializers import *
Expand Down Expand Up @@ -718,7 +718,7 @@ def cascade_info(self, request, pk=None):
"""
Cascade preview:
- deleted: objects actually deleted by cascade
- affected: objects not deleted but whose relationships will be removed (through rows, SET_NULL, local links)
- affected: objects not deleted but whose relationships will be removed
"""
instance = self.get_object()
collector = NestedObjects(using=router.db_for_write(instance))
Expand All @@ -734,46 +734,41 @@ def cascade_info(self, request, pk=None):
"Folder",
"Permission",
}
skip_model_classes = set()

# (source_model, field) treated as cascade
FORCED_CASCADE_FIELDS = {
(Representative, "user"),
}

def is_hidden_model(model):
return (skip_model_classes and model in skip_model_classes) or (
not skip_model_classes and model.__name__ in skip_model_names
)
return model.__name__ in skip_model_names

# Build index of concrete objects that will be deleted
def is_forced_cascade(source_model, field_name):
return (source_model, field_name) in FORCED_CASCADE_FIELDS

# Build index of objects Django will delete
deleted_index = set()
for model, objs in collector.model_objs.items():
if model is type(instance):
continue
if getattr(model._meta, "auto_created", False):
continue # skip through rows here; we will bubble endpoints separately
continue
for o in objs:
deleted_index.add((model.__name__, str(getattr(o, "pk", ""))))

def add_grouped(bucket, obj):
model = obj.__class__
if is_hidden_model(model):
return

key = model.__name__
pk = str(getattr(obj, "pk", "")) or ""
if (key, pk) in bucket["_seen"]:
return

if key == "RoleAssignment":
role_name = getattr(getattr(obj, "role", None), "name", "") or ""
if getattr(obj, "user", None):
display = f"{role_name} → {getattr(obj.user, 'email', '')}"
elif getattr(obj, "user_group", None):
display = f"{role_name} → {getattr(obj.user_group, 'name', '')}"
else:
display = role_name
else:
display = (
getattr(obj, "name", None)
or getattr(obj, "email", None)
or str(obj)
)
display = (
getattr(obj, "name", None) or getattr(obj, "email", None) or str(obj)
)

group = bucket["by_model"].setdefault(
key,
Expand All @@ -797,6 +792,24 @@ def finalize(bucket):
deleted_bucket = {"by_model": {}, "_seen": set()}
affected_bucket = {"by_model": {}, "_seen": set()}

# Centralized classification
def add_with_classification(obj, source_model=None, source_field=None):
pair = (obj.__class__.__name__, str(getattr(obj, "pk", "")) or "")

if pair in deleted_index:
add_grouped(deleted_bucket, obj)
return

if (
source_model == instance.__class__
and source_field
and is_forced_cascade(source_model, source_field)
):
add_grouped(deleted_bucket, obj)
return

add_grouped(affected_bucket, obj)

# 1) Concrete deletions
through_rows = []
for model, instances in collector.model_objs.items():
Expand All @@ -810,109 +823,88 @@ def finalize(bucket):
for obj in instances:
add_grouped(deleted_bucket, obj)

# 2) Bubble endpoints from through rows (M2M join tables)
# 2) Bubble endpoints from through rows
for through_model, t_instances in through_rows:
# Join tables have ≥2 FKs; skip others
fk_fields = [
f
for f in through_model._meta.fields
if isinstance(f, ForeignKey) and getattr(f, "remote_field", None)
f for f in through_model._meta.fields if isinstance(f, ForeignKey)
]
if len(fk_fields) < 2:
continue

for t in t_instances:
endpoints = []
for fk in fk_fields:
try:
rel_obj = getattr(t, fk.name, None)
except Exception:
rel_obj = None
if not rel_obj:
continue
if isinstance(rel_obj, type(instance)):
if not rel_obj or isinstance(rel_obj, type(instance)):
continue
if getattr(rel_obj._meta, "auto_created", False):
continue
endpoints.append(rel_obj)

for ep in endpoints:
key = ep.__class__.__name__
pk = str(getattr(ep, "pk", "")) or ""
if (key, pk) in deleted_index:
add_grouped(deleted_bucket, ep)
else:
add_grouped(affected_bucket, ep)
add_with_classification(
rel_obj,
source_model=fk.model,
source_field=fk.name,
)

# 2b) Incoming updates (SET_NULL/DEFAULT) from Django's plan
# 2b) Incoming SET_NULL / DEFAULT updates
updates = getattr(collector, "field_updates", {})

def _iter_objs(maybe_iter):
if maybe_iter is None:
return
if isinstance(maybe_iter, QuerySet):
for x in maybe_iter.iterator():
yield x
else:
for x in maybe_iter:
yield x
yield from maybe_iter.iterator()
elif maybe_iter:
yield from maybe_iter

for key, ops in updates.items():
# Shape A: (field, value) -> [objs, ...]
if isinstance(key, tuple) and len(key) == 2:
field, value = key
for objs in ops or []:
for o in _iter_objs(objs):
model = field.model # model whose rows will be updated
pair = (model.__name__, str(getattr(o, "pk", "")) or "")
if pair not in deleted_index:
# If you want only SET_NULL cases: if value is None: ...
add_grouped(affected_bucket, o)
# Shape B: model -> [(field, value, objs) ...]
elif isinstance(ops, (list, tuple)) and ops and isinstance(ops[0], tuple):
add_with_classification(
o,
source_model=field.model,
source_field=field.name,
)
elif isinstance(ops, (list, tuple)):
for item in ops:
if len(item) == 3:
field, value, objs = item
elif len(item) == 2:
field, objs = item
value = None
else:
continue
for o in _iter_objs(objs):
model = field.model
pair = (model.__name__, str(getattr(o, "pk", "")) or "")
if pair not in deleted_index:
add_grouped(affected_bucket, o)
else:
# Unknown shape; ignore safely
pass
add_with_classification(
o,
source_model=field.model,
source_field=field.name,
)

# 2c) Outgoing links from the instance (FK/O2O/M2M)
# 2c) Outgoing links from the instance
for f in instance._meta.get_fields():
if getattr(f, "auto_created", False):
continue # skip reverse relations
continue

# Local FK/O2O targets: will remain but lose the link to this instance.
if isinstance(f, (ForeignKey, OneToOneField)):
try:
rel_obj = getattr(instance, f.name, None)
except Exception:
rel_obj = None
continue
if rel_obj:
key = rel_obj.__class__.__name__
pk = str(getattr(rel_obj, "pk", "")) or ""
if (key, pk) not in deleted_index:
add_grouped(affected_bucket, rel_obj)
add_with_classification(
rel_obj,
source_model=instance.__class__,
source_field=f.name,
)

# Local M2M targets: will remain; join rows are removed.
elif isinstance(f, ManyToManyField):
try:
manager = getattr(instance, f.name)
for rel_obj in manager.all():
key = rel_obj.__class__.__name__
pk = str(getattr(rel_obj, "pk", "")) or ""
if (key, pk) not in deleted_index:
add_grouped(affected_bucket, rel_obj)
for rel_obj in getattr(instance, f.name).all():
add_with_classification(
rel_obj,
source_model=instance.__class__,
source_field=f.name,
)
except Exception:
pass

Expand Down Expand Up @@ -947,6 +939,7 @@ def flatten(groups):
"level": "info",
},
}

return Response(payload)

def _get_optimized_object_data(self, queryset):
Expand Down
4 changes: 3 additions & 1 deletion frontend/messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -1027,5 +1027,7 @@
}
}
],
"cascadeAffectedHint": "البنود أدناه تبقى في النظام ولكن روابطها بهذا العنصر ستتم إزالتها."
"cascadeAffectedHint": "البنود أدناه تبقى في النظام ولكن روابطها بهذا العنصر ستتم إزالتها.",
"thirdPartyRespondent": "الردود الخارجية",
"thirdPartyRespondentHelpText": "تقييد المستخدم على الإجابة على الاستبيانات/التدقيقات التي يمكن الوصول إليها فقط؛ لا توجد إجراءات أخرى متاحة."
}
4 changes: 3 additions & 1 deletion frontend/messages/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -1047,5 +1047,7 @@
}
}
],
"cascadeAffectedHint": "Níže uvedené položky zůstávají v systému, ale jejich odkazy na tento objekt budou odstraněny."
"cascadeAffectedHint": "Níže uvedené položky zůstávají v systému, ale jejich odkazy na tento objekt budou odstraněny.",
"thirdPartyRespondent": "Třetí strana - odpovědná osoba",
"thirdPartyRespondentHelpText": "Omezit uživatele na odpovídání na dotazníky/audity, ke kterým má přístup; žádné další akce nejsou k dispozici."
}
4 changes: 3 additions & 1 deletion frontend/messages/da.json
Original file line number Diff line number Diff line change
Expand Up @@ -1339,5 +1339,7 @@
}
}
],
"cascadeAffectedHint": "De følgende elementer forbliver i systemet, men deres forbindelser til dette objekt vil blive fjernet."
"cascadeAffectedHint": "De følgende elementer forbliver i systemet, men deres forbindelser til dette objekt vil blive fjernet.",
"thirdPartyRespondent": "Tredjeparts respondent",
"thirdPartyRespondentHelpText": "Begræns brugeren til at svare på spørgeskemaer/revisioner, som de kan få adgang til; ingen andre handlinger er tilgængelige."
}
4 changes: 3 additions & 1 deletion frontend/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -2925,5 +2925,7 @@
}
}
],
"cascadeAffectedHint": "Die unten aufgeführten Elemente bleiben bestehen, aber ihre Verknüpfungen zu diesem Objekt werden entfernt."
"cascadeAffectedHint": "Die unten aufgeführten Elemente bleiben bestehen, aber ihre Verknüpfungen zu diesem Objekt werden entfernt.",
"thirdPartyRespondent": "Drittanbieter-Antwortverantwortlicher",
"thirdPartyRespondentHelpText": "Begrenzen Sie den Benutzer darauf, auf Fragebögen/Audits zu antworten, auf die er Zugriff hat; keine anderen Aktionen sind verfügbar."
}
4 changes: 3 additions & 1 deletion frontend/messages/el.json
Original file line number Diff line number Diff line change
Expand Up @@ -1756,5 +1756,7 @@
}
}
],
"cascadeAffectedHint": "Τα παρακάτω αντικείμενα παραμένουν στο σύστημα αλλά οι σύνδεσμοι τους με αυτό το αντικείμενο θα αφαιρεθούν."
"cascadeAffectedHint": "Τα παρακάτω αντικείμενα παραμένουν στο σύστημα αλλά οι σύνδεσμοι τους με αυτό το αντικείμενο θα αφαιρεθούν.",
"thirdPartyRespondent": "Αναπάντης τρίτου μέρους",
"thirdPartyRespondentHelpText": "Περιορίστε τον χρήστη να απαντά σε ερωτηματολόγια/εξετάσεις στις οποίες έχει πρόσβαση· δεν είναι διαθέσιμες άλλες ενέργειες."
}
4 changes: 3 additions & 1 deletion frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2925,5 +2925,7 @@
}
}
],
"cascadeAffectedHint": "The items below remain in the system but their links to this object will be removed."
"cascadeAffectedHint": "The items below remain in the system but their links to this object will be removed.",
"thirdPartyRespondent": "Third-party Respondent",
"thirdPartyRespondentHelpText": "Limit the user to responding to questionnaires/audits they can access; no other actions are available."
}
4 changes: 3 additions & 1 deletion frontend/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -2924,5 +2924,7 @@
}
}
],
"cascadeAffectedHint": "Los elementos a continuación permanecen en el sistema pero se eliminarán sus enlaces a este objeto."
"cascadeAffectedHint": "Los elementos a continuación se mantendrán en el sistema pero se eliminarán sus enlaces a este objeto.",
"thirdPartyRespondent": "Respondente de Terceros",
"thirdPartyRespondentHelpText": "Limitar al usuario a responder a cuestionarios/auditorías a las que puede acceder; no están disponibles otras acciones."
}
4 changes: 3 additions & 1 deletion frontend/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -2925,5 +2925,7 @@
}
}
],
"cascadeAffectedHint": "Les éléments ci-dessous restent dans le système mais leurs liens avec cet objet seront supprimés."
"cascadeAffectedHint": "Les éléments ci-dessous restent dans le système mais leurs liens avec cet objet seront supprimés.",
"thirdPartyRespondent": "Répondant tiers",
"thirdPartyRespondentHelpText": "Limiter l’utilisateur aux questionnaires/audits auxquels il a accès ; aucune autre action n'est possible."
}
4 changes: 3 additions & 1 deletion frontend/messages/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1028,5 +1028,7 @@
}
}
],
"cascadeAffectedHint": "नीचे दिए गए आइटम सिस्टम में बने रहेंगे लेकिन इस वस्तु से उनके लिंक हटा दिए जाएंगे।"
"cascadeAffectedHint": "नीचे दिए गए आइटम सिस्टम में बने रहेंगे लेकिन इस वस्तु से उनके लिंक हटा दिए जाएंगे।",
"thirdPartyRespondent": "तृतीय-पक्ष प्रतिक्रियाशील",
"thirdPartyRespondentHelpText": "उपयोगकर्ता को केवल उन प्रश्नावली/ऑडिटों के लिए प्रतिक्रिया देने की अनुमति दें जिन तक वे पहुंच सकते हैं; कोई अन्य क्रियाएँ उपलब्ध नहीं हैं।"
}
4 changes: 3 additions & 1 deletion frontend/messages/hr.json
Original file line number Diff line number Diff line change
Expand Up @@ -2024,5 +2024,7 @@
}
}
],
"cascadeAffectedHint": "Stavke ispod ostaju u sustavu, ali njihovi linkovi na ovaj objekt će biti uklonjeni."
"cascadeAffectedHint": "Stavke ispod ostaju u sustavu, ali njihovi linkovi na ovaj objekt će biti uklonjeni.",
"thirdPartyRespondent": "Odgovornik treće strane",
"thirdPartyRespondentHelpText": "Ograničite korisnika na odgovaranje na ankete/audite kojima može pristupiti; ostale radnje nisu dostupne."
}
4 changes: 3 additions & 1 deletion frontend/messages/hu.json
Original file line number Diff line number Diff line change
Expand Up @@ -1059,5 +1059,7 @@
}
}
],
"cascadeAffectedHint": "Az alábbi elemek a rendszerben maradnak, de a kapcsolataik ebben az objektumban törlődnek."
"cascadeAffectedHint": "Az alábbi elemek a rendszerben maradnak, de a kapcsolataik ebben az objektumban törlődnek.",
"thirdPartyRespondent": "Harmadik fél válaszadó",
"thirdPartyRespondentHelpText": "Korlátozza a felhasználót a kérdőívek/auditok kitöltésére, amelyeket elérhet; más műveletek nem érhetők el."
}
4 changes: 3 additions & 1 deletion frontend/messages/id.json
Original file line number Diff line number Diff line change
Expand Up @@ -1244,5 +1244,7 @@
}
}
],
"cascadeAffectedHint": "Item-item di bawah ini tetap berada di sistem tetapi tautan mereka ke objek ini akan dihapus."
"cascadeAffectedHint": "Item-item di bawah ini tetap berada di sistem tetapi tautan mereka ke objek ini akan dihapus.",
"thirdPartyRespondent": "Responden pihak ketiga",
"thirdPartyRespondentHelpText": "Batasi pengguna untuk menanggapi kuesioner/audit yang dapat diakses; tidak ada tindakan lain yang tersedia."
}
4 changes: 3 additions & 1 deletion frontend/messages/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -1436,5 +1436,7 @@
}
}
],
"cascadeAffectedHint": "Gli elementi seguenti rimangono nel sistema ma i loro collegamenti con questo oggetto verranno rimossi."
"cascadeAffectedHint": "Gli elementi seguenti rimangono nel sistema ma i loro collegamenti con questo oggetto verranno rimossi.",
"thirdPartyRespondent": "Rispondente di terze parti",
"thirdPartyRespondentHelpText": "Limita l'utente a rispondere ai questionari/audit a cui può accedere; non sono disponibili altre azioni."
}
4 changes: 3 additions & 1 deletion frontend/messages/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -2924,5 +2924,7 @@
}
}
],
"cascadeAffectedHint": "De onderstaande items blijven in het systeem maar hun verbindingen met dit object worden verwijderd."
"cascadeAffectedHint": "De onderstaande items blijven in het systeem maar hun verbindingen met dit object worden verwijderd.",
"thirdPartyRespondent": "Derdepartij-respondent",
"thirdPartyRespondentHelpText": "Beperk de gebruiker tot het reageren op vragenlijsten/audits waartoe hij toegang heeft; geen andere acties zijn beschikbaar."
}
4 changes: 3 additions & 1 deletion frontend/messages/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -1799,5 +1799,7 @@
}
}
],
"cascadeAffectedHint": "Poniższe elementy pozostają w systemie, ale ich linki do tego obiektu zostaną usunięte."
"cascadeAffectedHint": "Elementy pozostaną w systemie, ale ich linki do tego obiektu zostaną usunięte.",
"thirdPartyRespondent": "Odpowiedź od strony trzeciej",
"thirdPartyRespondentHelpText": "Ogranicz użytkownika do odpowiedzi na ankiety/audyty, do których ma dostęp; żadne inne działania nie są dostępne."
}
Loading
Loading