From 41a09ea30d53806a8a429a4f1bef4a0413e464cd Mon Sep 17 00:00:00 2001 From: Ross Date: Tue, 24 May 2022 12:30:11 +0100 Subject: [PATCH] clean up --- anatomy/static/css/anatomy.css | 9 + anatomy/templates/anatomy/index.html | 14 - generic/admin.py | 4 + generic/forms.py | 20 +- generic/migrations/0033_userprofile_grade.py | 18 ++ generic/migrations/0034_auto_20220523_2010.py | 29 ++ generic/migrations/0035_userprofile_grade.py | 19 ++ generic/models.py | 13 + generic/templates/generic/base.html | 1 + generic/templates/generic/cid_group_view.html | 14 +- .../templates/generic/cidusergroup_form.html | 14 +- generic/templates/generic/exam_index.html | 25 ++ .../templates/generic/user_group_view.html | 14 +- .../templates/generic/userusergroup_form.html | 47 ++- generic/urls.py | 6 + generic/views.py | 273 +++++++++++------- longs/templates/longs/index.html | 11 - longs/templates/longs/index_old.html | 23 -- physics/templates/physics/index.html | 10 - rad/settings.py | 21 +- rad/urls.py | 4 + rad/views.py | 76 ++++- rapids/templates/rapids/index.html | 13 - rapids/templates/rapids/index_old.html | 23 -- rapids/tests/test_urls.py | 11 +- sbas/templates/sbas/index.html | 10 - templates/profile.html | 7 +- templates/user_list_view.html | 15 + templates/user_update.html | 9 + 29 files changed, 506 insertions(+), 247 deletions(-) delete mode 100644 anatomy/templates/anatomy/index.html create mode 100644 generic/migrations/0033_userprofile_grade.py create mode 100644 generic/migrations/0034_auto_20220523_2010.py create mode 100644 generic/migrations/0035_userprofile_grade.py create mode 100644 generic/templates/generic/exam_index.html delete mode 100644 longs/templates/longs/index.html delete mode 100755 longs/templates/longs/index_old.html delete mode 100644 physics/templates/physics/index.html delete mode 100644 rapids/templates/rapids/index.html delete mode 100755 rapids/templates/rapids/index_old.html delete mode 100644 sbas/templates/sbas/index.html create mode 100644 templates/user_list_view.html create mode 100644 templates/user_update.html diff --git a/anatomy/static/css/anatomy.css b/anatomy/static/css/anatomy.css index 8b96e14c..21226aa6 100644 --- a/anatomy/static/css/anatomy.css +++ b/anatomy/static/css/anatomy.css @@ -837,4 +837,13 @@ summary h5 { bottom: 0px; background-color: black; opacity: 80%; +} + +.small-url-button { + padding: 0px; +} + +button a, button a:link, button a:visited, button a:hover { + text-decoration: none; + color: inherit; } \ No newline at end of file diff --git a/anatomy/templates/anatomy/index.html b/anatomy/templates/anatomy/index.html deleted file mode 100644 index 0837ca97..00000000 --- a/anatomy/templates/anatomy/index.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends 'anatomy/base.html' %} - -{% block content %} - {% for exam in exams %} -
-

Exam: {{ exam.name }}

