From 2d963259ca2946ae16841e968a39a9e5fc9975e9 Mon Sep 17 00:00:00 2001 From: Ross Date: Thu, 6 Nov 2025 21:30:14 +0000 Subject: [PATCH] Add exam collection link to the navigation menu and clean up dropdown toggle --- generic/views.py | 61 +++++++++++++++++++++++++++++++++++++++++++++ templates/base.html | 8 +++--- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/generic/views.py b/generic/views.py index 70ff4fb5..2411fd36 100644 --- a/generic/views.py +++ b/generic/views.py @@ -4709,6 +4709,67 @@ class ExamCollectionClone(CreateView, AuthorRequiredMixin): object.author.set(self.author) object.save() + + # Clone exams linked to the original collection into the new one. + # For each supported exam app, find exams related to the old collection + # and create shallow copies (questions/authors re-applied, but + # active/archive/publish flags cleared). + if hasattr(self, "initial_object") and self.initial_object: + for _label, rel_name, ExamModel in GROUP_TYPES: + try: + old_exams = list(getattr(self.initial_object, rel_name).all()) + except Exception: + old_exams = [] + + for old_exam in old_exams: + # Build a dict of field values excluding the PK + data = model_to_dict(old_exam, exclude=["id"]) + + # Clear group membership and flags for cloned exams + data["valid_user_users"] = [] if "valid_user_users" in data else [] + data["valid_cid_users"] = [] if "valid_cid_users" in data else [] + data["cid_user_groups"] = [] if "cid_user_groups" in data else [] + data["user_user_groups"] = [] if "user_user_groups" in data else [] + + data["active"] = False + # some models use `archive` or `archived` - try both safely + if "archive" in data: + data["archive"] = False + if "archived" in data: + data["archived"] = False + data["publish_results"] = False if "publish_results" in data else data.get("publish_results", False) + + # Keep a copy of M2M fields to reapply after create + m2m_backup = {} + for m2m_field in ("exam_questions", "author", "markers", "valid_user_users", "valid_cid_users", "cid_user_groups", "user_user_groups"): + if m2m_field in data: + m2m_backup[m2m_field] = data.pop(m2m_field) + + # Set the new examcollection foreign key to the newly created collection + # model_to_dict returns the FK id; replace with instance for create() + data["examcollection"] = object + + try: + new_exam = ExamModel.objects.create(**data) + except Exception: + # If creation failed for any model-specific field, skip cloning this exam + continue + + # Reapply M2M relationships where possible + for field_name, ids in m2m_backup.items(): + try: + getattr(new_exam, field_name).set(ids) + except Exception: + # If the field doesn't exist on this model, ignore + pass + + try: + # Some exam models provide an ordering helper + if hasattr(new_exam, "order_questions"): + new_exam.order_questions() + except Exception: + pass + return HttpResponseRedirect(object.get_absolute_url()) diff --git a/templates/base.html b/templates/base.html index ea114b72..2f96b7bf 100644 --- a/templates/base.html +++ b/templates/base.html @@ -201,7 +201,7 @@ Atlas {% if request.user|has_group:"cid_user_manager" %} @@ -334,7 +337,6 @@ document.addEventListener('DOMContentLoaded', function() { //} // fallback to any table on the page that looks like it has selection var any = document.querySelector('table.js-row-selectable') //|| Array.from(document.querySelectorAll('table')).find(function(t){ return t.querySelector && (t.querySelector('input[name="selection"]') || t.querySelector('input[type="checkbox"]')); }); - console.log('findTableForControl fallback', any); return any || null; } @@ -375,7 +377,7 @@ document.addEventListener('DOMContentLoaded', function() { container.parentNode.insertBefore(wrapper, container); } - //// Create controls for any table that looks selectable but lacks a control block + // Create controls for any table that looks selectable but lacks a control block Array.from(document.querySelectorAll('table')).forEach(function(t){ if (t.querySelector && (t.querySelector('input[name="selection"]') || t.querySelector('input[type="checkbox"]'))) { ensureControlsForTable(t);