From 4666294979ba7f2a6e36be9c861e44c7a935d473 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 13 Feb 2023 11:05:20 +0000 Subject: [PATCH] add bulk edit grades --- generic/decorators.py | 9 +- generic/templates/generic/user_view.html | 36 ++-- generic/urls.py | 15 +- generic/views.py | 227 +++++++++++++++++++---- 4 files changed, 234 insertions(+), 53 deletions(-) diff --git a/generic/decorators.py b/generic/decorators.py index 25f11b5a..e486228d 100755 --- a/generic/decorators.py +++ b/generic/decorators.py @@ -1,11 +1,16 @@ from django.core.exceptions import PermissionDenied + def user_is_cid_user_manager(function): def wrap(request, *args, **kwargs): - if request.user.groups.filter(name='cid_user_manager').exists() or request.user.is_superuser: + if ( + request.user.groups.filter(name="cid_user_manager").exists() + or request.user.is_superuser + ): return function(request, *args, **kwargs) else: raise PermissionDenied + wrap.__doc__ = function.__doc__ wrap.__name__ = function.__name__ - return wrap \ No newline at end of file + return wrap diff --git a/generic/templates/generic/user_view.html b/generic/templates/generic/user_view.html index 88ad5967..5c0e0d5e 100644 --- a/generic/templates/generic/user_view.html +++ b/generic/templates/generic/user_view.html @@ -7,18 +7,18 @@ {% block content %}
-
- -

Filter Users

-
-
- {{ filter.form }} - -
-
+
+ +

Filter Users