- - {% if exam.exam_mode %} - {% if request.user.is_staff %}Mark answers{% endif %} - {% if request.user.is_staff %}Scores{% endif %} - {% endif %} -
- {% endfor %} -{% endblock %} diff --git a/generic/admin.py b/generic/admin.py index 20981ea2..cba9b4b1 100644 --- a/generic/admin.py +++ b/generic/admin.py @@ -13,6 +13,8 @@ from .models import ( NoteType, CidUser, CidUserExam, + UserGrades, + UserProfile, ) # from .models import Examination, Sign, Site, Condition @@ -28,3 +30,5 @@ admin.site.register(NoteType) admin.site.register(CidUser) admin.site.register(CidUserExam) admin.site.register(CidUserGroup) +admin.site.register(UserProfile) +admin.site.register(UserGrades) diff --git a/generic/forms.py b/generic/forms.py index 10ac3756..d308746b 100755 --- a/generic/forms.py +++ b/generic/forms.py @@ -11,7 +11,7 @@ from django.forms import inlineformset_factory from atlas.models import CaseCollection from django.contrib.auth.models import User -from generic.models import CidUser, CidUserGroup, Examination, QuestionNote, UserUserGroup +from generic.models import CidUser, CidUserGroup, Examination, QuestionNote, UserProfile, UserUserGroup from django.contrib.admin.widgets import FilteredSelectMultiple from django.forms.widgets import RadioSelect, TextInput, Textarea @@ -236,12 +236,26 @@ class CidUserGroupForm(ModelForm): model = CidUserGroup fields = ["name", "archive"] +class UserUserGroupModelChoiceField(ModelMultipleChoiceField): + def label_from_instance(self, obj): + return f"{obj.username} ({obj.userprofile.grade}) [{obj.email}]" + class UserUserGroupForm(ModelForm): - users = ModelMultipleChoiceField( + users = UserUserGroupModelChoiceField( required=False, queryset=User.objects.all(), widget=FilteredSelectMultiple(verbose_name="Users", is_stacked=False), ) class Meta: model = UserUserGroup - fields = ["name", "archive", "users"] \ No newline at end of file + fields = ["name", "archive", "users"] + +#class UserForm(ModelForm): +# class Meta: +# model = User +# fields = ["first_name", "last_name", "email"] +# +#class UserProfileForm(ModelForm): +# class Meta: +# model = UserProfile +# fields = ["supervisor_name", "supervisor_email", "registration_number", "peninsula_trainee"] \ No newline at end of file diff --git a/generic/migrations/0033_userprofile_grade.py b/generic/migrations/0033_userprofile_grade.py new file mode 100644 index 00000000..8c343ed3 --- /dev/null +++ b/generic/migrations/0033_userprofile_grade.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2022-05-23 17:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('generic', '0032_userprofile_registration_number'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='grade', + field=models.IntegerField(blank=True, help_text='ST grade of the user', null=True), + ), + ] diff --git a/generic/migrations/0034_auto_20220523_2010.py b/generic/migrations/0034_auto_20220523_2010.py new file mode 100644 index 00000000..506ba8a3 --- /dev/null +++ b/generic/migrations/0034_auto_20220523_2010.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.13 on 2022-05-23 19:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('generic', '0033_userprofile_grade'), + ] + + operations = [ + migrations.CreateModel( + name='UserGrades', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ], + ), + migrations.RemoveField( + model_name='userprofile', + name='grade', + ), + migrations.AddField( + model_name='userprofile', + name='peninsula_trainee', + field=models.BooleanField(default=False), + ), + ] diff --git a/generic/migrations/0035_userprofile_grade.py b/generic/migrations/0035_userprofile_grade.py new file mode 100644 index 00000000..84fdfaaa --- /dev/null +++ b/generic/migrations/0035_userprofile_grade.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-05-23 19:10 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('generic', '0034_auto_20220523_2010'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='grade', + field=models.ForeignKey(blank=True, help_text='User grade', null=True, on_delete=django.db.models.deletion.CASCADE, to='generic.usergrades'), + ), + ] diff --git a/generic/models.py b/generic/models.py index 0425c209..a6f0cbe2 100644 --- a/generic/models.py +++ b/generic/models.py @@ -563,12 +563,25 @@ class UserUserGroup(models.Model): def get_absolute_url(self): return reverse("generic:user_group_update", kwargs={"pk": self.pk}) +class UserGrades(models.Model): + name = models.CharField(max_length=100) + + def __str__(self): + return self.name class UserProfile(models.Model): user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) supervisor_email = models.EmailField(blank=True) supervisor_name = models.CharField(max_length=100, blank=True) registration_number = models.CharField(max_length=25, blank=True) + grade = models.ForeignKey(UserGrades, null=True, blank=True, help_text="User grade", on_delete=models.CASCADE) + peninsula_trainee = models.BooleanField(default=False) + + + def getusername(self): + return self.user.username + + username = property(getusername) @receiver(post_save, sender=User) diff --git a/generic/templates/generic/base.html b/generic/templates/generic/base.html index a180f3ca..3b934d00 100755 --- a/generic/templates/generic/base.html +++ b/generic/templates/generic/base.html @@ -21,6 +21,7 @@ Cids (exams) / Cid Groups / User Groups / + Manage Users / {% endif %} {% endblock %} diff --git a/generic/templates/generic/cid_group_view.html b/generic/templates/generic/cid_group_view.html index 05eb5365..d6fc119a 100755 --- a/generic/templates/generic/cid_group_view.html +++ b/generic/templates/generic/cid_group_view.html @@ -7,22 +7,24 @@

{% for group in groups %}

{{group.name}} - - - - - +

{% endfor %} + - + {% endblock %} {% block content %} -

Add / Edit Group / {{userusergroup.name}}

-Use this form to create / edit a CID user group. -
- {% csrf_token %} + {% if form.instance.id %} +

Edit Group: {{userusergroup.name}}

+ {% else %} +

Add New Group

+ {% endif %} + Use this form to create / edit a CID user group. + + {% csrf_token %} - - {{ form.as_table }} -
- + + {{ form.as_table }} +
+ -

Users

+

Users

