Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf9b514597 | ||
|
|
98d45fbdfc | ||
|
|
0054d832cd | ||
|
|
f5dcb7bd69 | ||
|
|
850d9637c3 | ||
|
|
ca071168c5 | ||
|
|
9154fa699c | ||
|
|
ce5623e251 | ||
|
|
4205d4c6d6 | ||
|
|
5de5367978 | ||
|
|
441c691832 | ||
|
|
062d7e7827 | ||
|
|
672e3adec1 | ||
|
|
b5dc3b3a61 | ||
|
|
58eeb4f442 |
@@ -53,6 +53,7 @@ urlpatterns = [
|
|||||||
"exam/<int:exam_pk>/<int:sk>/mark/review", views.mark_review, name="mark_review"
|
"exam/<int:exam_pk>/<int:sk>/mark/review", views.mark_review, name="mark_review"
|
||||||
),
|
),
|
||||||
path("exam/<int:pk>/markers", views.ExamMarkersUpdate.as_view(), name="exam_markers"),
|
path("exam/<int:pk>/markers", views.ExamMarkersUpdate.as_view(), name="exam_markers"),
|
||||||
|
path("exam/<int:pk>/groups", views.ExamGroupsUpdate.as_view(), name="exam_groups_edit"),
|
||||||
path("exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"),
|
path("exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"),
|
||||||
path("exam/create", views.ExamCreate.as_view(), name="exam_create"),
|
path("exam/create", views.ExamCreate.as_view(), name="exam_create"),
|
||||||
path("exam/<int:exam_id>/clone", views.ExamClone.as_view(), name="exam_clone"),
|
path("exam/<int:exam_id>/clone", views.ExamClone.as_view(), name="exam_clone"),
|
||||||
|
|||||||
+4
-1
@@ -57,7 +57,7 @@ from autocomplete import widgets as htmx_widgets
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from generic.forms import ExamAuthorFormMixin
|
from generic.forms import ExamAuthorFormMixin, ExamGroupsFormMixin
|
||||||
|
|
||||||
from crispy_forms.helper import FormHelper
|
from crispy_forms.helper import FormHelper
|
||||||
|
|
||||||
@@ -749,6 +749,9 @@ class SeriesAuthorForm(ExamAuthorFormMixin):
|
|||||||
class Meta(ExamAuthorFormMixin.Meta):
|
class Meta(ExamAuthorFormMixin.Meta):
|
||||||
model = Series
|
model = Series
|
||||||
|
|
||||||
|
class ExamGroupsForm(ExamGroupsFormMixin):
|
||||||
|
class Meta(ExamGroupsFormMixin.Meta):
|
||||||
|
model = CaseCollection
|
||||||
|
|
||||||
class AnswerJSONForm(Form):
|
class AnswerJSONForm(Form):
|
||||||
|
|
||||||
|
|||||||
@@ -1188,6 +1188,14 @@ class SelfReview(models.Model):
|
|||||||
review_date = models.DateTimeField(auto_now_add=True)
|
review_date = models.DateTimeField(auto_now_add=True)
|
||||||
review_update_date = models.DateTimeField(auto_now=True)
|
review_update_date = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
date = self.review_date
|
||||||
|
|
||||||
|
if self.review_update_date is not None:
|
||||||
|
date = self.review_update_date
|
||||||
|
|
||||||
|
return f"Self review: {date:%Y-%m-%d} / {self.user_exam.exam} - {self.case}"
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return self.user_exam.exam.get_case_take_url(self.case)
|
return self.user_exam.exam.get_case_take_url(self.case)
|
||||||
|
|
||||||
|
|||||||
@@ -168,10 +168,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{% if collection.self_review %}
|
{% if collection.self_review %}
|
||||||
|
<div>
|
||||||
<p>
|
<p>
|
||||||
<a href="{% url 'atlas:add_self_review' cid_user_exam.id case.id %}"><button>Add self review</button></a>
|
<a href="{% url 'atlas:add_self_review' cid_user_exam.id case.id %}"><button type="button">Add self review</button></a>
|
||||||
</p>
|
</p>
|
||||||
{% if self_review %}
|
{% if self_review %}
|
||||||
<h4>Self Feedback</h4>
|
<h4>Self Feedback</h4>
|
||||||
@@ -181,12 +181,12 @@
|
|||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<h4>Answer score: {{answer.score}}</h4>
|
<h4>Answer score: {{answer.score}}</h4>
|
||||||
Answer feedback: {{answer.feedback|safe}}
|
Answer feedback: {{answer.feedback|safe}}
|
||||||
<br/>
|
<br/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
|
|
||||||
<details><summary class="opacity-50">View questions</summary>
|
<details><summary class="opacity-50">View questions</summary>
|
||||||
{{form.json.errors}}
|
{{form.json.errors}}
|
||||||
|
|||||||
@@ -7,7 +7,11 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
Create <a href='{{request.path}}create'>new</a><br/>
|
Create a new <a href='{{request.path}}create'>collection</a> / <a href="{% url 'atlas:user_collections' %}">Collections to view / take</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<h2>My Collections</h2>
|
||||||
<details>
|
<details>
|
||||||
<summary>
|
<summary>
|
||||||
<h4>Filter</h4>
|
<h4>Filter</h4>
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ urlpatterns = [
|
|||||||
views.GenericExamViews.exam_users_edit,
|
views.GenericExamViews.exam_users_edit,
|
||||||
name="exam_users_edit",
|
name="exam_users_edit",
|
||||||
),
|
),
|
||||||
|
path("exam/<int:pk>/groups", views.ExamGroupsUpdate.as_view(), name="exam_groups_edit"),
|
||||||
path(
|
path(
|
||||||
"collection/<int:exam_id>/case/<int:case_id>/details",
|
"collection/<int:exam_id>/case/<int:case_id>/details",
|
||||||
views.collection_case_details,
|
views.collection_case_details,
|
||||||
|
|||||||
+18
-2
@@ -47,6 +47,7 @@ from .forms import (
|
|||||||
CidReportAnswerMarkForm,
|
CidReportAnswerMarkForm,
|
||||||
ConditionAutocompleteForm,
|
ConditionAutocompleteForm,
|
||||||
ConditionForm,
|
ConditionForm,
|
||||||
|
ExamGroupsForm,
|
||||||
FindingForm,
|
FindingForm,
|
||||||
JsonAnswerForm,
|
JsonAnswerForm,
|
||||||
PriorCaseForm,
|
PriorCaseForm,
|
||||||
@@ -144,6 +145,7 @@ from django.forms.models import model_to_dict
|
|||||||
from generic.views import (
|
from generic.views import (
|
||||||
AuthorRequiredMixin,
|
AuthorRequiredMixin,
|
||||||
ExamCloneMixin,
|
ExamCloneMixin,
|
||||||
|
ExamGroupsUpdateBase,
|
||||||
ExamViews,
|
ExamViews,
|
||||||
SeriesImagesZipViewBase,
|
SeriesImagesZipViewBase,
|
||||||
)
|
)
|
||||||
@@ -2713,6 +2715,15 @@ class CaseCollectionAuthorUpdate(RevisionMixin, AuthorRequiredMixin, UpdateView)
|
|||||||
context["collection"] = context["object"]
|
context["collection"] = context["object"]
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
class ExamGroupsUpdate(ExamGroupsUpdateBase):
|
||||||
|
model = CaseCollection
|
||||||
|
form_class = ExamGroupsForm
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(ExamGroupsUpdate, self).get_context_data(**kwargs)
|
||||||
|
context["exam"] = context["object"]
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class CaseAuthorUpdate(RevisionMixin, AuthorRequiredMixin, UpdateView):
|
class CaseAuthorUpdate(RevisionMixin, AuthorRequiredMixin, UpdateView):
|
||||||
model = Case
|
model = Case
|
||||||
@@ -2816,8 +2827,12 @@ def collection_reset_answers(request, exam_id: int):
|
|||||||
case_details = collection.casedetail_set.all().prefetch_related()
|
case_details = collection.casedetail_set.all().prefetch_related()
|
||||||
|
|
||||||
# Delete all answers
|
# Delete all answers
|
||||||
cid_answers = case_details.cidreportanswer_set.all()
|
for case in case_details:
|
||||||
user_answers = case_details.userreportanswer_set.all()
|
cid_answers = case.cidreportanswer_set.all()
|
||||||
|
|
||||||
|
cid_answers.delete()
|
||||||
|
user_answers = case.userreportanswer_set.all()
|
||||||
|
user_answers.delete()
|
||||||
|
|
||||||
|
|
||||||
# User statuses
|
# User statuses
|
||||||
@@ -2825,6 +2840,7 @@ def collection_reset_answers(request, exam_id: int):
|
|||||||
|
|
||||||
# CidUserExams
|
# CidUserExams
|
||||||
collection.cid_users.all().delete()
|
collection.cid_users.all().delete()
|
||||||
|
return HttpResponse("Success")
|
||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
+7
-1
@@ -789,7 +789,7 @@ class ExamOrCollectionGenericBase(models.Model, AuthorMixin):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def get_cid_user_exams(
|
def get_cid_user_exams(
|
||||||
self, cid_user: Optional["CidUser"] = None, user_user: User = None
|
self, cid_user: Optional["CidUser"] = None, user_user: User | None = None
|
||||||
) -> "CidUserExam":
|
) -> "CidUserExam":
|
||||||
content_type = ContentType.objects.get_for_model(self)
|
content_type = ContentType.objects.get_for_model(self)
|
||||||
if cid_user is None and user_user is None:
|
if cid_user is None and user_user is None:
|
||||||
@@ -992,6 +992,12 @@ class ExamBase(ExamOrCollectionGenericBase):
|
|||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def get_user_users_with_scores(self):
|
||||||
|
user_ids = [i[2:] for i in self.user_scores if i.startswith("u/")]
|
||||||
|
|
||||||
|
return User.objects.filter(pk__in=user_ids)
|
||||||
|
|
||||||
|
|
||||||
def generate_user_report(self, user):
|
def generate_user_report(self, user):
|
||||||
exam_text = [f"Candidate {user.first_name} [{user.email}]"]
|
exam_text = [f"Candidate {user.first_name} [{user.email}]"]
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,14 @@ Author(s): {% for author in exam.author.all %}
|
|||||||
{{ author }}{% if not forloop.last %}, {% endif %}
|
{{ author }}{% if not forloop.last %}, {% endif %}
|
||||||
{% endfor %}<br/>
|
{% endfor %}<br/>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Extra</summary>
|
||||||
|
<button title="This will clear all user answers and attempts"
|
||||||
|
hx-post="{% url exam.get_app_name|add:':order_questions' exam.pk %}"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>Order questions</button>
|
||||||
|
</details>
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
<style>
|
<style>
|
||||||
.answer-management[open] {
|
.answer-management[open] {
|
||||||
|
|||||||
@@ -1,16 +1,35 @@
|
|||||||
<ul>
|
<ul>
|
||||||
|
<h3>Email to send</h3>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Supervisor</th>
|
||||||
|
</tr>
|
||||||
|
{% for user in users %}
|
||||||
|
<tr>
|
||||||
|
<td>{{user.username}}</td>
|
||||||
|
<td>{{user.userprofile.supervisor}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</table>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
|
||||||
{% for user, status in status.items %}
|
{% for user, status in status.items %}
|
||||||
<li class='{% if status.0 %}text-success{% else %}text-warning{% endif %}'>
|
<li class='{% if status.0 %}text-success{% else %}text-warning{% endif %}'>
|
||||||
{{user.username}} {% if status.0 %}success{% else %}failed [{{status.1}}]{% endif %} ({{status.2}})
|
{{user.username}} {% if status.0 %}success{% else %}failed [{{status.1}}]{% endif %} ({{status.2}})
|
||||||
<button hx-get="{% url exam.app_name|add:':exam_user_report_email' exam_id=exam.pk user_id=user.pk %}"
|
<button hx-get="{% url exam.app_name|add:':exam_user_report_email' exam_id=exam.pk user_id=user.pk %}"
|
||||||
hx-prompt="Enter any additional emails"
|
hx-prompt="Enter any additional emails"
|
||||||
hx-confirm="Are you sure you wish to delete your account?"
|
hx-confirm="Are you sure you wish to resend all emails?"
|
||||||
hx-trigger="click, click from:#resend-all"
|
hx-trigger="click, click from:#resend-all"
|
||||||
>Resend</button>
|
>Resend</button>
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
No emails sent.
|
<p>No emails sent.</p>
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
<button id="resend-all">Resend all</button>
|
<button id="resend-all">Resend all</button>
|
||||||
@@ -131,7 +131,10 @@
|
|||||||
|
|
||||||
{% block breakdown %}{% endblock breakdown %}
|
{% block breakdown %}{% endblock breakdown %}
|
||||||
|
|
||||||
<details>
|
<details
|
||||||
|
hx-get="{% url exam.app_name|add:':exam_report_email_status' exam_id=exam.pk %}"
|
||||||
|
hx-trigger="revealed"
|
||||||
|
hx-target="#user-details">
|
||||||
<summary>Email results</summary>
|
<summary>Email results</summary>
|
||||||
User results emailed: {{exam.exam_results_emailed|default:"Never"}}<br/>
|
User results emailed: {{exam.exam_results_emailed|default:"Never"}}<br/>
|
||||||
<button id="email-results-button"
|
<button id="email-results-button"
|
||||||
@@ -149,7 +152,8 @@
|
|||||||
>Email unsent user results</button>
|
>Email unsent user results</button>
|
||||||
<button id="email-results-check-button" title="Check the status of emailed results"
|
<button id="email-results-check-button" title="Check the status of emailed results"
|
||||||
hx-get="{% url exam.app_name|add:':exam_report_email_status' exam_id=exam.pk %}"
|
hx-get="{% url exam.app_name|add:':exam_report_email_status' exam_id=exam.pk %}"
|
||||||
hx-target="#user-details">Check email status</button>
|
hx-trigger="click"
|
||||||
|
hx-target="#user-details">Refresh email status</button>
|
||||||
<p>Note: currently only works with registered users</p>
|
<p>Note: currently only works with registered users</p>
|
||||||
<div id="user-details"></div>
|
<div id="user-details"></div>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
{% comment %} Hide the toggle button if there are no users (to add or remove) {% endcomment %}
|
{% comment %} Hide the toggle button if there are no users (to add or remove) {% endcomment %}
|
||||||
{% if current_cid_users or available_cid_users %}
|
{% if current_user_users or available_user_users %}
|
||||||
<p><button class="toggle-all-btn">Toggle all</button></p>
|
<p><button class="toggle-all-btn">Toggle all</button></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -239,11 +239,11 @@ def generic_exam_urls(generic_exam_view: GenericExamViews):
|
|||||||
generic_exam_view.exam_question_detail,
|
generic_exam_view.exam_question_detail,
|
||||||
name="exam_question_detail",
|
name="exam_question_detail",
|
||||||
),
|
),
|
||||||
#path(
|
# path(
|
||||||
# "exam/<int:pk>/question/<int:sk>/answer/<str:user_or_cid>",
|
# "exam/<int:pk>/question/<int:sk>/answer/<str:user_or_cid>",
|
||||||
# generic_exam_view.exam_question_user_answer,
|
# generic_exam_view.exam_question_user_answer,
|
||||||
# name="exam_question_user_answer",
|
# name="exam_question_user_answer",
|
||||||
#),
|
# ),
|
||||||
path(
|
path(
|
||||||
"exam/<int:pk>/question/<int:sk>/answer/<str:c_or_u>/<str:user_or_cid>",
|
"exam/<int:pk>/question/<int:sk>/answer/<str:c_or_u>/<str:user_or_cid>",
|
||||||
generic_exam_view.exam_question_user_answer,
|
generic_exam_view.exam_question_user_answer,
|
||||||
@@ -342,8 +342,9 @@ def generic_exam_urls(generic_exam_view: GenericExamViews):
|
|||||||
),
|
),
|
||||||
path("exam/", generic_exam_view.exam_list, name="exam_list"),
|
path("exam/", generic_exam_view.exam_list, name="exam_list"),
|
||||||
path("exam/all", generic_exam_view.exam_list_all, name="exam_list_all"),
|
path("exam/all", generic_exam_view.exam_list_all, name="exam_list_all"),
|
||||||
|
path("exam/<int:exam_id>/order_questions", generic_exam_view.order_questions, name="order_questions"),
|
||||||
path("exam/<int:exam_id>/cids", generic_exam_view.exam_cids, name="exam_cids"),
|
path("exam/<int:exam_id>/cids", generic_exam_view.exam_cids, name="exam_cids"),
|
||||||
#path("exam/<int:exam_id>/groups", generic_exam_view.exam_groups_edit, name="exam_groups_edit"),
|
# path("exam/<int:exam_id>/groups", generic_exam_view.exam_groups_edit, name="exam_groups_edit"),
|
||||||
path(
|
path(
|
||||||
"exam/<int:exam_id>/reset_answers",
|
"exam/<int:exam_id>/reset_answers",
|
||||||
generic_exam_view.exam_reset_answers,
|
generic_exam_view.exam_reset_answers,
|
||||||
|
|||||||
+23
-9
@@ -374,7 +374,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
question_type,
|
question_type,
|
||||||
normalise_score=None,
|
normalise_score=None,
|
||||||
):
|
):
|
||||||
self.Exam = exam
|
self.Exam: ExamBase = exam
|
||||||
self.Question = question
|
self.Question = question
|
||||||
self.Answer = answer
|
self.Answer = answer
|
||||||
self.UserAnswer = cid_user_answer
|
self.UserAnswer = cid_user_answer
|
||||||
@@ -582,7 +582,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
@method_decorator(login_required)
|
@method_decorator(login_required)
|
||||||
def exam_toggle_results_published(self, request, pk):
|
def exam_toggle_results_published(self, request, pk):
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if not self.check_user_edit_access(request.user):
|
if not self.check_user_edit_access(request.user, exam_id=pk):
|
||||||
data = {"status": "error, insufficient permission"}
|
data = {"status": "error, insufficient permission"}
|
||||||
return JsonResponse(data, status=400)
|
return JsonResponse(data, status=400)
|
||||||
|
|
||||||
@@ -606,7 +606,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
@method_decorator(login_required)
|
@method_decorator(login_required)
|
||||||
def exam_toggle_active(self, request, pk):
|
def exam_toggle_active(self, request, pk):
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if not self.check_user_edit_access(request.user):
|
if not self.check_user_edit_access(request.user, exam_id=pk):
|
||||||
data = {"status": "error, insufficient permission"}
|
data = {"status": "error, insufficient permission"}
|
||||||
return JsonResponse(data, status=400)
|
return JsonResponse(data, status=400)
|
||||||
|
|
||||||
@@ -654,6 +654,16 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
def exam_list_all(self, request):
|
def exam_list_all(self, request):
|
||||||
return self.exam_list(request, all=True)
|
return self.exam_list(request, all=True)
|
||||||
|
|
||||||
|
def order_questions(self, request, exam_id):
|
||||||
|
if not self.check_user_edit_access(request.user):
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
|
exam = get_object_or_404(self.Exam, pk=exam_id)
|
||||||
|
|
||||||
|
exam.order_questions()
|
||||||
|
|
||||||
|
return HttpResponse("Done")
|
||||||
|
|
||||||
@method_decorator(login_required)
|
@method_decorator(login_required)
|
||||||
def exam_list(self, request, all=False):
|
def exam_list(self, request, all=False):
|
||||||
if not self.check_user_access(request.user):
|
if not self.check_user_access(request.user):
|
||||||
@@ -710,11 +720,11 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
if not self.check_user_access(request.user, exam_id):
|
if not self.check_user_access(request.user, exam_id):
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
|
||||||
users = [i[2:] for i in exam.user_scores if i.startswith("u/")]
|
users = exam.get_user_users_with_scores()
|
||||||
|
|
||||||
status = {}
|
status = {}
|
||||||
for u in users:
|
for user in users:
|
||||||
user = User.objects.get(pk=u)
|
#user = User.objects.get(pk=u)
|
||||||
user_exam = exam.get_or_create_cid_user_exam(user_user=user)
|
user_exam = exam.get_or_create_cid_user_exam(user_user=user)
|
||||||
if user_exam.results_emailed_status:
|
if user_exam.results_emailed_status:
|
||||||
status[user] = json.loads(user_exam.results_emailed_status)
|
status[user] = json.loads(user_exam.results_emailed_status)
|
||||||
@@ -726,6 +736,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
"exam": exam,
|
"exam": exam,
|
||||||
"status": status,
|
"status": status,
|
||||||
"app_name": self.app_name,
|
"app_name": self.app_name,
|
||||||
|
"users": users,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return JsonResponse(status)
|
return JsonResponse(status)
|
||||||
@@ -760,13 +771,13 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
# We only need to send emails to those who have scores
|
# We only need to send emails to those who have scores
|
||||||
# (who should be in exam.user_scores)
|
# (who should be in exam.user_scores)
|
||||||
if users is None:
|
if users is None:
|
||||||
users = [i[2:] for i in exam.user_scores if i.startswith("u/")]
|
users = exam.get_user_users_with_scores()
|
||||||
|
|
||||||
time = timezone.now()
|
time = timezone.now()
|
||||||
|
|
||||||
email_results = {}
|
email_results = {}
|
||||||
for u in users:
|
for user in users:
|
||||||
user = User.objects.get(pk=u)
|
#user = User.objects.get(pk=u)
|
||||||
user_exam = exam.get_or_create_cid_user_exam(user_user=user)
|
user_exam = exam.get_or_create_cid_user_exam(user_user=user)
|
||||||
|
|
||||||
if unsent_only and user_exam.results_emailed_status:
|
if unsent_only and user_exam.results_emailed_status:
|
||||||
@@ -1978,6 +1989,8 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
raise Http404("Packet not in exam mode")
|
raise Http404("Packet not in exam mode")
|
||||||
|
|
||||||
if not exam.check_cid_user(cid, passcode, request.user):
|
if not exam.check_cid_user(cid, passcode, request.user):
|
||||||
|
if not self.check_user_access(request.user, pk):
|
||||||
|
#raise PermissionDenied
|
||||||
raise Http404("Error accessing exam")
|
raise Http404("Error accessing exam")
|
||||||
|
|
||||||
if user is not None:
|
if user is not None:
|
||||||
@@ -2564,6 +2577,7 @@ class ExamCloneMixin():
|
|||||||
object.exam_questions.set(self.exam_questions)
|
object.exam_questions.set(self.exam_questions)
|
||||||
object.author.set(self.author)
|
object.author.set(self.author)
|
||||||
object.save()
|
object.save()
|
||||||
|
object.order_questions()
|
||||||
return HttpResponseRedirect(object.get_absolute_url())
|
return HttpResponseRedirect(object.get_absolute_url())
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,12 @@
|
|||||||
.not-answered {
|
.not-answered {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
.image-link {
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid purple;
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock css %}
|
{% endblock css %}
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
|||||||
+6
-6
@@ -62,11 +62,11 @@ urlpatterns = [
|
|||||||
# path("all_questions/", views.all_questions, name="all_questions"),
|
# path("all_questions/", views.all_questions, name="all_questions"),
|
||||||
path("question/<int:pk>/scrap", views.long_scrap, name="long_scrap"),
|
path("question/<int:pk>/scrap", views.long_scrap, name="long_scrap"),
|
||||||
path("question/<int:pk>/delete", views.LongDelete.as_view(), name="long_delete"),
|
path("question/<int:pk>/delete", views.LongDelete.as_view(), name="long_delete"),
|
||||||
path(
|
# path(
|
||||||
"exam/<int:pk>/review",
|
# "exam/<int:pk>/review",
|
||||||
views.question_review,
|
# views.question_review,
|
||||||
name="question_review",
|
# name="question_review",
|
||||||
),
|
# ),
|
||||||
path(
|
path(
|
||||||
"exam/<int:exam_id>/<int:question_number>/<int:answer_id>/mark",
|
"exam/<int:exam_id>/<int:question_number>/<int:answer_id>/mark",
|
||||||
views.mark_answer,
|
views.mark_answer,
|
||||||
@@ -157,5 +157,5 @@ urlpatterns = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
#urlpatterns.extend(generic_view_urls(views.GenericViews))
|
urlpatterns.extend(generic_view_urls(views.GenericViews))
|
||||||
urlpatterns.extend(generic_exam_urls(views.GenericExamViews))
|
urlpatterns.extend(generic_exam_urls(views.GenericExamViews))
|
||||||
@@ -85,6 +85,7 @@ from generic.views import (
|
|||||||
ExamGroupsUpdateBase,
|
ExamGroupsUpdateBase,
|
||||||
ExamUpdateBase,
|
ExamUpdateBase,
|
||||||
ExamViews,
|
ExamViews,
|
||||||
|
GenericViewBase,
|
||||||
RedirectMixin,
|
RedirectMixin,
|
||||||
SeriesImagesZipViewBase,
|
SeriesImagesZipViewBase,
|
||||||
UpdateQuestionMixin,
|
UpdateQuestionMixin,
|
||||||
@@ -954,6 +955,7 @@ def long_series_order_upload_filename(request, pk):
|
|||||||
|
|
||||||
|
|
||||||
GenericExamViews = ExamViews(Exam, Long, None, UserAnswer, "longs", "long")
|
GenericExamViews = ExamViews(Exam, Long, None, UserAnswer, "longs", "long")
|
||||||
|
GenericViews = GenericViewBase("longs", Long, UserAnswer, Exam)
|
||||||
|
|
||||||
|
|
||||||
class ExamCreate(ExamCreateBase):
|
class ExamCreate(ExamCreateBase):
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-08-28 17:22
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('physics', '0013_alter_exam_markers'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='examquestiondetail',
|
||||||
|
old_name='rapid',
|
||||||
|
new_name='question',
|
||||||
|
),
|
||||||
|
]
|
||||||
+1
-1
@@ -141,7 +141,7 @@ class ExamQuestionDetail(models.Model):
|
|||||||
sort_order = models.IntegerField(default=1000)
|
sort_order = models.IntegerField(default=1000)
|
||||||
|
|
||||||
exam = models.ForeignKey("Exam", on_delete=models.CASCADE)
|
exam = models.ForeignKey("Exam", on_delete=models.CASCADE)
|
||||||
rapid = models.ForeignKey(Question, on_delete=models.CASCADE)
|
question = models.ForeignKey(Question, on_delete=models.CASCADE)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("sort_order",)
|
ordering = ("sort_order",)
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ urlpatterns.extend(
|
|||||||
name="exam_take_overview_user",
|
name="exam_take_overview_user",
|
||||||
),
|
),
|
||||||
path("exam/<int:pk>/markers", views.ExamMarkersUpdate.as_view(), name="exam_markers"),
|
path("exam/<int:pk>/markers", views.ExamMarkersUpdate.as_view(), name="exam_markers"),
|
||||||
|
path("exam/<int:pk>/groups", views.ExamGroupsUpdate.as_view(), name="exam_groups_edit"),
|
||||||
path(
|
path(
|
||||||
"exam/<int:pk>/authors",
|
"exam/<int:pk>/authors",
|
||||||
views.ExamAuthorUpdate.as_view(),
|
views.ExamAuthorUpdate.as_view(),
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ urlpatterns = [
|
|||||||
"exam/<int:exam_pk>/<int:sk>/mark/review", views.mark_review, name="mark_review"
|
"exam/<int:exam_pk>/<int:sk>/mark/review", views.mark_review, name="mark_review"
|
||||||
),
|
),
|
||||||
path("exam/<int:pk>/markers", views.ExamMarkersUpdate.as_view(), name="exam_markers"),
|
path("exam/<int:pk>/markers", views.ExamMarkersUpdate.as_view(), name="exam_markers"),
|
||||||
|
path("exam/<int:pk>/groups", views.ExamGroupsUpdate.as_view(), name="exam_groups_edit"),
|
||||||
path("exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"),
|
path("exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"),
|
||||||
path("exam/create", views.ExamCreate.as_view(), name="exam_create"),
|
path("exam/create", views.ExamCreate.as_view(), name="exam_create"),
|
||||||
path("exam/<int:exam_id>/clone", views.ExamClone.as_view(), name="exam_clone"),
|
path("exam/<int:exam_id>/clone", views.ExamClone.as_view(), name="exam_clone"),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Specialty</th>
|
<th>Specialty</th>
|
||||||
@@ -13,6 +13,9 @@
|
|||||||
<th>Outcome (reason)</th>
|
<th>Outcome (reason)</th>
|
||||||
<th>Outcome (free text)</th>
|
<th>Outcome (free text)</th>
|
||||||
<th>Comments</th>
|
<th>Comments</th>
|
||||||
|
<th>Radiology Category</th>
|
||||||
|
<th>Oncology Category</th>
|
||||||
|
<th>Radiology Condition</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -27,10 +30,29 @@
|
|||||||
<td>{{item.outcome_reason}}</td>
|
<td>{{item.outcome_reason}}</td>
|
||||||
<td>{{item.outcome_free_text}}</td>
|
<td>{{item.outcome_free_text}}</td>
|
||||||
<td>{{item.comments}}</td>
|
<td>{{item.comments}}</td>
|
||||||
|
<td>{% for cat in item.radiology_categories.all %}
|
||||||
|
{% if forloop.last %}
|
||||||
|
{{cat}}
|
||||||
|
{% else %}
|
||||||
|
{{cat}} |
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td>{{item.oncology_category}}</td>
|
||||||
|
<td>{% for cat in item.radiology_condition.all %}
|
||||||
|
{% if forloop.last %}
|
||||||
|
{{cat}}
|
||||||
|
{% else %}
|
||||||
|
{{cat}} |
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-08-28 17:22
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('sbas', '0013_alter_exam_markers'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='examquestiondetail',
|
||||||
|
old_name='rapid',
|
||||||
|
new_name='question',
|
||||||
|
),
|
||||||
|
]
|
||||||
+1
-1
@@ -159,7 +159,7 @@ class ExamQuestionDetail(models.Model):
|
|||||||
sort_order = models.IntegerField(default=1000)
|
sort_order = models.IntegerField(default=1000)
|
||||||
|
|
||||||
exam = models.ForeignKey("Exam", on_delete=models.CASCADE)
|
exam = models.ForeignKey("Exam", on_delete=models.CASCADE)
|
||||||
rapid = models.ForeignKey(Question, on_delete=models.CASCADE)
|
question = models.ForeignKey(Question, on_delete=models.CASCADE)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("sort_order",)
|
ordering = ("sort_order",)
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
|
|
||||||
path("exam/<int:pk>/markers", views.ExamMarkersUpdate.as_view(), name="exam_markers"),
|
path("exam/<int:pk>/markers", views.ExamMarkersUpdate.as_view(), name="exam_markers"),
|
||||||
|
path("exam/<int:pk>/groups", views.ExamGroupsUpdate.as_view(), name="exam_groups_edit"),
|
||||||
path(
|
path(
|
||||||
"exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"
|
"exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user