This commit is contained in:
Ross
2022-05-24 12:30:11 +01:00
parent bbcbc02e0d
commit 41a09ea30d
29 changed files with 506 additions and 247 deletions
+9
View File
@@ -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;
}
-14
View File
@@ -1,14 +0,0 @@
{% extends 'anatomy/base.html' %}
{% block content %}
{% for exam in exams %}
<div class="anatomy">
<h1><a href="{% url 'anatomy:exam_overview' pk=exam.pk %}">Exam: {{ exam.name }} </a></h1>
{% if exam.exam_mode %}
{% if request.user.is_staff %}<a href="{% url 'anatomy:mark' exam_pk=exam.pk sk=0 %}">Mark answers</a>{% endif %}
{% if request.user.is_staff %}<a href="{% url 'anatomy:exam_scores_cid' pk=exam.pk %}">Scores</a>{% endif %}
{% endif %}
</div>
{% endfor %}
{% endblock %}
+4
View File
@@ -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)
+17 -3
View File
@@ -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"]
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"]
@@ -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),
),
]
@@ -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),
),
]
@@ -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'),
),
]
+13
View File
@@ -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)
+1
View File
@@ -21,6 +21,7 @@
<a href="{% url 'generic:manage_cid_exams' %}">Cids (exams)</a> /
<a href="{% url 'generic:cid_group_view' %}">Cid Groups</a> /
<a href="{% url 'generic:user_group_view' %}">User Groups</a> /
<a href="{% url 'accounts_list' %}">Manage Users</a> /
{% endif %}
{% endblock %}
@@ -7,22 +7,24 @@
</p>
{% for group in groups %}
<p>{{group.name}}
<button class="url-button" data-url="{% url 'generic:group_email' pk=group.pk %}
<button class="small-url-button" data-url="{% url 'generic:group_email' pk=group.pk %}
">Email candidate details</button>
<button class="url-button" data-url="{% url 'generic:group_email_resend' pk=group.pk %}
<button class="small-url-button" data-url="{% url 'generic:group_email_resend' pk=group.pk %}
">Resend candidate details</button>
<button class="url-button" data-url="{% url 'generic:group_email_results' pk=group.pk %}
<button class="small-url-button" data-url="{% url 'generic:group_email_results' pk=group.pk %}
">Email candidate results</button>
<button class="url-button" data-url="{% url 'generic:group_email_results_resend' pk=group.pk %}
<button class="small-url-button" data-url="{% url 'generic:group_email_results_resend' pk=group.pk %}
">Resend candidate results</button>
<button class="url-button" data-url="{% url 'generic:cid_group_update' pk=group.pk %}
<button class="small-url-button" data-url="{% url 'generic:cid_group_update' pk=group.pk %}
">Edit</button>
<button class="small-url-button" data-url="{% url 'generic:cid_group_delete' pk=group.pk %}
">Delete</button>
</p>
{% endfor %}
<script type="text/javascript">
$(".url-button").click((evt) => {
$(".small-url-button").click((evt) => {
window.location = evt.currentTarget.dataset.url;
})
@@ -13,8 +13,13 @@
<!-- {{ form.media }} -->
{% endblock %}
{% block content %}
<h2>Add / Edit Group / {{cidusergroup.name}}</h2>
Use this form to create / edit a CID user group.
{% if form.instance.id %}
<h2>Edit Group: {{cidusergroup.name}}</h2>
Use this form to edit a CID user group.
{% else %}
<h2>Add New Group</h2>
Use this form to create a CID user group.
{% endif %}
<form action="" method="post" enctype="multipart/form-data" id="condition-form">
{% csrf_token %}
@@ -25,7 +30,12 @@ Use this form to create / edit a CID user group.
<h3>Users</h3>
{% if cidusergroup.GetGroupUsers %}
{{cidusergroup.GetGroupUsers}}
{% else %}
No users currently added to group.
{% endif %}
</form>
{% endblock %}
+25
View File
@@ -0,0 +1,25 @@
{% extends app_name|add:'/base.html' %}
{% block content %}
<details>
<summary>
Filter exam list
</summary>
<form method="get">
{{ filter.form.as_p }}
<input type="submit" />
</form>
</details>
<h4>{{filter.qs|length}} exams found.</h4>
{% for exam in filter.qs %}
<div class="">
<h1><a href="{% url app_name|add:':exam_overview' pk=exam.pk %}">Exam: {{ exam.name }} </a></h1>
{% if exam.exam_mode %}
{% if app_name in "rapids longs anatomy physics" %}
{% if request.user.is_staff %}<a href="{% url app_name|add:':mark_overview' pk=exam.pk %}">Mark answers</a>{% endif %}
{% endif %}
{% if request.user.is_staff %}<a href="{% url app_name|add:':exam_scores_cid' pk=exam.pk %}">Scores</a>{% endif %}
{% endif %}
</div>
{% endfor %}
{% endblock %}
@@ -7,22 +7,24 @@
</p>
{% for group in groups %}
<p>{{group.name}}
{% comment %} <button class="url-button" data-url="{% url 'generic:group_email' pk=group.pk %}
{% comment %} <button class="small-url-button" data-url="{% url 'generic:group_email' pk=group.pk %}
">Email candidate details</button>
<button class="url-button" data-url="{% url 'generic:group_email_resend' pk=group.pk %}
<button class="small-url-button" data-url="{% url 'generic:group_email_resend' pk=group.pk %}
">Resend candidate details</button>
<button class="url-button" data-url="{% url 'generic:group_email_results' pk=group.pk %}
<button class="small-url-button" data-url="{% url 'generic:group_email_results' pk=group.pk %}
">Email candidate results</button>
<button class="url-button" data-url="{% url 'generic:group_email_results_resend' pk=group.pk %}
<button class="small-url-button" data-url="{% url 'generic:group_email_results_resend' pk=group.pk %}
">Resend candidate results</button> {% endcomment %}
<button class="url-button" data-url="{% url 'generic:user_group_update' pk=group.pk %}
<button class="small-url-button" data-url="{% url 'generic:user_group_update' pk=group.pk %}
">Edit</button>
<button class="small-url-button" data-url="{% url 'generic:user_group_delete' pk=group.pk %}
">Delete</button>
</p>
{% endfor %}
<script type="text/javascript">
$(".url-button").click((evt) => {
$(".small-url-button").click((evt) => {
window.location = evt.currentTarget.dataset.url;
})
@@ -4,28 +4,43 @@
{% block css %}
{% endblock %}
{% block js %}
<!--<script type="text/javascript" src="/admin/jsi18n/"></script>-->
{{form.media}}
<!--<script type="text/javascript" src="/admin/jsi18n/"></script>-->
{{form.media}}
<script type="text/javascript">
</script>
<script type="text/javascript">
</script>
<!-- {{ form.media }} -->
<!-- {{ form.media }} -->
{% endblock %}
{% block content %}
<h2>Add / Edit Group / {{userusergroup.name}}</h2>
Use this form to create / edit a CID user group.
<form action="" method="post" enctype="multipart/form-data" id="condition-form">
{% csrf_token %}
{% if form.instance.id %}
<h2>Edit Group: {{userusergroup.name}}</h2>
{% else %}
<h2>Add New Group</h2>
{% endif %}
Use this form to create / edit a CID user group.
<form action="" method="post" enctype="multipart/form-data" id="condition-form">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<input type="submit" class="submit-button" value="Submit" name="submit">
<table>
{{ form.as_table }}
</table>
<input type="submit" class="submit-button" value="Submit" name="submit">
<h3>Users</h3>
<h3>Users</h3>
{{userusergroup.GetGroupUsers}}
<ul>
{% for user in userusergroup.users.all %}
<li>{{user}}</li>{% empty %}
<li>Group currently has no users.</li>
{% endfor %}
</ul>
</form>
{% comment %} {% if userusergroup.users.all %}
{{userusergroup.GetGroupUsers}}
{% else %}
No users currently added to group.
{% endif %} {% endcomment %}
</form>
{% endblock %}
+6
View File
@@ -62,6 +62,9 @@ urlpatterns = [
path(
"cids/group/<int:pk>/update", views.CidUserGroupUpdate.as_view(), name="cid_group_update"
),
path(
"cids/group/<int:pk>/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/<int:pk>/update", views.UserUserGroupUpdate.as_view(), name="user_group_update"
),
path(
"user/group/<int:pk>/delete", views.UserUserGroupDelete.as_view(), name="user_group_delete"
),
path(
"user/group/create", views.UserUserGroupCreate.as_view(), name="user_group_create"
),
+176 -97
View File
@@ -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
form_class = UserUserGroupForm
class UserUserGroupDelete(RevisionMixin, CidManagerRequiredMixin, DeleteView):
model = UserUserGroup
template_name = "confirm_delete.html"
success_url = reverse_lazy("generic:user_group_view")
-11
View File
@@ -1,11 +0,0 @@
{% extends 'longs/base.html' %}
{% block content %}
{% for exam in exams %}
<div class="longs">
<h1><a href="{% url 'longs:exam_overview' pk=exam.pk %}">Exam: {{ exam.name }} </a></h1>
{% if request.user.is_staff %}<a href="{% url 'longs:mark_question_overview' exam_id=exam.pk sk=0 %}">Mark answers</a>{% endif %}
{% if request.user.is_staff %}<a href="{% url 'longs:exam_scores_cid' pk=exam.pk %}">Scores</a>{% endif %}
</div>
{% endfor %}
{% endblock %}
-23
View File
@@ -1,23 +0,0 @@
{% extends 'longs/base.html' %}
{% block content %}
My Questions:
<ul>
{% for long in user_longs %}
<li{% if long.scrapped %} class='long-scrapped' {% endif %}><a
href="{% url 'longs:question_detail' pk=long.pk %}">{{long}}
[{{long.get_authors}}]</a></li>
{% endfor %}
</ul>
Other Questions:
<ul>
{% for long in other_longs %}
<li{% if long.scrapped %} class='long-scrapped' {% endif %}><a
href="{% url 'longs:question_detail' pk=long.pk %}">{{long}} [{{long.get_authors}}]</a></li>
{% endfor %}
</ul>
{% endblock %}
-10
View File
@@ -1,10 +0,0 @@
{% extends 'physics/base.html' %}
{% block content %}
{% for exam in exams %}
<div class="physics">
<h1><a href="{% url 'physics:exam_overview' pk=exam.pk %}">Exam: {{ exam.name }} </a></h1>
{% if request.user.is_staff %}<a href="{% url 'physics:exam_scores_cid' pk=exam.pk %}">Scores</a>{% endif %}
</div>
{% endfor %}
{% endblock %}
+15 -6
View File
@@ -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 *
+4
View File
@@ -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/<str:slug>/", views.UpdateUserView.as_view(), name="account_update"),
path("accounts/update_profile/<str:slug>/", 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/<str:slug>/", views.account_profile, name="account_profile"),
path("", TemplateView.as_view(template_name="index.html"), name="home"),
path("cid/results/<int:cid>/", views.cid_results, name="cid_results"),
path("cid/<int:pk>/<str:passcode>", views.cid_scores, name="cid_scores"),
+72 -4
View File
@@ -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)
-13
View File
@@ -1,13 +0,0 @@
{% extends 'rapids/base.html' %}
{% block content %}
{% for exam in exams %}
<div class="rapids">
<h1><a href="{% url 'rapids:exam_overview' pk=exam.pk %}">Exam: {{ exam.name }} </a></h1>
{% if exam.exam_mode %}
{% if request.user.is_staff %}<a href="{% url 'rapids:mark' exam_pk=exam.pk sk=0 %}">Mark answers</a>{% endif %}
{% if request.user.is_staff %}<a href="{% url 'rapids:exam_scores_cid' pk=exam.pk %}">Scores</a>{% endif %}
{% endif %}
</div>
{% endfor %}
{% endblock %}
-23
View File
@@ -1,23 +0,0 @@
{% extends 'rapids/base.html' %}
{% block content %}
My Questions:
<ul>
{% for rapid in user_rapids %}
<li{% if rapid.scrapped %} class='rapid-scrapped' {% endif %}><a
href="{% url 'rapids:question_detail' pk=rapid.pk %}">{{rapid}}
[{{rapid.get_authors}}]</a></li>
{% endfor %}
</ul>
Other Questions:
<ul>
{% for rapid in other_rapids %}
<li{% if rapid.scrapped %} class='rapid-scrapped' {% endif %}><a
href="{% url 'rapids:question_detail' pk=rapid.pk %}">{{rapid}} [{{rapid.get_authors}}]</a></li>
{% endfor %}
</ul>
{% endblock %}
+9 -2
View File
@@ -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
-10
View File
@@ -1,10 +0,0 @@
{% extends 'sbas/base.html' %}
{% block content %}
{% for exam in exams %}
<div class="sbas">
<h1><a href="{% url 'sbas:exam_overview' pk=exam.pk %}">Exam: {{ exam.name }} </a></h1>
{% if request.user.is_staff %}<a href="{% url 'sbas:exam_scores_cid' pk=exam.pk %}">Scores</a>{% endif %}
</div>
{% endfor %}
{% endblock %}
+6 -1
View File
@@ -2,7 +2,7 @@
{% block content %}
<div class="anatomy">
<h1>Your profile</h1>
<h1>Profile</h1>
<div>
Username: {{ user.username }}
</div>
@@ -13,6 +13,9 @@
Name: {{ user.first_name }}
{{ user.last_name }}
</div>
<div>
Grade: {{ user.userprofile.grade }}
</div>
<div>
Supervisor: {{ user.userprofile.supervisor_name }}<br/>
Supervisor Email: {{ user.userprofile.supervisor_email }}<br/>
@@ -22,5 +25,7 @@
</div>
<a href="{% url 'password_change'%}">Change password</a>
<a href="{% url 'account_update' user.username %}">Update user details</a>
<a href="{% url 'account_profile_update' user.username %}">Update user profile</a>
</div>
{% endblock %}
+15
View File
@@ -0,0 +1,15 @@
{% extends "generic/base.html" %}
{% block content %}
<h2>Users</h2>
<ul>
{% for user in object_list %}
<li>Username: <a href="{% url 'account_profile' user.username %}">{{ user.username }}</a><br/>
Name: {{user.first_name}} {{user.last_name}}<br/>
Registration number: {{user.registration_number}}
<button class="small-url-button"><a href="{% url 'account_update' user.username %}">Edit user</a></button>
<button class="small-url-button"><a href="{% url 'account_profile_update' user.username %}">Edit profile</a></button>
</li>
{% endfor %}
</ul>
{% endblock %}
+9
View File
@@ -0,0 +1,9 @@
{% extends 'generic/base.html' %}
{% block content %}
<h2>Editing user: {{object.username}}</h2>
<form method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Update">
</form>
{% endblock %}