- {{userusergroup.GetGroupUsers}} + -
+ {% comment %} {% if userusergroup.users.all %} + {{userusergroup.GetGroupUsers}} + {% else %} + No users currently added to group. + {% endif %} {% endcomment %} + + {% endblock %} \ No newline at end of file diff --git a/generic/urls.py b/generic/urls.py index 476bb39b..72d6a23f 100755 --- a/generic/urls.py +++ b/generic/urls.py @@ -62,6 +62,9 @@ urlpatterns = [ path( "cids/group//update", views.CidUserGroupUpdate.as_view(), name="cid_group_update" ), + path( + "cids/group//delete", views.CidUserGroupDelete.as_view(), name="cid_group_delete" + ), path( "cids/group/create", views.CidUserGroupCreate.as_view(), name="cid_group_create" ), @@ -70,6 +73,9 @@ urlpatterns = [ path( "user/group//update", views.UserUserGroupUpdate.as_view(), name="user_group_update" ), + path( + "user/group//delete", views.UserUserGroupDelete.as_view(), name="user_group_delete" + ), path( "user/group/create", views.UserUserGroupCreate.as_view(), name="user_group_create" ), diff --git a/generic/views.py b/generic/views.py index 2cb1d00c..463692d7 100644 --- a/generic/views.py +++ b/generic/views.py @@ -8,6 +8,7 @@ from django.forms.models import model_to_dict from django.shortcuts import render, get_object_or_404, redirect from django.contrib.auth.decorators import login_required, user_passes_test +from django.urls import reverse_lazy from django.urls.base import reverse from django.utils import timezone from django.utils.html import format_html @@ -29,8 +30,9 @@ import json import urllib from django.views import View -from django.views.generic.edit import CreateView, UpdateView +from django.views.generic.edit import CreateView, UpdateView, DeleteView from django_filters.views import FilterView +from django_filters import FilterSet from django_tables2.views import SingleTableMixin from reversion.views import RevisionMixin from atlas.models import CaseCollection @@ -71,6 +73,7 @@ import plotly.express as px # from rad.views import get_question_and_content_type from django.db.models import Prefetch + class AuthorRequiredMixin(object): def get_object(self, *args, **kwargs): obj = super().get_object(*args, **kwargs) @@ -78,6 +81,7 @@ class AuthorRequiredMixin(object): raise PermissionDenied() # or Http404 return obj + class CidManagerRequiredMixin(UserPassesTestMixin): def test_func(self): return self.request.user.groups.filter(name="cid_user_manager").exists() @@ -289,7 +293,47 @@ class ExamViews(View, LoginRequiredMixin): # group_map = {"rapids" : "rapid_checker", "anatomy" : "anatomy_checker", "longs":"long_checker"} # exam_group = group_map[self.app_name] - def check_user_access(self, user, exam_id=None): + class ExamFilter(FilterSet): + class Meta: + model = exam + fields = { + "name": ["contains"], + "exam_mode": ["exact"], + "active": ["exact"], + "archive": ["exact"], + "open_access": ["exact"], + #'release_date': ['isnull'], + } + # filter_overrides = { + # models.CharField: { + # 'filter_class': django_filters.CharFilter, + # 'extra': lambda f: { + # 'lookup_expr': 'icontains', + # }, + # }, + # models.BooleanField: { + # 'filter_class': django_filters.BooleanFilter, + # 'extra': lambda f: { + # 'widget': forms.CheckboxInput, + # }, + # }, + # } + def __init__(self, data=None, *args, **kwargs): + # if filterset is bound, use initial values as defaults + if data is not None: + # get a mutable copy of the QueryDict + data = data.copy() + + #for name, f in self.base_filters.items(): + # filter param is either missing or empty, use initial as default + if not data.get("archive"): + data["archive"] = False + + super().__init__(data, *args, **kwargs) + + self.ExamFilter = ExamFilter + + def check_user_access(self, user: User, exam_id: int = None): """Check if a user should be able to access a view Args: @@ -547,9 +591,7 @@ class ExamViews(View, LoginRequiredMixin): ) elif self.app_name == "rapids": questions = ( - exam.exam_questions.select_related( - - ) + exam.exam_questions.select_related() .all() .prefetch_related( Prefetch( @@ -559,7 +601,11 @@ class ExamViews(View, LoginRequiredMixin): ), to_attr="prefetched_primary_answer" # queryset=self.Answer.objects.filter(), - ), "images", "abnormality", "region", "examination" + ), + "images", + "abnormality", + "region", + "examination", ) ) # questions = ( @@ -620,7 +666,7 @@ class ExamViews(View, LoginRequiredMixin): def exam_cids(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(): + # if not request.user.groups.filter(name="cid_user_manager").exists(): # raise PermissionDenied if request.user not in exam.author.all(): @@ -635,29 +681,24 @@ class ExamViews(View, LoginRequiredMixin): user_user_count = user_users.count() context = { - "exam": exam, - "cid_users": cid_users, - "cid_user_count": cid_user_count, - "user_users": user_users, - "user_user_count": user_user_count, - "app_name": self.app_name, - } - + "exam": exam, + "cid_users": cid_users, + "cid_user_count": cid_user_count, + "user_users": user_users, + "user_user_count": user_user_count, + "app_name": self.app_name, + } + if self.app_name == "atlas": context["collection"] = exam - return render( - request, - "exam_cids.html", - context - - ) + return render(request, "exam_cids.html", context) def exam_users_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(): - #raise PermissionDenied + # raise PermissionDenied if request.user not in exam.author.all(): if not self.check_user_access(request.user, exam_id): raise PermissionDenied @@ -666,33 +707,32 @@ class ExamViews(View, LoginRequiredMixin): exam_groups = exam.user_user_groups.all() - available_user_users = list(User.objects.filter(user_groups__in=exam_groups).difference(current_user_users)) + available_user_users = list( + User.objects.filter(user_groups__in=exam_groups).difference( + current_user_users + ) + ) print(available_user_users) - #available_user_users = User.objects.all() + # available_user_users = User.objects.all() context = { - "exam": exam, - "groups": exam_groups, - "current_user_users": current_user_users, - "available_user_users": available_user_users, - "app_name": self.app_name, - } - + "exam": exam, + "groups": exam_groups, + "current_user_users": current_user_users, + "available_user_users": available_user_users, + "app_name": self.app_name, + } + if self.app_name == "atlas": context["collection"] = exam - return render( - request, - "exam_users_edit.html", - context - ) - + return render(request, "exam_users_edit.html", context) def exam_cids_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(): - #raise PermissionDenied + # raise PermissionDenied if request.user not in exam.author.all(): if not self.check_user_access(request.user, exam_id): raise PermissionDenied @@ -701,24 +741,22 @@ class ExamViews(View, LoginRequiredMixin): exam_groups = exam.cid_user_groups.all() - available_cid_users = CidUser.objects.filter(group__in=exam_groups).difference(current_cid_users) + available_cid_users = CidUser.objects.filter(group__in=exam_groups).difference( + current_cid_users + ) context = { - "exam": exam, - "groups": exam_groups, - "current_cid_users": current_cid_users, - "available_cid_users": available_cid_users, - "app_name": self.app_name, - } - + "exam": exam, + "groups": exam_groups, + "current_cid_users": current_cid_users, + "available_cid_users": available_cid_users, + "app_name": self.app_name, + } + if self.app_name == "atlas": context["collection"] = exam - return render( - request, - "exam_cids_edit.html", - context - ) + return render(request, "exam_cids_edit.html", context) # @method_decorator(login_required) def exam_notes(self, request, pk): @@ -751,7 +789,7 @@ class ExamViews(View, LoginRequiredMixin): if "edit_cid_user" in request.POST: user_id = request.POST.get("edit_cid_user") - + add = request.POST.get("add") == "true" cid_user = CidUser.objects.get(pk=user_id) @@ -765,27 +803,25 @@ class ExamViews(View, LoginRequiredMixin): else: app_exam_map[self.app_name].remove(pk) - data = {"status": "success", "added": add} return JsonResponse(data, status=200) if "edit_user_user" in request.POST: user_id = request.POST.get("edit_user_user") - + add = request.POST.get("add") == "true" user_user = User.objects.get(pk=user_id) app_exam_map = {} app_exam_map["rapids"] = user_user.user_rapid_exams - #app_exam_map["atlas"] = user_user.casecollection_exams + # app_exam_map["atlas"] = user_user.casecollection_exams if add: app_exam_map[self.app_name].add(pk) else: app_exam_map[self.app_name].remove(pk) - data = {"status": "success", "added": add} return JsonResponse(data, status=200) @@ -927,25 +963,30 @@ class ExamViews(View, LoginRequiredMixin): @method_decorator(login_required) def index(self, request): - exams = self.Exam.objects.filter(author__id=request.user.id) + + # self.check_user_access(request.user, exam.pk) if ( - self.app_name == "rapids" - and request.user.groups.filter(name="rapid_checker").exists() - ): - exams = self.Exam.objects.all() - if ( - self.app_name == "anatomy" - and request.user.groups.filter(name="anatomy_checker").exists() - ): - exams = self.Exam.objects.all() - if ( - self.app_name == "longs" - and request.user.groups.filter(name="long_checker").exists() + ( + self.app_name == "rapids" + and request.user.groups.filter(name="rapid_checker").exists() + ) + or ( + self.app_name == "anatomy" + and request.user.groups.filter(name="anatomy_checker").exists() + ) + or ( + self.app_name == "longs" + and request.user.groups.filter(name="long_checker").exists() + ) ): exams = self.Exam.objects.all() + else: + exams = self.Exam.objects.filter(author__id=request.user.id) - return render(request, "{}/index.html".format(self.app_name), {"exams": exams}) + filter = self.ExamFilter(request.GET, queryset=exams) + + return render(request, "generic/exam_index.html", {"filter": filter, "app_name": self.app_name}) @method_decorator(login_required) def exam_question_detail(self, request, pk, sk): @@ -996,7 +1037,9 @@ class ExamViews(View, LoginRequiredMixin): for exam in exams: if exam.active or self.check_user_access(request.user, exam.pk): print(exam.name, cid, passcode) - if exam.exam_mode and not exam.check_cid_user(cid, passcode, user_id=request.user.pk): + if exam.exam_mode and not exam.check_cid_user( + cid, passcode, user_id=request.user.pk + ): print(exam.name, "fail") continue @@ -1061,7 +1104,10 @@ class ExamViews(View, LoginRequiredMixin): else: return JsonResponse( - {"success": False, "error": "cid or eid or answers not defined"} + { + "success": False, + "error": "cid or eid or answers not defined", + } ) if not uid: @@ -1070,7 +1116,10 @@ class ExamViews(View, LoginRequiredMixin): not isinstance(eid, int) or (not isinstance(answer["ans"], str)) ): return JsonResponse( - {"success": False, "error": "cid or eid or answers not defined"} + { + "success": False, + "error": "cid or eid or answers not defined", + } ) exam = get_object_or_404(self.Exam, pk=eid) @@ -1100,9 +1149,15 @@ class ExamViews(View, LoginRequiredMixin): if not existing_answers: if uid: - ans = self.CidUserAnswer(answer=posted_answer, normal=normal, user=User.objects.get(id=uid)) + ans = self.CidUserAnswer( + answer=posted_answer, + normal=normal, + user=User.objects.get(id=uid), + ) else: - ans = self.CidUserAnswer(answer=posted_answer, normal=normal, cid=cid) + ans = self.CidUserAnswer( + answer=posted_answer, normal=normal, cid=cid + ) ans.question_id = answer["qid"] ans.exam_id = eid @@ -1118,11 +1173,12 @@ class ExamViews(View, LoginRequiredMixin): case "anatomy": if not existing_answers: if uid: - ans = self.CidUserAnswer(answer=posted_answer, uid=User.objects.get(id=uid)) + ans = self.CidUserAnswer( + answer=posted_answer, uid=User.objects.get(id=uid) + ) else: ans = self.CidUserAnswer(answer=posted_answer, cid=cid) - ans.question_id = answer["qid"] ans.exam_id = eid ans.score = self.Answer.MarkOptions.UNMARKED @@ -1168,14 +1224,12 @@ class ExamViews(View, LoginRequiredMixin): ans.full_clean() ans.save() - - # if ret is not True: # return ret n = n + 1 - # print(UserAnswer.objects.filter(exam__id=q["eid"])) + # print(UserAnswer.objects.filter(exam__id=q["eid"])) # print(request.urlencode()) exam_type, exam_id = request.POST.get("eid").split("/") @@ -1185,10 +1239,14 @@ class ExamViews(View, LoginRequiredMixin): ) if request.POST.get("cid").startswith("u-"): - cid_user_exam = exam.get_or_create_cid_user_exam(user_user=request.user, start_time=t) + cid_user_exam = exam.get_or_create_cid_user_exam( + user_user=request.user, start_time=t + ) else: c = CidUser.objects.filter(cid=request.POST.get("cid")).first() - cid_user_exam = exam.get_or_create_cid_user_exam(cid_user=c, start_time=t) + cid_user_exam = exam.get_or_create_cid_user_exam( + cid_user=c, start_time=t + ) cid_user_exam.end_time = timezone.now() cid_user_exam.save() @@ -1303,10 +1361,10 @@ class ExamViews(View, LoginRequiredMixin): if not exam.exam_mode: raise Http404("Packet not in exam mode") - #user_answers_and_marks = defaultdict(list) + # user_answers_and_marks = defaultdict(list) user_answers_marks = defaultdict(list) user_answers = defaultdict(list) - #user_names = {} + # user_names = {} # cid_passcodes = {} by_question = defaultdict(dict) @@ -1333,7 +1391,7 @@ class ExamViews(View, LoginRequiredMixin): cids.add(cid) s = cid_user_answer - #user_names[cid] = cid + # user_names[cid] = cid q = cid_user_answer.question @@ -1352,7 +1410,7 @@ class ExamViews(View, LoginRequiredMixin): unmarked.add(index) user_answers[cid].append(ans) user_answers_marks[cid].append(answer_score) - #user_answers_and_marks[cid].append((ans, answer_score)) + # user_answers_and_marks[cid].append((ans, answer_score)) if self.app_name in ("rapids", "anatomy", "sbas"): by_question[q][cid] = (ans, answer_score) @@ -1440,13 +1498,13 @@ class ExamViews(View, LoginRequiredMixin): "unmarked": unmarked, "questions": questions, "by_question": by_question, - #"user_answers": dict(user_answers), - #"user_answers_marks": dict(user_answers_marks), + # "user_answers": dict(user_answers), + # "user_answers_marks": dict(user_answers_marks), "user_scores": user_scores, "user_scores_normalised": user_scores_normalised, - #"user_scores_list": user_scores_list, - #"user_names": user_names, - #"user_answers_and_marks": user_answers_and_marks, + # "user_scores_list": user_scores_list, + # "user_names": user_names, + # "user_answers_and_marks": user_answers_and_marks, "max_score": max_score, "mean": mean, "median": median, @@ -1621,7 +1679,10 @@ class CidUserExamView(CidManagerRequiredMixin, SingleTableMixin, FilterView): sba_exams = [(i.name, i.pk) for i in SbasExam.objects.filter(**filters)] longs_exams = [(i.name, i.pk) for i in LongExam.objects.filter(**filters)] anatomy_exams = [(i.name, i.pk) for i in AnatomyExam.objects.filter(**filters)] - casecollection_exams = [(i.name, i.pk) for i in CaseCollection.objects.filter(archive=False, collection_type__gt = 0)] + casecollection_exams = [ + (i.name, i.pk) + for i in CaseCollection.objects.filter(archive=False, collection_type__gt=0) + ] cid_user_groups = [ (i.name, i.pk) for i in CidUserGroup.objects.filter(archive=False) @@ -1656,7 +1717,10 @@ class CidUserView(CidManagerRequiredMixin, SingleTableMixin, FilterView): sba_exams = [(i.name, i.pk) for i in SbasExam.objects.filter(**filters)] longs_exams = [(i.name, i.pk) for i in LongExam.objects.filter(**filters)] anatomy_exams = [(i.name, i.pk) for i in AnatomyExam.objects.filter(**filters)] - casecollection_exams = [(i.name, i.pk) for i in CaseCollection.objects.filter(archive=False, collection_type__gt = 0)] + casecollection_exams = [ + (i.name, i.pk) + for i in CaseCollection.objects.filter(archive=False, collection_type__gt=0) + ] context["physics_exams"] = physics_exams context["rapid_exams"] = rapid_exams @@ -1683,6 +1747,7 @@ def cid_group_view(request): {"groups": groups}, ) + @user_is_cid_user_manager def cid_group_view_all(request): groups = CidUserGroup.objects.filter() @@ -1693,6 +1758,7 @@ def cid_group_view_all(request): {"groups": groups, "view_all": True}, ) + @user_is_cid_user_manager def user_group_view(request): groups = UserUserGroup.objects.filter(archive=False) @@ -1703,6 +1769,7 @@ def user_group_view(request): {"groups": groups}, ) + @user_is_cid_user_manager def user_group_view_all(request): groups = UserUserGroup.objects.filter() @@ -1776,12 +1843,11 @@ def group_email_results(request, pk, resend=False): def candidate_email_details(request, cid, resend=False): user = CidUser.objects.get(cid=cid) - email_sent, comment = user.email_details( - resend=False - ) + email_sent, comment = user.email_details(resend=False) return JsonResponse({"sent": email_sent, "comment": comment}) + @user_is_cid_user_manager def candidate_email_results(request, cid, resend=False): user = CidUser.objects.get(cid=cid) @@ -1965,6 +2031,12 @@ class CidUserUpdate(RevisionMixin, CidManagerRequiredMixin, UpdateView): form_class = CidUserForm +class CidUserGroupDelete(RevisionMixin, CidManagerRequiredMixin, DeleteView): + model = CidUserGroup + template_name = "confirm_delete.html" + success_url = reverse_lazy("generic:cid_group_view") + + class CidUserGroupCreate(RevisionMixin, CidManagerRequiredMixin, CreateView): model = CidUserGroup form_class = CidUserGroupForm @@ -1974,6 +2046,7 @@ class CidUserGroupUpdate(RevisionMixin, CidManagerRequiredMixin, UpdateView): model = CidUserGroup form_class = CidUserGroupForm + class UserUserGroupCreate(RevisionMixin, CidManagerRequiredMixin, CreateView): model = UserUserGroup form_class = UserUserGroupForm @@ -1981,4 +2054,10 @@ class UserUserGroupCreate(RevisionMixin, CidManagerRequiredMixin, CreateView): class UserUserGroupUpdate(RevisionMixin, CidManagerRequiredMixin, UpdateView): model = UserUserGroup - form_class = UserUserGroupForm \ No newline at end of file + form_class = UserUserGroupForm + + +class UserUserGroupDelete(RevisionMixin, CidManagerRequiredMixin, DeleteView): + model = UserUserGroup + template_name = "confirm_delete.html" + success_url = reverse_lazy("generic:user_group_view") diff --git a/longs/templates/longs/index.html b/longs/templates/longs/index.html deleted file mode 100644 index d2a9f4fc..00000000 --- a/longs/templates/longs/index.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends 'longs/base.html' %} - -{% block content %} -{% for exam in exams %} -
-

Exam: {{ exam.name }}

- {% if request.user.is_staff %}Mark answers{% endif %} - {% if request.user.is_staff %}Scores{% endif %} -
-{% endfor %} -{% endblock %} \ No newline at end of file diff --git a/longs/templates/longs/index_old.html b/longs/templates/longs/index_old.html deleted file mode 100755 index c6044e0d..00000000 --- a/longs/templates/longs/index_old.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends 'longs/base.html' %} - -{% block content %} -My Questions: - - -Other Questions: - - -{% endblock %} \ No newline at end of file diff --git a/physics/templates/physics/index.html b/physics/templates/physics/index.html deleted file mode 100644 index d7e2ca20..00000000 --- a/physics/templates/physics/index.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends 'physics/base.html' %} - -{% block content %} -{% for exam in exams %} -
-

