Merge ssh://penracourses.org.uk:/home/django/rad
This commit is contained in:
@@ -206,3 +206,8 @@ LongSeriesImageFormSet = inlineformset_factory(
|
||||
max_num=2000,
|
||||
field_classes="testing",
|
||||
)
|
||||
|
||||
class ExamForm(ModelForm):
|
||||
class Meta:
|
||||
model = Exam
|
||||
fields = ["name", "time_limit", "exam_mode", "active", "archive"]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.6 on 2021-08-16 12:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('longs', '0043_rename_sorted_series_long_series'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='exam',
|
||||
name='archive',
|
||||
field=models.BooleanField(default=False, help_text='Archived exams will remain on the test system but will not be displayed by default'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='exam',
|
||||
name='active',
|
||||
field=models.BooleanField(default=False, help_text='If an exam should be available to take'),
|
||||
),
|
||||
]
|
||||
@@ -23,6 +23,9 @@ Longs:
|
||||
<a href="{% url 'longs:long_create' %}" title="Create a new long case">Create Case</a> /
|
||||
<a href="{% url 'longs:long_series_create' %}" title="Create a new image series">Create Series</a>
|
||||
{% endif %}
|
||||
{% if request.user.is_superuser %}
|
||||
/ <a href="{% url 'longs:user_answer_table_view' %}" title="User answers">Answers</a>
|
||||
{% endif %}
|
||||
{% comment %} </br>
|
||||
Questions by:
|
||||
<span id="authors-link"><a href="{% url 'longs:author_list' %}">author</a></span> {% endcomment %}
|
||||
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
{% extends "longs/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Add Exam</h2>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
{{ form.as_table }}
|
||||
</table>
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -4,22 +4,22 @@
|
||||
<h1>Examinations</h1>
|
||||
<div class="longs">
|
||||
Active exams:<br/>
|
||||
<ul>
|
||||
<ul class="exam-list">
|
||||
{% for exam in exams %}
|
||||
{% if exam.active %}
|
||||
<li>
|
||||
<a href="{% url 'longs:exam_overview' pk=exam.pk %}">{{exam.name}}</a> <a href="{% url 'longs:mark_overview' pk=exam.pk %}">(mark)</a> <a href="{% url 'longs:exam_scores_cid' pk=exam.pk %}">(scores)</a> [Results are {% if not exam.publish_results %} not {% endif %}published]
|
||||
<a href="{% url 'longs:exam_overview' pk=exam.pk %}" class="flex-col">{{exam.name}}</a> <a href="{% url 'longs:mark_overview' pk=exam.pk %}" class="flex-col">Mark</a><a href="{% url 'longs:exam_scores_cid' pk=exam.pk %}" class="flex-col">Scores</a> <span class="flex-col icon-container"><span class="published-icon {% if exam.publish_results %}published{% endif %}">Results Published</span></span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
Inactive exams:<br/>
|
||||
<ul>
|
||||
<ul class="exam-list">
|
||||
{% for exam in exams %}
|
||||
{% if not exam.active %}
|
||||
<li>
|
||||
<a href="{% url 'longs:exam_overview' pk=exam.pk %}">{{exam.name}}</a> <a href="{% url 'longs:mark_overview' pk=exam.pk %}">(mark)</a> <a href="{% url 'longs:exam_scores_cid' pk=exam.pk %}">(scores)</a> [Results are {% if not exam.publish_results %} not {% endif %}published]
|
||||
<a href="{% url 'longs:exam_overview' pk=exam.pk %}" class="flex-col">{{exam.name}}</a> <a href="{% url 'longs:mark_overview' pk=exam.pk %}" class="flex-col">Mark</a><a href="{% url 'longs:exam_scores_cid' pk=exam.pk %}" class="flex-col">Scores</a> <span class="flex-col icon-container"><span class="published-icon {% if exam.publish_results %}published{% endif %}">Results Published</span></span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
{% load thumbnail %}
|
||||
<div class="longs">
|
||||
<a href="{% url 'longs:exam_update' exam.id %}" title="Edit the Exam">Edit</a>
|
||||
<a href="{% url 'longs:exam_delete' exam.id %}" title="Delete the Exam">Delete</a>
|
||||
<a href="{% url 'admin:longs_exam_change' exam.id %}" title="Edit the Exam using the admin interface">Admin Edit</a>
|
||||
<h1>Exam: {{ exam.name }}</h1>
|
||||
This exam has {{question_number}} questions. Time limit: {{exam.time_limit}} seconds.<br />
|
||||
|
||||
@@ -13,44 +13,9 @@
|
||||
<input class="btn btn-primary btn-sm mt-1 mb-1" type="submit" />
|
||||
</form>
|
||||
</div>
|
||||
<button id="delete-all-button">Delete all</button>
|
||||
{% render_table table %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
//$("thead input:checked").each((n, el) => {
|
||||
|
||||
answer_ids = [];
|
||||
$("tbody input:checked").each((n, el) => {
|
||||
answer_ids.push(el.value);
|
||||
})
|
||||
$("#delete-all-button").on("click", function () {
|
||||
$.ajax({
|
||||
url: "{% url 'longs:user_answer_delete_multiple' %}",
|
||||
data: {
|
||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||
answer_ids: JSON.stringify(answer_ids) // true if checked else false
|
||||
},
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
})
|
||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||
.done(function (data) {
|
||||
console.log(data);
|
||||
|
||||
if (data.status == "success") {
|
||||
toastr.info('Deleted.')
|
||||
}
|
||||
// show some message according to the response.
|
||||
// For eg. A message box showing that the status has been changed
|
||||
})
|
||||
.always(function () {
|
||||
console.log('[Done]');
|
||||
})
|
||||
})
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,61 @@
|
||||
{% extends 'longs/base.html' %}
|
||||
|
||||
{% load render_table from django_tables2 %}
|
||||
{% block css %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div id="view-filter-options">
|
||||
<h3>Filter User Answers</h3>
|
||||
<form action="" method="get">
|
||||
{{ filter.form }}
|
||||
<input class="btn btn-primary btn-sm mt-1 mb-1" type="submit" />
|
||||
</form>
|
||||
</div>
|
||||
{% render_table table %}
|
||||
<button id="delete-selected-button">Delete selected answers</button>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$("table thead th input").click((e) => {
|
||||
let status = e.currentTarget.checked; console.log(status, $("table tbody input")); $("table tbody input").prop("checked", status);
|
||||
})
|
||||
//$("thead input:checked").each((n, el) => {
|
||||
|
||||
$("#delete-selected-button").on("click", function () {
|
||||
answer_ids = [];
|
||||
$("tbody input:checked").each((n, el) => {
|
||||
answer_ids.push(el.value);
|
||||
})
|
||||
if (confirm(`Delete ${answer_ids.length} answers?`)) {
|
||||
$.ajax({
|
||||
url: "{% url 'longs:user_answer_delete_multiple' %}",
|
||||
data: {
|
||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||
answer_ids: JSON.stringify(answer_ids) // true if checked else false
|
||||
},
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
})
|
||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||
.done(function (data) {
|
||||
console.log(data);
|
||||
|
||||
if (data.status == "success") {
|
||||
toastr.info('Deleted.')
|
||||
}
|
||||
// show some message according to the response.
|
||||
// For eg. A message box showing that the status has been changed
|
||||
})
|
||||
.always(function () {
|
||||
console.log('[Done]');
|
||||
})
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
+98
-26
@@ -11,18 +11,50 @@ urlpatterns = [
|
||||
path("question/", views.LongView.as_view(), name="long_view"),
|
||||
path("series/", views.LongSeriesView.as_view(), name="long_series_view"),
|
||||
path("series/<int:pk>", views.long_series_detail, name="long_series_detail"),
|
||||
path("series/<int:pk>/order_dicom", views.long_series_order_dicom, name="long_series_order_dicom"),
|
||||
path("series/<int:pk>/download", views.SeriesImagesZipView.as_view(), name="long_series_download"),
|
||||
path("series/<int:pk>/order_dicom_instance", views.long_series_order_dicom_instance, name="long_series_order_dicom_instance"),
|
||||
path("series/<int:pk>/order_dicom_SeriesInstanceUID", views.long_series_order_dicom_SeriesInstanceUID, name="long_series_order_dicom_SeriesInstanceUID"),
|
||||
path("series/<int:pk>/order_upload_filename", views.long_series_order_upload_filename, name="long_series_order_upload_filename"),
|
||||
path("series/<int:pk>/delete", views.LongSeriesDelete.as_view(), name="long_series_delete"),
|
||||
path(
|
||||
"series/<int:pk>/order_dicom",
|
||||
views.long_series_order_dicom,
|
||||
name="long_series_order_dicom",
|
||||
),
|
||||
path(
|
||||
"series/<int:pk>/download",
|
||||
views.SeriesImagesZipView.as_view(),
|
||||
name="long_series_download",
|
||||
),
|
||||
path(
|
||||
"series/<int:pk>/order_dicom_instance",
|
||||
views.long_series_order_dicom_instance,
|
||||
name="long_series_order_dicom_instance",
|
||||
),
|
||||
path(
|
||||
"series/<int:pk>/order_dicom_SeriesInstanceUID",
|
||||
views.long_series_order_dicom_SeriesInstanceUID,
|
||||
name="long_series_order_dicom_SeriesInstanceUID",
|
||||
),
|
||||
path(
|
||||
"series/<int:pk>/order_upload_filename",
|
||||
views.long_series_order_upload_filename,
|
||||
name="long_series_order_upload_filename",
|
||||
),
|
||||
path(
|
||||
"series/<int:pk>/delete",
|
||||
views.LongSeriesDelete.as_view(),
|
||||
name="long_series_delete",
|
||||
),
|
||||
# path("unchecked/", views.unchecked_list, name="unchecked_list"),
|
||||
# path("verified/<int:pk>/", views.verified_detail, name="verified_detail"),
|
||||
path("question/<int:pk>/", views.question_detail, name="question_detail"),
|
||||
path("question/<int:pk>/json", views.question_json, name="question_json"),
|
||||
path("question/<int:pk>/json/unbased", views.question_json_unbased, name="question_json_unbased"),
|
||||
path("question/<int:pk>/json/recreate", views.question_json_recreate, name="question_json_recreate"),
|
||||
path(
|
||||
"question/<int:pk>/json/unbased",
|
||||
views.question_json_unbased,
|
||||
name="question_json_unbased",
|
||||
),
|
||||
path(
|
||||
"question/<int:pk>/json/recreate",
|
||||
views.question_json_recreate,
|
||||
name="question_json_recreate",
|
||||
),
|
||||
path("question/<int:pk>/split", views.long_split, name="long_split"),
|
||||
path("question/<int:pk>/clone", views.LongClone.as_view(), name="long_clone"),
|
||||
# path("verified/", views.verified, name="verified"),
|
||||
@@ -39,8 +71,16 @@ urlpatterns = [
|
||||
name="exam_question_detail",
|
||||
),
|
||||
path("exam/<int:pk>/", views.LongExamViews.exam_overview, name="exam_overview"),
|
||||
path("exam/<int:pk>/json_edit", views.LongExamViews.exam_json_edit, name="exam_json_edit"),
|
||||
path("exam/<int:pk>/question_json_refresh", views.refresh_exam_question_json, name="refresh_exam_question_json"),
|
||||
path(
|
||||
"exam/<int:pk>/json_edit",
|
||||
views.LongExamViews.exam_json_edit,
|
||||
name="exam_json_edit",
|
||||
),
|
||||
path(
|
||||
"exam/<int:pk>/question_json_refresh",
|
||||
views.refresh_exam_question_json,
|
||||
name="refresh_exam_question_json",
|
||||
),
|
||||
path("exam/<int:pk>/scores", views.exam_scores_cid, name="exam_scores_cid"),
|
||||
path(
|
||||
"exam/<int:pk>/scores/<int:sk>/",
|
||||
@@ -57,13 +97,31 @@ urlpatterns = [
|
||||
views.LongExamViews.exam_toggle_results_published,
|
||||
name="exam_toggle_results_published",
|
||||
),
|
||||
path("exam/submit", views.LongExamViews.postExamAnswers, name="exam_answers_submit"),
|
||||
path(
|
||||
"exam/submit", views.LongExamViews.postExamAnswers, name="exam_answers_submit"
|
||||
),
|
||||
path("exam/", views.LongExamViews.exam_list, name="exam_list"),
|
||||
path("exam/all", views.LongExamViews.exam_list_all, name="exam_list_all"),
|
||||
path("exam/create", views.ExamCreate.as_view(), name="exam_create"),
|
||||
path("exam/<int:pk>/update", views.ExamUpdate.as_view(), name="exam_update"),
|
||||
path("exam/<int:pk>/delete", views.ExamDelete.as_view(), name="exam_delete"),
|
||||
path("exam/json/", views.LongExamViews.active_exams, name="active_exams"),
|
||||
path("exam/json/<int:pk>", views.LongExamViews.exam_json, name="exam_json"),
|
||||
path("exam/json/<int:pk>/unbased", views.LongExamViews.exam_json_unbased, name="exam_json_unbased"),
|
||||
path("exam/json/<int:pk>/<int:sk>", views.LongExamViews.exam_question_json, name="exam_question_json"),
|
||||
path("exam/json/<int:pk>/unbased/<int:sk>", views.LongExamViews.exam_question_json_unbased, name="exam_question_json_unbased"),
|
||||
path(
|
||||
"exam/json/<int:pk>/unbased",
|
||||
views.LongExamViews.exam_json_unbased,
|
||||
name="exam_json_unbased",
|
||||
),
|
||||
path(
|
||||
"exam/json/<int:pk>/<int:sk>",
|
||||
views.LongExamViews.exam_question_json,
|
||||
name="exam_question_json",
|
||||
),
|
||||
path(
|
||||
"exam/json/<int:pk>/unbased/<int:sk>",
|
||||
views.LongExamViews.exam_question_json_unbased,
|
||||
name="exam_question_json_unbased",
|
||||
),
|
||||
path(
|
||||
"exam/json/<int:pk>/recreate",
|
||||
views.LongExamViews.exam_json_recreate,
|
||||
@@ -81,14 +139,14 @@ urlpatterns = [
|
||||
views.LongCreationDefaultView.as_view(),
|
||||
name="long_create_defaults",
|
||||
),
|
||||
# path("region/create/", views.create_region, name="create_region"),
|
||||
# path("region/ajax/get_region_id", views.get_region_id, name="get_region_id"),
|
||||
# path("abnormality/create/", views.create_abnormality, name="create_abnormality"),
|
||||
# path(
|
||||
# "abnormality/ajax/get_abnormality_id",
|
||||
# views.get_abnormality_id,
|
||||
# name="get_abnormality_id",
|
||||
# ),
|
||||
# path("region/create/", views.create_region, name="create_region"),
|
||||
# path("region/ajax/get_region_id", views.get_region_id, name="get_region_id"),
|
||||
# path("abnormality/create/", views.create_abnormality, name="create_abnormality"),
|
||||
# path(
|
||||
# "abnormality/ajax/get_abnormality_id",
|
||||
# views.get_abnormality_id,
|
||||
# name="get_abnormality_id",
|
||||
# ),
|
||||
path("examination/create/", views.create_examination, name="create_examination"),
|
||||
path(
|
||||
"examination/ajax/get_examination_id",
|
||||
@@ -106,8 +164,22 @@ urlpatterns = [
|
||||
name="long_series_update",
|
||||
),
|
||||
path("<int:pk>/add_note", views.AddNote.as_view(), name="long_add_note"),
|
||||
path("user_answers/", views.UserAnswerTableView.as_view(), name="user_answer_table_view"),
|
||||
path("user_answers/<int:pk>", views.UserAnswerView.as_view(), name="user_answer_view"),
|
||||
path("user_answers/<int:pk>/delete", views.UserAnswerDelete.as_view(), name="user_answer_delete"),
|
||||
path("user_answers/delete", views.user_answer_delete_multiple, name="user_answer_delete_multiple"),
|
||||
path(
|
||||
"user_answers/",
|
||||
views.UserAnswerTableView.as_view(),
|
||||
name="user_answer_table_view",
|
||||
),
|
||||
path(
|
||||
"user_answers/<int:pk>", views.UserAnswerView.as_view(), name="user_answer_view"
|
||||
),
|
||||
path(
|
||||
"user_answers/<int:pk>/delete",
|
||||
views.UserAnswerDelete.as_view(),
|
||||
name="user_answer_delete",
|
||||
),
|
||||
path(
|
||||
"user_answers/delete",
|
||||
views.user_answer_delete_multiple,
|
||||
name="user_answer_delete_multiple",
|
||||
),
|
||||
]
|
||||
|
||||
+36
-3
@@ -27,6 +27,7 @@ from .forms import (
|
||||
SeriesFormSet,
|
||||
NoteForm,
|
||||
ExaminationForm,
|
||||
ExamForm,
|
||||
)
|
||||
from .models import (
|
||||
Long, LongSeries,
|
||||
@@ -955,6 +956,38 @@ def long_series_order_upload_filename(request, pk):
|
||||
|
||||
LongExamViews = ExamViews(Exam, Long, "longs", "long", loadJsonAnswer)
|
||||
|
||||
|
||||
class ExamCreate(RevisionMixin, LoginRequiredMixin, CreateView):
|
||||
model = Exam
|
||||
form_class = ExamForm
|
||||
|
||||
def form_valid(self, form):
|
||||
self.object = form.save(commit=False)
|
||||
self.object.save()
|
||||
|
||||
form.instance.author.add(self.request.user.id)
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class ExamUpdate(
|
||||
RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView
|
||||
):
|
||||
model = Exam
|
||||
form_class = ExamForm
|
||||
|
||||
def form_valid(self, form):
|
||||
self.object = form.save(commit=False)
|
||||
self.object.save()
|
||||
|
||||
form.instance.author.add(self.request.user.id)
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
|
||||
model = Exam
|
||||
success_url = reverse_lazy("longs:index")
|
||||
|
||||
|
||||
class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
|
||||
queryset = Exam.objects.all().order_by('name')
|
||||
serializer_class = ExamSerializer
|
||||
@@ -1014,7 +1047,7 @@ class UserAnswerView(LoginRequiredMixin, DetailView):
|
||||
class UserAnswerTableView(LoginRequiredMixin, SingleTableMixin, FilterView):
|
||||
model = CidUserAnswer
|
||||
table_class = UserAnswerTable
|
||||
template_name = "longs/question_view.html"
|
||||
template_name = "longs/user_answer_question_view.html"
|
||||
|
||||
filterset_class = UserAnswerFilter
|
||||
|
||||
@@ -1031,8 +1064,8 @@ def user_answer_delete_multiple(request):
|
||||
# We could probably delete them all at once....
|
||||
for id in answer_ids:
|
||||
CidUserAnswer.objects.get(pk=id).delete()
|
||||
return HttpResponseRedirect(request.path_info)
|
||||
return HttpResponse("/")
|
||||
return JsonResponse({"status": "success"})
|
||||
return JsonResponse({"status": "error"})
|
||||
|
||||
|
||||
@user_passes_test(lambda u: u.is_superuser)
|
||||
|
||||
Reference in New Issue
Block a user