diff --git a/sbas/templates/sbas/merge_category.html b/sbas/templates/sbas/merge_category.html new file mode 100644 index 00000000..c5608f0e --- /dev/null +++ b/sbas/templates/sbas/merge_category.html @@ -0,0 +1,48 @@ +{% extends 'sbas/base.html' %} + +{% block content %} +

Merge Category

+ + {% if error %} +
{{ error }}
+ {% endif %} + + {% if success %} +
{{ success }}
+ {% endif %} + +

This admin tool will move all questions assigned to the Source category into the Target category. Optionally you may delete the Source category after the move.

+ +
+ {% csrf_token %} +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ +{% endblock %} diff --git a/sbas/urls.py b/sbas/urls.py index 1b32f189..48b8b6e7 100644 --- a/sbas/urls.py +++ b/sbas/urls.py @@ -110,6 +110,7 @@ urlpatterns = [ views.generate_exam, name="generate_exam", ), + path("category/merge/", views.merge_category, name="merge_category"), #path( # "exam//scores///", # views.exam_scores_cid_user, diff --git a/sbas/views.py b/sbas/views.py index 0d12b3ec..5bbf43e4 100644 --- a/sbas/views.py +++ b/sbas/views.py @@ -1012,6 +1012,59 @@ def import_llm_questions(request): return JsonResponse({"ok": True, "report": report}) +@login_required +@user_passes_test(lambda u: u.is_superuser) +@require_http_methods(["GET", "POST"]) +def merge_category(request): + """Admin view: merge one Category into another. + + GET: render form to choose source and target categories. + POST: expects 'source' and 'target' (category PKs) and optional 'delete_source' checkbox. + The view will reassign all Question.category==source -> target and optionally delete the source Category. + """ + categories = Category.objects.all().order_by('category') + + if request.method == 'GET': + return render(request, 'sbas/merge_category.html', {'categories': categories}) + + # POST + source_id = request.POST.get('source') + target_id = request.POST.get('target') + delete_source = request.POST.get('delete_source') in ('on', 'true', '1', 'True') + + if not source_id or not target_id: + return render(request, 'sbas/merge_category.html', {'categories': categories, 'error': 'Both source and target categories must be selected.'}) + + try: + source = Category.objects.get(pk=int(source_id)) + target = Category.objects.get(pk=int(target_id)) + except Category.DoesNotExist: + return render(request, 'sbas/merge_category.html', {'categories': categories, 'error': 'Invalid category selected.'}) + + if source.pk == target.pk: + return render(request, 'sbas/merge_category.html', {'categories': categories, 'error': 'Source and target must be different.'}) + + # perform update inside a transaction + from django.db import transaction + + with transaction.atomic(): + qs = Question.objects.filter(category=source) + count = qs.count() + qs.update(category=target) + + deleted = False + if delete_source: + # safe to delete now (no FK references from Question) + source.delete() + deleted = True + + msg = f"Moved {count} question{'s' if count != 1 else ''} from '{source}' to '{target}'." + if deleted: + msg += f" Deleted source category '{source}'." + + return render(request, 'sbas/merge_category.html', {'categories': Category.objects.all().order_by('category'), 'success': msg}) + + @login_required @user_passes_test(lambda u: u.is_superuser) @require_http_methods(["POST"])