Exam: {{ exam.name }}

- {% if request.user.is_staff %}Scores{% endif %} -
-{% endfor %} -{% endblock %} \ No newline at end of file diff --git a/rad/settings.py b/rad/settings.py index 23791d7e..9ae7f581 100644 --- a/rad/settings.py +++ b/rad/settings.py @@ -269,18 +269,27 @@ SENDGRID_API_KEY = ( "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" ) -EMAIL_HOST = "smtp.sendgrid.net" -EMAIL_HOST_USER = "apikey" # this is exactly the value 'apikey' -EMAIL_HOST_PASSWORD = SENDGRID_API_KEY -EMAIL_PORT = 587 -EMAIL_USE_TLS = True +# SENDGRID +#EMAIL_HOST = "smtp.sendgrid.net" +#EMAIL_HOST_USER = "apikey" # this is exactly the value 'apikey' +#EMAIL_HOST_PASSWORD = SENDGRID_API_KEY +#EMAIL_PORT = 587 +#EMAIL_USE_TLS = True + # EMAIL_HOST = "smtp.office365.com " # EMAIL_HOST_USER = "Penra.courses@nhs.net" # EMAIL_HOST_PASSWORD = '"got3<}%o"~\J",\Di3<' # EMAIL_PORT = 587 # EMAIL_USE_TLS = True # EMAIL_USE_SSl = False -# DEFAULT_FROM_EMAIL = EMAIL_HOST_USER + +DEFAULT_FROM_EMAIL = "no-reply@penracourses.org.uk" +EMAIL_HOST = "webhosting2027.is.cc" +EMAIL_HOST_USER = "no-reply@penracourses.org.uk" +EMAIL_HOST_PASSWORD = "I(5E)&1w+Bh)" +EMAIL_PORT = 465 +#EMAIL_USE_TLS = True +EMAIL_USE_SSL = True try: from .settings_local import * diff --git a/rad/urls.py b/rad/urls.py index 2467921b..de8bdced 100644 --- a/rad/urls.py +++ b/rad/urls.py @@ -53,8 +53,12 @@ urlpatterns = [ path("sbas/", include("sbas.urls"), name="sbas"), path("generic/", include("generic.urls"), name="generic"), path("atlas/", include("atlas.urls"), name="atlas"), + path("accounts/update//", views.UpdateUserView.as_view(), name="account_update"), + path("accounts/update_profile//", views.UpdateUserProfileView.as_view(), name="account_profile_update"), + path("accounts/", views.UserListView.as_view(), name="accounts_list"), path("accounts/", include("django.contrib.auth.urls")), path("accounts/profile", views.profile, name="profile"), + path("accounts/profile//", views.account_profile, name="account_profile"), path("", TemplateView.as_view(template_name="index.html"), name="home"), path("cid/results//", views.cid_results, name="cid_results"), path("cid//", views.cid_scores, name="cid_scores"), diff --git a/rad/views.py b/rad/views.py index 6fe17c97..500cba95 100644 --- a/rad/views.py +++ b/rad/views.py @@ -1,6 +1,6 @@ from atlas.models import CaseCollection, CidReportAnswer from generic.decorators import user_is_cid_user_manager -from generic.views import get_question_and_content_type +from generic.views import CidManagerRequiredMixin, get_question_and_content_type from django.core.exceptions import PermissionDenied from django.shortcuts import render, get_object_or_404, redirect from django.views.decorators.csrf import csrf_exempt @@ -15,7 +15,7 @@ from django.contrib.contenttypes.models import ContentType from django.contrib.auth.mixins import LoginRequiredMixin from django.views.generic.edit import CreateView, UpdateView, DeleteView -from django.views.generic import ListView +from django.views.generic import ListView, TemplateView from django.db.models.functions import Lower @@ -54,7 +54,7 @@ from rapids.views import GenericExamViews as RapidsExamsViews from longs.views import GenericExamViews as LongsExamViews from generic.forms import QuestionNoteForm -from generic.models import CidUser, QuestionNote +from generic.models import CidUser, QuestionNote, UserProfile import json @@ -71,6 +71,12 @@ def profile(request): return render(request, "profile.html", {"user": user}) +@user_is_cid_user_manager +def account_profile(request, slug): + user = get_object_or_404(User, username=slug) + return render(request, "profile.html", {"user": user}) + + def cid_selector(request): return render( request, @@ -87,7 +93,7 @@ def cid_scores_admin(request, cid): @login_required -#@user_passes_test(lambda u: u.is_superuser) +# @user_passes_test(lambda u: u.is_superuser) @user_is_cid_user_manager def cid_results(request, cid): cid_user = CidUser.objects.filter(cid=cid).first() @@ -432,3 +438,65 @@ def server_error(request): # response.status_code = 404 return response + + +class UserListView(CidManagerRequiredMixin, ListView): + model = User + template_name = "user_list_view.html" + + +class UpdateUserView(CidManagerRequiredMixin, UpdateView): + model = User + fields = ["first_name", "last_name", "email"] # Keep listing whatever fields + # the combined UserProfile and User exposes. + template_name = "user_update.html" + slug_field = "username" + slug_url_kwarg = "slug" + success_url = reverse_lazy("profile") + + +class UpdateUserProfileView(CidManagerRequiredMixin, UpdateView): + model = UserProfile + fields = [ + "supervisor_name", + "supervisor_email", + "grade", + "registration_number", + "peninsula_trainee", + ] # Keep listing whatever fields + template_name = "user_update.html" + slug_field = "user__username" + slug_url_kwarg = "slug" + success_url = reverse_lazy("profile") + + +# class UpdateUser(TemplateView): +# +# user_form_class = UserForm +# comment_form_class = UserProfileForm +# template_name = 'user_update.html' +# slug_field = 'username' +# slug_url_kwarg = 'slug' +# +# def post(self, request): +# post_data = request.POST or None +# user_form = self.user_form_class(post_data, prefix='user') +# profile_form = self.profile_form_class(post_data, prefix='userprofile') +# +# context = self.get_context_data(post_form=user_form, +# profile_form=profile_form) +# +# if user_form.is_valid(): +# self.form_save(user_form) +# if profile_form.is_valid(): +# self.form_save(profile_form) +# +# return self.render_to_response(context) +# +# def form_save(self, form): +# obj = form.save() +# #messages.success(self.request, "{} saved successfully".format(obj)) +# return obj +# +# def get(self, request, *args, **kwargs): +# return self.post(request, *args, **kwargs) diff --git a/rapids/templates/rapids/index.html b/rapids/templates/rapids/index.html deleted file mode 100644 index 8a4b0138..00000000 --- a/rapids/templates/rapids/index.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends 'rapids/base.html' %} - -{% block content %} - {% for exam in exams %} -
-

