feat: Enhance security by adding @login_required to multiple views and replacing print statements with logger.debug

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Ross
2026-04-30 21:35:08 +01:00
parent 5d37cad48d
commit 2aff9516ac
13 changed files with 389 additions and 113 deletions
-1
View File
@@ -24,7 +24,6 @@ def check_user_in_group(*groups: Group):
def decorator(function):
@wraps(function)
def wrapper(request, *args, **kwargs):
print(request.user)
if request.user.is_superuser or request.user.groups.filter(
name__in=[group.name for group in groups]
).exists():
+41 -44
View File
@@ -134,11 +134,14 @@ def cid_user_exam_partial(request, app_name: str, exam_id: int, cid: int):
return render(request, "generic/partials/ciduserexam_detail_partial.html", {"cid_user_exam": cux})
@login_required
def exam_collection_exams_partial(request, collection_id: int, exam_type: str):
"""Return the `exam-list` partial for a given collection and exam type.
"""Return the exam-list partial for a given collection and exam type.
This endpoint is intended for HTMX lazy-loading. It annotates candidate counts
and prefetches authors to avoid per-exam DB queries in the template.
Scope: authenticated users only.
Functionality: HTMX lazy-loading endpoint that returns the exam list partial
for a specific exam type within a collection. Annotates candidate counts and
prefetches authors to avoid N+1 DB queries in the template.
"""
collection = get_object_or_404(ExamCollection, pk=collection_id)
@@ -605,10 +608,16 @@ def examination_merge(request, pk):
)
@csrf_exempt
@login_required
def get_examination_id(request):
if request.accepts("application/json")():
examination_name = request.GET["examination_name"]
"""Return the primary key for an Examination looked up by name.
Scope: authenticated users only (admin popup helper).
Functionality: looks up an Examination by exact name and returns its ID
as JSON; used by inline admin form popups.
"""
if request.accepts("application/json"):
examination_name = request.GET.get("examination_name", "")
examination_id = Examination.objects.get(name=examination_name).id
data = {
"examination_id": examination_id,
@@ -975,7 +984,7 @@ class ExamViews(View, LoginRequiredMixin):
Returns:
[boolean]: True if the user has access
"""
print("****", user, exam_id)
logger.debug("check_user_access called for user={} exam_id={}", user, exam_id)
if user.is_superuser:
return True
@@ -2017,7 +2026,7 @@ class ExamViews(View, LoginRequiredMixin):
if request.htmx.prompt is not None:
additional_email = request.htmx.prompt
print(f"{additional_email=}")
logger.debug("exam_send_email: additional_email={}", additional_email)
# check addition email is valid
if additional_email:
@@ -2102,7 +2111,7 @@ class ExamViews(View, LoginRequiredMixin):
raise PermissionDenied
res = exam.generate_user_report(u)
print(res)
logger.debug("generate_user_report result for exam={}: {}", exam_id, res)
return HttpResponse(res)
@@ -2409,7 +2418,7 @@ class ExamViews(View, LoginRequiredMixin):
current_user_users
)
)
print(available_user_users)
logger.debug("available_user_users count: {}", len(available_user_users))
# available_user_users = User.objects.all()
context = {
@@ -2994,9 +3003,7 @@ class ExamViews(View, LoginRequiredMixin):
):
exams = self.Exam.objects.all()
filter = self.ExtraExamFilter(request.GET, queryset=exams)
print("1")
else:
print("2")
exams = self.Exam.objects.filter(
author__id=request.user.id
) | self.Exam.objects.filter(open_access=True)
@@ -3023,7 +3030,7 @@ class ExamViews(View, LoginRequiredMixin):
else:
raise Http404
print(answer)
logger.debug("exam_question_user_answer result: {}", answer)
return HttpResponse(answer)
@method_decorator(login_required)
@@ -3033,7 +3040,7 @@ class ExamViews(View, LoginRequiredMixin):
cid_user = CidUser.objects.filter(cid=cid)
answer = exam.get_question_cid_user_answer(sk, cid_user)
print(answer)
logger.debug("exam_question_cid_user_answer result: {}", answer)
@method_decorator(login_required)
def exam_question_detail(self, request, pk, sk):
@@ -3156,7 +3163,7 @@ class ExamViews(View, LoginRequiredMixin):
n = 0
for answer in json.loads(request.POST.get("answers")):
print("ANSWER", answer)
logger.debug("Processing answer: {}", answer)
eid = int(answer["eid"].split("/")[1])
uid = False
@@ -3366,19 +3373,17 @@ class ExamViews(View, LoginRequiredMixin):
raise Http404("No available exam")
# TODO: check access for logged in users
print("TEST", cid)
logger.debug("exam_question_json: cid={}", cid)
# Check access for users
if request.user.is_anonymous:
user_id = None
else:
user_id = request.user.pk
print(0)
if exam.exam_mode and not exam.check_cid_user(
cid, passcode, request.user, user_id
):
raise Http404("No available exam")
print(1)
# exam_json_cache = cache.get("{}_exam_json_{}".format(self.app_name, pk))
@@ -3503,7 +3508,7 @@ class ExamViews(View, LoginRequiredMixin):
question = get_object_or_404(self.Question, pk=sk)
exam = get_object_or_404(self.Exam, pk=pk)
print("CID", cid)
logger.debug("exam_question_json_unbased: cid={}", cid)
if not exam.active and not self.check_user_access(request.user, pk):
raise Http404("No available exam")
@@ -3514,8 +3519,6 @@ class ExamViews(View, LoginRequiredMixin):
else:
user_id = request.user.pk
print("CID", cid)
if not exam.check_cid_user(cid, passcode, request.user, user_id):
raise Http404("No available exam")
@@ -3652,7 +3655,7 @@ class ExamViews(View, LoginRequiredMixin):
answers.append(ans)
answers_marks.append(answer_score)
print(q.get_questions(), ans, answer_score, correct_answer)
logger.debug("physics answer row: questions={} ans={} score={} correct={}", q.get_questions(), ans, answer_score, correct_answer)
merged_ans = zip(
q.get_questions(), ans, answer_score, correct_answer
)
@@ -4090,8 +4093,7 @@ class GenericViewBase:
"""
exam: ExamBase = get_object_or_404(self.exam_object, pk=pk)
print(request.POST["question_number"])
print(type(request.POST["question_number"]))
logger.debug("question_published_json: question_number={}", request.POST.get("question_number"))
if not exam.publish_results:
raise Http404
@@ -4417,7 +4419,6 @@ class GenericViewBase:
@method_decorator(user_passes_test(lambda u: u.is_superuser))
def user_answer_delete_multiple(self, request):
print(request.POST["answer_ids"])
answer_ids = json.loads(request.POST["answer_ids"])
# We could probably delete them all at once....
@@ -4476,13 +4477,11 @@ class GenericViewBase:
# TODO: improve permissions on these
@method_decorator(login_required)
def question_user_answers_by_compare(self, request, pk, answer_compare):
print(answer_compare)
answer_compare = urllib.parse.unquote(answer_compare)
return self.question_user_answers(request, pk, answer_compare)
@method_decorator(login_required)
def question_user_answers(self, request, pk, answer_compare=None):
print(locals())
question = get_object_or_404(self.question_object, pk=pk)
if answer_compare is None:
@@ -4950,8 +4949,7 @@ def user_not_trainee(request, user_id):
@user_is_cid_user_manager
def users_bulk_edit(request):
print(request.htmx.trigger)
print(request.htmx.trigger_name)
logger.debug("users_bulk_edit: htmx trigger={} name={}", request.htmx.trigger, request.htmx.trigger_name)
try:
match request.htmx.trigger:
@@ -5320,12 +5318,14 @@ def create_cid_email(request):
@user_is_cid_user_manager
def cid_details(request, cid: int):
print(cid)
"""Return a brief display string for a CID user (HTMX partial).
Scope: cid_user_manager and superusers only.
Functionality: returns the CID user's name and email as plain text for
HTMX inline display.
"""
if request.htmx:
cid_user = get_object_or_404(CidUser, cid=cid)
print(cid_user.name)
return HttpResponse(f"{cid_user.name} ({cid_user.email})")
raise PermissionDenied() # or Http404
@@ -5454,7 +5454,7 @@ def manage_cid_users(request):
except ValidationError as e:
invalid_emails.append(f"Invalid email: {email}")
print(f"Email list {email_list}")
logger.debug("bulk_create_cid_users: email_list count={}", len(email_list))
if not email_list:
return JsonResponse({"status": "fail", "details": "No valid emails"})
if invalid_emails or not email_list:
@@ -5629,9 +5629,6 @@ class ExamCreateBase(RevisionMixin, LoginRequiredMixin, CreateView):
def get_context_data(self, **kwargs):
# Call the base implementation first to get a context
context = super().get_context_data(**kwargs)
print(dir(context["view"]))
print(context["view"].template_name_suffix)
print(context["view"].model.app_name)
return context
def get_form_kwargs(self):
@@ -6478,10 +6475,6 @@ def supervisor_trainee(request, pk, trainee_id):
exams = get_user_exams(trainee, supervisor_view=True)
print(exams)
print(trainee)
return render(
request,
"generic/supervisor_trainee.html",
@@ -6542,13 +6535,17 @@ def supervisor_request_account(request):
return render(request, "generic/supervisor_request_account.html", {})
@login_required
def toggle_share_with_supervisor(request, pk):
"""Toggle the share_with_supervisor flag on a CidUserExam record.
Scope: authenticated owner of the CidUserExam record only.
Functionality: HTMX endpoint that toggles whether the exam result is
shared with the user's supervisor.
"""
if request.htmx:
cid_user_exam = get_object_or_404(CidUserExam, pk=pk)
print(request.user)
print(cid_user_exam)
print(cid_user_exam.user_user)
if request.user != cid_user_exam.user_user:
return HttpResponse("Incorrect permissions")