+
+
+ {{ filter.form }} + +
+
-
+
{% render_table table %} @@ -31,7 +31,21 @@ Bulk edit - +
+
+ + {% endblock %} diff --git a/generic/urls.py b/generic/urls.py index 1d424a2a..30e1192b 100755 --- a/generic/urls.py +++ b/generic/urls.py @@ -77,6 +77,8 @@ urlpatterns = [ path( "cids/group/create", views.CidUserGroupCreate.as_view(), name="cid_group_create" ), + path("user/bulk_delete_supervisors/", views.users_bulk_delete_supervisors, name="users_bulk_delete_supervisors"), + path("user/bulk_edit_grade/", views.users_bulk_edit_grade, name="users_bulk_edit_grade"), path("user/group/", views.user_group_view, name="user_group_view"), path("user/group/all", views.user_group_view_all, name="user_group_view_all"), path("user/group//", views.user_group_view_detail, name="user_group_detail"), @@ -246,16 +248,27 @@ def generic_exam_urls(generic_exam_view: GenericExamViews): generic_exam_view.exam_json_cid_unbased, name="exam_json_cid_unbased", ), + # These are a little more ambiguous than ideal path( "exam/json//", generic_exam_view.exam_question_json, - name="exam_question_json_unbased", + name="exam_question_json", ), path( "exam/json///unbased", generic_exam_view.exam_question_json_unbased, name="exam_question_json_unbased", ), + path( + "exam/json////", + generic_exam_view.exam_question_json_cid, + name="exam_question_json_cid", + ), + path( + "exam/json/////unbased", + generic_exam_view.exam_question_json_unbased_cid, + name="exam_question_json_unbased_cid", + ), path( "exam/json//recreate", generic_exam_view.exam_json_recreate, diff --git a/generic/views.py b/generic/views.py index 6bd25391..2f26e3fd 100644 --- a/generic/views.py +++ b/generic/views.py @@ -58,7 +58,19 @@ from .forms import ( UserUserGroupForm, ) -from .models import CidUser, CidUserGroup, ExamBase, ExamUserStatus, Examination, QuestionNote, Supervisor, UserGrades, UserProfile, UserUserGroup, get_next_cid +from .models import ( + CidUser, + CidUserGroup, + ExamBase, + ExamUserStatus, + Examination, + QuestionNote, + Supervisor, + UserGrades, + UserProfile, + UserUserGroup, + get_next_cid, +) from rapids.models import Rapid as RapidQuestion from rapids.models import Exam as RapidExam @@ -355,6 +367,7 @@ class ExamViews(View, LoginRequiredMixin): self.ExamFilter = ExamFilter + # TODO: these may be better implemented as decorators def check_user_access(self, user: User, exam_id: int = None): """Check if a user should be able to access a view @@ -479,9 +492,7 @@ class ExamViews(View, LoginRequiredMixin): exam = get_object_or_404(self.Exam, pk=pk) - exam.archive = ( - True if request.POST.get("archive") == "true" else False - ) + exam.archive = True if request.POST.get("archive") == "true" else False print(f"{exam.archive=}") exam.save() data = { @@ -842,7 +853,7 @@ class ExamViews(View, LoginRequiredMixin): "can_edit": can_edit, "notes": notes, "app_name": self.app_name, - "candidate_count": (cid_candidates, users_candidates) + "candidate_count": (cid_candidates, users_candidates), }, ) @@ -885,7 +896,7 @@ class ExamViews(View, LoginRequiredMixin): Returns: _type_: _description_ - """ + """ exam = get_object_or_404(self.Exam, pk=exam_id) # if not request.user.groups.filter(name="cid_user_manager").exists(): @@ -1020,25 +1031,26 @@ class ExamViews(View, LoginRequiredMixin): else: statuses = exam.exam_user_status.all() - return render(request, "exam_user_status.html", {"exam": exam, "statuses": statuses}) + return render( + request, "exam_user_status.html", {"exam": exam, "statuses": statuses} + ) - #if not self.check_user_access(request.user, pk): + # if not self.check_user_access(request.user, pk): # raise PermissionDenied - #q, content_type = get_question_and_content_type(self.question_type) + # q, content_type = get_question_and_content_type(self.question_type) ## Get active notes for type - #statuses = ExamUserStatus.objects.filter( + # statuses = ExamUserStatus.objects.filter( # content_type=content_type, object_id__in=pk - #) + # ) - #return statuses + # return statuses @method_decorator(login_required) def exam_json_edit(self, request, pk): if request.method == "POST": - if request.user.groups.filter(name="cid_user_manager").exists(): # This may give more permissions than we actually want pass @@ -1306,7 +1318,14 @@ class ExamViews(View, LoginRequiredMixin): }, ) - def active_exams(self, request: HttpRequest, json: bool=True, based: bool=True, cid: int=None, passcode: str=None): + def active_exams( + self, + request: HttpRequest, + json: bool = True, + based: bool = True, + cid: int = None, + passcode: str = None, + ): exams = self.Exam.objects.filter(archive=False) active_exams = {"exams": []} @@ -1321,7 +1340,9 @@ class ExamViews(View, LoginRequiredMixin): exam: ExamBase for exam in exams: if exam.active or self.check_user_access(request.user, exam.pk): - print(f"{exam.app_name=}, {exam.name=}, {cid=}, {passcode=}, {request.user=}") + print( + f"{exam.app_name=}, {exam.name=}, {cid=}, {passcode=}, {request.user=}" + ) if exam.exam_mode and not exam.check_cid_user( cid, passcode, request=request, user_id=request.user.pk ): @@ -1333,7 +1354,9 @@ class ExamViews(View, LoginRequiredMixin): else: creation_time = "None" - url = request.build_absolute_uri(exam.get_json_url(cid=cid, passcode=passcode)) + url = request.build_absolute_uri( + exam.get_json_url(cid=cid, passcode=passcode) + ) # hacky if not based: url = url + "/unbased" @@ -1544,7 +1567,6 @@ class ExamViews(View, LoginRequiredMixin): return JsonResponse({"success": True, "question_count": n}) return JsonResponse({"success": False, "error": "Invalid data"}) - def exam_json_cid(self, request, pk, cid, passcode): return self.exam_json(request, pk, cid, passcode) @@ -1578,7 +1600,9 @@ class ExamViews(View, LoginRequiredMixin): cid_user = CidUser.objects.get(cid=cid) else: cid_user = None - exam.exam_user_status.create(cid_user=cid_user, user_user=user, status="downloaded") + exam.exam_user_status.create( + cid_user=cid_user, user_user=user, status="downloaded" + ) path = "{0}{1}/exam/{2}.json".format(settings.MEDIA_ROOT, self.app_name, pk) url = "{0}{1}/exam/{2}.json".format(settings.MEDIA_URL, self.app_name, pk) @@ -1629,6 +1653,14 @@ class ExamViews(View, LoginRequiredMixin): if not exam.active and not self.check_user_access(request.user, pk): raise Http404("No available exam") + # Check access for users + if request.user.is_anonymous: + user_id = None + else: + user_id = request.user.pk + if not exam.check_cid_user(cid, passcode, request, user_id): + raise Http404("No available exam") + time = datetime.now() exam_json = exam.get_exam_json(based=False) @@ -1642,17 +1674,30 @@ class ExamViews(View, LoginRequiredMixin): cid_user = CidUser.objects.get(cid=cid) else: cid_user = None - exam.exam_user_status.create(cid_user=cid, user_user=user, status="downloaded", extra="unbased") + exam.exam_user_status.create( + cid_user=cid, user_user=user, status="downloaded", extra="unbased" + ) return JsonResponse(exam_json) - def exam_question_json(self, request, pk, sk): + def exam_question_json_cid(self, request, pk, sk, cid, passcode): + return self.exam_question_json(request, pk, sk, cid, passcode) + + def exam_question_json(self, request, pk, sk, cid=None, passcode=None): question = get_object_or_404(self.Question, pk=sk) exam = get_object_or_404(self.Exam, pk=pk) if not exam.active and not self.check_user_access(request.user, pk): raise Http404("No available exam") + # Check access for users + if request.user.is_anonymous: + user_id = None + else: + user_id = request.user.pk + if not exam.check_cid_user(cid, passcode, request, user_id): + raise Http404("No available exam") + if request.GET.get("_"): base_url = redirect("{}:question_json".format(self.app_name), pk=sk) query_string = urllib.parse.urlencode({"_": request.GET.get("_")}) @@ -1661,6 +1706,9 @@ class ExamViews(View, LoginRequiredMixin): return redirect("{}:question_json".format(self.app_name), pk=sk) + def exam_question_json_unbased_cid(self, request, pk, sk, cid, passcode): + return self.exam_question_json_unbased(request, pk, sk, cid, passcode) + def exam_question_json_unbased(self, request, pk, sk): question = get_object_or_404(self.Question, pk=sk) exam = get_object_or_404(self.Exam, pk=pk) @@ -1815,7 +1863,6 @@ class ExamViews(View, LoginRequiredMixin): cached_scores = True - valid_cid_users = set(exam.valid_cid_users.all().values_list("cid", flat=True)) valid_user_users = set(exam.valid_user_users.all().values_list("pk", flat=True)) @@ -2242,7 +2289,6 @@ class CidUserView(CidManagerRequiredMixin, SingleTableMixin, FilterView): return context - @user_is_cid_user_manager def cid_group_view(request): groups = CidUserGroup.objects.filter(archive=False) @@ -2253,6 +2299,7 @@ def cid_group_view(request): {"groups": groups}, ) + @user_is_cid_user_manager def cid_group_view_detail(request, group_id): group = get_object_or_404(CidUserGroup, pk=group_id) @@ -2265,6 +2312,7 @@ def cid_group_view_detail(request, group_id): {"group": group, "users": users, "group_type": "cid"}, ) + @user_is_cid_user_manager def cid_group_view_all(request): groups = CidUserGroup.objects.filter() @@ -2275,11 +2323,12 @@ def cid_group_view_all(request): {"groups": groups, "view_all": True}, ) + @user_is_cid_user_manager def user_group_view_detail(request, group_id): group = get_object_or_404(UserUserGroup, pk=group_id) - users = group.users.filter() # Filter inactive users? + users = group.users.filter() # Filter inactive users? return render( request, @@ -2287,6 +2336,89 @@ def user_group_view_detail(request, group_id): {"group": group, "users": users, "group_type": "user"}, ) + +@user_is_cid_user_manager +def users_bulk_delete_supervisors(request): + if "selection" in request.POST: + selected_users = request.POST.getlist("selection") + user_models = User.objects.filter(id__in=selected_users) + + for u in user_models: + u.userprofile.supervisor = None + u.save() + + modified_users = ",".join([user.username for user in user_models]) + + return HttpResponse( + f"Supervisors removed from {modified_users}. Refresh page to see update", + content_type="text/plain", + ) + + else: + return HttpResponse("No users selected", content_type="text/plain") + + +@user_is_cid_user_manager +def users_bulk_edit_grade(request): + + if request.htmx.trigger == "bulk-edit-grade-button": + user_grades = UserGrades.objects.all() + + html = "".join( + [ + f"""""" + for grade in user_grades + ] + ) + # TODO: work out how to actually use hyperscript + html = ( + html + + "" + ) + + return HttpResponse(html, content_type="text/html") + elif request.htmx.trigger_name.startswith("add-grade"): + selected_users = request.POST.getlist("selection") + + if not selected_users: + return HttpResponse("No users selected", content_type="text/html") + user_models = User.objects.filter(id__in=selected_users) + + # u: User + for u in user_models: + u.userprofile.grade_id = request.htmx.trigger_name.split("--")[-1] + u.save() + return HttpResponse("Grades update", content_type="text/html") + else: + return HttpResponse("Error", content_type="text/html") + + +@user_is_cid_user_manager +def users_bulk_delete_supervisors(request): + if "selection" in request.POST: + selected_users = request.POST.getlist("selection") + user_models = User.objects.filter(id__in=selected_users) + + for u in user_models: + u.userprofile.supervisor = None + u.save() + + modified_users = ",".join([user.username for user in user_models]) + + return HttpResponse( + f"Supervisors removed from {modified_users}. Refresh page to see update", + content_type="text/plain", + ) + + else: + return HttpResponse("No users selected", content_type="text/plain") + + @user_is_cid_user_manager def user_group_view(request): groups = UserUserGroup.objects.filter(archive=False) @@ -2391,10 +2523,12 @@ def candidate_email_results(request, cid, resend=False): def candidate_email_results_resend(request, cid, resend=True): return candidate_email_results(request, cid, resend=True) + @user_is_cid_user_manager def create_cid_email(request): pass + @user_is_cid_user_manager def manage_cid_users(request): if request.method == "POST": @@ -2410,7 +2544,6 @@ def manage_cid_users(request): emails = json.loads(request.POST.get("emails")) - number_to_create = int(request.POST.get("number_to_create")) add_group = request.POST.get("add_group") @@ -2513,13 +2646,15 @@ def manage_cid_users(request): validate_email(email) except ValidationError as e: invalid_emails.append(f"Invalid email: {email}") - + print(f"Email list {email_list}") if not email_list: return JsonResponse({"status": "fail", "details": "No valid emails"}) if invalid_emails or not email_list: e = "
".join(invalid_emails) - return JsonResponse({"status": "fail", "details": f"Unable to create users
{e}"}) + return JsonResponse( + {"status": "fail", "details": f"Unable to create users
{e}"} + ) for e in email_list: passcode = "".join(random.choices(string.ascii_uppercase, k=4)) @@ -2578,16 +2713,18 @@ class CidUserUpdate(CidManagerRequiredMixin, UpdateView): model = CidUser form_class = CidUserForm + class CidUserExamUpdate(CidManagerRequiredMixin, UpdateView): model = CidUser form_class = CidUserExamForm template_name = "generic/ciduserexam_form.html" - #def get_context_data(self, *args, **kwargs): + # def get_context_data(self, *args, **kwargs): # context = super().get_context_data(**kwargs) # context["test"] = "HELLO" # return context + class CidUserGroupDelete(RevisionMixin, CidManagerRequiredMixin, DeleteView): model = CidUserGroup template_name = "confirm_delete.html" @@ -2672,16 +2809,18 @@ class ExamDeleteBase(RevisionMixin, DeleteView): def exam_inactive(request, context): return render(request, "exam_inactive.html", context) -#class UserUserCreate(CidManagerRequiredMixin, CreateView): + +# class UserUserCreate(CidManagerRequiredMixin, CreateView): # model = User # form_class = UserUserForm # template_name: str = "generic/user_creation_form.html" # # success_url = reverse_lazy("accounts_list") + def create_user(request, context=None): # if this is a POST request we need to process the form data - if request.method == 'POST': + if request.method == "POST": # create a form instance and populate it with data from the request: form = UserUserForm(request.POST) # check whether it's valid: @@ -2692,20 +2831,22 @@ def create_user(request, context=None): "first_name": request.POST["first_name"], "last_name": request.POST["last_name"], "email": request.POST["username"], - "password": secrets.token_hex(nbytes=16) + "password": secrets.token_hex(nbytes=16), } - new_user = User.objects.create_user(**user_dict) except Exception as error: errors = f"Unable to create account: {error}" - return render(request, 'generic/user_creation_form.html', {'form': form, "errors": errors}) + return render( + request, + "generic/user_creation_form.html", + {"form": form, "errors": errors}, + ) try: user_profile = UserProfile.objects.get(user=new_user) user_profile.peninsula_trainee = True - if request.POST["grade"]: grade = UserGrades.objects.get(pk=request.POST["grade"]) @@ -2714,12 +2855,16 @@ def create_user(request, context=None): if request.POST["supervisor"]: supervisor = Supervisor.objects.get(pk=request.POST["supervisor"]) user_profile.supervisor = supervisor - + user_profile.save() except Exception as error: errors = f"Unable to create account profile {error}" - return render(request, 'generic/user_creation_form.html', {'form': form, "errors": errors}) + return render( + request, + "generic/user_creation_form.html", + {"form": form, "errors": errors}, + ) return HttpResponseRedirect(reverse_lazy("accounts_list")) return reverse_lazy("accounts_list") @@ -2728,7 +2873,8 @@ def create_user(request, context=None): else: form = UserUserForm() - return render(request, 'generic/user_creation_form.html', {'form': form}) + return render(request, "generic/user_creation_form.html", {"form": form}) + class SupervisorDetail(CidManagerRequiredMixin, DetailView): model = Supervisor @@ -2739,15 +2885,18 @@ class SupervisorDelete(CidManagerRequiredMixin, DeleteView): template_name: str = "confirm_delete.html" success_url = reverse_lazy("generic:supervisor") + class SupervisorUpdate(CidManagerRequiredMixin, UpdateView): model = Supervisor form_class = SupervisorForm - #success_url = reverse_lazy("generic:supervisor_detail", kwargs={'pk': self.pk}) + # success_url = reverse_lazy("generic:supervisor_detail", kwargs={'pk': self.pk}) + class SupervisorCreate(CidManagerRequiredMixin, CreateView): model = Supervisor form_class = SupervisorForm - #success_url = reverse_lazy("generic:supervisor_detail") + # success_url = reverse_lazy("generic:supervisor_detail") + class SupervisorList(CidManagerRequiredMixin, ListView): - model = Supervisor \ No newline at end of file + model = Supervisor