Exam: {{ exam.name }}

- {% if exam.exam_mode %} - {% if request.user.is_staff %}Mark answers{% endif %} - {% if request.user.is_staff %}Scores{% endif %} - {% endif %} -
- {% endfor %} -{% endblock %} diff --git a/rapids/templates/rapids/index_old.html b/rapids/templates/rapids/index_old.html deleted file mode 100755 index 35f2576a..00000000 --- a/rapids/templates/rapids/index_old.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends 'rapids/base.html' %} - -{% block content %} -My Questions: - - -Other Questions: - - -{% endblock %} \ No newline at end of file diff --git a/rapids/tests/test_urls.py b/rapids/tests/test_urls.py index ecd6e105..a2734e62 100644 --- a/rapids/tests/test_urls.py +++ b/rapids/tests/test_urls.py @@ -2,11 +2,18 @@ import pytest #from django.contrib.auth.models import User from django.urls import reverse +APP_NAMES = ("rapids", "anatomy", "longs", "physics", "sbas") + @pytest.fixture def create_superuser(db, django_user_model): return django_user_model.objects.create_superuser("admin", "ross@xkjq.uk", "adminpassword") -def test_index(client, create_superuser): +# Very basic tests, make sure we can hit key urls without error + +@pytest.mark.parametrize("app_name", APP_NAMES) +def test_index(client, create_superuser, app_name): client.login(username="admin", password="adminpassword") - response = client.get(reverse('rapids:index')) + response = client.get(reverse(f'{app_name}:index')) + print(response.status_code) + print(response) assert response.status_code == 200 \ No newline at end of file diff --git a/sbas/templates/sbas/index.html b/sbas/templates/sbas/index.html deleted file mode 100644 index 2af12136..00000000 --- a/sbas/templates/sbas/index.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends 'sbas/base.html' %} - -{% block content %} -{% for exam in exams %} -
-

