Merge branch 'master' of ssh://git.xkjq.uk:30001/ross/rad
This commit is contained in:
+2
-1
@@ -5,4 +5,5 @@ __pycache__
|
||||
.vscode/
|
||||
.DS_Store
|
||||
rad/settings_local.py
|
||||
venv
|
||||
venv
|
||||
.venv/
|
||||
|
||||
@@ -44,6 +44,7 @@ class AnatomyAdmin(VersionAdmin):
|
||||
#IncorrectAnatomyAnswersInline,
|
||||
ExamInline,
|
||||
]
|
||||
view_on_site = True
|
||||
|
||||
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
@@ -130,7 +130,7 @@ class AnatomyQuestion(models.Model):
|
||||
def __str__(self):
|
||||
# Get first answer
|
||||
return "{} [{}, {}]".format(
|
||||
self.GetPrimaryAnswer(), self.modality, self.structure
|
||||
self.get_primary_answer(), self.modality, self.structure
|
||||
)
|
||||
# Get associated exams
|
||||
e = self.exams.all().values_list("name", flat=True)
|
||||
@@ -146,19 +146,19 @@ class AnatomyQuestion(models.Model):
|
||||
def get_absolute_url(self):
|
||||
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:
|
||||
return self.answers.filter(proposed=False).first().answer
|
||||
else:
|
||||
return "None yet..."
|
||||
|
||||
def GetExams(self):
|
||||
def get_exams(self):
|
||||
e = self.exams.all().values_list("name", flat=True)
|
||||
exams = ", ".join(e)
|
||||
return exams
|
||||
|
||||
def GetUnmarkedAnswersString(self):
|
||||
unmarked_answers = self.GetUnmarkedAnswers()
|
||||
def get_unmarked_answer_string(self):
|
||||
unmarked_answers = self.get_unmarked_answers()
|
||||
|
||||
if not unmarked_answers:
|
||||
return "No answers to mark"
|
||||
@@ -168,18 +168,18 @@ class AnatomyQuestion(models.Model):
|
||||
)
|
||||
)
|
||||
|
||||
def GetUnmarkedAnswers(self):
|
||||
def get_unmarked_answers(self):
|
||||
user_answers = set(
|
||||
[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
|
||||
|
||||
def GetUnmarkedAnswerCount(self):
|
||||
return len(self.GetUnmarkedAnswers())
|
||||
def get_unmarked_answer_count(self):
|
||||
return len(self.get_unmarked_answers())
|
||||
|
||||
def GetMarkedAnswers(self):
|
||||
def get_marked_answers(self):
|
||||
return set(
|
||||
[
|
||||
i.answer_compare
|
||||
@@ -205,6 +205,7 @@ class AnatomyQuestion(models.Model):
|
||||
|
||||
|
||||
|
||||
@reversion.register
|
||||
class Answer(models.Model):
|
||||
question = models.ForeignKey(
|
||||
AnatomyQuestion, related_name="answers", on_delete=models.CASCADE
|
||||
@@ -350,7 +351,7 @@ class CidUserAnswer(models.Model):
|
||||
except (Exam.DoesNotExist, KeyError) as e:
|
||||
exam = "None"
|
||||
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):
|
||||
|
||||
@@ -78,6 +78,7 @@ $(document).ready(function () {
|
||||
|
||||
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',
|
||||
@@ -106,6 +107,12 @@ $(document).ready(function () {
|
||||
$("table input:checked").each((n, el) => {
|
||||
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 type = document.getElementById("button-select-add-exam").dataset.type;
|
||||
let csrf = document.getElementById("button-select-add-exam").dataset.csrf;
|
||||
|
||||
@@ -26,12 +26,14 @@
|
||||
{% for question in questions.all %}
|
||||
|
||||
<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" />
|
||||
</a>
|
||||
{{ question.description }}
|
||||
<br />
|
||||
{{ question.question_type }}: {{ question.GetPrimaryAnswer }}
|
||||
{{ question.question_type }}: {{ question.get_primary_answer }}
|
||||
<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>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
<ul id="question-mark-list">
|
||||
{% for question in questions.all %}
|
||||
<li data-markcount={{question.GetUnmarkedAnswerCount}}><a href="{% url 'anatomy:mark' pk=exam.pk sk=forloop.counter0 %}">Question {{forloop.counter }}:
|
||||
{{ question }}</a><br /> {{ question.GetUnmarkedAnswersString }}</li>
|
||||
<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.get_unmarked_answer_string }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<div class="date">
|
||||
Created: {{ question.created_date }}
|
||||
</div>
|
||||
<h1>{{ question.GetPrimaryAnswer }}</h1>
|
||||
<h1>{{ question.get_primary_answer }}</h1>
|
||||
<h2>{{question.question_type}}</h2>
|
||||
Answers (score): {% for answer in question.answers.all %}
|
||||
{{ answer }} ({{answer.status}}),
|
||||
|
||||
+2
-2
@@ -333,7 +333,7 @@ def mark(request, pk, sk):
|
||||
# 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":
|
||||
|
||||
@@ -576,7 +576,7 @@ def exam_scores_cid_user(request, pk, sk):
|
||||
|
||||
answer_score = user_answer.get_answer_score()
|
||||
|
||||
correct_answer = q.GetPrimaryAnswer()
|
||||
correct_answer = q.get_primary_answer()
|
||||
|
||||
if not exam.publish_results:
|
||||
correct_answer = "*****"
|
||||
|
||||
+2
-2
@@ -53,7 +53,7 @@ class Sign(tagulous.models.TagModel):
|
||||
|
||||
|
||||
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")
|
||||
|
||||
active = models.BooleanField(
|
||||
@@ -61,7 +61,7 @@ class ExamBase(models.Model):
|
||||
)
|
||||
|
||||
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(
|
||||
|
||||
@@ -406,6 +406,9 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
if self.question_type == "long":
|
||||
h = {}
|
||||
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()
|
||||
obj["multi_question_json"] = h
|
||||
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)'),
|
||||
),
|
||||
]
|
||||
@@ -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
@@ -145,12 +145,12 @@ class Long(models.Model):
|
||||
examinations = [series.get_examination() for series in self.series.all()]
|
||||
return "{} : {}".format(self.description, ", ".join(examinations))
|
||||
|
||||
def GetExams(self):
|
||||
def get_exams(self):
|
||||
e = self.exams.all().values_list("name", flat=True)
|
||||
exams = ", ".join(e)
|
||||
return exams
|
||||
|
||||
def GetImages(self, feedback=False):
|
||||
def get_images(self, feedback=False):
|
||||
qs = self.images.all()
|
||||
|
||||
if not feedback:
|
||||
@@ -160,16 +160,16 @@ class Long(models.Model):
|
||||
|
||||
return images
|
||||
|
||||
def GetImageUrls(self):
|
||||
def get_image_urls(self):
|
||||
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()
|
||||
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()
|
||||
return [ans for ans in answers if not ans.is_marked()]
|
||||
|
||||
@@ -266,7 +266,7 @@ class Long(models.Model):
|
||||
|
||||
|
||||
# def GetNonFeedbackQuestionImages(self):
|
||||
# return self.GetImages()
|
||||
# return self.get_images()
|
||||
|
||||
|
||||
def test_image_validator(file):
|
||||
@@ -527,7 +527,7 @@ class Exam(ExamBase):
|
||||
exam_questions = SortedManyToManyField(Long, related_name="exams", blank="true")
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -568,7 +568,7 @@ class Exam(ExamBase):
|
||||
return exam_question
|
||||
|
||||
|
||||
def get_exam_json(self, based=False):
|
||||
def get_exam_json(self, based=True):
|
||||
questions = self.exam_questions.all()
|
||||
|
||||
exam_questions = defaultdict(dict)
|
||||
@@ -619,6 +619,7 @@ class Exam(ExamBase):
|
||||
"exam_mode": True,
|
||||
"exam_order": exam_order,
|
||||
"questions": exam_questions,
|
||||
"based": based,
|
||||
#"question_requests": exam_questions,
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<p class="pre-whitespace"><b>Mark Scheme:</b> <pre>{{ question.mark_scheme }}</pre></p>
|
||||
</div>
|
||||
</p>
|
||||
<p><b>Exam(s):</b> {{question.GetExams}}</p>
|
||||
<p><b>Exam(s):</b> {{question.get_exams}}</p>
|
||||
</div>
|
||||
<div>
|
||||
Notes:
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
<ul id="question-mark-list">
|
||||
{% for question in questions.all %}
|
||||
<li data-markcount={{question.GetUnmarkedAnswerCount}}><a href="{% url 'longs:mark' pk=exam.pk sk=forloop.counter0 %}">Question {{forloop.counter }}:
|
||||
{{ question }}</a><br /> Answers to mark: {{question.GetUnmarkedAnswerCount}}</li>
|
||||
<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.get_unmarked_answer_count}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -655,7 +655,7 @@ def mark_answer(request, pk, sk, tk):
|
||||
raise Http404("User answer does not exist")
|
||||
|
||||
try:
|
||||
unmarked = question.GetUnmarkedAnswers()
|
||||
unmarked = question.get_unmarked_answers()
|
||||
next_unmarked_id = unmarked[0].cid
|
||||
if next_unmarked_id == tk:
|
||||
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
@@ -59,9 +59,9 @@ INSTALLED_APPS = [
|
||||
MIDDLEWARE = [
|
||||
"corsheaders.middleware.CorsMiddleware",
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
|
||||
@@ -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/user_answer', rapid_views.UserAnswerViewSet, basename="rapid-question-answers")
|
||||
router.register(r'rapids', rapid_views.RapidViewSet, basename="rapid")
|
||||
#router.register(r'rapids/laterality', rapid_views.RapidLateralityViewSet, basename="rapid")
|
||||
|
||||
urlpatterns = [
|
||||
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/", views.AddQuestionNote.as_view(), name="feedback"),
|
||||
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/view", views.view_feedback, name="view_feedback"),
|
||||
|
||||
|
||||
@@ -248,6 +248,37 @@ def answer_suggestion_submit(request):
|
||||
# postExamAnswers
|
||||
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
|
||||
def answer_suggestion_confirm(request):
|
||||
if request.is_ajax and request.method == "POST":
|
||||
|
||||
+2
-1
@@ -41,7 +41,8 @@ class RapidAdminForm(ModelForm):
|
||||
fields = [
|
||||
"normal", "abnormality", "region", "laterality", "examination",
|
||||
#"site",
|
||||
"sign", "condition", "feedback", "author"
|
||||
#"sign", "condition",
|
||||
"feedback", "author"
|
||||
]
|
||||
|
||||
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
@@ -37,6 +37,10 @@ def image_directory_path(instance, 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):
|
||||
name = models.CharField(max_length=200,
|
||||
@@ -101,15 +105,9 @@ class Answer(models.Model):
|
||||
|
||||
def clean(self):
|
||||
if self.answer:
|
||||
self.answer = self.answer.strip()
|
||||
|
||||
s = self.answer.lower()
|
||||
s = s.translate(str.maketrans('', '', string.punctuation.replace("#", "")))
|
||||
|
||||
self.answer_compare = s
|
||||
self.answer_compare = get_answer_compare(self.answer)
|
||||
|
||||
|
||||
@reversion.register
|
||||
class Rapid(models.Model):
|
||||
#author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
|
||||
#image = models.ImageField()
|
||||
@@ -146,14 +144,14 @@ class Rapid(models.Model):
|
||||
choices=LATERALITY_CHOICES,
|
||||
default=NONE,
|
||||
help_text="Applies to the answer, not the examination")
|
||||
condition = tagulous.models.TagField(
|
||||
to=Condition,
|
||||
blank=True,
|
||||
help_text=
|
||||
"Associated condition. Will allow searching / filtering and tips / hints to be displayed. Conditions with spaces must be enclosed in quotes \"...\""
|
||||
)
|
||||
sign = tagulous.models.TagField(
|
||||
to=Sign, blank=True, help_text='Radiological signs in the question')
|
||||
#condition = tagulous.models.TagField(
|
||||
# to=Condition,
|
||||
# blank=True,
|
||||
# help_text=
|
||||
# "Associated condition. Will allow searching / filtering and tips / hints to be displayed. Conditions with spaces must be enclosed in quotes \"...\""
|
||||
#)
|
||||
#sign = tagulous.models.TagField(
|
||||
# to=Sign, blank=True, help_text='Radiological signs in the question')
|
||||
|
||||
DEFAULT_SITE_ID = 1
|
||||
#site = models.ManyToManyField(
|
||||
@@ -216,7 +214,7 @@ class Rapid(models.Model):
|
||||
lat = self.laterality
|
||||
return "{} / {} {}".format(exams, lat, n)
|
||||
|
||||
def GetPrimaryAnswer(self):
|
||||
def get_primary_answer(self):
|
||||
if self.normal:
|
||||
return "Normal"
|
||||
elif len(self.answers.filter(proposed=False)) > 0:
|
||||
@@ -224,13 +222,13 @@ class Rapid(models.Model):
|
||||
else:
|
||||
return "None yet..."
|
||||
|
||||
def GetExams(self):
|
||||
def get_exams(self):
|
||||
e = self.exams.all().values_list("name", flat=True)
|
||||
exams = ", ".join(e)
|
||||
return exams
|
||||
|
||||
def GetUnmarkedAnswersString(self):
|
||||
unmarked_answers = self.GetUnmarkedAnswers()
|
||||
def get_unmarked_answer_string(self):
|
||||
unmarked_answers = self.get_unmarked_answers()
|
||||
|
||||
if not unmarked_answers:
|
||||
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 self.normal:
|
||||
return []
|
||||
@@ -248,14 +246,22 @@ class Rapid(models.Model):
|
||||
user_answers = set(
|
||||
[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
|
||||
|
||||
def GetUnmarkedAnswerCount(self):
|
||||
return len(self.GetUnmarkedAnswers())
|
||||
def get_unmarked_answer_count(self):
|
||||
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(
|
||||
[
|
||||
i.answer_compare
|
||||
@@ -276,7 +282,7 @@ class Rapid(models.Model):
|
||||
marked_answers = correct_answers | half_mark_answers | incorrect_answers
|
||||
return marked_answers
|
||||
|
||||
def GetCorrectUnstrippedAnswers(self):
|
||||
def get_correct_unstripped_answers(self):
|
||||
return set(
|
||||
[
|
||||
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()
|
||||
|
||||
if not feedback:
|
||||
@@ -295,12 +301,42 @@ class Rapid(models.Model):
|
||||
|
||||
return images
|
||||
|
||||
def GetImageUrls(self):
|
||||
return ",".join(["https://penracourses.org.uk{}".format(i.url) for i in self.GetImages()])
|
||||
def get_image_urls(self):
|
||||
return ",".join(["https://penracourses.org.uk{}".format(i.url) for i in self.get_images()])
|
||||
#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):
|
||||
rapid = models.ForeignKey(Rapid,
|
||||
related_name="images",
|
||||
@@ -357,7 +393,7 @@ class RapidCreationDefault(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return reverse('rapids:rapid_create')
|
||||
|
||||
@reversion.register
|
||||
#@reversion.register
|
||||
class Exam(ExamBase):
|
||||
app_name = "rapids"
|
||||
|
||||
@@ -419,15 +455,15 @@ class Exam(ExamBase):
|
||||
|
||||
exam_questions[q.id] = {
|
||||
"images": images,
|
||||
"normal": q.normal,
|
||||
#"feedback_image": [],
|
||||
#"annotations": [str(q.image_annotations)],
|
||||
"type": "rapid",
|
||||
}
|
||||
|
||||
if not self.exam_mode:
|
||||
exam_questions[q.id]["normal"] = q.normal
|
||||
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:
|
||||
@@ -482,7 +518,7 @@ class CidUserAnswer(models.Model):
|
||||
except (Exam.DoesNotExist, KeyError) as e:
|
||||
exam = "None"
|
||||
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):
|
||||
|
||||
@@ -29,5 +29,11 @@ class UserAnswerSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class RapidSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Rapid
|
||||
fields = ('id', 'question', 'normal', "abnormality", "region", "examination", "exams", "author", "open_access")
|
||||
view_name = "rapid-detail"
|
||||
fields = ('id', 'question', 'normal', "abnormality", "region", "laterality", "examination", "exams", "author", "open_access")
|
||||
view_name = "rapid-detail"
|
||||
|
||||
class RapidLateralitySerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Rapid
|
||||
fields = ('id', "laterality")
|
||||
view_name = "rapid-laterality"
|
||||
@@ -62,6 +62,10 @@ class RapidUserAnswerTable(tables.Table):
|
||||
delete = tables.LinkColumn(
|
||||
"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:
|
||||
model = CidUserAnswer
|
||||
template_name = "django_tables2/bootstrap4.html"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
{% extends 'rapids/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
{{ciduseranswer.question}}
|
||||
{{ciduseranswer.normal}}
|
||||
{{ciduseranswer.cid}}
|
||||
{% endblock content %}
|
||||
@@ -26,18 +26,20 @@
|
||||
{% for question in questions.all %}
|
||||
|
||||
<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" />
|
||||
{% endfor %}
|
||||
</a>
|
||||
{% if not question.normal %}
|
||||
<b>Abnormality:</b> {{ question.get_abnormalities }} <b>Region:</b> {{ question.get_regions }}
|
||||
<br />
|
||||
{{ question.GetPrimaryAnswer }}
|
||||
{{ question.get_primary_answer }}
|
||||
{% else %}
|
||||
<b>Normal</b>
|
||||
{% endif %}
|
||||
<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>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
|
||||
@@ -13,7 +13,7 @@ Answers will be automatically marked.
|
||||
Answers marked as normal will be automatically marked.<br/>
|
||||
Region: {{ question.get_regions }}, Abnormalities: {{ question.get_abnormalities }}
|
||||
{% 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}}'>
|
||||
</div>
|
||||
<div class="marking">
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
<ul id="question-mark-list">
|
||||
{% for question in questions.all %}
|
||||
<li data-markcount={{question.GetUnmarkedAnswerCount}}><a href="{% url 'rapids:mark' pk=exam.pk sk=forloop.counter0 %}">Question {{forloop.counter }}:
|
||||
{{ question }}</a><br /> {{ question.GetUnmarkedAnswersString }}</li>
|
||||
<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.get_unmarked_answer_string }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -4,22 +4,33 @@
|
||||
</div>
|
||||
|
||||
<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>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>
|
||||
<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 %}
|
||||
<span class="image-block">
|
||||
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>
|
||||
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>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
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 %}
|
||||
|
||||
<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>
|
||||
<p class="pre-whitespace"><b>Open Access:</b> {{ question.open_access }}</p>
|
||||
<p class="pre-whitespace"><b>Feedback:</b> {{ question.feedback }}</p>
|
||||
@@ -28,16 +39,17 @@
|
||||
<p><b>Checked by:</b> {% for verified in question.verified.all %} <a
|
||||
href="{% url 'rapids:verified_detail' pk=verified.pk %}">{{verified}}</a>, {% endfor %}</p>
|
||||
<p><b>Scrapped:</b> {{ question.scrapped }} <a href="{% url 'rapids:rapid_scrap' pk=question.pk %}">(toggle)</a>
|
||||
<p class="pre-whitespace">
|
||||
Answers (score): {% for answer in question.answers.all %}
|
||||
<span {% if answer.proposed %}class="proposed-answer" data-aid="{{answer.pk}}" data-question-type="rapid" {% endif %}>
|
||||
{{ answer }} ({{answer.status}}),
|
||||
</span>
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p class="pre-whitespace">
|
||||
Answers (score): {% for answer in question.answers.all %}
|
||||
<span {% if answer.proposed %}class="proposed-answer" data-aid="{{answer.pk}}" data-question-type="rapid"
|
||||
{% endif %}>
|
||||
{{ answer }} ({{answer.status}}),
|
||||
</span>
|
||||
{% endfor %}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
|
||||
{% if view_feedback %}
|
||||
Notes:
|
||||
<ul>
|
||||
@@ -51,85 +63,266 @@
|
||||
<ul class="notes">
|
||||
{% for note in question.anon_notes.all %}
|
||||
<li {% if note.complete %}class='complete' {% endif %}>
|
||||
{{ note.created_on }} by {{ note.author }}: {{note.note_type}} / {{ note.note }}
|
||||
{% if not note.complete %}
|
||||
(<a href="{% url 'feedback_mark_complete' pk=note.pk %}">Mark complete</a>)
|
||||
{% endif %}
|
||||
{{ note.created_on }} by {{ note.author }}: {{note.note_type}} / {{ note.note }}
|
||||
{% if not note.complete %}
|
||||
(<a href="{% url 'feedback_mark_complete' pk=note.pk %}">Mark complete</a>)
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<div id="single-dicom-viewer" class="dicom-viewer" data-images="{{ question.GetImageUrls }}" data-annotations=''></div>
|
||||
<details>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
$(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
|
||||
$(".proposed-answer").each((n, el) => {
|
||||
|
||||
// Add button to confirm answer is correct
|
||||
$(el).append($("<span class='confirm'>[Correct]</span>").on("click", function () {
|
||||
$.ajax({
|
||||
url: "{% url 'answer_suggestion_confirm' %}",
|
||||
data: {
|
||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||
//active: this.checked // true if checked else false
|
||||
question_type: "rapid",
|
||||
aid: el.dataset.aid,
|
||||
status: 2,
|
||||
},
|
||||
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(".confirm").remove()
|
||||
$(el).removeClass("proposed-answer")
|
||||
}
|
||||
// show some message according to the response.
|
||||
// For eg. A message box showing that the status has been changed
|
||||
})
|
||||
.always(function () {
|
||||
console.log('[Done]');
|
||||
})
|
||||
}))
|
||||
// Add button to confirm answer is correct
|
||||
$(el).append($("<span class='confirm'>[Add Correct]</span>").on("click", function () {
|
||||
$.ajax({
|
||||
url: "{% url 'answer_suggestion_confirm' %}",
|
||||
data: {
|
||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||
//active: this.checked // true if checked else false
|
||||
question_type: "rapid",
|
||||
aid: el.dataset.aid,
|
||||
status: 2,
|
||||
},
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
})
|
||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||
.done(function (data) {
|
||||
console.log(data);
|
||||
|
||||
// Add button to confirm answer is incorrect
|
||||
$(el).append($("<span class='confirm'>[Incorrect]</span>").on("click", function () {
|
||||
$.ajax({
|
||||
url: "{% url 'answer_suggestion_confirm' %}",
|
||||
data: {
|
||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||
//active: this.checked // true if checked else false
|
||||
question_type: "rapid",
|
||||
aid: el.dataset.aid,
|
||||
status: 0,
|
||||
},
|
||||
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(".confirm").remove()
|
||||
$(el).removeClass("proposed-answer")
|
||||
}
|
||||
// show some message according to the response.
|
||||
// For eg. A message box showing that the status has been changed
|
||||
})
|
||||
.always(function () {
|
||||
console.log('[Done]');
|
||||
})
|
||||
}))
|
||||
|
||||
if (data.success) {
|
||||
toastr.info('Answer saved')
|
||||
$(el).find(".confirm").remove()
|
||||
$(el).removeClass("proposed-answer")
|
||||
}
|
||||
// show some message according to the response.
|
||||
// For eg. A message box showing that the status has been changed
|
||||
})
|
||||
.always(function () {
|
||||
console.log('[Done]');
|
||||
})
|
||||
}))
|
||||
// Add button to confirm answer is incorrect
|
||||
$(el).append($("<span class='confirm'>[Incorrect]</span>").on("click", function () {
|
||||
$.ajax({
|
||||
url: "{% url 'answer_suggestion_confirm' %}",
|
||||
data: {
|
||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||
//active: this.checked // true if checked else false
|
||||
question_type: "rapid",
|
||||
aid: el.dataset.aid,
|
||||
status: 0,
|
||||
},
|
||||
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(".confirm").remove()
|
||||
$(el).removeClass("proposed-answer")
|
||||
}
|
||||
// show some message according to the response.
|
||||
// For eg. A message box showing that the status has been changed
|
||||
})
|
||||
.always(function () {
|
||||
console.log('[Done]');
|
||||
})
|
||||
}))
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
</script>
|
||||
<style>
|
||||
.toggle-button {
|
||||
font-size: x-small;
|
||||
opacity: 10%;
|
||||
}
|
||||
|
||||
.toggle-button:hover {
|
||||
font-size: x-small;
|
||||
opacity: 100%;
|
||||
}
|
||||
</style>
|
||||
+2
-1
@@ -66,6 +66,7 @@ urlpatterns = [
|
||||
name="rapid_update",
|
||||
),
|
||||
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"),
|
||||
]
|
||||
|
||||
+106
-41
@@ -8,6 +8,7 @@ from django.contrib.auth.models import User
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
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 import ListView
|
||||
@@ -28,7 +29,7 @@ from .forms import (
|
||||
ExaminationForm,
|
||||
AnswerFormSet,
|
||||
AnswerUpdateFormSet,
|
||||
ExamForm
|
||||
ExamForm,
|
||||
)
|
||||
from .models import (
|
||||
Rapid,
|
||||
@@ -69,10 +70,16 @@ from rapids.forms import RapidCreationDefaultForm
|
||||
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 .serializers import ExamSerializer, QuestionAnswerSerializer, RapidSerializer, UserAnswerSerializer
|
||||
from .serializers import (
|
||||
ExamSerializer,
|
||||
QuestionAnswerSerializer,
|
||||
RapidLateralitySerializer,
|
||||
RapidSerializer,
|
||||
UserAnswerSerializer,
|
||||
)
|
||||
|
||||
from generic.mixins import SuperuserRequiredMixin
|
||||
|
||||
@@ -89,32 +96,44 @@ class AuthorOrCheckerRequiredMixin(object):
|
||||
return obj
|
||||
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
def question_detail(request, pk):
|
||||
rapid = get_object_or_404(Rapid, pk=pk)
|
||||
|
||||
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()
|
||||
# if request.user not in rapid.author.all():
|
||||
# raise PermissionDenied
|
||||
|
||||
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
|
||||
|
||||
# logging.debug(rapid.rapid_notes.first())
|
||||
# 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
|
||||
def rapid_split(request, 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()
|
||||
|
||||
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)
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super(RapidCreateBase, self).get_form_kwargs()
|
||||
kwargs.update({'user': self.request.user})
|
||||
kwargs.update({"user": self.request.user})
|
||||
return kwargs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@@ -328,24 +347,24 @@ class RapidCreateBase(RevisionMixin, LoginRequiredMixin, CreateView):
|
||||
# @login_required
|
||||
class RapidCreate(RapidCreateBase):
|
||||
|
||||
#initial = {"laterality": Rapid.NONE}
|
||||
# initial = {"laterality": Rapid.NONE}
|
||||
|
||||
def get_initial(self):
|
||||
if "pk" in self.kwargs:
|
||||
initial = super().get_initial()
|
||||
exam = get_object_or_404(Exam, pk=self.kwargs["pk"])
|
||||
|
||||
initial['exams'] = [exam.id]
|
||||
initial["exams"] = [exam.id]
|
||||
|
||||
return initial
|
||||
|
||||
## There has to be a better way...
|
||||
#try:
|
||||
# try:
|
||||
# s = (i.pk for i in self.request.user.rapid_default.site.all())
|
||||
# self.initial.update({"site": s})
|
||||
#except AttributeError:
|
||||
# except AttributeError:
|
||||
# pass
|
||||
#return self.initial
|
||||
# return self.initial
|
||||
|
||||
# fields = '__all__'
|
||||
# #fields = [ 'condition' ]
|
||||
@@ -359,7 +378,9 @@ class RapidCreate(RapidCreateBase):
|
||||
# return super().form_valid(form)
|
||||
|
||||
|
||||
class RapidUpdate(RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView):
|
||||
class RapidUpdate(
|
||||
RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView
|
||||
):
|
||||
model = Rapid
|
||||
form_class = RapidForm
|
||||
|
||||
@@ -369,7 +390,7 @@ class RapidUpdate(RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixi
|
||||
# exclude = [ 'created_date', 'published_date' ]
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super(RapidUpdate, self).get_form_kwargs()
|
||||
kwargs.update({'user': self.request.user})
|
||||
kwargs.update({"user": self.request.user})
|
||||
return kwargs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@@ -445,7 +466,10 @@ def rapid_scrap(request, pk):
|
||||
except Rapid.DoesNotExist:
|
||||
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()
|
||||
|
||||
rapid.scrapped = not rapid.scrapped
|
||||
@@ -578,9 +602,9 @@ def loadJsonAnswer(answer):
|
||||
|
||||
posted_answer = answer["ans"]
|
||||
|
||||
# qidn 1 does not hold answer data (but should always arrive prior to 2)
|
||||
if answer["qidn"] == "1":
|
||||
posted_answer = ""
|
||||
## qidn 1 does not hold answer data (but should always arrive prior to 2)
|
||||
# if answer["qidn"] == "1":
|
||||
# posted_answer = ""
|
||||
|
||||
# Normal answers are just posted with the answer "Normal"
|
||||
normal = False
|
||||
@@ -601,7 +625,8 @@ def loadJsonAnswer(answer):
|
||||
ans = exiting_answers[0]
|
||||
if answer["qidn"] == "1":
|
||||
ans.normal = normal
|
||||
elif answer["qidn"] == "2":
|
||||
ans.answer = ""
|
||||
elif answer["qidn"] == "2" and not ans.normal:
|
||||
ans.answer = posted_answer
|
||||
ans.full_clean()
|
||||
ans.save()
|
||||
@@ -643,7 +668,7 @@ def mark(request, pk, sk):
|
||||
# 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":
|
||||
|
||||
@@ -878,11 +903,10 @@ def exam_scores_cid_user(request, pk, sk):
|
||||
# Get user answer
|
||||
user_answer = q.cid_user_answers.filter(cid=cid).first()
|
||||
|
||||
|
||||
if not user_answer or user_answer is None:
|
||||
# skip if no answer
|
||||
answers_marks.append(0)
|
||||
#answers.append("")
|
||||
# answers.append("")
|
||||
answer_score = 0
|
||||
ans = "Not answered"
|
||||
else:
|
||||
@@ -892,7 +916,7 @@ def exam_scores_cid_user(request, pk, sk):
|
||||
ans = user_answer.answer
|
||||
answer_score = user_answer.get_answer_score()
|
||||
|
||||
correct_answer = q.GetPrimaryAnswer()
|
||||
correct_answer = q.get_primary_answer()
|
||||
|
||||
if not exam.publish_results:
|
||||
correct_answer = "*****"
|
||||
@@ -931,6 +955,7 @@ class QuestionDelete(AuthorOrCheckerRequiredMixin, DeleteView):
|
||||
model = Rapid
|
||||
success_url = reverse_lazy("rapids:rapid_view")
|
||||
|
||||
|
||||
RapidExamViews = ExamViews(Exam, Rapid, "rapids", "rapid", loadJsonAnswer)
|
||||
|
||||
|
||||
@@ -945,7 +970,10 @@ class ExamCreate(RevisionMixin, LoginRequiredMixin, CreateView):
|
||||
form.instance.author.add(self.request.user.id)
|
||||
return super().form_valid(form)
|
||||
|
||||
class ExamUpdate(RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView):
|
||||
|
||||
class ExamUpdate(
|
||||
RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView
|
||||
):
|
||||
model = Exam
|
||||
form_class = ExamForm
|
||||
|
||||
@@ -956,28 +984,38 @@ class ExamUpdate(RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin
|
||||
form.instance.author.add(self.request.user.id)
|
||||
return super().form_valid(form)
|
||||
|
||||
class RapidUserAnswerView(LoginRequiredMixin, SingleTableMixin, FilterView):
|
||||
|
||||
class RapidUserAnswerView(LoginRequiredMixin, DetailView):
|
||||
model = CidUserAnswer
|
||||
|
||||
|
||||
class RapidUserAnswerTableView(LoginRequiredMixin, SingleTableMixin, FilterView):
|
||||
model = CidUserAnswer
|
||||
table_class = RapidUserAnswerTable
|
||||
template_name = "rapids/rapid_question_view.html"
|
||||
|
||||
filterset_class = RapidUserAnswerFilter
|
||||
|
||||
|
||||
class UserAnswerDelete(SuperuserRequiredMixin, DeleteView):
|
||||
model = CidUserAnswer
|
||||
success_url = reverse_lazy("rapids:rapid_user_answer_view")
|
||||
success_url = reverse_lazy("rapids:rapid_user_answer_table_view")
|
||||
|
||||
|
||||
class LargeResultsSetPagination(PageNumberPagination):
|
||||
page_size = 1000
|
||||
page_size_query_param = 'page_size'
|
||||
page_size_query_param = "page_size"
|
||||
max_page_size = 10000
|
||||
|
||||
|
||||
class StandardResultsSetPagination(PageNumberPagination):
|
||||
page_size = 100
|
||||
page_size_query_param = 'page_size'
|
||||
page_size_query_param = "page_size"
|
||||
max_page_size = 1000
|
||||
|
||||
|
||||
class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
|
||||
#queryset = Exam.objects.all().order_by('name')
|
||||
# queryset = Exam.objects.all().order_by('name')
|
||||
serializer_class = ExamSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -987,35 +1025,62 @@ class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
|
||||
user = self.request.user
|
||||
if user.groups.filter(name="rapid_checker").exists():
|
||||
return Exam.objects.all()
|
||||
|
||||
|
||||
return Exam.objects.filter(author__id=user.id)
|
||||
|
||||
|
||||
class QuestionAnswerViewSet(viewsets.ModelViewSet):
|
||||
queryset = Answer.objects.all()#.order_by('name')
|
||||
queryset = Answer.objects.all() # .order_by('name')
|
||||
serializer_class = QuestionAnswerSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
|
||||
class UserAnswerViewSet(viewsets.ModelViewSet):
|
||||
queryset = CidUserAnswer.objects.all()#.order_by('name')
|
||||
queryset = CidUserAnswer.objects.all() # .order_by('name')
|
||||
serializer_class = UserAnswerSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
|
||||
class RapidViewSet(RevisionMixin, viewsets.ModelViewSet):
|
||||
queryset = Rapid.objects.all()#.order_by('name')
|
||||
class RapidViewSet(
|
||||
RevisionMixin,
|
||||
mixins.CreateModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
):
|
||||
queryset = Rapid.objects.all() # .order_by('name')
|
||||
serializer_class = RapidSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
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):
|
||||
model = Exam
|
||||
success_url = reverse_lazy("rapids:index")
|
||||
#class AddSuggestedAnswer(LoginRequiredMixin, CreateView):
|
||||
|
||||
|
||||
# class AddSuggestedAnswer(LoginRequiredMixin, CreateView):
|
||||
# model = QuestionNote
|
||||
# form_class = QuestionNoteForm
|
||||
#
|
||||
# #fields = ("author", "note_type", "note")
|
||||
#
|
||||
#
|
||||
#
|
||||
# def get_form_kwargs(self):
|
||||
# kwargs = super(AddQuestionNote, self).get_form_kwargs()
|
||||
@@ -1037,7 +1102,7 @@ class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
|
||||
# content_type = ContentType.objects.get(model="long")
|
||||
# else:
|
||||
# raise PermissionError()
|
||||
#
|
||||
#
|
||||
# get_object_or_404(question, pk=pk)
|
||||
# return { "content_type" : content_type,
|
||||
# "object_id" : pk }
|
||||
# "object_id" : pk }
|
||||
|
||||
Reference in New Issue
Block a user