Merge branch 'master' of ssh://git.xkjq.uk:30001/ross/rad

This commit is contained in:
Ross
2021-07-26 10:38:27 +01:00
37 changed files with 735 additions and 200 deletions
+1
View File
@@ -6,3 +6,4 @@ __pycache__
.DS_Store .DS_Store
rad/settings_local.py rad/settings_local.py
venv venv
.venv/
+1
View File
@@ -44,6 +44,7 @@ class AnatomyAdmin(VersionAdmin):
#IncorrectAnatomyAnswersInline, #IncorrectAnatomyAnswersInline,
ExamInline, ExamInline,
] ]
view_on_site = True
exclude = [ "image_annotations" ] exclude = [ "image_annotations" ]
@@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-07-06 19:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('anatomy', '0039_answer_proposed'),
]
operations = [
migrations.AlterField(
model_name='exam',
name='exam_mode',
field=models.BooleanField(default=False, help_text='If an exam should be taken in exam mode (users results will be submited to the server for marking)'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-07-09 11:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('anatomy', '0040_alter_exam_exam_mode'),
]
operations = [
migrations.AlterField(
model_name='exam',
name='name',
field=models.CharField(help_text='Name of the exam', max_length=200),
),
]
+12 -11
View File
@@ -130,7 +130,7 @@ class AnatomyQuestion(models.Model):
def __str__(self): def __str__(self):
# Get first answer # Get first answer
return "{} [{}, {}]".format( return "{} [{}, {}]".format(
self.GetPrimaryAnswer(), self.modality, self.structure self.get_primary_answer(), self.modality, self.structure
) )
# Get associated exams # Get associated exams
e = self.exams.all().values_list("name", flat=True) e = self.exams.all().values_list("name", flat=True)
@@ -146,19 +146,19 @@ class AnatomyQuestion(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse("anatomy:question_detail", kwargs={"pk": self.pk}) return reverse("anatomy:question_detail", kwargs={"pk": self.pk})
def GetPrimaryAnswer(self): def get_primary_answer(self):
if len(self.answers.filter(proposed=False)) > 0: if len(self.answers.filter(proposed=False)) > 0:
return self.answers.filter(proposed=False).first().answer return self.answers.filter(proposed=False).first().answer
else: else:
return "None yet..." return "None yet..."
def GetExams(self): def get_exams(self):
e = self.exams.all().values_list("name", flat=True) e = self.exams.all().values_list("name", flat=True)
exams = ", ".join(e) exams = ", ".join(e)
return exams return exams
def GetUnmarkedAnswersString(self): def get_unmarked_answer_string(self):
unmarked_answers = self.GetUnmarkedAnswers() unmarked_answers = self.get_unmarked_answers()
if not unmarked_answers: if not unmarked_answers:
return "No answers to mark" return "No answers to mark"
@@ -168,18 +168,18 @@ class AnatomyQuestion(models.Model):
) )
) )
def GetUnmarkedAnswers(self): def get_unmarked_answers(self):
user_answers = set( user_answers = set(
[i.answer_compare for i in self.cid_user_answers.all()] [i.answer_compare for i in self.cid_user_answers.all()]
) )
unmarked_answers = user_answers - self.GetMarkedAnswers() unmarked_answers = user_answers - self.get_marked_answers()
return unmarked_answers return unmarked_answers
def GetUnmarkedAnswerCount(self): def get_unmarked_answer_count(self):
return len(self.GetUnmarkedAnswers()) return len(self.get_unmarked_answers())
def GetMarkedAnswers(self): def get_marked_answers(self):
return set( return set(
[ [
i.answer_compare i.answer_compare
@@ -205,6 +205,7 @@ class AnatomyQuestion(models.Model):
@reversion.register
class Answer(models.Model): class Answer(models.Model):
question = models.ForeignKey( question = models.ForeignKey(
AnatomyQuestion, related_name="answers", on_delete=models.CASCADE AnatomyQuestion, related_name="answers", on_delete=models.CASCADE
@@ -350,7 +351,7 @@ class CidUserAnswer(models.Model):
except (Exam.DoesNotExist, KeyError) as e: except (Exam.DoesNotExist, KeyError) as e:
exam = "None" exam = "None"
return "{}/{}/{}: {}".format( return "{}/{}/{}: {}".format(
exam, self.cid, self.question.GetPrimaryAnswer(), self.answer exam, self.cid, self.question.get_primary_answer(), self.answer
) )
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
+7
View File
@@ -78,6 +78,7 @@ $(document).ready(function () {
var jqxhr = $.get(evt.target.dataset.exam_list_url, function (data) { var jqxhr = $.get(evt.target.dataset.exam_list_url, function (data) {
console.log(data); console.log(data);
$("#exam-options").empty();
data.forEach((obj, n) => { data.forEach((obj, n) => {
$("#exam-options").append($(document.createElement('button')).prop({ $("#exam-options").append($(document.createElement('button')).prop({
type: 'button', type: 'button',
@@ -106,6 +107,12 @@ $(document).ready(function () {
$("table input:checked").each((n, el) => { $("table input:checked").each((n, el) => {
ids.push(el.value) ids.push(el.value)
}) })
// if no question selected
if (ids.length < 1) {
toastr.info('No question selected.');
return
}
let post_url = document.getElementById("button-select-add-exam").dataset.exam_json_edit_url; let post_url = document.getElementById("button-select-add-exam").dataset.exam_json_edit_url;
let type = document.getElementById("button-select-add-exam").dataset.type; let type = document.getElementById("button-select-add-exam").dataset.type;
let csrf = document.getElementById("button-select-add-exam").dataset.csrf; let csrf = document.getElementById("button-select-add-exam").dataset.csrf;
+4 -2
View File
@@ -26,12 +26,14 @@
{% for question in questions.all %} {% for question in questions.all %}
<li data-question_pk={{question.pk}}> <li data-question_pk={{question.pk}}>
<a href="{% url 'anatomy:exam_question_detail' pk=exam.pk sk=forloop.counter0 %}">
<img src="{{ question.image|thumbnail_url:'exam-list' }}" alt="thumbail" /> <img src="{{ question.image|thumbnail_url:'exam-list' }}" alt="thumbail" />
</a>
{{ question.description }} {{ question.description }}
<br /> <br />
{{ question.question_type }}: {{ question.GetPrimaryAnswer }} {{ question.question_type }}: {{ question.get_primary_answer }}
<br /> <br />
Modality: {{ question.modality }}, <a href="{% url 'anatomy:exam_question_detail' pk=exam.pk sk=forloop.counter0 %}">View</a>, <a href="{% url 'anatomy:mark' pk=exam.pk sk=forloop.counter0 %}">Mark</a> Modality: {{ question.modality }}, <a href="{% url 'anatomy:question_detail' pk=question.pk %}">View</a>, <a href="{% url 'anatomy:mark' pk=exam.pk sk=forloop.counter0 %}">Mark</a>
</li> </li>
{% endfor %} {% endfor %}
</ol> </ol>
+2 -2
View File
@@ -12,8 +12,8 @@
<ul id="question-mark-list"> <ul id="question-mark-list">
{% for question in questions.all %} {% for question in questions.all %}
<li data-markcount={{question.GetUnmarkedAnswerCount}}><a href="{% url 'anatomy:mark' pk=exam.pk sk=forloop.counter0 %}">Question {{forloop.counter }}: <li data-markcount={{question.get_unmarked_answer_count}}><a href="{% url 'anatomy:mark' pk=exam.pk sk=forloop.counter0 %}">Question {{forloop.counter }}:
{{ question }}</a><br /> {{ question.GetUnmarkedAnswersString }}</li> {{ question }}</a><br /> {{ question.get_unmarked_answer_string }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
@@ -29,7 +29,7 @@
<div class="date"> <div class="date">
Created: {{ question.created_date }} Created: {{ question.created_date }}
</div> </div>
<h1>{{ question.GetPrimaryAnswer }}</h1> <h1>{{ question.get_primary_answer }}</h1>
<h2>{{question.question_type}}</h2> <h2>{{question.question_type}}</h2>
Answers (score): {% for answer in question.answers.all %} Answers (score): {% for answer in question.answers.all %}
{{ answer }} ({{answer.status}}), {{ answer }} ({{answer.status}}),
+2 -2
View File
@@ -333,7 +333,7 @@ def mark(request, pk, sk):
# i.answer.lower() for i in question.incorrect_answers.all() # i.answer.lower() for i in question.incorrect_answers.all()
# ] # ]
unmarked_user_answers = question.GetUnmarkedAnswers() unmarked_user_answers = question.get_unmarked_answers()
if request.method == "POST": if request.method == "POST":
@@ -576,7 +576,7 @@ def exam_scores_cid_user(request, pk, sk):
answer_score = user_answer.get_answer_score() answer_score = user_answer.get_answer_score()
correct_answer = q.GetPrimaryAnswer() correct_answer = q.get_primary_answer()
if not exam.publish_results: if not exam.publish_results:
correct_answer = "*****" correct_answer = "*****"
+2 -2
View File
@@ -53,7 +53,7 @@ class Sign(tagulous.models.TagModel):
class ExamBase(models.Model): class ExamBase(models.Model):
name = models.CharField(max_length=200) name = models.CharField(max_length=200, help_text="Name of the exam")
#exam_questions = SortedManyToManyField(Long, related_name="exams", blank="true") #exam_questions = SortedManyToManyField(Long, related_name="exams", blank="true")
active = models.BooleanField( active = models.BooleanField(
@@ -61,7 +61,7 @@ class ExamBase(models.Model):
) )
exam_mode = models.BooleanField( exam_mode = models.BooleanField(
help_text="If an exam should be taken in exam mode", default=True help_text="If an exam should be taken in exam mode (users results will be submited to the server for marking)", default=False
) )
publish_results = models.BooleanField( publish_results = models.BooleanField(
+3
View File
@@ -406,6 +406,9 @@ class ExamViews(View, LoginRequiredMixin):
if self.question_type == "long": if self.question_type == "long":
h = {} h = {}
for question in exam.exam_questions.all(): for question in exam.exam_questions.all():
# Generate json if needed
if question.json_creation_time is None:
question.get_json(question.pk)
h[question.pk] = question.json_creation_time.isoformat() h[question.pk] = question.json_creation_time.isoformat()
obj["multi_question_json"] = h obj["multi_question_json"] = h
active_exams["exams"].append( obj ) active_exams["exams"].append( obj )
@@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-07-06 19:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('longs', '0035_longseries_open_access'),
]
operations = [
migrations.AlterField(
model_name='exam',
name='exam_mode',
field=models.BooleanField(default=False, help_text='If an exam should be taken in exam mode (users results will be submited to the server for marking)'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-07-06 19:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('longs', '0036_alter_exam_exam_mode'),
]
operations = [
migrations.AlterField(
model_name='exam',
name='time_limit',
field=models.IntegerField(default=4500, help_text='Exam time limit (in seconds). Default is 4500 seconds (75 minutes)'),
),
]
+18
View File
@@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-07-09 11:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('longs', '0037_alter_exam_time_limit'),
]
operations = [
migrations.AlterField(
model_name='exam',
name='name',
field=models.CharField(help_text='Name of the exam', max_length=200),
),
]
+10 -9
View File
@@ -145,12 +145,12 @@ class Long(models.Model):
examinations = [series.get_examination() for series in self.series.all()] examinations = [series.get_examination() for series in self.series.all()]
return "{} : {}".format(self.description, ", ".join(examinations)) return "{} : {}".format(self.description, ", ".join(examinations))
def GetExams(self): def get_exams(self):
e = self.exams.all().values_list("name", flat=True) e = self.exams.all().values_list("name", flat=True)
exams = ", ".join(e) exams = ", ".join(e)
return exams return exams
def GetImages(self, feedback=False): def get_images(self, feedback=False):
qs = self.images.all() qs = self.images.all()
if not feedback: if not feedback:
@@ -160,16 +160,16 @@ class Long(models.Model):
return images return images
def GetImageUrls(self): def get_image_urls(self):
return ",".join( return ",".join(
["https://penracourses.org.uk{}".format(i.url) for i in self.GetImages()] ["https://penracourses.org.uk{}".format(i.url) for i in self.get_images()]
) )
def GetUnmarkedAnswerCount(self): def get_unmarked_answer_count(self):
answers = self.cid_user_answers.all() answers = self.cid_user_answers.all()
return len([ans for ans in answers if not ans.is_marked()]) return len([ans for ans in answers if not ans.is_marked()])
def GetUnmarkedAnswers(self): def get_unmarked_answers(self):
answers = self.cid_user_answers.all() answers = self.cid_user_answers.all()
return [ans for ans in answers if not ans.is_marked()] return [ans for ans in answers if not ans.is_marked()]
@@ -266,7 +266,7 @@ class Long(models.Model):
# def GetNonFeedbackQuestionImages(self): # def GetNonFeedbackQuestionImages(self):
# return self.GetImages() # return self.get_images()
def test_image_validator(file): def test_image_validator(file):
@@ -527,7 +527,7 @@ class Exam(ExamBase):
exam_questions = SortedManyToManyField(Long, related_name="exams", blank="true") exam_questions = SortedManyToManyField(Long, related_name="exams", blank="true")
time_limit = models.IntegerField( time_limit = models.IntegerField(
help_text="Exam time limit (in seconds). Default is 4500 secondse (75 minutes)", help_text="Exam time limit (in seconds). Default is 4500 seconds (75 minutes)",
default=4500, default=4500,
) )
@@ -568,7 +568,7 @@ class Exam(ExamBase):
return exam_question return exam_question
def get_exam_json(self, based=False): def get_exam_json(self, based=True):
questions = self.exam_questions.all() questions = self.exam_questions.all()
exam_questions = defaultdict(dict) exam_questions = defaultdict(dict)
@@ -619,6 +619,7 @@ class Exam(ExamBase):
"exam_mode": True, "exam_mode": True,
"exam_order": exam_order, "exam_order": exam_order,
"questions": exam_questions, "questions": exam_questions,
"based": based,
#"question_requests": exam_questions, #"question_requests": exam_questions,
} }
@@ -40,7 +40,7 @@
<p class="pre-whitespace"><b>Mark Scheme:</b> <pre>{{ question.mark_scheme }}</pre></p> <p class="pre-whitespace"><b>Mark Scheme:</b> <pre>{{ question.mark_scheme }}</pre></p>
</div> </div>
</p> </p>
<p><b>Exam(s):</b> {{question.GetExams}}</p> <p><b>Exam(s):</b> {{question.get_exams}}</p>
</div> </div>
<div> <div>
Notes: Notes:
+2 -2
View File
@@ -12,8 +12,8 @@
<ul id="question-mark-list"> <ul id="question-mark-list">
{% for question in questions.all %} {% for question in questions.all %}
<li data-markcount={{question.GetUnmarkedAnswerCount}}><a href="{% url 'longs:mark' pk=exam.pk sk=forloop.counter0 %}">Question {{forloop.counter }}: <li data-markcount={{question.get_unmarked_answer_count}}><a href="{% url 'longs:mark' pk=exam.pk sk=forloop.counter0 %}">Question {{forloop.counter }}:
{{ question }}</a><br /> Answers to mark: {{question.GetUnmarkedAnswerCount}}</li> {{ question }}</a><br /> Answers to mark: {{question.get_unmarked_answer_count}}</li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
+1 -1
View File
@@ -655,7 +655,7 @@ def mark_answer(request, pk, sk, tk):
raise Http404("User answer does not exist") raise Http404("User answer does not exist")
try: try:
unmarked = question.GetUnmarkedAnswers() unmarked = question.get_unmarked_answers()
next_unmarked_id = unmarked[0].cid next_unmarked_id = unmarked[0].cid
if next_unmarked_id == tk: if next_unmarked_id == tk:
next_unmarked_id = unmarked[1].cid next_unmarked_id = unmarked[1].cid
@@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-07-06 19:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('physics', '0007_exam_author'),
]
operations = [
migrations.AlterField(
model_name='exam',
name='exam_mode',
field=models.BooleanField(default=False, help_text='If an exam should be taken in exam mode (users results will be submited to the server for marking)'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-07-09 11:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('physics', '0008_alter_exam_exam_mode'),
]
operations = [
migrations.AlterField(
model_name='exam',
name='name',
field=models.CharField(help_text='Name of the exam', max_length=200),
),
]
+1 -1
View File
@@ -59,9 +59,9 @@ INSTALLED_APPS = [
MIDDLEWARE = [ MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware", "corsheaders.middleware.CorsMiddleware",
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
+2
View File
@@ -37,6 +37,7 @@ router.register(r'longs/exams', long_views.ExamViewSet, basename="long-exams")
router.register(r'rapids/answers', rapid_views.QuestionAnswerViewSet, basename="rapid-question-answers") router.register(r'rapids/answers', rapid_views.QuestionAnswerViewSet, basename="rapid-question-answers")
router.register(r'rapids/user_answer', rapid_views.UserAnswerViewSet, basename="rapid-question-answers") router.register(r'rapids/user_answer', rapid_views.UserAnswerViewSet, basename="rapid-question-answers")
router.register(r'rapids', rapid_views.RapidViewSet, basename="rapid") router.register(r'rapids', rapid_views.RapidViewSet, basename="rapid")
#router.register(r'rapids/laterality', rapid_views.RapidLateralityViewSet, basename="rapid")
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls, name="admin"), path("admin/", admin.site.urls, name="admin"),
@@ -60,6 +61,7 @@ urlpatterns = [
path("feedback/note/<int:pk>/complete", views.feedback_mark_complete, name="feedback_mark_complete"), path("feedback/note/<int:pk>/complete", views.feedback_mark_complete, name="feedback_mark_complete"),
#path("feedback/", views.AddQuestionNote.as_view(), name="feedback"), #path("feedback/", views.AddQuestionNote.as_view(), name="feedback"),
path("feedback/answer", views.answer_suggestion_submit, name="answer_suggestion_submit"), path("feedback/answer", views.answer_suggestion_submit, name="answer_suggestion_submit"),
path("feedback/answer_submit", views.answer_submit, name="answer_submit"),
path("feedback/answer/confirm", views.answer_suggestion_confirm, name="answer_suggestion_confirm"), path("feedback/answer/confirm", views.answer_suggestion_confirm, name="answer_suggestion_confirm"),
path("feedback/view", views.view_feedback, name="view_feedback"), path("feedback/view", views.view_feedback, name="view_feedback"),
+31
View File
@@ -248,6 +248,37 @@ def answer_suggestion_submit(request):
# postExamAnswers # postExamAnswers
return JsonResponse({"success": False, "error": "Invalid data"}) return JsonResponse({"success": False, "error": "Invalid data"})
@login_required
def answer_submit(request):
if request.is_ajax and request.method == "POST":
qid = request.POST.get("qid")
question_type = request.POST.get("question_type")
answer_string = request.POST.get("answer")
status = request.POST.get("status")
if str(status) not in "012":
return JsonResponse({"success": False, "error": "Invalid status"})
if question_type == "anatomy":
AnswerModel = AnatomyAnswer
question = AnatomyQuestion
elif question_type == "rapid":
AnswerModel = RapidAnswer
question = Rapid
else:
return JsonResponse({"success": False, "error": "Invalid question type"})
q = get_object_or_404(question, pk=qid)
if AnswerModel.objects.filter(answer=answer_string, question=q):
return JsonResponse({"success": False, "error": "Answer already exists"})
a = AnswerModel(question=q, answer=answer_string, status=status)
a.save()
return JsonResponse({"success": True, "error": "Answer added"})
return JsonResponse({"success": False, "error": "Invalid data"})
@login_required @login_required
def answer_suggestion_confirm(request): def answer_suggestion_confirm(request):
if request.is_ajax and request.method == "POST": if request.is_ajax and request.method == "POST":
+2 -1
View File
@@ -41,7 +41,8 @@ class RapidAdminForm(ModelForm):
fields = [ fields = [
"normal", "abnormality", "region", "laterality", "examination", "normal", "abnormality", "region", "laterality", "examination",
#"site", #"site",
"sign", "condition", "feedback", "author" #"sign", "condition",
"feedback", "author"
] ]
widgets = { widgets = {
@@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-07-06 19:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rapids', '0023_answer_proposed'),
]
operations = [
migrations.AlterField(
model_name='exam',
name='exam_mode',
field=models.BooleanField(default=False, help_text='If an exam should be taken in exam mode (users results will be submited to the server for marking)'),
),
]
@@ -0,0 +1,26 @@
# Generated by Django 3.2.4 on 2021-07-09 11:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rapids', '0024_alter_exam_exam_mode'),
]
operations = [
migrations.RemoveField(
model_name='rapid',
name='condition',
),
migrations.RemoveField(
model_name='rapid',
name='sign',
),
migrations.AlterField(
model_name='exam',
name='name',
field=models.CharField(help_text='Name of the exam', max_length=200),
),
]
+69 -33
View File
@@ -37,6 +37,10 @@ def image_directory_path(instance, filename):
return u"rapids/picture/{0}".format(filename) return u"rapids/picture/{0}".format(filename)
def get_answer_compare(s):
s = s.strip().lower()
s = s.translate(str.maketrans('', '', string.punctuation.replace("#", "")))
return s
class Abnormality(models.Model): class Abnormality(models.Model):
name = models.CharField(max_length=200, name = models.CharField(max_length=200,
@@ -101,15 +105,9 @@ class Answer(models.Model):
def clean(self): def clean(self):
if self.answer: if self.answer:
self.answer = self.answer.strip() self.answer_compare = get_answer_compare(self.answer)
s = self.answer.lower()
s = s.translate(str.maketrans('', '', string.punctuation.replace("#", "")))
self.answer_compare = s
@reversion.register
class Rapid(models.Model): class Rapid(models.Model):
#author = models.ForeignKey('auth.User', on_delete=models.CASCADE) #author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
#image = models.ImageField() #image = models.ImageField()
@@ -146,14 +144,14 @@ class Rapid(models.Model):
choices=LATERALITY_CHOICES, choices=LATERALITY_CHOICES,
default=NONE, default=NONE,
help_text="Applies to the answer, not the examination") help_text="Applies to the answer, not the examination")
condition = tagulous.models.TagField( #condition = tagulous.models.TagField(
to=Condition, # to=Condition,
blank=True, # blank=True,
help_text= # help_text=
"Associated condition. Will allow searching / filtering and tips / hints to be displayed. Conditions with spaces must be enclosed in quotes \"...\"" # "Associated condition. Will allow searching / filtering and tips / hints to be displayed. Conditions with spaces must be enclosed in quotes \"...\""
) #)
sign = tagulous.models.TagField( #sign = tagulous.models.TagField(
to=Sign, blank=True, help_text='Radiological signs in the question') # to=Sign, blank=True, help_text='Radiological signs in the question')
DEFAULT_SITE_ID = 1 DEFAULT_SITE_ID = 1
#site = models.ManyToManyField( #site = models.ManyToManyField(
@@ -216,7 +214,7 @@ class Rapid(models.Model):
lat = self.laterality lat = self.laterality
return "{} / {} {}".format(exams, lat, n) return "{} / {} {}".format(exams, lat, n)
def GetPrimaryAnswer(self): def get_primary_answer(self):
if self.normal: if self.normal:
return "Normal" return "Normal"
elif len(self.answers.filter(proposed=False)) > 0: elif len(self.answers.filter(proposed=False)) > 0:
@@ -224,13 +222,13 @@ class Rapid(models.Model):
else: else:
return "None yet..." return "None yet..."
def GetExams(self): def get_exams(self):
e = self.exams.all().values_list("name", flat=True) e = self.exams.all().values_list("name", flat=True)
exams = ", ".join(e) exams = ", ".join(e)
return exams return exams
def GetUnmarkedAnswersString(self): def get_unmarked_answer_string(self):
unmarked_answers = self.GetUnmarkedAnswers() unmarked_answers = self.get_unmarked_answers()
if not unmarked_answers: if not unmarked_answers:
return "No answers to mark" return "No answers to mark"
@@ -240,7 +238,7 @@ class Rapid(models.Model):
) )
) )
def GetUnmarkedAnswers(self): def get_unmarked_answers(self):
# If normal no answers to mark # If normal no answers to mark
if self.normal: if self.normal:
return [] return []
@@ -248,14 +246,22 @@ class Rapid(models.Model):
user_answers = set( user_answers = set(
[i.answer_compare for i in self.cid_user_answers.all() if i.normal == False] [i.answer_compare for i in self.cid_user_answers.all() if i.normal == False]
) )
unmarked_answers = user_answers - self.GetMarkedAnswers() unmarked_answers = user_answers - self.get_marked_answers()
return unmarked_answers return unmarked_answers
def GetUnmarkedAnswerCount(self): def get_unmarked_answer_count(self):
return len(self.GetUnmarkedAnswers()) return len(self.get_unmarked_answers())
def GetMarkedAnswers(self): def get_compare_answers(self):
return set(
[
i.answer_compare
for i in self.answers.filter()
]
)
def get_marked_answers(self):
return set( return set(
[ [
i.answer_compare i.answer_compare
@@ -276,7 +282,7 @@ class Rapid(models.Model):
marked_answers = correct_answers | half_mark_answers | incorrect_answers marked_answers = correct_answers | half_mark_answers | incorrect_answers
return marked_answers return marked_answers
def GetCorrectUnstrippedAnswers(self): def get_correct_unstripped_answers(self):
return set( return set(
[ [
str(i) str(i)
@@ -285,7 +291,7 @@ class Rapid(models.Model):
] ]
) )
def GetImages(self, feedback=False): def get_images(self, feedback=False):
qs = self.images.all() qs = self.images.all()
if not feedback: if not feedback:
@@ -295,12 +301,42 @@ class Rapid(models.Model):
return images return images
def GetImageUrls(self): def get_image_urls(self):
return ",".join(["https://penracourses.org.uk{}".format(i.url) for i in self.GetImages()]) return ",".join(["https://penracourses.org.uk{}".format(i.url) for i in self.get_images()])
#def GetNonFeedbackQuestionImages(self): #def GetNonFeedbackQuestionImages(self):
#return self.GetImages() #return self.get_images()
def get_laterality_string(self):
if self.laterality == self.NONE:
s = ""
elif self.laterality == self.BILATERAL:
s = "bilateral "
elif self.laterality == self.RIGHT:
s = "right "
elif self.laterality == self.LEFT:
s = "left "
return s
def get_suggested_answers(self):
answers = []
for r in self.region.all():
laterality = self.get_laterality_string()
for a in self.abnormality.all():
answers.append("{}{} {}".format(laterality, r.name, a.name))
answers.append("{} {}{}".format(a.name, laterality, r.name))
answers.append("{} {}".format(r.name, a.name))
answers.append("{} {}".format(a.name, r.name))
compare_answers = self.get_compare_answers()
new_answers = [a for a in answers if get_answer_compare(a) not in compare_answers]
return new_answers
#reversion.register(Rapid, follow=["images"])
#@reversion.register
class RapidImage(models.Model): class RapidImage(models.Model):
rapid = models.ForeignKey(Rapid, rapid = models.ForeignKey(Rapid,
related_name="images", related_name="images",
@@ -357,7 +393,7 @@ class RapidCreationDefault(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('rapids:rapid_create') return reverse('rapids:rapid_create')
@reversion.register #@reversion.register
class Exam(ExamBase): class Exam(ExamBase):
app_name = "rapids" app_name = "rapids"
@@ -419,15 +455,15 @@ class Exam(ExamBase):
exam_questions[q.id] = { exam_questions[q.id] = {
"images": images, "images": images,
"normal": q.normal,
#"feedback_image": [], #"feedback_image": [],
#"annotations": [str(q.image_annotations)], #"annotations": [str(q.image_annotations)],
"type": "rapid", "type": "rapid",
} }
if not self.exam_mode: if not self.exam_mode:
exam_questions[q.id]["normal"] = q.normal
exam_questions[q.id]["feedback_images"] = feedback_images exam_questions[q.id]["feedback_images"] = feedback_images
exam_questions[q.id]["answers"] = list(q.GetCorrectUnstrippedAnswers()) exam_questions[q.id]["answers"] = list(q.get_correct_unstripped_answers())
#if feedback_images: #if feedback_images:
@@ -482,7 +518,7 @@ class CidUserAnswer(models.Model):
except (Exam.DoesNotExist, KeyError) as e: except (Exam.DoesNotExist, KeyError) as e:
exam = "None" exam = "None"
return "{}/{}/{}: {}".format( return "{}/{}/{}: {}".format(
exam, self.cid, self.question.GetPrimaryAnswer(), self.answer exam, self.cid, self.question.get_primary_answer(), self.answer
) )
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
+7 -1
View File
@@ -29,5 +29,11 @@ class UserAnswerSerializer(serializers.HyperlinkedModelSerializer):
class RapidSerializer(serializers.ModelSerializer): class RapidSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Rapid model = Rapid
fields = ('id', 'question', 'normal', "abnormality", "region", "examination", "exams", "author", "open_access") fields = ('id', 'question', 'normal', "abnormality", "region", "laterality", "examination", "exams", "author", "open_access")
view_name = "rapid-detail" view_name = "rapid-detail"
class RapidLateralitySerializer(serializers.ModelSerializer):
class Meta:
model = Rapid
fields = ('id', "laterality")
view_name = "rapid-laterality"
+4
View File
@@ -62,6 +62,10 @@ class RapidUserAnswerTable(tables.Table):
delete = tables.LinkColumn( delete = tables.LinkColumn(
"rapids:rapid_user_answer_delete", text="Delete", args=[A("pk")], orderable=False "rapids:rapid_user_answer_delete", text="Delete", args=[A("pk")], orderable=False
) )
view = tables.LinkColumn('rapids:rapid_user_answer_view',
text='View',
args=[A('pk')],
orderable=False)
class Meta: class Meta:
model = CidUserAnswer model = CidUserAnswer
template_name = "django_tables2/bootstrap4.html" template_name = "django_tables2/bootstrap4.html"
@@ -0,0 +1,8 @@
{% extends 'rapids/base.html' %}
{% block content %}
{{ciduseranswer.question}}
{{ciduseranswer.normal}}
{{ciduseranswer.cid}}
{% endblock content %}
+5 -3
View File
@@ -26,18 +26,20 @@
{% for question in questions.all %} {% for question in questions.all %}
<li data-question_pk={{question.pk}}> <li data-question_pk={{question.pk}}>
{% for image in question.GetImages %} <a href="{% url 'rapids:exam_question_detail' pk=exam.pk sk=forloop.counter0 %}">
{% for image in question.get_images %}
<img src="{{ image|thumbnail_url:'exam-list' }}" alt="thumbail" /> <img src="{{ image|thumbnail_url:'exam-list' }}" alt="thumbail" />
{% endfor %} {% endfor %}
</a>
{% if not question.normal %} {% if not question.normal %}
<b>Abnormality:</b> {{ question.get_abnormalities }} <b>Region:</b> {{ question.get_regions }} <b>Abnormality:</b> {{ question.get_abnormalities }} <b>Region:</b> {{ question.get_regions }}
<br /> <br />
{{ question.GetPrimaryAnswer }} {{ question.get_primary_answer }}
{% else %} {% else %}
<b>Normal</b> <b>Normal</b>
{% endif %} {% endif %}
<br /> <br />
Examination: {{ question.get_examinations }}, <a href="{% url 'rapids:exam_question_detail' pk=exam.pk sk=forloop.counter0 %}">View</a>, <a href="{% url 'rapids:mark' pk=exam.pk sk=forloop.counter0 %}">Mark</a> Examination: {{ question.get_examinations }}, <a href="{% url 'rapids:question_detail' pk=question.pk %}">View</a>, <a href="{% url 'rapids:mark' pk=exam.pk sk=forloop.counter0 %}">Mark</a>
</li> </li>
{% endfor %} {% endfor %}
</ol> </ol>
+1 -1
View File
@@ -13,7 +13,7 @@ Answers will be automatically marked.
Answers marked as normal will be automatically marked.<br/> Answers marked as normal will be automatically marked.<br/>
Region: {{ question.get_regions }}, Abnormalities: {{ question.get_abnormalities }} Region: {{ question.get_regions }}, Abnormalities: {{ question.get_abnormalities }}
{% endif %} {% endif %}
<div id="single-dicom-viewer" class="marking-dicom" data-images="{{question.GetImageUrls}}" <div id="single-dicom-viewer" class="marking-dicom" data-images="{{question.get_image_urls}}"
data-annotations='{{question.image_annotations}}'> data-annotations='{{question.image_annotations}}'>
</div> </div>
<div class="marking"> <div class="marking">
+2 -2
View File
@@ -12,8 +12,8 @@
<ul id="question-mark-list"> <ul id="question-mark-list">
{% for question in questions.all %} {% for question in questions.all %}
<li data-markcount={{question.GetUnmarkedAnswerCount}}><a href="{% url 'rapids:mark' pk=exam.pk sk=forloop.counter0 %}">Question {{forloop.counter }}: <li data-markcount={{question.get_unmarked_answer_count}}><a href="{% url 'rapids:mark' pk=exam.pk sk=forloop.counter0 %}">Question {{forloop.counter }}:
{{ question }}</a><br /> {{ question.GetUnmarkedAnswersString }}</li> {{ question }}</a><br /> {{ question.get_unmarked_answer_string }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
@@ -4,15 +4,23 @@
</div> </div>
<p class="pre-whitespace"><b>Rapid:</b> {{ question }}</p> <p class="pre-whitespace"><b>Rapid:</b> {{ question }}</p>
<p class="pre-whitespace"><b>Normal:</b> {{ question.normal }} <button id="toggle-normal-button"
class="toggle-button">toggle</button></p>
<p class="pre-whitespace"><b>Region:</b> {{ question.get_regions }}</p> <p class="pre-whitespace"><b>Region:</b> {{ question.get_regions }}</p>
<p class="pre-whitespace"><b>Examination:</b> {{ question.get_examinations }}</p> <p class="pre-whitespace"><b>Examination:</b> {{ question.get_examinations }}</p>
<p class="pre-whitespace"><b>Laterality:</b> {{ question.laterality }}</p> <p class="pre-whitespace"><b>Laterality:</b> {{ question.laterality }}
<button class="toggle-laterality-button toggle-button" data-lat="LEFT">Left</button>
<button class="toggle-laterality-button toggle-button" data-lat="RIGHT">Right</button>
<button class="toggle-laterality-button toggle-button" data-lat="BILAT">Bilateral</button>
<button class="toggle-laterality-button toggle-button" data-lat="NONE">None</button>
</p>
<p class="pre-whitespace"><b>Abnormality:</b> {{ question.get_abnormalities }}</p> <p class="pre-whitespace"><b>Abnormality:</b> {{ question.get_abnormalities }}</p>
<div class="pre-whitespace multi-image-block"><b>Images:</b> <div class="pre-whitespace multi-image-block"><b>Images:</b>
{% for image in question.images.all %} {% for image in question.images.all %}
<span class="image-block"> <span class="image-block">
Image {{ forloop.counter }}{% if image.feedback_image %} [feedback image]{% endif %}: Image {{ forloop.counter }}{% if image.feedback_image %} [feedback image]{% endif %}:
<div class="dicom-image rapid-img {% if image.feedback_image %}feedback-img{% endif %}" data-url="https://penracourses.org.uk{{ image.image.url}}"></div> <div class="dicom-image rapid-img {% if image.feedback_image %}feedback-img{% endif %}"
data-url="https://penracourses.org.uk{{ image.image.url}}"></div>
</span> </span>
{% endfor %} {% endfor %}
</div> </div>
@@ -20,6 +28,9 @@
Exams: {% for exam in question.exams.all %} Exams: {% for exam in question.exams.all %}
<a href="{% url 'rapids:exam_overview' pk=exam.pk %}">{{ exam.name }}</a>, <a href="{% url 'rapids:exam_overview' pk=exam.pk %}">{{ exam.name }}</a>,
{% endfor %} {% endfor %}
<button id="add-to-exam" data-exam_json_edit_url="{% url 'generic:generic_exam_json_edit' %}" data-exam_list_url="{% url 'rapid-exam-list' %}" data-type="rapid" data-csrf="{{ csrf_token}}" data-qid="{{question.pk}}">Add to exam</button>
<span id="exam-options"></span>
</div> </div>
<p class="pre-whitespace"><b>Open Access:</b> {{ question.open_access }}</p> <p class="pre-whitespace"><b>Open Access:</b> {{ question.open_access }}</p>
<p class="pre-whitespace"><b>Feedback:</b> {{ question.feedback }}</p> <p class="pre-whitespace"><b>Feedback:</b> {{ question.feedback }}</p>
@@ -30,7 +41,8 @@
<p><b>Scrapped:</b> {{ question.scrapped }} <a href="{% url 'rapids:rapid_scrap' pk=question.pk %}">(toggle)</a> <p><b>Scrapped:</b> {{ question.scrapped }} <a href="{% url 'rapids:rapid_scrap' pk=question.pk %}">(toggle)</a>
<p class="pre-whitespace"> <p class="pre-whitespace">
Answers (score): {% for answer in question.answers.all %} Answers (score): {% for answer in question.answers.all %}
<span {% if answer.proposed %}class="proposed-answer" data-aid="{{answer.pk}}" data-question-type="rapid" {% endif %}> <span {% if answer.proposed %}class="proposed-answer" data-aid="{{answer.pk}}" data-question-type="rapid"
{% endif %}>
{{ answer }} ({{answer.status}}), {{ answer }} ({{answer.status}}),
</span> </span>
{% endfor %} {% endfor %}
@@ -59,17 +71,188 @@
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
<details>
<div id="single-dicom-viewer" class="dicom-viewer" data-images="{{ question.GetImageUrls }}" data-annotations=''></div> <summary>
Suggested answers
</summary>
<ul class="suggested_answers">
{% for ans in question.get_suggested_answers %}
<li data-string="{{ans}}">{{ans}}</li>
{% endfor %}
</ul>
</details>
</div> </div>
<details open>
<summary>
Image viewer
</summary>
<div id="single-dicom-viewer" class="dicom-viewer" data-images="{{ question.get_image_urls }}" data-annotations=''>
</div>
</details>
<script> <script>
$(document).ready(function () { $(document).ready(function () {
$("#add-to-exam").click(function (evt) {
var jqxhr = $.get(evt.target.dataset.exam_list_url, function (data) {
console.log(data);
$("#exam-options").empty();
data.forEach((obj, n) => {
$("#exam-options").append($(document.createElement('button')).prop({
type: 'button',
innerHTML: obj.name,
class: '',
}).data("exam-id", obj.id).click((evt) => {
addToExam(evt)
}))
})
})
.done(function () {
//alert( "second success" );
})
.fail(function () {
//alert( "error" );
})
.always(function () {
//alert( "finished" );
});
return;
function addToExam(evt) {
let exam_button = document.getElementById("add-to-exam");
let ids = [];
ids.push(exam_button.dataset.qid);
// if no question selected
if (ids.length < 1) {
toastr.info('No question selected.');
return
}
let post_url = exam_button.dataset.exam_json_edit_url;
let type = exam_button.dataset.type;
let csrf = exam_button.dataset.csrf;
$.ajax({
url: post_url,
data: {
csrfmiddlewaretoken: csrf,
add_exam_questions: JSON.stringify(ids),
type: type,
exam_id: $(evt.target).data("exam-id"),
},
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('Questions added to exams.')
}
})
.always(function () {
console.log('[Done]');
$("#exam-options").empty();
})
}
});
//var $crf_token = $('[name="csrfmiddlewaretoken"]').attr('value');
/* beautify ignore:start */
n = !{{ question.normal | lower }}
/* beautify ignore:end */
$("#toggle-normal-button").click(function () {
$.ajax({
url: "{% url 'rapid-detail' question.id %}",
type: 'PATCH',
headers: {
"X-CSRFToken": "{{ csrf_token }}"
},
timeout: 3000,
data: {
csrfmiddlewaretoken: "{{ csrf_token }}",
normal: n,
}
})
.done(function (data) {
console.log(data);
toastr.info('Answer updated')
})
.fail(function () {
alert('Error updating this model instance.');
});
});
$(".toggle-laterality-button").each((n, el) => {
$(el).click(function () {
$.ajax({
url: "{% url 'rapid-detail' question.id %}",
type: 'PATCH',
headers: {
"X-CSRFToken": "{{ csrf_token }}"
},
timeout: 3000,
data: {
csrfmiddlewaretoken: "{{ csrf_token }}",
laterality: el.dataset.lat,
}
})
.done(function (data) {
console.log(data);
toastr.info('Answer updated')
})
.fail(function () {
alert('Error updating this model instance.');
});
});
});
$(".suggested_answers li").each((n, el) => {
$(el).append($("<span class='correct'>[Add Correct]</span>").on("click", function () {
$.ajax({
url: "{% url 'answer_submit' %}",
data: {
csrfmiddlewaretoken: "{{ csrf_token }}",
//active: this.checked // true if checked else false
question_type: "rapid",
qid: "{{question.pk}}",
status: 2,
answer: el.dataset.string,
},
type: "POST",
dataType: "json",
})
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
.done(function (data) {
console.log(data);
if (data.success) {
toastr.info('Answer saved')
$(el).find(".correct").remove()
}
// show some message according to the response.
// For eg. A message box showing that the status has been changed
})
.always(function () {
console.log('[Done]');
})
}))
});
// send request to change the is_private state on customSwitches toggle // send request to change the is_private state on customSwitches toggle
$(".proposed-answer").each((n, el) => { $(".proposed-answer").each((n, el) => {
// Add button to confirm answer is correct // Add button to confirm answer is correct
$(el).append($("<span class='confirm'>[Correct]</span>").on("click", function () { $(el).append($("<span class='confirm'>[Add Correct]</span>").on("click", function () {
$.ajax({ $.ajax({
url: "{% url 'answer_suggestion_confirm' %}", url: "{% url 'answer_suggestion_confirm' %}",
data: { data: {
@@ -131,5 +314,15 @@
})) }))
}); });
}); });
</script> </script>
<style>
.toggle-button {
font-size: x-small;
opacity: 10%;
}
.toggle-button:hover {
font-size: x-small;
opacity: 100%;
}
</style>
+2 -1
View File
@@ -66,6 +66,7 @@ urlpatterns = [
name="rapid_update", name="rapid_update",
), ),
path("<int:pk>/add_note", views.AddNote.as_view(), name="rapid_add_note"), path("<int:pk>/add_note", views.AddNote.as_view(), name="rapid_add_note"),
path("user_answers/", views.RapidUserAnswerView.as_view(), name="rapid_user_answer_view"), path("user_answers/", views.RapidUserAnswerTableView.as_view(), name="rapid_user_answer_table_view"),
path("user_answers/<int:pk>", views.RapidUserAnswerView.as_view(), name="rapid_user_answer_view"),
path("user_answers/<int:pk>/delete", views.UserAnswerDelete.as_view(), name="rapid_user_answer_delete"), path("user_answers/<int:pk>/delete", views.UserAnswerDelete.as_view(), name="rapid_user_answer_delete"),
] ]
+92 -27
View File
@@ -8,6 +8,7 @@ from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
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.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
@@ -28,7 +29,7 @@ from .forms import (
ExaminationForm, ExaminationForm,
AnswerFormSet, AnswerFormSet,
AnswerUpdateFormSet, AnswerUpdateFormSet,
ExamForm ExamForm,
) )
from .models import ( from .models import (
Rapid, Rapid,
@@ -69,10 +70,16 @@ from rapids.forms import RapidCreationDefaultForm
from rapids.models import RapidCreationDefault from rapids.models import RapidCreationDefault
from rest_framework import viewsets, permissions from rest_framework import viewsets, permissions, mixins
from rest_framework.pagination import PageNumberPagination from rest_framework.pagination import PageNumberPagination
from .serializers import ExamSerializer, QuestionAnswerSerializer, RapidSerializer, UserAnswerSerializer from .serializers import (
ExamSerializer,
QuestionAnswerSerializer,
RapidLateralitySerializer,
RapidSerializer,
UserAnswerSerializer,
)
from generic.mixins import SuperuserRequiredMixin from generic.mixins import SuperuserRequiredMixin
@@ -89,32 +96,44 @@ class AuthorOrCheckerRequiredMixin(object):
return obj return obj
@login_required @login_required
def question_detail(request, pk): def question_detail(request, pk):
rapid = get_object_or_404(Rapid, pk=pk) rapid = get_object_or_404(Rapid, pk=pk)
if not rapid.open_access: if not rapid.open_access:
if not request.user.groups.filter(name="rapid_checker").exists() and request.user not in rapid.author.all(): if (
not request.user.groups.filter(name="rapid_checker").exists()
and request.user not in rapid.author.all()
):
raise PermissionDenied() raise PermissionDenied()
# if request.user not in rapid.author.all(): # if request.user not in rapid.author.all():
# raise PermissionDenied # raise PermissionDenied
view_feedback = False view_feedback = False
if request.user.groups.filter(name="rapid_checker").exists() or request.user.groups.filter(name="feedback_checker").exists() or request.user in rapid.author.all(): if (
request.user.groups.filter(name="rapid_checker").exists()
or request.user.groups.filter(name="feedback_checker").exists()
or request.user in rapid.author.all()
):
view_feedback = True view_feedback = True
# logging.debug(rapid.rapid_notes.first()) # logging.debug(rapid.rapid_notes.first())
# logging.debug(rapid.subspecialty.first().name.all()) # logging.debug(rapid.subspecialty.first().name.all())
return render(request, "rapids/question_detail.html", {"question": rapid, "view_feedback" : view_feedback}) return render(
request,
"rapids/question_detail.html",
{"question": rapid, "view_feedback": view_feedback},
)
@login_required @login_required
def rapid_split(request, pk): def rapid_split(request, pk):
rapid = get_object_or_404(Rapid, pk=pk) rapid = get_object_or_404(Rapid, pk=pk)
if not request.user.groups.filter(name="rapid_checker").exists() and request.user not in question.author.all(): if (
not request.user.groups.filter(name="rapid_checker").exists()
and request.user not in question.author.all()
):
raise PermissionDenied() raise PermissionDenied()
images = rapid.images.all() images = rapid.images.all()
@@ -281,7 +300,7 @@ class RapidCreateBase(RevisionMixin, LoginRequiredMixin, CreateView):
# Sending user object to the form, to verify which fields to display/remove (depending on group) # Sending user object to the form, to verify which fields to display/remove (depending on group)
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super(RapidCreateBase, self).get_form_kwargs() kwargs = super(RapidCreateBase, self).get_form_kwargs()
kwargs.update({'user': self.request.user}) kwargs.update({"user": self.request.user})
return kwargs return kwargs
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@@ -335,7 +354,7 @@ class RapidCreate(RapidCreateBase):
initial = super().get_initial() initial = super().get_initial()
exam = get_object_or_404(Exam, pk=self.kwargs["pk"]) exam = get_object_or_404(Exam, pk=self.kwargs["pk"])
initial['exams'] = [exam.id] initial["exams"] = [exam.id]
return initial return initial
@@ -359,7 +378,9 @@ class RapidCreate(RapidCreateBase):
# return super().form_valid(form) # return super().form_valid(form)
class RapidUpdate(RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView): class RapidUpdate(
RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView
):
model = Rapid model = Rapid
form_class = RapidForm form_class = RapidForm
@@ -369,7 +390,7 @@ class RapidUpdate(RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixi
# exclude = [ 'created_date', 'published_date' ] # exclude = [ 'created_date', 'published_date' ]
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super(RapidUpdate, self).get_form_kwargs() kwargs = super(RapidUpdate, self).get_form_kwargs()
kwargs.update({'user': self.request.user}) kwargs.update({"user": self.request.user})
return kwargs return kwargs
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@@ -445,7 +466,10 @@ def rapid_scrap(request, pk):
except Rapid.DoesNotExist: except Rapid.DoesNotExist:
raise Http404("Rapid does not exist") raise Http404("Rapid does not exist")
if not request.user.groups.filter(name="rapid_checker").exists() and request.user not in question.author.all(): if (
not request.user.groups.filter(name="rapid_checker").exists()
and request.user not in question.author.all()
):
raise PermissionDenied() raise PermissionDenied()
rapid.scrapped = not rapid.scrapped rapid.scrapped = not rapid.scrapped
@@ -578,9 +602,9 @@ def loadJsonAnswer(answer):
posted_answer = answer["ans"] posted_answer = answer["ans"]
# qidn 1 does not hold answer data (but should always arrive prior to 2) ## qidn 1 does not hold answer data (but should always arrive prior to 2)
if answer["qidn"] == "1": # if answer["qidn"] == "1":
posted_answer = "" # posted_answer = ""
# Normal answers are just posted with the answer "Normal" # Normal answers are just posted with the answer "Normal"
normal = False normal = False
@@ -601,7 +625,8 @@ def loadJsonAnswer(answer):
ans = exiting_answers[0] ans = exiting_answers[0]
if answer["qidn"] == "1": if answer["qidn"] == "1":
ans.normal = normal ans.normal = normal
elif answer["qidn"] == "2": ans.answer = ""
elif answer["qidn"] == "2" and not ans.normal:
ans.answer = posted_answer ans.answer = posted_answer
ans.full_clean() ans.full_clean()
ans.save() ans.save()
@@ -643,7 +668,7 @@ def mark(request, pk, sk):
# i.answer.lower() for i in question.incorrect_answers.all() # i.answer.lower() for i in question.incorrect_answers.all()
# ] # ]
unmarked_user_answers = question.GetUnmarkedAnswers() unmarked_user_answers = question.get_unmarked_answers()
if request.method == "POST": if request.method == "POST":
@@ -878,7 +903,6 @@ def exam_scores_cid_user(request, pk, sk):
# Get user answer # Get user answer
user_answer = q.cid_user_answers.filter(cid=cid).first() user_answer = q.cid_user_answers.filter(cid=cid).first()
if not user_answer or user_answer is None: if not user_answer or user_answer is None:
# skip if no answer # skip if no answer
answers_marks.append(0) answers_marks.append(0)
@@ -892,7 +916,7 @@ def exam_scores_cid_user(request, pk, sk):
ans = user_answer.answer ans = user_answer.answer
answer_score = user_answer.get_answer_score() answer_score = user_answer.get_answer_score()
correct_answer = q.GetPrimaryAnswer() correct_answer = q.get_primary_answer()
if not exam.publish_results: if not exam.publish_results:
correct_answer = "*****" correct_answer = "*****"
@@ -931,6 +955,7 @@ class QuestionDelete(AuthorOrCheckerRequiredMixin, DeleteView):
model = Rapid model = Rapid
success_url = reverse_lazy("rapids:rapid_view") success_url = reverse_lazy("rapids:rapid_view")
RapidExamViews = ExamViews(Exam, Rapid, "rapids", "rapid", loadJsonAnswer) RapidExamViews = ExamViews(Exam, Rapid, "rapids", "rapid", loadJsonAnswer)
@@ -945,7 +970,10 @@ class ExamCreate(RevisionMixin, LoginRequiredMixin, CreateView):
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 ExamUpdate(RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView):
class ExamUpdate(
RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView
):
model = Exam model = Exam
form_class = ExamForm form_class = ExamForm
@@ -956,26 +984,36 @@ class ExamUpdate(RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin
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 RapidUserAnswerView(LoginRequiredMixin, SingleTableMixin, FilterView):
class RapidUserAnswerView(LoginRequiredMixin, DetailView):
model = CidUserAnswer
class RapidUserAnswerTableView(LoginRequiredMixin, SingleTableMixin, FilterView):
model = CidUserAnswer model = CidUserAnswer
table_class = RapidUserAnswerTable table_class = RapidUserAnswerTable
template_name = "rapids/rapid_question_view.html" template_name = "rapids/rapid_question_view.html"
filterset_class = RapidUserAnswerFilter filterset_class = RapidUserAnswerFilter
class UserAnswerDelete(SuperuserRequiredMixin, DeleteView): class UserAnswerDelete(SuperuserRequiredMixin, DeleteView):
model = CidUserAnswer model = CidUserAnswer
success_url = reverse_lazy("rapids:rapid_user_answer_view") success_url = reverse_lazy("rapids:rapid_user_answer_table_view")
class LargeResultsSetPagination(PageNumberPagination): class LargeResultsSetPagination(PageNumberPagination):
page_size = 1000 page_size = 1000
page_size_query_param = 'page_size' page_size_query_param = "page_size"
max_page_size = 10000 max_page_size = 10000
class StandardResultsSetPagination(PageNumberPagination): class StandardResultsSetPagination(PageNumberPagination):
page_size = 100 page_size = 100
page_size_query_param = 'page_size' page_size_query_param = "page_size"
max_page_size = 1000 max_page_size = 1000
class ExamViewSet(RevisionMixin, viewsets.ModelViewSet): class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
# queryset = Exam.objects.all().order_by('name') # queryset = Exam.objects.all().order_by('name')
serializer_class = ExamSerializer serializer_class = ExamSerializer
@@ -990,26 +1028,53 @@ class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
return Exam.objects.filter(author__id=user.id) return Exam.objects.filter(author__id=user.id)
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 = CidUserAnswer.objects.all() # .order_by('name') queryset = CidUserAnswer.objects.all() # .order_by('name')
serializer_class = UserAnswerSerializer serializer_class = UserAnswerSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
class RapidViewSet(RevisionMixin, viewsets.ModelViewSet): class RapidViewSet(
RevisionMixin,
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.ListModelMixin,
mixins.DestroyModelMixin,
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 RapidLateralityViewSet(
RevisionMixin,
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet,
):
queryset = Rapid.objects.all() # .order_by('name')
serializer_class = RapidLateralitySerializer
permission_classes = [permissions.IsAuthenticated]
pagination_class = StandardResultsSetPagination
class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView): class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
model = Exam model = Exam
success_url = reverse_lazy("rapids:index") success_url = reverse_lazy("rapids:index")
# class AddSuggestedAnswer(LoginRequiredMixin, CreateView): # class AddSuggestedAnswer(LoginRequiredMixin, CreateView):
# model = QuestionNote # model = QuestionNote
# form_class = QuestionNoteForm # form_class = QuestionNoteForm