Exam: {{ exam.name }}

- {% if request.user.is_staff %}Scores{% endif %} -
-{% endfor %} -{% endblock %} \ No newline at end of file diff --git a/templates/profile.html b/templates/profile.html index d43a62ed..74e28f80 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -2,7 +2,7 @@ {% block content %}
-

Your profile

+

Profile

Username: {{ user.username }}
@@ -13,6 +13,9 @@ Name: {{ user.first_name }} {{ user.last_name }}
+
+ Grade: {{ user.userprofile.grade }} +
Supervisor: {{ user.userprofile.supervisor_name }}
Supervisor Email: {{ user.userprofile.supervisor_email }}
@@ -22,5 +25,7 @@
Change password + Update user details + Update user profile {% endblock %} \ No newline at end of file diff --git a/templates/user_list_view.html b/templates/user_list_view.html new file mode 100644 index 00000000..15ef4e5e --- /dev/null +++ b/templates/user_list_view.html @@ -0,0 +1,15 @@ +{% extends "generic/base.html" %} + +{% block content %} +

Users

+
    + {% for user in object_list %} +
  • Username: {{ user.username }}
    + Name: {{user.first_name}} {{user.last_name}}
    + Registration number: {{user.registration_number}} + + +
  • + {% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/user_update.html b/templates/user_update.html new file mode 100644 index 00000000..23864176 --- /dev/null +++ b/templates/user_update.html @@ -0,0 +1,9 @@ +{% extends 'generic/base.html' %} + +{% block content %} +

Editing user: {{object.username}}

+
{% csrf_token %} + {{ form.as_p }} + +
+{% endblock %} \ No newline at end of file