start taking more advantage of class based views

This commit is contained in:
Ross
2024-03-11 11:08:35 +00:00
parent 78e4ed871e
commit 0cb38f5c31
27 changed files with 244 additions and 104 deletions
+6
View File
@@ -127,6 +127,9 @@ class AnatomyQuestion(QuestionBase):
class Meta: class Meta:
permissions = () permissions = ()
def get_app_name(self):
return "anatomy"
def __str__(self): def __str__(self):
# Get first answer # Get first answer
return "{}/{}: {} [{}, {}]".format( return "{}/{}: {} [{}, {}]".format(
@@ -410,6 +413,9 @@ class Exam(ExamBase):
examcollection = models.ForeignKey(ExamCollection, blank=True, null=True, on_delete=models.SET_NULL, related_name="anatomy_exams") examcollection = models.ForeignKey(ExamCollection, blank=True, null=True, on_delete=models.SET_NULL, related_name="anatomy_exams")
def get_app_name(self):
return "anatomy"
def get_exam_json(self, based=True): def get_exam_json(self, based=True):
questions = self.get_questions() questions = self.get_questions()
+5 -2
View File
@@ -1,11 +1,14 @@
{% extends 'anatomy/exams.html' %} {% extends 'anatomy/exams.html' %}
{% block navigation %}
{{ block.super }}
{% include 'generic/exam_overview_headers.html' %}
{% endblock navigation %}
{% block content %} {% block content %}
{% load thumbnail %} {% load thumbnail %}
<div class="anatomy"> <div class="anatomy">
{% include 'generic/exam_overview_headers.html' %}
<ol id="full-question-list" class="sortable"> <ol id="full-question-list" class="sortable">
{% for question in questions %} {% for question in questions %}
+11 -3
View File
@@ -71,7 +71,7 @@ from helpers.images import image_as_base64
from django.template.defaulttags import register from django.template.defaulttags import register
from generic.mixins import SuperuserRequiredMixin from generic.mixins import CheckCanEditMixin, SuperuserRequiredMixin
from rest_framework import viewsets, permissions from rest_framework import viewsets, permissions
@@ -805,10 +805,16 @@ class ExamCreate(ExamCreateBase):
form_class = ExamForm form_class = ExamForm
class ExamUpdate(ExamUpdateBase, AuthorOrCheckerRequiredMixin): class ExamUpdate(CheckCanEditMixin, ExamUpdateBase, AuthorOrCheckerRequiredMixin):
model = Exam model = Exam
form_class = ExamForm form_class = ExamForm
#def get_context_data(self, **kwargs):
# context = super().get_context_data(**kwargs)
# context["can_edit"] = self.object.check_user_can_edit(self.request.user)
# return context
class ExamDelete(AuthorOrCheckerRequiredMixin, ExamDeleteBase): class ExamDelete(AuthorOrCheckerRequiredMixin, ExamDeleteBase):
model = Exam model = Exam
@@ -831,10 +837,12 @@ class ExamDelete(AuthorOrCheckerRequiredMixin, ExamDeleteBase):
# return Exam.objects.filter(author__id=user.id) # return Exam.objects.filter(author__id=user.id)
class ExamAuthorUpdate(RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView): class ExamAuthorUpdate(RevisionMixin, CheckCanEditMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView):
model = Exam model = Exam
form_class = ExamAuthorForm form_class = ExamAuthorForm
template_name = "author_form.html"
GenericViews = GenericViewBase("anatomy", AnatomyQuestion, UserAnswer, Exam) GenericViews = GenericViewBase("anatomy", AnatomyQuestion, UserAnswer, Exam)
+15 -2
View File
@@ -5,7 +5,7 @@ import pathlib
from typing import Tuple from typing import Tuple
from django.http import Http404, HttpRequest from django.http import Http404, HttpRequest
from generic.mixins import AuthorMixin from generic.mixins import AuthorMixin, QuestionMixin
from rad.settings import REMOTE_URL from rad.settings import REMOTE_URL
from django.db.models.fields.files import ImageField from django.db.models.fields.files import ImageField
from django.db.models.fields.related import ForeignKey from django.db.models.fields.related import ForeignKey
@@ -273,7 +273,7 @@ class Structure(SynMixin, models.Model):
@reversion.register @reversion.register
class Case(models.Model, AuthorMixin): class Case(models.Model, AuthorMixin, QuestionMixin):
# class SubspecialtyChoices(models.TextChoices): # class SubspecialtyChoices(models.TextChoices):
# BREAST = "BR", _("Breast") # BREAST = "BR", _("Breast")
# CARDIAC = "CA", _("Cardiac") # CARDIAC = "CA", _("Cardiac")
@@ -364,6 +364,9 @@ class Case(models.Model, AuthorMixin):
blank=True, blank=True,
) )
def get_app_name(self):
return "atlas"
# question_file = models.FileField(upload_to=question_file_directory_path, blank=True, null=True) # question_file = models.FileField(upload_to=question_file_directory_path, blank=True, null=True)
def get_absolute_url(self): def get_absolute_url(self):
@@ -373,6 +376,10 @@ class Case(models.Model, AuthorMixin):
# return f"{self.pk}: {self.title}" # return f"{self.pk}: {self.title}"
return format_html("<a href='{}'>{}</a>", self.get_absolute_url(), str(self)) return format_html("<a href='{}'>{}</a>", self.get_absolute_url(), str(self))
#def get_base_template(self):
#def get_edit_template_links(self):
def __str__(self): def __str__(self):
return f"{self.pk}: {self.title}" return f"{self.pk}: {self.title}"
@@ -773,6 +780,12 @@ class CaseCollection(ExamOrCollectionGenericBase):
default=VIEWER_MODE_CHOICES.BUILT_IN, default=VIEWER_MODE_CHOICES.BUILT_IN,
) )
def get_app_name(self):
return "atlas"
def get_link_headers(self):
return f"{self.get_app_name()}/collection_headers.html"
def get_absolute_url(self): def get_absolute_url(self):
return reverse("atlas:collection_detail", kwargs={"pk": self.pk}) return reverse("atlas:collection_detail", kwargs={"pk": self.pk})
+1 -27
View File
@@ -3,33 +3,7 @@
{% block content %} {% block content %}
{% include 'atlas/question_link_header.html' %}
<div class="floating-header">
<a href="{% url 'atlas:case_update' pk=case.pk %}" title="Edit the Case">Edit</a>
<a href="{% url 'atlas:case_clone' pk=case.pk %}"
title="Clone the Case (duplicate everything but the images)">Clone</a>
<a href="{% url 'atlas:case_delete' pk=case.pk %}" title="Delete the Case">Delete</a>
<a href="#"
onclick="return window.create_popup_window('{% url 'feedback_create' question_type='atlas' pk=case.pk %}')"> Add
Note</a>
<a href="{% url 'atlas:case_authors' case.pk %}" title="Edit Authors">Authors</a>
{% if request.user.is_superuser %}
<a href="{% url 'admin:atlas_case_change' case.id %}" title="Edit the Case using the admin interface">Admin Edit</a>
{% endif %}
{% if collection %}
<div>
{% if previous %}
<a href="{% url 'atlas:collection_case_view' collection.id case_number|add:-1 %}">Previous question</a>
{% endif %}
Viewing question as part of collection: <a href="{% url 'atlas:collection_detail' collection.id %}">{{collection.name}}</a> [{{case_number|add:1}}/{{collection_length}}]
{% if next %}
<a href="{% url 'atlas:collection_case_view' collection.id case_number|add:1 %}">Next question</a>
{% endif %}
</div>
{% endif %}
</div>
{% include 'atlas/case_display_block.html' %} {% include 'atlas/case_display_block.html' %}
+4
View File
@@ -49,6 +49,10 @@
<!-- {{ form.media }} --> <!-- {{ form.media }} -->
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if object %}
{% include "atlas/question_link_header.html" %}
{% endif %}
<h2>Submit Case</h2> <h2>Submit Case</h2>
Use this form to create a atlas case. Existing associated image sets can be added using this form. Use this form to create a atlas case. Existing associated image sets can be added using this form.
@@ -1,15 +1,14 @@
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<br/>Collection: {{collection.name}}-> <a href="{% url 'atlas:collection_detail' pk=collection.pk %}">Overview</a> / <br/>Collection: {{collection.name}}-> <a href="{% url 'atlas:collection_detail' pk=collection.pk %}">Overview</a> /
<a href="{% url 'atlas:collection_mark_overview' collection.pk %}">Mark</a> / <a href="{% url 'atlas:collection_mark_overview' collection.pk %}">Mark</a> /
<a href="{% url 'atlas:collection_scores_cid' collection.pk %}">Scores</a> / <a href="{% url 'atlas:collection_scores_cid' collection.pk %}">Scores</a> /
<a href="{% url 'atlas:exam_cids' collection.pk %}">Candidates</a> / <a href="{% url 'atlas:exam_cids' collection.pk %}">Candidates</a> /
<a href="{% url 'atlas:atlas_create_exam' pk=collection.pk %}">Add New Case</a> / <a href="{% url 'atlas:atlas_create_exam' pk=collection.pk %}">Add New Case</a> /
<a href="{% url 'atlas:collection_authors' collection.pk %}" title="Edit Authors">Authors</a>
<div class="floating-header"> <div class="floating-header">
<a href="{% url 'atlas:exam_update' collection.id %}" title="Edit the Collection">Edit</a> <a href="{% url 'atlas:exam_update' collection.id %}" title="Edit the Collection">Edit</a>
\ <a href="{% url 'atlas:exam_deleted' collection.id %}" title="Delete the Collection">Delete</a> \ <a href="{% url 'atlas:exam_deleted' collection.id %}" title="Delete the Collection">Delete</a>
\ <a href="{% url 'atlas:exam_clone' collection.id %}" title="Clone the Collection">Clone</a> \ <a href="{% url 'atlas:exam_clone' collection.id %}" title="Clone the Collection">Clone</a>
\ <a href="{% url 'atlas:collection_authors' collection.pk %}" title="Edit Authors">Authors</a>
{% if request.user.is_superuser %} {% if request.user.is_superuser %}
\ <a href="{% url 'admin:atlas_casecollection_change' collection.id %}" title="Edit the Collection using the admin interface">Admin Edit</a> \ <a href="{% url 'admin:atlas_casecollection_change' collection.id %}" title="Edit the Collection using the admin interface">Admin Edit</a>
@@ -0,0 +1,26 @@
<div class="floating-header">
<a href="{% url 'atlas:case_update' pk=case.pk %}" title="Edit the Case">Edit</a>
<a href="{% url 'atlas:case_clone' pk=case.pk %}"
title="Clone the Case (duplicate everything but the images)">Clone</a>
<a href="{% url 'atlas:case_delete' pk=case.pk %}" title="Delete the Case">Delete</a>
<a href="#"
onclick="return window.create_popup_window('{% url 'feedback_create' question_type='atlas' pk=case.pk %}')"> Add
Note</a>
<a href="{% url 'atlas:case_authors' case.pk %}" title="Edit Authors">Authors</a>
{% if request.user.is_superuser %}
<a href="{% url 'admin:atlas_case_change' case.id %}" title="Edit the Case using the admin interface">Admin Edit</a>
{% endif %}
{% if collection %}
<div>
{% if previous %}
<a href="{% url 'atlas:collection_case_view' collection.id case_number|add:-1 %}">Previous question</a>
{% endif %}
Viewing question as part of collection: <a href="{% url 'atlas:collection_detail' collection.id %}">{{collection.name}}</a> [{{case_number|add:1}}/{{collection_length}}]
{% if next %}
<a href="{% url 'atlas:collection_case_view' collection.id case_number|add:1 %}">Next question</a>
{% endif %}
</div>
{% endif %}
</div>
+10
View File
@@ -2247,7 +2247,17 @@ class CaseCollectionAuthorUpdate(RevisionMixin, AuthorRequiredMixin, UpdateView)
form_class = CaseCollectionAuthorForm form_class = CaseCollectionAuthorForm
template_name = "author_form.html" template_name = "author_form.html"
def get_context_data(self, **kwargs):
context = super(CaseCollectionAuthorUpdate, self).get_context_data(**kwargs)
context["collection"] = context["object"]
return context
class CaseAuthorUpdate(RevisionMixin, AuthorRequiredMixin, UpdateView): class CaseAuthorUpdate(RevisionMixin, AuthorRequiredMixin, UpdateView):
model = Case model = Case
form_class = CaseAuthorForm form_class = CaseAuthorForm
template_name = "author_form.html" template_name = "author_form.html"
def get_context_data(self, **kwargs):
context = super(CaseAuthorUpdate, self).get_context_data(**kwargs)
context["collection"] = context["object"]
return context
+23
View File
@@ -8,6 +8,29 @@ class SuperuserRequiredMixin(UserPassesTestMixin):
def test_func(self): def test_func(self):
return self.request.user.is_superuser return self.request.user.is_superuser
class CheckCanEditMixin():
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
print(self.object)
print("1234", self.object.check_user_can_edit(self.request.user))
context["can_edit"] = self.object.check_user_can_edit(self.request.user)
return context
class QuestionMixin():
def get_app_name(self):
raise NotImplementedError("You must implement get_app_name in the derived class")
def get_base_template(self):
return f"{self.get_app_name()}/base.html"
def get_link_headers(self):
return f"{self.get_app_name()}/question_link_header.html"
class AuthorMixin(): class AuthorMixin():
"""Mixin class for models that have authors """Mixin class for models that have authors
+11 -2
View File
@@ -26,7 +26,7 @@ from django.contrib.auth.models import User
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from generic.mixins import AuthorMixin from generic.mixins import AuthorMixin, QuestionMixin
from helpers.images import get_image_hash, pretty_print_dicom from helpers.images import get_image_hash, pretty_print_dicom
from rad.settings import REMOTE_URL from rad.settings import REMOTE_URL
from django.utils.html import format_html from django.utils.html import format_html
@@ -154,7 +154,7 @@ class Site(models.Model):
return self.short_code return self.short_code
class QuestionBase(models.Model, AuthorMixin): class QuestionBase(models.Model, AuthorMixin, QuestionMixin):
authors_only = models.BooleanField( authors_only = models.BooleanField(
help_text="If true only question authors will be able to view.", help_text="If true only question authors will be able to view.",
default=False, default=False,
@@ -580,6 +580,15 @@ class ExamOrCollectionGenericBase(models.Model, AuthorMixin):
class Meta: class Meta:
abstract = True abstract = True
def get_app_name(self):
raise NotImplementedError("You must implement get_app_name in the derived class")
def get_base_template(self):
return f"{self.get_app_name()}/exams.html"
def get_link_headers(self):
return "generic/exam_link_headers.html"
def check_user_can_edit(self, user: User): def check_user_can_edit(self, user: User):
if user.is_superuser: if user.is_superuser:
return True return True
@@ -0,0 +1,10 @@
<br/>
{% if can_edit %}
<a href="{% url exam.get_app_name|add:':exam_update' exam.id %}" title="Edit the Exam">Edit</a>
\ <a href="{% url exam.get_app_name|add:':exam_delete' exam.id %}" title="Delete the Exam">Delete</a>
\ <a href="{% url exam.get_app_name|add:':exam_clone' exam.id %}" title="Clone the Exam">Clone</a>
\ <a href="{% url exam.get_app_name|add:':exam_authors' exam.id %}" title="Edit Exam Authors">Authors</a>
{% endif %}
{% if request.user.is_superuser %}
\ <a href="{% url 'admin:'|add:exam.get_app_name|add:'_exam_change' exam.id %}" title="Edit the Exam using the admin interface">Admin Edit</a>
{% endif %}
@@ -1,12 +1,4 @@
{% if can_edit %} {% include "generic/exam_link_headers.html" %}
<a href="{% url app_name|add:':exam_update' exam.id %}" title="Edit the Exam">Edit</a>
\ <a href="{% url app_name|add:':exam_delete' exam.id %}" title="Delete the Exam">Delete</a>
\ <a href="{% url app_name|add:':exam_clone' exam.id %}" title="Clone the Exam">Clone</a>
\ <a href="{% url app_name|add:':exam_authors' exam.id %}" title="Edit Exam Authors">Authors</a>
{% endif %}
{% if request.user.is_superuser %}
\ <a href="{% url 'admin:'|add:app_name|add:'_exam_change' exam.id %}" title="Edit the Exam using the admin interface">Admin Edit</a>
{% endif %}
<h1>Exam: {{ exam.name }}</h1> <h1>Exam: {{ exam.name }}</h1>
{% include 'exam_notes.html' %} {% include 'exam_notes.html' %}
@@ -15,7 +7,7 @@
<div class="alert alert-warning" role="alert"> <div class="alert alert-warning" role="alert">
Exam JSON may be out of date. Exam JSON may be out of date.
<a href="{% url app_name|add:':exam_json_recreate' pk=exam.pk %}">Click here to force refresh</a> <a href="{% url exam.get_app_name|add:':exam_json_recreate' pk=exam.pk %}">Click here to force refresh</a>
</div> </div>
{% endif %} {% endif %}
@@ -44,15 +36,15 @@ Exam mode: {{ exam.exam_mode }}<br />
Open access: {{ exam.open_access }}<br /> Open access: {{ exam.open_access }}<br />
<div class="parent-help" title="Click to enable / disable the exam"> <div class="parent-help" title="Click to enable / disable the exam">
Exam active: <input type="checkbox" id="exam-active-switch" {% if exam.active %}checked{% endif %} data-posturl="{% url app_name|add:':exam_toggle_active' pk=exam.pk %}"> <span class="help-text">[When checked the exam will be available to take in the test system]</span> Exam active: <input type="checkbox" id="exam-active-switch" {% if exam.active %}checked{% endif %} data-posturl="{% url exam.get_app_name|add:':exam_toggle_active' pk=exam.pk %}"> <span class="help-text">[When checked the exam will be available to take in the test system]</span>
</div> </div>
{% if exam.exam_mode %} {% if exam.exam_mode %}
<div class="parent-help" title="Click to enable / disable the exam results"> <div class="parent-help" title="Click to enable / disable the exam results">
Publish results: <input type="checkbox" id="exam-publish-results-switch" data-posturl="{% url app_name|add:':exam_toggle_results_published' pk=exam.pk %}" Publish results: <input type="checkbox" id="exam-publish-results-switch" data-posturl="{% url exam.get_app_name|add:':exam_toggle_results_published' pk=exam.pk %}"
{% if exam.publish_results %}checked{% endif %}> <span class="help-text">[When checked the exam results will be available to users on this site]</span> {% if exam.publish_results %}checked{% endif %}> <span class="help-text">[When checked the exam results will be available to users on this site]</span>
</div> </div>
{% if app_name == "anatomy" or app_name == "rapids" or app_name == "longs" %} {% if exam.get_app_name == "anatomy" or exam.get_app_name == "rapids" or exam.get_app_name == "longs" %}
<p><a href="{% url app_name|add:':mark_overview' pk=exam.pk %}"><button>Mark exam</button></a></p> <p><a href="{% url exam.get_app_name|add:':mark_overview' pk=exam.pk %}"><button>Mark exam</button></a></p>
{% endif %} {% endif %}
{% endif %} {% endif %}
+6
View File
@@ -131,6 +131,9 @@ class Long(QuestionBase):
# question_file = models.FileField(upload_to=question_file_directory_path, blank=True, null=True) # question_file = models.FileField(upload_to=question_file_directory_path, blank=True, null=True)
def get_app_name(self):
return "longs"
def get_absolute_url(self): def get_absolute_url(self):
return reverse("longs:question_detail", kwargs={"pk": self.pk}) return reverse("longs:question_detail", kwargs={"pk": self.pk})
@@ -472,6 +475,9 @@ class Exam(ExamBase):
examcollection = models.ForeignKey(ExamCollection, blank=True, null=True, on_delete=models.SET_NULL, related_name="longs_exams") examcollection = models.ForeignKey(ExamCollection, blank=True, null=True, on_delete=models.SET_NULL, related_name="longs_exams")
def get_app_name(self):
return "longs"
def get_exam_question_json(self, question_id): def get_exam_question_json(self, question_id):
q = get_object_or_404(Long, pk=question_id) q = get_object_or_404(Long, pk=question_id)
+5 -1
View File
@@ -1,10 +1,14 @@
{% extends 'longs/exams.html' %} {% extends 'longs/exams.html' %}
{% block navigation %}
{{ block.super }}
{% include 'generic/exam_overview_headers.html' %}
{% endblock navigation %}
{% block content %} {% block content %}
{% load thumbnail %} {% load thumbnail %}
<div class="longs"> <div class="longs">
{% include 'generic/exam_overview_headers.html' %}
<ol id="full-question-list" class="sortable"> <ol id="full-question-list" class="sortable">
+4 -3
View File
@@ -13,7 +13,7 @@ from django.core.exceptions import (
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
from generic.mixins import SuperuserRequiredMixin from generic.mixins import CheckCanEditMixin, SuperuserRequiredMixin
from django.views.generic.edit import CreateView, UpdateView, DeleteView from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.views.generic import ListView from django.views.generic import ListView
@@ -1130,7 +1130,7 @@ class ExamCreate(ExamCreateBase):
form_class = ExamForm form_class = ExamForm
class ExamUpdate(ExamUpdateBase, AuthorOrCheckerRequiredMixin): class ExamUpdate(CheckCanEditMixin, ExamUpdateBase, AuthorOrCheckerRequiredMixin):
model = Exam model = Exam
form_class = ExamForm form_class = ExamForm
@@ -1142,9 +1142,10 @@ class ExamClone(ExamCloneMixin, ExamCreate):
"""Clone exam view""" """Clone exam view"""
class ExamAuthorUpdate(RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView): class ExamAuthorUpdate(RevisionMixin, CheckCanEditMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView):
model = Exam model = Exam
form_class = ExamAuthorForm form_class = ExamAuthorForm
template_name = "author_form.html"
#class ExamViewSet(RevisionMixin, viewsets.ModelViewSet): #class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
+6
View File
@@ -93,6 +93,9 @@ class Question(QuestionBase):
#notes = GenericRelation(QuestionNote) #notes = GenericRelation(QuestionNote)
def get_app_name(self):
return "physics"
def __str__(self): def __str__(self):
return self.stem return self.stem
@@ -194,6 +197,9 @@ class Exam(ExamBase):
def get_take_url(self): def get_take_url(self):
return reverse("physics:exam_start", kwargs={"pk": self.pk}) return reverse("physics:exam_start", kwargs={"pk": self.pk})
def get_app_name(self):
return "physics"
@reversion.register @reversion.register
class UserAnswer(UserAnswerBase): class UserAnswer(UserAnswerBase):
+4 -1
View File
@@ -1,10 +1,13 @@
{% extends 'physics/exams.html' %} {% extends 'physics/exams.html' %}
{% block navigation %}
{{ block.super }}
{% include 'generic/exam_overview_headers.html' %}
{% endblock navigation %}
{% block content %} {% block content %}
{% load thumbnail %} {% load thumbnail %}
<div class="physics"> <div class="physics">
{% include 'generic/exam_overview_headers.html' %}
This exam will be available to take <a href="{% url 'physics:exam_start' pk=exam.pk %}">here</a> (when active). This exam will be available to take <a href="{% url 'physics:exam_start' pk=exam.pk %}">here</a> (when active).
+4 -3
View File
@@ -3,7 +3,7 @@ from django.utils import timezone
from generic.models import CidUser from generic.models import CidUser
from physics.decorators import user_is_author_or_physics_checker from physics.decorators import user_is_author_or_physics_checker
from physics.filters import QuestionFilter, UserAnswerFilter from physics.filters import QuestionFilter, UserAnswerFilter
from generic.mixins import SuperuserRequiredMixin from generic.mixins import CheckCanEditMixin, SuperuserRequiredMixin
from physics.tables import QuestionTable, UserAnswerTable from physics.tables import QuestionTable, UserAnswerTable
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
@@ -339,7 +339,7 @@ class ExamCreate(ExamCreateBase):
form_class = ExamForm form_class = ExamForm
class ExamUpdate(ExamUpdateBase, AuthorOrCheckerRequiredMixin): class ExamUpdate(CheckCanEditMixin, ExamUpdateBase, AuthorOrCheckerRequiredMixin):
model = Exam model = Exam
form_class = ExamForm form_class = ExamForm
@@ -353,10 +353,11 @@ class ExamClone(AuthorOrCheckerRequiredMixin, ExamCloneMixin, ExamCreate):
class ExamAuthorUpdate( class ExamAuthorUpdate(
RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView RevisionMixin, CheckCanEditMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView
): ):
model = Exam model = Exam
form_class = ExamAuthorForm form_class = ExamAuthorForm
template_name = "author_form.html"
class QuestionView( class QuestionView(
+7
View File
@@ -224,6 +224,9 @@ class Rapid(QuestionBase):
default=False, help_text="Question has been scrapped and will not be shown" default=False, help_text="Question has been scrapped and will not be shown"
) )
def get_app_name(self):
return "rapids"
def get_absolute_url(self): def get_absolute_url(self):
return reverse("rapids:question_detail", kwargs={"pk": self.pk}) return reverse("rapids:question_detail", kwargs={"pk": self.pk})
@@ -652,6 +655,10 @@ class Exam(ExamBase):
examcollection = models.ForeignKey(ExamCollection, blank=True, null=True, on_delete=models.SET_NULL, related_name="rapids_exams") examcollection = models.ForeignKey(ExamCollection, blank=True, null=True, on_delete=models.SET_NULL, related_name="rapids_exams")
def get_app_name(self):
return "rapids"
def get_normal_abnormal_breakdown(self): def get_normal_abnormal_breakdown(self):
# Inefficient but more extendible # Inefficient but more extendible
questions = self.exam_questions.all() questions = self.exam_questions.all()
+4 -1
View File
@@ -1,10 +1,13 @@
{% extends 'rapids/exams.html' %} {% extends 'rapids/exams.html' %}
{% block navigation %}
{{ block.super }}
{% include 'generic/exam_overview_headers.html' %}
{% endblock navigation %}
{% block content %} {% block content %}
{% load thumbnail %} {% load thumbnail %}
<div class="rapids"> <div class="rapids">
{% include 'generic/exam_overview_headers.html' %}
<a href="{% url 'rapids:mark_review' exam_pk=exam.pk sk=0 %}"><button>Review exam</button></a></p> <a href="{% url 'rapids:mark_review' exam_pk=exam.pk sk=0 %}"><button>Review exam</button></a></p>
+27 -20
View File
@@ -90,7 +90,7 @@ from .serializers import (
UserAnswerSerializer, UserAnswerSerializer,
) )
from generic.mixins import SuperuserRequiredMixin from generic.mixins import CheckCanEditMixin, SuperuserRequiredMixin
from dal import autocomplete from dal import autocomplete
@@ -213,12 +213,14 @@ def rapid_clone(request, pk):
new_item.pk = None # autogen a new pk (item_id) new_item.pk = None # autogen a new pk (item_id)
# new_item.name = "Copy of " + new_item.name #need to change uniques # new_item.name = "Copy of " + new_item.name #need to change uniques
if request.user not in new_item.get_author_objects() and not request.user.is_superuser: if (
request.user not in new_item.get_author_objects()
and not request.user.is_superuser
):
raise PermissionDenied() # or Http404 raise PermissionDenied() # or Http404
form = RapidForm(request.POST or None, instance=new_item) form = RapidForm(request.POST or None, instance=new_item)
image_formset = ImageFormSet() image_formset = ImageFormSet()
answer_formset = AnswerFormSet() answer_formset = AnswerFormSet()
@@ -240,7 +242,7 @@ def rapid_clone(request, pk):
context = { context = {
"form": form, "form": form,
"image_formset": image_formset, "image_formset": image_formset,
"answer_formset": answer_formset "answer_formset": answer_formset,
# other context # other context
} }
@@ -331,6 +333,7 @@ class RapidCreate(RapidCreateBase):
# form.instance.author.add(self.request.user.id) # form.instance.author.add(self.request.user.id)
# return super().form_valid(form) # return super().form_valid(form)
class RapidAnswerUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView): class RapidAnswerUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView):
model = Rapid model = Rapid
form_class = RapidQuestionAnswerForm form_class = RapidQuestionAnswerForm
@@ -353,7 +356,7 @@ class RapidAnswerUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView)
self.object = form.save(commit=False) self.object = form.save(commit=False)
self.object.save() self.object.save()
#form.instance.author.add(self.request.user.id) # form.instance.author.add(self.request.user.id)
context = self.get_context_data(form=form) context = self.get_context_data(form=form)
formset = context["answer_formset"] formset = context["answer_formset"]
@@ -367,9 +370,8 @@ class RapidAnswerUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView)
else: else:
return super().form_invalid(form) return super().form_invalid(form)
class RapidUpdate(
RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView class RapidUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView):
):
model = Rapid model = Rapid
form_class = RapidForm form_class = RapidForm
@@ -728,13 +730,14 @@ def mark(request, exam_pk, sk, unmarked_exam_answers_only=True, review=False):
) )
class QuestionDelete(AuthorOrCheckerRequiredMixin, DeleteView): class QuestionDelete(AuthorOrCheckerRequiredMixin, DeleteView):
model = Rapid model = Rapid
success_url = reverse_lazy("rapids:rapid_view") success_url = reverse_lazy("rapids:rapid_view")
GenericExamViews = ExamViews(Exam, Rapid, Answer, UserAnswer, "rapids", "rapid", normalise_score=normaliseScore) GenericExamViews = ExamViews(
Exam, Rapid, Answer, UserAnswer, "rapids", "rapid", normalise_score=normaliseScore
)
class ExamCreate(ExamCreateBase): class ExamCreate(ExamCreateBase):
@@ -742,7 +745,7 @@ class ExamCreate(ExamCreateBase):
form_class = ExamForm form_class = ExamForm
class ExamUpdate(ExamUpdateBase, AuthorOrCheckerRequiredMixin): class ExamUpdate(CheckCanEditMixin, ExamUpdateBase, AuthorOrCheckerRequiredMixin):
model = Exam model = Exam
form_class = ExamForm form_class = ExamForm
@@ -750,12 +753,17 @@ class ExamUpdate(ExamUpdateBase, AuthorOrCheckerRequiredMixin):
class ExamDelete(AuthorOrCheckerRequiredMixin, ExamDeleteBase): class ExamDelete(AuthorOrCheckerRequiredMixin, ExamDeleteBase):
model = Exam model = Exam
class ExamAuthorUpdate( class ExamAuthorUpdate(
RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView RevisionMixin,
CheckCanEditMixin,
LoginRequiredMixin,
AuthorRequiredMixin,
UpdateView,
): ):
model = Exam model = Exam
form_class = ExamAuthorForm form_class = ExamAuthorForm
template_name = "author_form.html"
class UserAnswerView(LoginRequiredMixin, DetailView): class UserAnswerView(LoginRequiredMixin, DetailView):
@@ -798,24 +806,24 @@ class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
""" """
user = self.request.user user = self.request.user
if user.groups.filter(name="rapid_checker").exists(): if user.groups.filter(name="rapid_checker").exists():
return Exam.objects.filter(archive=False).order_by('name') return Exam.objects.filter(archive=False).order_by("name")
return Exam.objects.filter(author__id=user.id, archive=False).order_by('name') return Exam.objects.filter(author__id=user.id, archive=False).order_by("name")
#class QuestionAnswerViewSet(viewsets.ModelViewSet): # class QuestionAnswerViewSet(viewsets.ModelViewSet):
# queryset = Answer.objects.all() # .order_by('name') # queryset = Answer.objects.all() # .order_by('name')
# serializer_class = QuestionAnswerSerializer # serializer_class = QuestionAnswerSerializer
# permission_classes = [permissions.IsAuthenticated] # permission_classes = [permissions.IsAuthenticated]
# #
# #
#class UserAnswerViewSet(viewsets.ModelViewSet): # class UserAnswerViewSet(viewsets.ModelViewSet):
# queryset = UserAnswer.objects.all() # .order_by('name') # queryset = UserAnswer.objects.all() # .order_by('name')
# serializer_class = UserAnswerSerializer # serializer_class = UserAnswerSerializer
# permission_classes = [permissions.IsAuthenticated] # permission_classes = [permissions.IsAuthenticated]
#class RapidViewSet( # class RapidViewSet(
# RevisionMixin, # RevisionMixin,
# mixins.CreateModelMixin, # mixins.CreateModelMixin,
# mixins.RetrieveModelMixin, # mixins.RetrieveModelMixin,
@@ -823,14 +831,13 @@ class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
# mixins.ListModelMixin, # mixins.ListModelMixin,
# mixins.DestroyModelMixin, # mixins.DestroyModelMixin,
# viewsets.GenericViewSet, # viewsets.GenericViewSet,
#): # ):
# queryset = Rapid.objects.all() # .order_by('name') # queryset = Rapid.objects.all() # .order_by('name')
# serializer_class = RapidSerializer # serializer_class = RapidSerializer
# permission_classes = [permissions.IsAuthenticated] # permission_classes = [permissions.IsAuthenticated]
# pagination_class = StandardResultsSetPagination # pagination_class = StandardResultsSetPagination
# class AddSuggestedAnswer(LoginRequiredMixin, CreateView): # class AddSuggestedAnswer(LoginRequiredMixin, CreateView):
# model = QuestionNote # model = QuestionNote
# form_class = QuestionNoteForm # form_class = QuestionNoteForm
+6
View File
@@ -108,6 +108,9 @@ class Question(QuestionBase):
if self.e_answer: if self.e_answer:
self.e_answer = self.e_answer.strip() self.e_answer = self.e_answer.strip()
def get_app_name(self):
return "sbas"
def get_absolute_url(self): def get_absolute_url(self):
return reverse("sbas:question_detail", kwargs={"pk": self.pk}) return reverse("sbas:question_detail", kwargs={"pk": self.pk})
@@ -210,6 +213,9 @@ class Exam(ExamBase):
def get_take_url(self): def get_take_url(self):
return reverse("sbas:exam_start", kwargs={"pk": self.pk}) return reverse("sbas:exam_start", kwargs={"pk": self.pk})
def get_app_name(self):
return "sbas"
@reversion.register @reversion.register
class UserAnswer(UserAnswerBase): class UserAnswer(UserAnswerBase):
+19 -15
View File
@@ -1,10 +1,14 @@
{% extends 'sbas/exams.html' %} {% extends 'sbas/exams.html' %}
{% block navigation %}
{{ block.super }}
{% include 'generic/exam_overview_headers.html' %}
{% endblock navigation %}
{% block content %} {% block content %}
{% load thumbnail %} {% load thumbnail %}
<div class="sbas"> <div class="sbas">
{% include 'generic/exam_overview_headers.html' %}
This exam will be available to take <a href="{% url 'sbas:exam_start' pk=exam.pk %}">here</a> (when active). This exam will be available to take <a href="{% url 'sbas:exam_start' pk=exam.pk %}">here</a> (when active).
@@ -47,17 +51,17 @@
}) })
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly // $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
.done(function (data) { .done(function (data) {
console.log(data); console.log(data);
if (data.status == "success") { if (data.status == "success") {
toastr.info('Exam state changed.') toastr.info('Exam state changed.')
} }
// show some message according to the response. // show some message according to the response.
// For eg. A message box showing that the status has been changed // For eg. A message box showing that the status has been changed
}) })
.always(function () { .always(function () {
console.log('[Done]'); console.log('[Done]');
}) })
}) })
$("#exam-publish-results-switch").on("change", function () { $("#exam-publish-results-switch").on("change", function () {
$.ajax({ $.ajax({
@@ -71,17 +75,17 @@
}) })
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly // $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
.done(function (data) { .done(function (data) {
console.log(data); console.log(data);
if (data.status == "success") { if (data.status == "success") {
toastr.info('Publish results state changed.') toastr.info('Publish results state changed.')
} }
// show some message according to the response. // show some message according to the response.
// For eg. A message box showing that the status has been changed // For eg. A message box showing that the status has been changed
}) })
.always(function () { .always(function () {
console.log('[Done]'); console.log('[Done]');
}) })
}) })
}); });
</script> </script>
+4 -3
View File
@@ -1,5 +1,5 @@
from reversion.views import RevisionMixin from reversion.views import RevisionMixin
from generic.mixins import SuperuserRequiredMixin from generic.mixins import CheckCanEditMixin, SuperuserRequiredMixin
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
from generic.models import CidUser from generic.models import CidUser
from sbas.forms import UserAnswerForm, ExamAuthorForm, ExamForm from sbas.forms import UserAnswerForm, ExamAuthorForm, ExamForm
@@ -402,7 +402,7 @@ class ExamCreate(ExamCreateBase):
form_class = ExamForm form_class = ExamForm
class ExamUpdate(ExamUpdateBase, AuthorOrCheckerRequiredMixin): class ExamUpdate(CheckCanEditMixin, ExamUpdateBase, AuthorOrCheckerRequiredMixin):
model = Exam model = Exam
form_class = ExamForm form_class = ExamForm
@@ -428,11 +428,12 @@ class ExamDelete(AuthorOrCheckerRequiredMixin, ExamDeleteBase):
class ExamAuthorUpdate( class ExamAuthorUpdate(
RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView RevisionMixin, CheckCanEditMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView
): ):
model = Exam model = Exam
form_class = ExamAuthorForm form_class = ExamAuthorForm
template_name = "author_form.html"
class ExamClone(ExamCloneMixin, ExamCreate): class ExamClone(ExamCloneMixin, ExamCreate):
"""Clone exam view""" """Clone exam view"""
+8 -1
View File
@@ -1,10 +1,17 @@
{% extends "base.html" %} {% extends object.get_base_template %}
{% block js %} {% block js %}
{{ form.media }} {{ form.media }}
{% endblock %} {% endblock %}
{% block navigation %}
{{ block.super }}
{% include object.get_link_headers %}
{% endblock navigation %}
{% block content %} {% block content %}
<h2>Edit Authors</h2> <h2>Edit Authors</h2>
+8 -1
View File
@@ -4,8 +4,15 @@
{{ form.media }} {{ form.media }}
{% endblock %} {% endblock %}
{% block content %} {% block navigation %}
{{ block.super }}
{% if object %}
{% include object.get_link_headers %}
{% endif %}
{% endblock navigation %}
{% block content %}
<h2>Update Exam</h2> <h2>Update Exam</h2>
<form action="" method="post"> <form action="" method="post">
{% csrf_token %} {% csrf_token %}