diff --git a/anatomy/models.py b/anatomy/models.py index e8df503c..c65eec9a 100644 --- a/anatomy/models.py +++ b/anatomy/models.py @@ -329,6 +329,12 @@ class Answer(models.Model): self.answer = self.answer.strip() self.answer_compare = get_answer_compare(self.answer) + def can_edit(self, user): + """Check if user can edit the answer + + This just mirrors the result from the attached question""" + return self.question.can_edit(user) + # def get_compare_string(self): # s = self.answer.lower().strip() # s = s.translate(str.maketrans('', '', string.punctuation)) diff --git a/anatomy/templates/anatomy/question_detail.html b/anatomy/templates/anatomy/question_detail.html index c4aa5729..fe47d248 100644 --- a/anatomy/templates/anatomy/question_detail.html +++ b/anatomy/templates/anatomy/question_detail.html @@ -20,25 +20,40 @@

Question type: {{question.question_type}}

Primary answer: {{ question.get_primary_answer }}

-
- - Answers: - - - - {% for answer in question.answers.all|dictsortreversed:"status" %} - - - - {% endfor %} -
AnswerScore
- - {{ answer }} - - - {{answer.status}}
-
+
+
+ + Answers: + + + + {% for answer in question.answers.all|dictsortreversed:"status" %} + + + {% if answer.proposed %} + + {% endif %} + + + + {% endfor %} +
AnswerScore
+ + {{ answer }} + + + {{answer.status}} + + +
+
+
Answer help: {{ question.answer_help|safe }}
@@ -52,6 +67,10 @@ Exams: {% for exam in question.exams.all %} {{ exam }} {% endfor %} + +
Modality: {{ question.modality }} @@ -148,4 +167,17 @@ } -{% endblock %} \ No newline at end of file +{% endblock %} + +{% block css %} + +{% endblock css %} \ No newline at end of file diff --git a/anatomy/urls.py b/anatomy/urls.py index 806c9ef9..63ab6f93 100644 --- a/anatomy/urls.py +++ b/anatomy/urls.py @@ -47,6 +47,13 @@ urlpatterns = [ views.QuestionDelete.as_view(), name="question_delete", ), + path( + "question//add_exam", + views.question_add_exam, + name="question_add_exam", + ), + path("answer//confirm", views.confirm_answer, name="confirm_answer"), + path("answer//delete", views.delete_answer, name="delete_answer"), path("exam///mark", views.mark, name="mark"), path("exam///mark/all", views.mark_all, name="mark_all"), path( diff --git a/anatomy/views.py b/anatomy/views.py index ccac0bc6..5011d956 100644 --- a/anatomy/views.py +++ b/anatomy/views.py @@ -27,6 +27,8 @@ from django.http import HttpResponseRedirect, HttpResponse from dal import autocomplete from django.conf import settings +from django.utils.html import format_html_join +from django.utils.safestring import mark_safe from .forms import ( @@ -851,3 +853,81 @@ GenericViews = GenericViewBase("anatomy", AnatomyQuestion, UserAnswer, Exam) class ExamClone(ExamCloneMixin, ExamCreate): """Clone exam view""" + + +def confirm_answer(request, answer_id: int): + if request.htmx: + answer = get_object_or_404(Answer, pk=answer_id) + + # Check if the user has permission to confirm the answer + if not answer.can_edit(request.user): + return HttpResponse("Invalid permissions", status=403) + + answer.clean() + answer.proposed = False + answer.save() + return HttpResponse("Answer accepted") + + raise PermissionDenied() + +def delete_answer(request, answer_id: int): + if request.htmx: + answer = get_object_or_404(Answer, pk=answer_id) + + # Check if the user has permission to confirm the answer + if not answer.can_edit(request.user): + return HttpResponse("Invalid permissions", status=403) + + answer.delete() + return HttpResponse("Answer deleted") + raise PermissionDenied() + +def question_add_exam(request, question_id: int): + if request.htmx: + question = get_object_or_404(AnatomyQuestion, pk=question_id) + + if not question.can_edit(request.user): + return HttpResponse("Invalid permissions", status=403) + + + if request.POST: + exam_id = request.POST.get("exam_id", None) + + if exam_id is None: + return HttpResponse("No exam id provided", status=400) + + exam = get_object_or_404(Exam, pk=exam_id) + + if request.POST.get("remove", False) == "true": + question.exams.remove(exam) + return HttpResponse("Question removed from exam.") + else: + question.exams.add(exam) + + return HttpResponse("Question added to exam.") + else: + # Return a list of exams that we can add to the question + + exams = Exam.objects.filter(author=request.user) | Exam.objects.filter(open_access=True) + exams = exams.difference(question.exams.all()) + + if not exams: + return HttpResponse("No exams available to add") + + html = "Exams to add to question:
" + for exam in exams: + html = html + (f"
" + ) + html = html + "
Exams to remove from question:
" + for exam in question.exams.all(): + html = html + (f"
" + ) + html = html + "" + + return HttpResponse(mark_safe(html)) + + + raise PermissionDenied() \ No newline at end of file diff --git a/generic/views.py b/generic/views.py index 2ce2f02b..ea75b5ba 100644 --- a/generic/views.py +++ b/generic/views.py @@ -48,7 +48,12 @@ from atlas.models import CaseCollection, CaseDetail from generic.decorators import user_is_cid_user_manager from generic.filters import CidUserFilter, ExaminationFilter, SupervisorFilter -from generic.tables import CidUserExamTable, CidUserTable, ExaminationTable, SupervisorTable +from generic.tables import ( + CidUserExamTable, + CidUserTable, + ExaminationTable, + SupervisorTable, +) from generic.mixins import CheckCanEditMixin, SuperuserRequiredMixin import zipfile @@ -79,6 +84,7 @@ from .models import ( ExamCollection, ExamUserStatus, Examination, + QuestionBase, QuestionNote, Supervisor, UserGrades, @@ -87,15 +93,31 @@ from .models import ( get_next_cid, ) -from rapids.models import Rapid as RapidQuestion, ExamQuestionDetail as RapidsExamQuestionDetail +from rapids.models import ( + Rapid as RapidQuestion, + ExamQuestionDetail as RapidsExamQuestionDetail, +) from rapids.models import Exam as RapidsExam -from longs.models import Long as LongQuestion, LongSeries, ExamQuestionDetail as LongsExamQuestionDetail +from longs.models import ( + Long as LongQuestion, + LongSeries, + ExamQuestionDetail as LongsExamQuestionDetail, +) from longs.models import Exam as LongsExam -from anatomy.models import AnatomyQuestion as AnatomyQuestion, ExamQuestionDetail as AnatomyExamQuestionDetail +from anatomy.models import ( + AnatomyQuestion as AnatomyQuestion, + ExamQuestionDetail as AnatomyExamQuestionDetail, +) from anatomy.models import Exam as AnatomyExam -from sbas.models import Question as SbasQuestion, ExamQuestionDetail as SbasExamQuestionDetail +from sbas.models import ( + Question as SbasQuestion, + ExamQuestionDetail as SbasExamQuestionDetail, +) from sbas.models import Exam as SbasExam -from physics.models import Question as PhysicsQuestion, ExamQuestionDetail as PhysicsExamQuestionDetail +from physics.models import ( + Question as PhysicsQuestion, + ExamQuestionDetail as PhysicsExamQuestionDetail, +) from physics.models import Exam as PhysicsExam from django.db.models import Case, When @@ -112,13 +134,13 @@ import plotly.express as px from django.db.models import Prefetch - -class RedirectMixin(): +class RedirectMixin: def get_success_url(self) -> str: if "redirect" in self.request.GET: return self.request.GET["redirect"] return super().get_success_url() + class AuthorRequiredMixin(object): def get_object(self, *args, **kwargs): obj = super().get_object(*args, **kwargs) @@ -141,6 +163,7 @@ class CidManagerRequiredMixin(UserPassesTestMixin): # return obj # raise PermissionDenied() # or Http404 + def get_exam_model_from_app_name(app_name: str) -> ExamBase: EXAM_MAP = { "physics": PhysicsExam, @@ -471,7 +494,7 @@ class ExamViews(View, LoginRequiredMixin): if (exam.open_access and exam.active) or user in exam.get_author_objects(): return True - if marker and user in exam.markers.all(): + if marker and user in exam.markers.all(): return True if exam.authors_only: @@ -535,7 +558,7 @@ class ExamViews(View, LoginRequiredMixin): if exam_id is not None: exam = get_object_or_404(self.Exam, pk=exam_id) # Remove open_access check for the momement - #if exam.open_access or user in exam.get_author_objects(): + # if exam.open_access or user in exam.get_author_objects(): # return True if user in exam.get_author_objects(): return True @@ -687,13 +710,21 @@ class ExamViews(View, LoginRequiredMixin): def exam_list(self, request, all=False): if not self.check_user_access(request.user): # raise PermissionDenied - exam_list = self.Exam.objects.filter(author__id=request.user.id, exam_mode=True).order_by( - "name" - ) + exam_list = self.Exam.objects.filter( + author__id=request.user.id, exam_mode=True + ).order_by("name") - exam_list = exam_list | self.Exam.objects.filter(markers__id=request.user.id, exam_mode=True).order_by("name") + exam_list = exam_list | self.Exam.objects.filter( + markers__id=request.user.id, exam_mode=True + ).order_by("name") else: - exam_list = self.Exam.objects.prefetch_related("valid_user_users", "valid_cid_users").filter(exam_mode=True).order_by("name") + exam_list = ( + self.Exam.objects.prefetch_related( + "valid_user_users", "valid_cid_users" + ) + .filter(exam_mode=True) + .order_by("name") + ) if not all: exam_list = exam_list.filter(archive=False).order_by("name") @@ -743,7 +774,7 @@ class ExamViews(View, LoginRequiredMixin): status = {} for user in users: - #user = User.objects.get(pk=u) + # user = User.objects.get(pk=u) user_exam = exam.get_or_create_cid_user_exam(user_user=user) if user_exam.results_emailed_status: status[user] = json.loads(user_exam.results_emailed_status) @@ -796,7 +827,7 @@ class ExamViews(View, LoginRequiredMixin): email_results = {} for user in users: - #user = User.objects.get(pk=u) + # user = User.objects.get(pk=u) user_exam = exam.get_or_create_cid_user_exam(user_user=user) if unsent_only and user_exam.results_emailed_status: @@ -904,7 +935,7 @@ class ExamViews(View, LoginRequiredMixin): queryset=self.Answer.objects.filter( proposed=False, status=self.Answer.MarkOptions.CORRECT ), - to_attr="prefetched_primary_answer" + to_attr="prefetched_primary_answer", # queryset=self.Answer.objects.filter(), ), ) @@ -913,21 +944,20 @@ class ExamViews(View, LoginRequiredMixin): elif self.app_name == "longs": questions = ( exam.exam_questions.prefetch_related( - #Prefetch( + # Prefetch( # "series", # queryset=LongSeries.objects.select_related( # "modality", "examination", "plane", "contrast" # ).prefetch_related(Prefetch("images")), - #), + # ), "author", - #"examquestiondetail", - #"longsseries", + # "examquestiondetail", + # "longsseries", "series", - #"seriesdetail", + # "seriesdetail", "series__images", "series__plane", - "series__examination" - + "series__examination", # to_attr="prefetched_primary_answer" # queryset=self.Answer.objects.filter(), # "series", @@ -948,7 +978,7 @@ class ExamViews(View, LoginRequiredMixin): queryset=self.Answer.objects.filter( proposed=False, status=self.Answer.MarkOptions.CORRECT ), - to_attr="prefetched_primary_answer" + to_attr="prefetched_primary_answer", # queryset=self.Answer.objects.filter(), ), "images", @@ -966,25 +996,29 @@ class ExamViews(View, LoginRequiredMixin): # ) elif self.app_name == "physics": questions = ( - exam.exam_questions.select_related("category").all() + exam.exam_questions.select_related("category") + .all() .order_by("examquestiondetail__sort_order") # .prefetch_related("images", "abnormality", "region", "examination") ) else: - questions = exam.exam_questions.select_related().all().order_by("examquestiondetail__sort_order") + questions = ( + exam.exam_questions.select_related() + .all() + .order_by("examquestiondetail__sort_order") + ) question_number = len(questions) cid_candidates = exam.valid_cid_users.count() users_candidates = exam.valid_user_users.count() - #question_orders = [q.examquestiondetail.sort_order for q in questions] + # question_orders = [q.examquestiondetail.sort_order for q in questions] # Exam order can be missing when, this can be checked with # exam.exam_questions.select_related().all().order_by("examquestiondetail__sort_order").values_list("examquestiondetail__sort_order") # TODO decide if / how this should be flagged to a user (updating teh exam order will fix) - return render( request, "{}/exam_overview.html".format(self.app_name), @@ -1049,10 +1083,12 @@ class ExamViews(View, LoginRequiredMixin): if not request.user.is_superuser: raise PermissionDenied - cid_users = exam.valid_cid_users.all()#.order_by("cid")#.prefetch_related("*") + cid_users = ( + exam.valid_cid_users.all() + ) # .order_by("cid")#.prefetch_related("*") cid_user_count = len(cid_users) - user_users = exam.valid_user_users.all()#.order_by("username") + user_users = exam.valid_user_users.all() # .order_by("username") user_user_count = len(user_users) user_exam_data = exam.cid_users.all().prefetch_related("cid_user", "user_user") @@ -1072,7 +1108,7 @@ class ExamViews(View, LoginRequiredMixin): return render(request, "exam_cids.html", context) - #def exam_groups_edit(self, request, exam_id): + # def exam_groups_edit(self, request, exam_id): # exam = get_object_or_404(self.Exam, pk=exam_id) # if not request.user.groups.filter(name="cid_user_manager").exists(): @@ -1080,8 +1116,7 @@ class ExamViews(View, LoginRequiredMixin): # if request.user not in exam.author.all(): # raise PermissionDenied - - # + # # context = { # "exam": exam, @@ -1208,7 +1243,9 @@ class ExamViews(View, LoginRequiredMixin): if cid is not None: statuses = exam.exam_user_status.filter(cid_user_exam__cid_user__cid=cid) elif user_id is not None: - statuses = exam.exam_user_status.filter(cid_user_exam__user_user__id=user_id) + statuses = exam.exam_user_status.filter( + cid_user_exam__user_user__id=user_id + ) else: statuses = exam.exam_user_status.all() @@ -1332,7 +1369,8 @@ class ExamViews(View, LoginRequiredMixin): ) case "longs": question_detail = LongsExamQuestionDetail.objects.get( - question=question, exam=exam) + question=question, exam=exam + ) case "physics": question_detail = PhysicsExamQuestionDetail.objects.get( question=question, exam=exam @@ -1341,7 +1379,7 @@ class ExamViews(View, LoginRequiredMixin): question_detail = SbasExamQuestionDetail.objects.get( question=question, exam=exam ) - case _: + case _: data = {"status": "error"} return JsonResponse(data, status=200) question_detail.sort_order = n @@ -1408,7 +1446,7 @@ class ExamViews(View, LoginRequiredMixin): queryset=self.Answer.objects.filter( proposed=False, status=self.Answer.MarkOptions.CORRECT ), - to_attr="prefetched_primary_answer" + to_attr="prefetched_primary_answer", # queryset=self.Answer.objects.filter(), ), # Prefetch( @@ -1490,15 +1528,17 @@ class ExamViews(View, LoginRequiredMixin): ) @method_decorator(login_required) - def exam_question_user_answer(self, request, pk: int, sk: int, c_or_u: str, user_or_cid: str): + def exam_question_user_answer( + self, request, pk: int, sk: int, c_or_u: str, user_or_cid: str + ): exam: ExamBase = get_object_or_404(self.Exam, pk=pk) if c_or_u == "u": - user = get_object_or_404(User,pk=user_or_cid) + user = get_object_or_404(User, pk=user_or_cid) answer = exam.get_question_user_user_answer(sk, user) elif c_or_u == "c": - #cid_user = CidUser.objects.filter(cid=user_or_cid) - answer =exam.get_question_cid_user_answer(sk, user_or_cid) + # cid_user = CidUser.objects.filter(cid=user_or_cid) + answer = exam.get_question_cid_user_answer(sk, user_or_cid) else: raise Http404 @@ -1510,7 +1550,7 @@ class ExamViews(View, LoginRequiredMixin): exam: ExamBase = get_object_or_404(self.Exam, pk=pk) cid_user = CidUser.objects.filter(cid=cid) - answer =exam.get_question_cid_user_answer(sk, cid_user) + answer = exam.get_question_cid_user_answer(sk, cid_user) print(answer) @@ -1522,12 +1562,12 @@ class ExamViews(View, LoginRequiredMixin): if not self.check_user_access(request.user, pk, marker=True): raise PermissionDenied - #question = exam.get_questions()[sk] + # question = exam.get_questions()[sk] question = exam.get_question_by_index(sk) exam_length = len(exam.exam_questions.all()) - #pos = exam.get_question_index(question) + 1 + # pos = exam.get_question_index(question) + 1 pos = sk + 1 previous = -1 @@ -1801,7 +1841,9 @@ class ExamViews(View, LoginRequiredMixin): cid_user=c, start_time=t ) exam.exam_user_status.create( - cid_user_exam=cid_user_exam, status="submitted", extra="manual submission" + cid_user_exam=cid_user_exam, + status="submitted", + extra="manual submission", ) cid_user_exam.end_time = timezone.now() @@ -1831,7 +1873,9 @@ class ExamViews(View, LoginRequiredMixin): user_id = None else: user_id = request.user.pk - if exam.exam_mode and not exam.check_cid_user(cid, passcode, request.user, user_id): + if exam.exam_mode and not exam.check_cid_user( + cid, passcode, request.user, user_id + ): raise Http404("No available exam") # exam_json_cache = cache.get("{}_exam_json_{}".format(self.app_name, pk)) @@ -2010,7 +2054,7 @@ class ExamViews(View, LoginRequiredMixin): if not exam.check_cid_user(cid, passcode, request.user): if not self.check_user_access(request.user, pk): - #raise PermissionDenied + # raise PermissionDenied raise Http404("Error accessing exam") if user is not None: @@ -2060,7 +2104,6 @@ class ExamViews(View, LoginRequiredMixin): if self.app_name == "longs": feedback = user_answer.candidate_feedback - match self.app_name: case "sbas": correct_answer = q.get_correct_answer() @@ -2084,7 +2127,9 @@ class ExamViews(View, LoginRequiredMixin): answers_marks.append(answer_score) print(q.get_questions(), ans, answer_score, correct_answer) - merged_ans = zip(q.get_questions(), ans, answer_score, correct_answer) + merged_ans = zip( + q.get_questions(), ans, answer_score, correct_answer + ) answers_and_marks.append((q, merged_ans)) case _: correct_answer = q.get_primary_answer() @@ -2096,7 +2141,9 @@ class ExamViews(View, LoginRequiredMixin): answers_marks.append(answer_score) if self.app_name == "longs": - answers_and_marks.append((ans, answer_score, correct_answer, feedback)) + answers_and_marks.append( + (ans, answer_score, correct_answer, feedback) + ) else: answers_and_marks.append((ans, answer_score, correct_answer)) @@ -2110,7 +2157,9 @@ class ExamViews(View, LoginRequiredMixin): answered = [i for i in answers_marks if type(i) == int] total_score = sum(answered) unmarked_number = len(answers_marks) - len(answered) - total_score = "{} ({} unmarked)".format(total_score, unmarked_number) + total_score = "{} ({} unmarked)".format( + total_score, unmarked_number + ) else: total_score = sum(answers_marks) @@ -2172,7 +2221,7 @@ class ExamViews(View, LoginRequiredMixin): # user_answers_and_marks = defaultdict(list) user_answers_marks = defaultdict(list) - #user_answers = defaultdict(list) + # user_answers = defaultdict(list) # user_names = {} # cid_passcodes = {} @@ -2246,7 +2295,7 @@ class ExamViews(View, LoginRequiredMixin): if answer_score == "unmarked": index = exam.get_question_index(q) unmarked.add(index) - #user_answers[cid].append(ans) + # user_answers[cid].append(ans) user_answers_marks[cid].append(answer_score) if self.app_name == "rapids": @@ -2259,9 +2308,6 @@ class ExamViews(View, LoginRequiredMixin): zipped_ans_scores = zip(ans, answer_score) by_question[q][cid] = zipped_ans_scores - - - user_scores = {} user_scores_normalised = {} user_answer_count = {} @@ -2274,11 +2320,10 @@ class ExamViews(View, LoginRequiredMixin): # If either question or user do not exist we give a default # not answered == score of 3 if user not in by_question[question]: - #print("NOT in") + # print("NOT in") by_question[question][user] = ("Not answered", 3.0) user_answers_marks[user].append(3.0) - if self.app_name in ("rapids", "anatomy", "sbas", "longs"): user_scores[user] = sum( [i for i in user_answers_marks[user] if i != "unmarked"] @@ -2313,7 +2358,7 @@ class ExamViews(View, LoginRequiredMixin): mode = 0 fig_html = "" else: - #Exclude very low scores from statistics + # Exclude very low scores from statistics lower_score_bound = max_score / 5 bounded_scores = [i for i in user_scores_list if i > lower_score_bound] if bounded_scores: @@ -2400,9 +2445,9 @@ class ExamViews(View, LoginRequiredMixin): } if self.app_name == "rapids": - template_variables[ - "user_answers_callstates" - ] = user_answers_callstates_counted + template_variables["user_answers_callstates"] = ( + user_answers_callstates_counted + ) return render( request, @@ -2419,9 +2464,7 @@ class ExamViews(View, LoginRequiredMixin): if not exam.exam_mode: raise Http404("Packet not in exam mode") - template_variables = { - "exam": exam - } + template_variables = {"exam": exam} return render( request, @@ -2476,7 +2519,7 @@ class GenericViewBase: @method_decorator(login_required) def question_detail(self, request, pk): - question = get_object_or_404(self.question_object, pk=pk) + question: QuestionBase = get_object_or_404(self.question_object, pk=pk) if not question.open_access: if ( @@ -2499,7 +2542,12 @@ class GenericViewBase: return render( request, f"{self.app_name}/question_detail.html", - {"question": question, "view_feedback": view_feedback, "remote_url": settings.REMOTE_URL}, + { + "question": question, + "view_feedback": view_feedback, + "remote_url": settings.REMOTE_URL, + "can_edit": question.can_edit(request.user), + }, ) # TODO: improve permissions on these @@ -2514,15 +2562,22 @@ class GenericViewBase: question = get_object_or_404(self.question_object, pk=pk) if answer_compare is None: - answers = self.cid_user_answer_object.objects.filter(question__id=question.pk).prefetch_related("question", "exam", "user") + answers = self.cid_user_answer_object.objects.filter( + question__id=question.pk + ).prefetch_related("question", "exam", "user") else: - answers = self.cid_user_answer_object.objects.filter(question__id=question.pk, answer_compare=answer_compare).prefetch_related("question", "exam", "user") - + answers = self.cid_user_answer_object.objects.filter( + question__id=question.pk, answer_compare=answer_compare + ).prefetch_related("question", "exam", "user") return render( request, f"{self.app_name}/question_user_answers.html", - {"question": question, "answers": answers, "answer_compare": answer_compare}, + { + "question": question, + "answers": answers, + "answer_compare": answer_compare, + }, ) @method_decorator(login_required) @@ -2548,11 +2603,10 @@ class GenericViewBase: ) -class ExamCloneMixin(): +class ExamCloneMixin: def get_template_names(self) -> list[str]: return "exam_clone_form.html" - #return super().get_template_names() - + # return super().get_template_names() def get_initial(self): old_object = get_object_or_404(self.model, pk=self.kwargs["exam_id"]) @@ -2617,6 +2671,8 @@ class ExaminationAutocomplete(autocomplete.Select2QuerySetView): return Examination.objects.none() return qs + + class SupervisorAutocomplete(autocomplete.Select2QuerySetView): def get_queryset(self): # TODO: we should probably filter this to only @@ -2746,6 +2802,7 @@ def cid_group_view_all(request): {"groups": groups, "view_all": True}, ) + @user_is_cid_user_manager def user_group_add_candidates_to_exams(request, group_id): if request.htmx: @@ -2764,13 +2821,12 @@ def user_group_add_candidates_to_exams(request, group_id): else: exam.valid_user_users.add(*group.users.all()) return HttpResponse(f"Candidates added") - #exam.save() - - + # exam.save() raise PermissionDenied() # or Http404 pass + @user_is_cid_user_manager def user_group_view_detail(request, group_id): group = get_object_or_404(UserUserGroup, pk=group_id) @@ -2817,6 +2873,7 @@ def get_user_selection_from_request(request): user_models = User.objects.filter(id__in=selected_users) return user_models + @user_is_cid_user_manager def user_not_trainee(request, user_id): user = get_object_or_404(User, pk=user_id) @@ -2824,7 +2881,10 @@ def user_not_trainee(request, user_id): user.userprofile.peninsula_trainee = False user.userprofile.save() - return HttpResponse(f"{user.username} is no longer a trainee", content_type="text/plain") + return HttpResponse( + f"{user.username} is no longer a trainee", content_type="text/plain" + ) + @user_is_cid_user_manager def users_bulk_edit(request): @@ -2863,8 +2923,7 @@ def users_bulk_edit(request): # TODO: work out how to actually use hyperscript html = ( html - + - f"""