Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3381ce6dc9 | ||
|
|
e5ff3ed4dc | ||
|
|
344fd1695a | ||
|
|
2b6ac79439 | ||
|
|
fcf22fdd73 | ||
|
|
6b6d909038 | ||
|
|
bf670d355f | ||
|
|
732ba1b25b | ||
|
|
b14b1c20bd | ||
|
|
fc40aacd22 |
@@ -0,0 +1,53 @@
|
|||||||
|
from typing import List
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from ninja import ModelSchema, Router
|
||||||
|
from .models import AnatomyQuestion as Question, Exam
|
||||||
|
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
|
||||||
|
from generic.decorators import check_user_in_group
|
||||||
|
from generic.constants import Group
|
||||||
|
|
||||||
|
from ninja.security import django_auth
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionSchema(ModelSchema):
|
||||||
|
class Config:
|
||||||
|
model = Question
|
||||||
|
|
||||||
|
#model_fields = ["question", "history", "feedback", "normal", "laterality"]
|
||||||
|
model_fields = "__all__"
|
||||||
|
|
||||||
|
#model_exclude = ["answers"]
|
||||||
|
|
||||||
|
class ExamSchema(ModelSchema):
|
||||||
|
class Config:
|
||||||
|
model = Exam
|
||||||
|
|
||||||
|
model_fields = ["id", "name", "active", "publish_results"]
|
||||||
|
|
||||||
|
@router.get('/')
|
||||||
|
def list_questions(request):
|
||||||
|
return [
|
||||||
|
{"id": e.id, "normal": e.normal}
|
||||||
|
for e in Question.objects.all()
|
||||||
|
]
|
||||||
|
|
||||||
|
@router.get('/question/{question_id}', response=QuestionSchema)
|
||||||
|
@check_user_in_group(Group.cid_user_manager)
|
||||||
|
def get_rapid_details(request, question_id: int):
|
||||||
|
|
||||||
|
rapid = get_object_or_404(Question, id=question_id)
|
||||||
|
return rapid
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/user_exams', response=List[ExamSchema], url_name="anatomy_user_exams")
|
||||||
|
def user_exams(request):
|
||||||
|
"""Returns a list of exams that the user has access to"""
|
||||||
|
user = request.user
|
||||||
|
if user.groups.filter(name="anatomy_checker").exists():
|
||||||
|
return Exam.objects.filter(archive=False).order_by('name')
|
||||||
|
|
||||||
|
return Exam.objects.filter(author__id=user.id, archive=False).order_by('name')
|
||||||
+4
-1
@@ -265,7 +265,7 @@ class AnatomyQuestion(models.Model):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_question_json(self, based: bool=True, answers: bool=True):
|
def get_question_json(self, based: bool=True, answers: bool=True, feedback=False):
|
||||||
"""Returns a json representation of the question"""
|
"""Returns a json representation of the question"""
|
||||||
|
|
||||||
images = []
|
images = []
|
||||||
@@ -291,6 +291,9 @@ class AnatomyQuestion(models.Model):
|
|||||||
if answers:
|
if answers:
|
||||||
json["answers"] = list(self.get_correct_unstripped_answers())
|
json["answers"] = list(self.get_correct_unstripped_answers())
|
||||||
|
|
||||||
|
#if feedback:
|
||||||
|
# json["feedback"] = self.feedback
|
||||||
|
|
||||||
return json
|
return json
|
||||||
|
|
||||||
def get_image_url(self):
|
def get_image_url(self):
|
||||||
|
|||||||
@@ -40,9 +40,25 @@
|
|||||||
</div>
|
</div>
|
||||||
<h1>{{ question.get_primary_answer }}</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 %}
|
<details>
|
||||||
{{ answer }} ({{answer.status}}),
|
<summary>
|
||||||
{% endfor %}
|
Answers:
|
||||||
|
</summary>
|
||||||
|
<table>
|
||||||
|
<tr><th>Answer</th><th>Score</th></tr>
|
||||||
|
{% for answer in question.answers.all|dictsortreversed:"status" %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span {% if answer.proposed %}class="proposed-answer" data-aid="{{answer.pk}}" data-question-type="anatomy"
|
||||||
|
{% endif %}>
|
||||||
|
{{ answer }}
|
||||||
|
</span>
|
||||||
|
<td>
|
||||||
|
<td>{{answer.status}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</details>
|
||||||
<div>
|
<div>
|
||||||
Description: {{ question.description }}
|
Description: {{ question.description }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{% render_table table %}
|
{% render_table table %}
|
||||||
|
|
||||||
<button id="button-select-add-exam" data-exam_json_edit_url="{% url 'generic:generic_exam_json_edit' %}" data-exam_list_url="{% url 'anatomy-exam-list' %}" data-type="anatomy" data-csrf="{{ csrf_token}}">Add selected questions to
|
<button id="button-select-add-exam"
|
||||||
|
data-exam_json_edit_url="{% url 'generic:generic_exam_json_edit' %}"
|
||||||
|
data-exam_list_url="{% url 'api-1:anatomy_user_exams' %}"
|
||||||
|
data-type="anatomy"
|
||||||
|
data-csrf="{{ csrf_token}}">Add selected questions to
|
||||||
exam</button>
|
exam</button>
|
||||||
<div id="exam-options"></div>
|
<div id="exam-options"></div>
|
||||||
|
|
||||||
|
|||||||
+15
-15
@@ -775,20 +775,20 @@ class ExamDelete(AuthorOrCheckerRequiredMixin, ExamDeleteBase):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
permission_classes = [permissions.IsAuthenticated]
|
# permission_classes = [permissions.IsAuthenticated]
|
||||||
|
#
|
||||||
def get_queryset(self):
|
# def get_queryset(self):
|
||||||
"""
|
# """
|
||||||
This view should return a list exams available to a user.
|
# This view should return a list exams available to a user.
|
||||||
"""
|
# """
|
||||||
user = self.request.user
|
# user = self.request.user
|
||||||
if user.groups.filter(name="anatomy_checker").exists():
|
# if user.groups.filter(name="anatomy_checker").exists():
|
||||||
return Exam.objects.all()
|
# return Exam.objects.all()
|
||||||
|
#
|
||||||
return Exam.objects.filter(author__id=user.id)
|
# return Exam.objects.filter(author__id=user.id)
|
||||||
|
|
||||||
|
|
||||||
class ExamAuthorUpdate(RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView):
|
class ExamAuthorUpdate(RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView):
|
||||||
@@ -815,7 +815,7 @@ class StructureAutocomplete(autocomplete.Select2QuerySetView):
|
|||||||
if self.q:
|
if self.q:
|
||||||
# This raises a fielderror which breaks creating a new item if not caught
|
# This raises a fielderror which breaks creating a new item if not caught
|
||||||
try:
|
try:
|
||||||
qs = qs.filter(name__istartswith=self.q)
|
qs = qs.filter(structure__icontains=self.q)
|
||||||
except FieldError:
|
except FieldError:
|
||||||
return Structure.objects.none()
|
return Structure.objects.none()
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ from .models import (
|
|||||||
Presentation,
|
Presentation,
|
||||||
)
|
)
|
||||||
|
|
||||||
import tagulous.admin
|
|
||||||
|
|
||||||
from django.forms import ModelForm
|
from django.forms import ModelForm
|
||||||
|
|
||||||
from reversion.admin import VersionAdmin
|
from reversion.admin import VersionAdmin
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ from atlas.models import (
|
|||||||
|
|
||||||
from anatomy.models import Modality
|
from anatomy.models import Modality
|
||||||
|
|
||||||
from generic.models import Examination, Sign
|
from generic.models import Examination#, Sign
|
||||||
|
|
||||||
# from generic.models import Examination, Site, Condition, Sign
|
# from generic.models import Examination, Site, Condition, Sign
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 4.1.4 on 2023-04-03 09:04
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('atlas', '0048_auto_20230116_1119'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='condition',
|
||||||
|
name='parent',
|
||||||
|
field=models.ManyToManyField(blank=True, to='atlas.condition'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='condition',
|
||||||
|
name='synonym',
|
||||||
|
field=models.ManyToManyField(blank=True, to='atlas.condition'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='finding',
|
||||||
|
name='synonym',
|
||||||
|
field=models.ManyToManyField(blank=True, to='atlas.finding'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='structure',
|
||||||
|
name='synonym',
|
||||||
|
field=models.ManyToManyField(blank=True, to='atlas.structure'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -7,8 +7,6 @@ from django.db.models.fields.related import ForeignKey
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
import tagulous
|
|
||||||
import tagulous.models
|
|
||||||
|
|
||||||
|
|
||||||
import pydicom
|
import pydicom
|
||||||
|
|||||||
+4
-4
@@ -5,8 +5,8 @@ from django.contrib import admin
|
|||||||
from .models import (
|
from .models import (
|
||||||
CidUserGroup,
|
CidUserGroup,
|
||||||
Examination,
|
Examination,
|
||||||
Sign,
|
#Sign,
|
||||||
Condition,
|
#Condition,
|
||||||
Plane,
|
Plane,
|
||||||
Contrast,
|
Contrast,
|
||||||
QuestionNote,
|
QuestionNote,
|
||||||
@@ -22,9 +22,9 @@ from .models import (
|
|||||||
# from .models import Examination, Sign, Site, Condition
|
# from .models import Examination, Sign, Site, Condition
|
||||||
|
|
||||||
admin.site.register(Examination)
|
admin.site.register(Examination)
|
||||||
admin.site.register(Sign)
|
#admin.site.register(Sign)
|
||||||
admin.site.register(Site)
|
admin.site.register(Site)
|
||||||
admin.site.register(Condition)
|
#admin.site.register(Condition)
|
||||||
admin.site.register(Plane)
|
admin.site.register(Plane)
|
||||||
admin.site.register(Contrast)
|
admin.site.register(Contrast)
|
||||||
admin.site.register(QuestionNote)
|
admin.site.register(QuestionNote)
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# constants.py
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
class Group(Enum):
|
||||||
|
cid_user_manager = "cid_user_manager"
|
||||||
|
marker = "marker"
|
||||||
|
anatomy_marker = "anatomy_marker"
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
|
|
||||||
|
from django.http import Http404
|
||||||
|
from .constants import Group
|
||||||
|
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
def user_is_cid_user_manager(function):
|
def user_is_cid_user_manager(function):
|
||||||
def wrap(request, *args, **kwargs):
|
def wrap(request, *args, **kwargs):
|
||||||
@@ -14,3 +18,21 @@ def user_is_cid_user_manager(function):
|
|||||||
wrap.__doc__ = function.__doc__
|
wrap.__doc__ = function.__doc__
|
||||||
wrap.__name__ = function.__name__
|
wrap.__name__ = function.__name__
|
||||||
return wrap
|
return wrap
|
||||||
|
|
||||||
|
|
||||||
|
def check_user_in_group(*groups: Group):
|
||||||
|
print("DEC")
|
||||||
|
print(groups)
|
||||||
|
|
||||||
|
def decorator(function):
|
||||||
|
@wraps(function)
|
||||||
|
def wrapper(request, *args, **kwargs):
|
||||||
|
print(request.user)
|
||||||
|
if request.user.is_superuser or request.user.groups.filter(
|
||||||
|
name__in=[group.name for group in groups]
|
||||||
|
).exists():
|
||||||
|
return function(request, *args, **kwargs)
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 4.1.4 on 2023-04-03 09:04
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('longs', '0065_remove_long_condition_remove_long_sign'),
|
||||||
|
('generic', '0049_auto_20230116_1119'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='Condition',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='Sign',
|
||||||
|
),
|
||||||
|
]
|
||||||
+2
-10
@@ -10,9 +10,6 @@ from django.urls import reverse
|
|||||||
from django.views.generic.edit import CreateView
|
from django.views.generic.edit import CreateView
|
||||||
from reversion.views import RevisionMixin
|
from reversion.views import RevisionMixin
|
||||||
|
|
||||||
import tagulous
|
|
||||||
import tagulous.models
|
|
||||||
|
|
||||||
from sortedm2m.fields import SortedManyToManyField
|
from sortedm2m.fields import SortedManyToManyField
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
@@ -67,13 +64,6 @@ class Site(models.Model):
|
|||||||
return self.short_code
|
return self.short_code
|
||||||
|
|
||||||
|
|
||||||
class Condition(tagulous.models.TagModel):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Sign(tagulous.models.TagModel):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class QuestionBase(models.Model):
|
class QuestionBase(models.Model):
|
||||||
|
|
||||||
authors_only = models.BooleanField(
|
authors_only = models.BooleanField(
|
||||||
@@ -203,6 +193,8 @@ class ExamBase(models.Model):
|
|||||||
def get_authors(self):
|
def get_authors(self):
|
||||||
"""Returns a comma seperated text list of authors"""
|
"""Returns a comma seperated text list of authors"""
|
||||||
authors = ", ".join([i.username for i in self.author.all()])
|
authors = ", ".join([i.username for i in self.author.all()])
|
||||||
|
if not authors:
|
||||||
|
return "None"
|
||||||
return authors
|
return authors
|
||||||
|
|
||||||
def get_author_objects(self):
|
def get_author_objects(self):
|
||||||
|
|||||||
@@ -13,7 +13,15 @@
|
|||||||
<h4 class="exam-number-title">{{filter.qs|length}} exams found.</h4>
|
<h4 class="exam-number-title">{{filter.qs|length}} exams found.</h4>
|
||||||
{% for exam in filter.qs %}
|
{% for exam in filter.qs %}
|
||||||
<div class="">
|
<div class="">
|
||||||
<h1><a href="{% url app_name|add:':exam_overview' pk=exam.pk %}">Exam: {{ exam.name }} </a></h1>
|
<h1><a href="{% url app_name|add:':exam_overview' pk=exam.pk %}">
|
||||||
|
|
||||||
|
{% if exam.exam_mode %}
|
||||||
|
Exam:
|
||||||
|
{% else %}
|
||||||
|
Packet:
|
||||||
|
{% endif %}
|
||||||
|
{{ exam.name }} </a></h1>
|
||||||
|
<span class="authors">Authors: {{exam.get_authors}}</span>
|
||||||
{% if exam.exam_mode %}
|
{% if exam.exam_mode %}
|
||||||
{% if app_name in "rapids longs anatomy" %}
|
{% if app_name in "rapids longs anatomy" %}
|
||||||
{% if request.user.is_staff %}<a href="{% url app_name|add:':mark_overview' pk=exam.pk %}">Mark answers</a>{% endif %}
|
{% if request.user.is_staff %}<a href="{% url app_name|add:':mark_overview' pk=exam.pk %}">Mark answers</a>{% endif %}
|
||||||
@@ -23,3 +31,16 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.authors {
|
||||||
|
float: right;
|
||||||
|
opacity: 50%;}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
|
{% endblock css %}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>Edit User / {{ciduser.cid}}</h2>
|
<h2>Edit User / {{ciduser.cid}}</h2>
|
||||||
Use this form to create a user. Only existing supervisors can be added (create it first or add it later if it does not exist).
|
Use this form to create a user. Only existing supervisors can be added (<a href="{% url 'generic:supervisor_create' %}">create them first</a> and refresh this page or add them later if they do not exist).
|
||||||
{% if errors %}
|
{% if errors %}
|
||||||
<div class="alert alert-info" role="alert">{{errors}}</a></div>
|
<div class="alert alert-info" role="alert">{{errors}}</a></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
+13
-21
@@ -39,7 +39,7 @@ from django.views.generic.edit import CreateView, UpdateView, DeleteView
|
|||||||
from django.views.generic.list import ListView
|
from django.views.generic.list import ListView
|
||||||
from django.views.generic.detail import DetailView
|
from django.views.generic.detail import DetailView
|
||||||
from django_filters.views import FilterView
|
from django_filters.views import FilterView
|
||||||
from django_filters import FilterSet, OrderingFilter
|
from django_filters import FilterSet, OrderingFilter, ModelMultipleChoiceFilter
|
||||||
from django_tables2.views import SingleTableMixin
|
from django_tables2.views import SingleTableMixin
|
||||||
from reversion.views import RevisionMixin
|
from reversion.views import RevisionMixin
|
||||||
from atlas.models import CaseCollection
|
from atlas.models import CaseCollection
|
||||||
@@ -195,10 +195,10 @@ def generic_exam_json_edit(request):
|
|||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
question_type = request.POST.get("type")
|
question_type = request.POST.get("type")
|
||||||
|
|
||||||
if question_type == "rapid":
|
if question_type == "rapids":
|
||||||
Exam = RapidExam
|
Exam = RapidExam
|
||||||
Question = RapidQuestion
|
Question = RapidQuestion
|
||||||
elif question_type == "long":
|
elif question_type == "longs":
|
||||||
Exam = LongExam
|
Exam = LongExam
|
||||||
Question = LongQuestion
|
Question = LongQuestion
|
||||||
elif question_type == "anatomy":
|
elif question_type == "anatomy":
|
||||||
@@ -320,7 +320,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
# group_map = {"rapids" : "rapid_checker", "anatomy" : "anatomy_checker", "longs":"long_checker"}
|
# group_map = {"rapids" : "rapid_checker", "anatomy" : "anatomy_checker", "longs":"long_checker"}
|
||||||
# exam_group = group_map[self.app_name]
|
# exam_group = group_map[self.app_name]
|
||||||
|
|
||||||
class ExamFilter(FilterSet):
|
class BasicExamFilter(FilterSet):
|
||||||
sort_order = OrderingFilter(
|
sort_order = OrderingFilter(
|
||||||
fields=(("name", "name"), ("exam_mode", "exam_mode"))
|
fields=(("name", "name"), ("exam_mode", "exam_mode"))
|
||||||
)
|
)
|
||||||
@@ -333,22 +333,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
"active": ["exact"],
|
"active": ["exact"],
|
||||||
"archive": ["exact"],
|
"archive": ["exact"],
|
||||||
"open_access": ["exact"],
|
"open_access": ["exact"],
|
||||||
#'release_date': ['isnull'],
|
|
||||||
}
|
}
|
||||||
# filter_overrides = {
|
|
||||||
# models.CharField: {
|
|
||||||
# 'filter_class': django_filters.CharFilter,
|
|
||||||
# 'extra': lambda f: {
|
|
||||||
# 'lookup_expr': 'icontains',
|
|
||||||
# },
|
|
||||||
# },
|
|
||||||
# models.BooleanField: {
|
|
||||||
# 'filter_class': django_filters.BooleanFilter,
|
|
||||||
# 'extra': lambda f: {
|
|
||||||
# 'widget': forms.CheckboxInput,
|
|
||||||
# },
|
|
||||||
# },
|
|
||||||
# }
|
|
||||||
|
|
||||||
def __init__(self, data=None, *args, **kwargs):
|
def __init__(self, data=None, *args, **kwargs):
|
||||||
# if filterset is bound, use initial values as defaults
|
# if filterset is bound, use initial values as defaults
|
||||||
@@ -365,7 +350,13 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
|
|
||||||
super().__init__(data, *args, **kwargs)
|
super().__init__(data, *args, **kwargs)
|
||||||
|
|
||||||
self.ExamFilter = ExamFilter
|
self.BasicExamFilter = BasicExamFilter
|
||||||
|
|
||||||
|
# We give some users the ability to filter by authors
|
||||||
|
class ExtraExamFilter(BasicExamFilter):
|
||||||
|
author = ModelMultipleChoiceFilter(queryset=User.objects.all(), null_label="No author")
|
||||||
|
|
||||||
|
self.ExtraExamFilter = ExtraExamFilter
|
||||||
|
|
||||||
# TODO: these may be better implemented as decorators
|
# TODO: these may be better implemented as decorators
|
||||||
def check_user_access(self, user: User, exam_id: int = None):
|
def check_user_access(self, user: User, exam_id: int = None):
|
||||||
@@ -1264,10 +1255,11 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
)
|
)
|
||||||
):
|
):
|
||||||
exams = self.Exam.objects.all()
|
exams = self.Exam.objects.all()
|
||||||
|
filter = self.ExtraExamFilter(request.GET, queryset=exams)
|
||||||
else:
|
else:
|
||||||
exams = self.Exam.objects.filter(author__id=request.user.id)
|
exams = self.Exam.objects.filter(author__id=request.user.id)
|
||||||
|
filter = self.BasicExamFilter(request.GET, queryset=exams)
|
||||||
|
|
||||||
filter = self.ExamFilter(request.GET, queryset=exams)
|
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
|
|||||||
+2
-6
@@ -1,8 +1,6 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import AnswerMarks, Long, LongSeries, LongSeriesImage, LongCreationDefault, Exam, CidUserAnswer
|
from .models import AnswerMarks, Long, LongSeries, LongSeriesImage, LongCreationDefault, Exam, CidUserAnswer
|
||||||
|
|
||||||
import tagulous.admin
|
|
||||||
|
|
||||||
from django.forms import ModelForm
|
from django.forms import ModelForm
|
||||||
|
|
||||||
from reversion.admin import VersionAdmin
|
from reversion.admin import VersionAdmin
|
||||||
@@ -11,7 +9,7 @@ from django.forms.widgets import RadioSelect
|
|||||||
from tinymce.widgets import TinyMCE
|
from tinymce.widgets import TinyMCE
|
||||||
|
|
||||||
# from generic.models import Examination, Sign, Site, Condition
|
# from generic.models import Examination, Sign, Site, Condition
|
||||||
from generic.models import Examination, Sign, Condition
|
#from generic.models import Examination, Sign, Condition
|
||||||
|
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
# admin.site.register(Examination)
|
# admin.site.register(Examination)
|
||||||
@@ -44,7 +42,7 @@ class LongAdminForm(ModelForm):
|
|||||||
fields = [
|
fields = [
|
||||||
"description",
|
"description",
|
||||||
"history",
|
"history",
|
||||||
"sign",
|
#"sign",
|
||||||
"feedback",
|
"feedback",
|
||||||
"mark_scheme",
|
"mark_scheme",
|
||||||
"model_observations",
|
"model_observations",
|
||||||
@@ -88,8 +86,6 @@ class LongSeriesAdmin(VersionAdmin):
|
|||||||
admin.site.register(LongSeries, LongSeriesAdmin)
|
admin.site.register(LongSeries, LongSeriesAdmin)
|
||||||
|
|
||||||
admin.site.register(Exam, ExamAdmin)
|
admin.site.register(Exam, ExamAdmin)
|
||||||
# tagulous.admin.register(Long.condition)
|
|
||||||
|
|
||||||
|
|
||||||
#@admin.register(AnswerMarks)
|
#@admin.register(AnswerMarks)
|
||||||
#class AnswerMarksAdmin(admin.ModelAdmin):
|
#class AnswerMarksAdmin(admin.ModelAdmin):
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
from typing import List
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from ninja import ModelSchema, Router
|
||||||
|
from .models import Exam, Long as Question
|
||||||
|
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
|
||||||
|
from generic.decorators import check_user_in_group
|
||||||
|
from generic.constants import Group
|
||||||
|
|
||||||
|
from ninja.security import django_auth
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionSchema(ModelSchema):
|
||||||
|
class Config:
|
||||||
|
model = Question
|
||||||
|
|
||||||
|
#model_fields = ["question", "history", "feedback", "normal", "laterality"]
|
||||||
|
model_fields = "__all__"
|
||||||
|
|
||||||
|
#model_exclude = ["answers"]
|
||||||
|
|
||||||
|
class ExamSchema(ModelSchema):
|
||||||
|
class Config:
|
||||||
|
model = Exam
|
||||||
|
|
||||||
|
model_fields = ["id", "name", "active", "publish_results"]
|
||||||
|
|
||||||
|
@router.get('/')
|
||||||
|
def list_questions(request):
|
||||||
|
return [
|
||||||
|
{"id": e.id, "normal": e.normal}
|
||||||
|
for e in Question.objects.all()
|
||||||
|
]
|
||||||
|
|
||||||
|
@router.get('/question/{question_id}', response=QuestionSchema)
|
||||||
|
@check_user_in_group(Group.cid_user_manager)
|
||||||
|
def get_rapid_details(request, question_id: int):
|
||||||
|
|
||||||
|
rapid = get_object_or_404(Question, id=question_id)
|
||||||
|
return rapid
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/user_exams', response=List[ExamSchema], url_name="longs_user_exams")
|
||||||
|
def user_exams(request):
|
||||||
|
"""Returns a list of exams that the user has access to"""
|
||||||
|
user = request.user
|
||||||
|
if user.groups.filter(name="long_checker").exists():
|
||||||
|
return Exam.objects.filter(archive=False).order_by('name')
|
||||||
|
|
||||||
|
return Exam.objects.filter(author__id=user.id, archive=False).order_by('name')
|
||||||
+3
-3
@@ -22,7 +22,7 @@ from longs.models import (
|
|||||||
|
|
||||||
from anatomy.models import Modality
|
from anatomy.models import Modality
|
||||||
|
|
||||||
from generic.models import Examination, Condition, Sign
|
from generic.models import Examination#, Condition, Sign
|
||||||
|
|
||||||
# from generic.models import Examination, Site, Condition, Sign
|
# from generic.models import Examination, Site, Condition, Sign
|
||||||
|
|
||||||
@@ -201,8 +201,8 @@ class LongForm(ModelForm):
|
|||||||
"description",
|
"description",
|
||||||
"history",
|
"history",
|
||||||
"feedback",
|
"feedback",
|
||||||
"condition",
|
#"condition",
|
||||||
"sign",
|
#"sign",
|
||||||
"model_observations",
|
"model_observations",
|
||||||
"model_interpretation",
|
"model_interpretation",
|
||||||
"model_principle_diagnosis",
|
"model_principle_diagnosis",
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 4.1.4 on 2023-04-03 09:04
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('longs', '0064_remove_exam_stats_graph'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='long',
|
||||||
|
name='condition',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='long',
|
||||||
|
name='sign',
|
||||||
|
),
|
||||||
|
]
|
||||||
+11
-12
@@ -6,8 +6,6 @@ from django.db.models.fields.related import ForeignKey
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
import tagulous
|
|
||||||
import tagulous.models
|
|
||||||
|
|
||||||
|
|
||||||
from django.core.files.storage import FileSystemStorage
|
from django.core.files.storage import FileSystemStorage
|
||||||
@@ -34,8 +32,8 @@ from generic.models import (
|
|||||||
CidUserGroup,
|
CidUserGroup,
|
||||||
ExamUserStatus,
|
ExamUserStatus,
|
||||||
Examination,
|
Examination,
|
||||||
Condition,
|
#Condition,
|
||||||
Sign,
|
#Sign,
|
||||||
ExamBase,
|
ExamBase,
|
||||||
Plane,
|
Plane,
|
||||||
Contrast,
|
Contrast,
|
||||||
@@ -90,15 +88,16 @@ class Long(models.Model):
|
|||||||
|
|
||||||
feedback = models.TextField(null=True, blank=True)
|
feedback = models.TextField(null=True, blank=True)
|
||||||
|
|
||||||
condition = tagulous.models.TagField(
|
# TODO: merge with atlas condition / signs / finding
|
||||||
to=Condition,
|
#condition = tagulous.models.TagField(
|
||||||
blank=True,
|
# to=Condition,
|
||||||
help_text='Associated condition. Will allow searching / filtering and tips / hints to be displayed. Conditions with spaces must be enclosed in quotes "..."',
|
# 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(
|
#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"
|
||||||
)
|
#)
|
||||||
|
|
||||||
# Model answers
|
# Model answers
|
||||||
model_observations = models.TextField(null=True, blank=True)
|
model_observations = models.TextField(null=True, blank=True)
|
||||||
|
|||||||
+3
-3
@@ -1135,9 +1135,9 @@ class ExamAuthorUpdate(RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, U
|
|||||||
form_class = ExamAuthorForm
|
form_class = ExamAuthorForm
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
def question_json_unbased(request, pk):
|
def question_json_unbased(request, pk):
|
||||||
|
|||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
from ninja import NinjaAPI
|
||||||
|
|
||||||
|
from rapids.api import router as rapids_router
|
||||||
|
from longs.api import router as longs_router
|
||||||
|
from anatomy.api import router as anatomy_router
|
||||||
|
|
||||||
|
api = NinjaAPI(csrf=True, version="1")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
api.add_router("rapid/", rapids_router)
|
||||||
|
api.add_router("anatomy/", anatomy_router)
|
||||||
|
api.add_router("longs/", longs_router)
|
||||||
|
|
||||||
|
@api.get("/hello")
|
||||||
|
def hello(request):
|
||||||
|
return "Hello world"
|
||||||
+3
-18
@@ -67,13 +67,13 @@ INSTALLED_APPS = [
|
|||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
"debug_toolbar",
|
"debug_toolbar",
|
||||||
"tagulous",
|
|
||||||
"dbbackup",
|
"dbbackup",
|
||||||
"rest_framework",
|
"rest_framework",
|
||||||
"tinymce",
|
"tinymce",
|
||||||
"django_unused_media",
|
"django_unused_media",
|
||||||
"django_htmx",
|
"django_htmx",
|
||||||
"crispy_forms",
|
"crispy_forms",
|
||||||
|
"crispy_bootstrap4",
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@@ -149,13 +149,6 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
SERIALIZATION_MODULES = {
|
|
||||||
"xml": "tagulous.serializers.xml_serializer",
|
|
||||||
"json": "tagulous.serializers.json",
|
|
||||||
"python": "tagulous.serializers.python",
|
|
||||||
"yaml": "tagulous.serializers.pyyaml",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
||||||
|
|
||||||
@@ -193,15 +186,6 @@ MEDIA_ROOT = "media/"
|
|||||||
LOGIN_REDIRECT_URL = "/"
|
LOGIN_REDIRECT_URL = "/"
|
||||||
LOGOUT_REDIRECT_URL = "/"
|
LOGOUT_REDIRECT_URL = "/"
|
||||||
|
|
||||||
TAGULOUS_AUTOCOMPLETE_JS = (
|
|
||||||
"tagulous/lib/jquery.js",
|
|
||||||
"tagulous/lib/select2-3/select2.min.js",
|
|
||||||
"tagulous/tagulous.js",
|
|
||||||
"tagulous/adaptor/select2-3.js",
|
|
||||||
)
|
|
||||||
|
|
||||||
TAGULOUS_AUTOCOMPLETE_CSS = {"all": ["tagulous/lib/select2-3/select2.css"]}
|
|
||||||
|
|
||||||
INTERNAL_IPS = ["localhost", "127.0.0.1"]
|
INTERNAL_IPS = ["localhost", "127.0.0.1"]
|
||||||
|
|
||||||
#LOGGING = {
|
#LOGGING = {
|
||||||
@@ -299,7 +283,8 @@ EMAIL_USE_SSL = True
|
|||||||
|
|
||||||
ADMINS = [("Ross","ross@xkjq.uk")]
|
ADMINS = [("Ross","ross@xkjq.uk")]
|
||||||
|
|
||||||
CRISPY_TEMPLATE_PACK = 'bootstrap'
|
CRISPY_ALLOWED_TEMPLATE_PACK = 'bootstrap4'
|
||||||
|
CRISPY_TEMPLATE_PACK = 'bootstrap4'
|
||||||
|
|
||||||
DEBUG_CONTAINER = False
|
DEBUG_CONTAINER = False
|
||||||
|
|
||||||
|
|||||||
+21
-18
@@ -33,25 +33,28 @@ from rest_framework import routers
|
|||||||
|
|
||||||
from django.conf.urls import handler400, handler403, handler404, handler500
|
from django.conf.urls import handler400, handler403, handler404, handler500
|
||||||
|
|
||||||
router = routers.DefaultRouter()
|
from .api import api
|
||||||
router.register(r"rapids/exams", rapid_views.ExamViewSet, basename="rapid-exam")
|
|
||||||
router.register(r"anatomy/exams", anatomy_views.ExamViewSet, basename="anatomy-exam")
|
|
||||||
router.register(r"longs/exams", long_views.ExamViewSet, basename="long-exam")
|
router_drf = routers.DefaultRouter()
|
||||||
router.register(
|
#router_drf.register(r"rapids/exams", rapid_views.ExamViewSet, basename="rapid-exam")
|
||||||
r"rapids/answers",
|
#router_drf.register(r"anatomy/exams", anatomy_views.ExamViewSet, basename="anatomy-exam")
|
||||||
rapid_views.QuestionAnswerViewSet,
|
#router_drf.register(r"longs/exams", long_views.ExamViewSet, basename="long-exam")
|
||||||
basename="rapid-question-answers",
|
#router_drf.register(
|
||||||
)
|
# r"rapids/answers",
|
||||||
router.register(
|
# rapid_views.QuestionAnswerViewSet,
|
||||||
r"rapids/user_answer",
|
# basename="rapid-question-answers",
|
||||||
rapid_views.UserAnswerViewSet,
|
#)
|
||||||
basename="rapid-question-answers",
|
#router_drf.register(
|
||||||
)
|
# r"rapids/user_answer",
|
||||||
router.register(r"rapids", rapid_views.RapidViewSet, basename="rapid")
|
# rapid_views.UserAnswerViewSet,
|
||||||
# router.register(r'rapids/laterality', rapid_views.RapidLateralityViewSet, basename="rapid")
|
# basename="rapid-question-answers",
|
||||||
|
#)
|
||||||
|
#router_drf.register(r"rapids", rapid_views.RapidViewSet, basename="rapid")
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("admin/", admin.site.urls, name="admin"),
|
path("admin/", admin.site.urls, name="admin"),
|
||||||
|
path("api/", api.urls),
|
||||||
path("anatomy/", include("anatomy.urls"), name="anatomy"),
|
path("anatomy/", include("anatomy.urls"), name="anatomy"),
|
||||||
path("physics/", include("physics.urls"), name="physics"),
|
path("physics/", include("physics.urls"), name="physics"),
|
||||||
# path("sbas/", include("sbas.urls"), name="sbas"),
|
# path("sbas/", include("sbas.urls"), name="sbas"),
|
||||||
@@ -139,8 +142,8 @@ urlpatterns = [
|
|||||||
name="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"),
|
||||||
path("api/", include(router.urls)),
|
#path("api/", include(router_drf.urls)),
|
||||||
path("api-auth/", include("rest_framework.urls")),
|
#path("api-auth/", include("rest_framework.urls")),
|
||||||
path("tinymce/", include("tinymce.urls")),
|
path("tinymce/", include("tinymce.urls")),
|
||||||
path("cookies/", include("cookie_consent.urls")),
|
path("cookies/", include("cookie_consent.urls")),
|
||||||
#path("cookies/", include("cookie_consent.urls")),
|
#path("cookies/", include("cookie_consent.urls")),
|
||||||
|
|||||||
@@ -556,11 +556,13 @@ class UserListTableView(CidManagerRequiredMixin, SingleTableMixin, FilterView):
|
|||||||
# context["cid_user_groups"] = cid_user_groups
|
# context["cid_user_groups"] = cid_user_groups
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class DeleteUserView(CidManagerRequiredMixin, DeleteView):
|
class DeleteUserView(CidManagerRequiredMixin, DeleteView):
|
||||||
model = User
|
model = User
|
||||||
template_name: str = "confirm_delete.html"
|
template_name: str = "confirm_delete.html"
|
||||||
success_url = reverse_lazy("accounts_list")
|
success_url = reverse_lazy("accounts_list")
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserView(CidManagerRequiredMixin, UpdateView):
|
class UpdateUserView(CidManagerRequiredMixin, UpdateView):
|
||||||
model = User
|
model = User
|
||||||
fields = ["first_name", "last_name", "email"] # Keep listing whatever fields
|
fields = ["first_name", "last_name", "email"] # Keep listing whatever fields
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import Rapid, RapidImage, Examination, Site, Abnormality, Region, RapidCreationDefault, Answer, Exam, CidUserAnswer
|
from .models import Rapid, RapidImage, Examination, Site, Abnormality, Region, RapidCreationDefault, Answer, Exam, CidUserAnswer
|
||||||
|
|
||||||
import tagulous.admin
|
|
||||||
|
|
||||||
from django.forms import ModelForm
|
from django.forms import ModelForm
|
||||||
|
|
||||||
from reversion.admin import VersionAdmin
|
from reversion.admin import VersionAdmin
|
||||||
@@ -73,8 +71,6 @@ class ExamAdmin(VersionAdmin, admin.ModelAdmin):
|
|||||||
admin.site.register(Rapid, RapidAdmin)
|
admin.site.register(Rapid, RapidAdmin)
|
||||||
admin.site.register(Exam, ExamAdmin)
|
admin.site.register(Exam, ExamAdmin)
|
||||||
|
|
||||||
#tagulous.admin.register(Rapid.condition)
|
|
||||||
|
|
||||||
class CidUserAnswerAdmin(admin.ModelAdmin):
|
class CidUserAnswerAdmin(admin.ModelAdmin):
|
||||||
exclude = []
|
exclude = []
|
||||||
readonly_fields = ["created", "updated"]
|
readonly_fields = ["created", "updated"]
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
from typing import List
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from ninja import ModelSchema, Router
|
||||||
|
from .models import Rapid, Exam
|
||||||
|
|
||||||
|
from .decorators import user_is_author_or_rapid_checker
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
|
||||||
|
from generic.decorators import check_user_in_group
|
||||||
|
from generic.constants import Group
|
||||||
|
|
||||||
|
from ninja.security import django_auth
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
|
class RapidSchema(ModelSchema):
|
||||||
|
class Config:
|
||||||
|
model = Rapid
|
||||||
|
|
||||||
|
#model_fields = ["question", "history", "feedback", "normal", "laterality"]
|
||||||
|
model_fields = "__all__"
|
||||||
|
|
||||||
|
#model_exclude = ["answers"]
|
||||||
|
|
||||||
|
class ExamSchema(ModelSchema):
|
||||||
|
class Config:
|
||||||
|
model = Exam
|
||||||
|
|
||||||
|
model_fields = ["id", "name", "active", "publish_results"]
|
||||||
|
|
||||||
|
@router.get('/')
|
||||||
|
def list_rapids(request):
|
||||||
|
return [
|
||||||
|
{"id": e.id, "normal": e.normal}
|
||||||
|
for e in Rapid.objects.all()
|
||||||
|
]
|
||||||
|
|
||||||
|
@router.get('/question/{question_id}', response=RapidSchema)
|
||||||
|
@check_user_in_group(Group.cid_user_manager)
|
||||||
|
def get_rapid_details(request, question_id: int):
|
||||||
|
|
||||||
|
rapid = get_object_or_404(Rapid, id=question_id)
|
||||||
|
return rapid
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/user_exams', response=List[ExamSchema], url_name="rapids_user_exams")
|
||||||
|
def user_exams(request):
|
||||||
|
"""Returns a list of exams that the user has access to"""
|
||||||
|
user = request.user
|
||||||
|
if user.groups.filter(name="rapid_checker").exists():
|
||||||
|
return Exam.objects.filter(archive=False).order_by('name')
|
||||||
|
|
||||||
|
return Exam.objects.filter(author__id=user.id, archive=False).order_by('name')
|
||||||
@@ -3,7 +3,14 @@ from .models import Exam, Rapid
|
|||||||
|
|
||||||
def user_is_author_or_rapid_checker(function):
|
def user_is_author_or_rapid_checker(function):
|
||||||
def wrap(request, *args, **kwargs):
|
def wrap(request, *args, **kwargs):
|
||||||
question = Rapid.objects.get(pk=kwargs['pk'])
|
if "pk" in kwargs:
|
||||||
|
question_id = kwargs["pk"]
|
||||||
|
elif "question_id" in kwargs:
|
||||||
|
question_id = kwargs["question_id"]
|
||||||
|
else:
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
|
question = Rapid.objects.get(pk=question_id)
|
||||||
if request.user in question.author.all() or request.user.groups.filter(name='rapid_checker').exists():
|
if request.user in question.author.all() or request.user.groups.filter(name='rapid_checker').exists():
|
||||||
return function(request, *args, **kwargs)
|
return function(request, *args, **kwargs)
|
||||||
else:
|
else:
|
||||||
|
|||||||
+3
-5
@@ -6,8 +6,6 @@ from django.db import models
|
|||||||
from django.http.response import JsonResponse
|
from django.http.response import JsonResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
import tagulous
|
|
||||||
import tagulous.models
|
|
||||||
|
|
||||||
|
|
||||||
from django.core.files.storage import FileSystemStorage
|
from django.core.files.storage import FileSystemStorage
|
||||||
@@ -32,8 +30,8 @@ from generic.models import (
|
|||||||
CidUserGroup,
|
CidUserGroup,
|
||||||
ExamUserStatus,
|
ExamUserStatus,
|
||||||
Site,
|
Site,
|
||||||
Condition,
|
#Condition,
|
||||||
Sign,
|
#Sign,
|
||||||
ExamBase,
|
ExamBase,
|
||||||
QuestionNote,
|
QuestionNote,
|
||||||
UserUserGroup,
|
UserUserGroup,
|
||||||
@@ -513,7 +511,7 @@ class Rapid(models.Model):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if annotations:
|
if annotations:
|
||||||
json["annotations"] = self.annotations
|
json["annotations"] = annotations
|
||||||
|
|
||||||
if history:
|
if history:
|
||||||
json["history"] = self.history
|
json["history"] = self.history
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<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"
|
<p class="pre-whitespace"><b>Normal:</b> {{ question.normal }} <button id="toggle-normal-button"
|
||||||
class="toggle-button">toggle</button></p>
|
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 class="pre-whitespace"><b>Laterality:</b> {{ question.laterality }}
|
||||||
@@ -21,20 +21,20 @@
|
|||||||
<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.description %} ({{image.description}}){% endif %}{% if image.feedback_image %} [feedback image]{% endif %}:
|
Image {{ forloop.counter }}{% if image.description %} ({{image.description}}){% endif %}{% if image.feedback_image %} [feedback image]{% endif %}:
|
||||||
<div class="dicom-image rapid-img {% if image.feedback_image %}feedback-img{% endif %}"
|
<div class="dicom-image rapid-img {% if image.feedback_image %}feedback-img{% endif %}"
|
||||||
data-url="https://www.penracourses.org.uk{{ image.image.url}}"></div>
|
data-url="https://www.penracourses.org.uk{{ image.image.url}}"></div>
|
||||||
</span>
|
</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Exam(s): {% for exam in question.exams.all %}
|
Exam(s): {% 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' %}"
|
<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-exam_list_url="{% url 'api-1:rapid_user_exams' %}" data-type="rapids" data-csrf="{{ csrf_token}}"
|
||||||
data-qid="{{question.pk}}">Add to exam</button>
|
data-qid="{{question.pk}}">Add to exam</button>
|
||||||
<button id="cancel-add-to-exam" style="display:none">Cancel</button>
|
<button id="cancel-add-to-exam" style="display:none">Cancel</button>
|
||||||
<span id="exam-options"></span>
|
<span id="exam-options"></span>
|
||||||
@@ -43,36 +43,49 @@
|
|||||||
<p class="pre-whitespace"><b>Feedback:</b> {{ question.feedback }}</p>
|
<p class="pre-whitespace"><b>Feedback:</b> {{ question.feedback }}</p>
|
||||||
<p class="pre-whitespace"><b>History:</b> {{ question.history }}</p>
|
<p class="pre-whitespace"><b>History:</b> {{ question.history }}</p>
|
||||||
<p><b>Author(s):</b> {% for author in question.author.all %} <a
|
<p><b>Author(s):</b> {% for author in question.author.all %} <a
|
||||||
href="{% url 'rapids:author_detail' pk=author.pk %}">{{author}}</a>, {% endfor %}</p>
|
href="{% url 'rapids:author_detail' pk=author.pk %}">{{author}}</a>, {% endfor %}</p>
|
||||||
<p><b>Checked by:</b> {% for verified in question.verified.all %} <a
|
<p><b>Checked by:</b> {% for verified in question.verified.all %} <a
|
||||||
href="{% url 'rapids:verified_detail' pk=verified.pk %}">{{verified}}</a>, {% endfor %}</p>
|
href="{% url 'rapids:verified_detail' pk=verified.pk %}">{{verified}}</a>, {% endfor %}</p>
|
||||||
{% comment %} <p><b>Scrapped:</b> {{ question.scrapped }} <a
|
{% comment %} <p><b>Scrapped:</b> {{ question.scrapped }} <a
|
||||||
href="{% url 'rapids:rapid_scrap' pk=question.pk %}">(toggle)</a></p> {% endcomment %}
|
href="{% url 'rapids:rapid_scrap' pk=question.pk %}">(toggle)</a></p> {% endcomment %}
|
||||||
<p class="pre-whitespace">
|
<p class="pre-whitespace">
|
||||||
Answers (score): {% for answer in question.answers.all %}
|
<details>
|
||||||
<span {% if answer.proposed %}class="proposed-answer" data-aid="{{answer.pk}}" data-question-type="rapid"
|
<summary>
|
||||||
{% endif %}>
|
Answers:
|
||||||
{{ answer }} ({{answer.status}}),
|
</summary>
|
||||||
</span>
|
<table>
|
||||||
{% endfor %}
|
<tr><th>Answer</th><th>Score</th></tr>
|
||||||
|
{% for answer in question.answers.all|dictsortreversed:"status" %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span {% if answer.proposed %}class="proposed-answer" data-aid="{{answer.pk}}" data-question-type="rapid"
|
||||||
|
{% endif %}>
|
||||||
|
{{ answer }}
|
||||||
|
</span>
|
||||||
|
<td>
|
||||||
|
<td>{{answer.status}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</details>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
{% if view_feedback %}
|
{% if view_feedback %}
|
||||||
{% include 'question_notes.html' %}
|
{% include 'question_notes.html' %}
|
||||||
{% if not question.normal %}
|
{% if not question.normal %}
|
||||||
<details>
|
<details>
|
||||||
<summary>
|
<summary>
|
||||||
Suggested answers
|
Suggested answers
|
||||||
</summary>
|
</summary>
|
||||||
<ul class="suggested_answers">
|
<ul class="suggested_answers">
|
||||||
{% for ans in question.get_suggested_answers %}
|
{% for ans in question.get_suggested_answers %}
|
||||||
<li data-string="{{ans}}">{{ans}}</li>
|
<li data-string="{{ans}}">{{ans}}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -91,13 +104,13 @@
|
|||||||
Image annotations:
|
Image annotations:
|
||||||
</summary>
|
</summary>
|
||||||
{% for image in question.images.all %}
|
{% for image in question.images.all %}
|
||||||
<span class="image-block">
|
<span class="image-block">
|
||||||
Image {{ forloop.counter }}: {{image.image_annotations}}<br />
|
Image {{ forloop.counter }}: {{image.image_annotations}}<br />
|
||||||
</span>
|
</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</details>
|
</details>
|
||||||
<a href="{% url 'rapids:question_anonymise_dicom' pk=question.pk %}"
|
<a href="{% url 'rapids:question_anonymise_dicom' pk=question.pk %}"
|
||||||
title="Anonymise dicom images">Anonymise dicoms</a><br />
|
title="Anonymise dicom images">Anonymise dicoms</a><br />
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
$("#edit-button").click(() => {
|
$("#edit-button").click(() => {
|
||||||
@@ -127,34 +140,34 @@
|
|||||||
console.log("json_toolstates", json_toolstates)
|
console.log("json_toolstates", json_toolstates)
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "{% url 'rapids:question_save_annotation' pk=question.pk %}",
|
url: "{% url 'rapids:question_save_annotation' pk=question.pk %}",
|
||||||
data: {
|
data: {
|
||||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||||
// Yes we do double encode the json....
|
// Yes we do double encode the json....
|
||||||
annotation: JSON.stringify(json_toolstates),
|
annotation: JSON.stringify(json_toolstates),
|
||||||
},
|
},
|
||||||
type: "POST",
|
type: "POST",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
error: function (e) {
|
error: function (e) {
|
||||||
toastr.warning(`Error saving annotations`)
|
toastr.warning(`Error saving annotations`)
|
||||||
console.log(e);
|
console.log(e);
|
||||||
},
|
},
|
||||||
|
|
||||||
})
|
})
|
||||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||||
.done(function (data) {
|
.done(function (data) {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
// show some message according to the response.
|
// show some message according to the response.
|
||||||
// For eg. A message box showing that the status has been changed
|
// For eg. A message box showing that the status has been changed
|
||||||
if (data.status == "success") {
|
if (data.status == "success") {
|
||||||
toastr.info('Annotations saved')
|
toastr.info('Annotations saved')
|
||||||
} else {
|
} else {
|
||||||
toastr.warning('Error saving annotations')
|
toastr.warning('Error saving annotations')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.always(function () {
|
.always(function () {
|
||||||
console.log('[Done]');
|
console.log('[Done]');
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
$("#cancel-add-to-exam").click(function (evt) {
|
$("#cancel-add-to-exam").click(function (evt) {
|
||||||
@@ -168,28 +181,28 @@
|
|||||||
$("#cancel-add-to-exam").toggle();
|
$("#cancel-add-to-exam").toggle();
|
||||||
|
|
||||||
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();
|
$("#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',
|
||||||
innerHTML: obj.name,
|
innerHTML: obj.name,
|
||||||
class: '',
|
class: '',
|
||||||
|
|
||||||
}).data("exam-id", obj.id).click((evt) => {
|
}).data("exam-id", obj.id).click((evt) => {
|
||||||
addToExam(evt)
|
addToExam(evt)
|
||||||
}))
|
}))
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
})
|
||||||
.done(function () {
|
.done(function () {
|
||||||
//alert( "second success" );
|
//alert( "second success" );
|
||||||
})
|
})
|
||||||
.fail(function () {
|
.fail(function () {
|
||||||
//alert( "error" );
|
//alert( "error" );
|
||||||
})
|
})
|
||||||
.always(function () {
|
.always(function () {
|
||||||
//alert( "finished" );
|
//alert( "finished" );
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -209,28 +222,28 @@
|
|||||||
let type = exam_button.dataset.type;
|
let type = exam_button.dataset.type;
|
||||||
let csrf = exam_button.dataset.csrf;
|
let csrf = exam_button.dataset.csrf;
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: post_url,
|
url: post_url,
|
||||||
data: {
|
data: {
|
||||||
csrfmiddlewaretoken: csrf,
|
csrfmiddlewaretoken: csrf,
|
||||||
add_exam_questions: JSON.stringify(ids),
|
add_exam_questions: JSON.stringify(ids),
|
||||||
type: type,
|
type: type,
|
||||||
exam_id: $(evt.target).data("exam-id"),
|
exam_id: $(evt.target).data("exam-id"),
|
||||||
},
|
},
|
||||||
type: "POST",
|
type: "POST",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
})
|
})
|
||||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||||
.done(function (data) {
|
.done(function (data) {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
|
|
||||||
if (data.status == "success") {
|
if (data.status == "success") {
|
||||||
toastr.info('Questions added to exams.')
|
toastr.info('Questions added to exams.')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.always(function () {
|
.always(function () {
|
||||||
console.log('[Done]');
|
console.log('[Done]');
|
||||||
$("#exam-options").empty();
|
$("#exam-options").empty();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -241,6 +254,29 @@
|
|||||||
|
|
||||||
$("#toggle-normal-button").click(function () {
|
$("#toggle-normal-button").click(function () {
|
||||||
$.ajax({
|
$.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 %}",
|
url: "{% url 'rapid-detail' question.id %}",
|
||||||
type: 'PATCH',
|
type: 'PATCH',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -249,71 +285,48 @@
|
|||||||
timeout: 3000,
|
timeout: 3000,
|
||||||
data: {
|
data: {
|
||||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||||
normal: n,
|
laterality: el.dataset.lat,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.done(function (data) {
|
.done(function (data) {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
toastr.info('Answer updated')
|
toastr.info('Answer updated')
|
||||||
})
|
})
|
||||||
.fail(function () {
|
.fail(function () {
|
||||||
alert('Error updating this model instance.');
|
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) => {
|
$(".suggested_answers li").each((n, el) => {
|
||||||
$(el).append($("<span class='correct'>[Add Correct]</span>").on("click", function () {
|
$(el).append($("<span class='correct'>[Add Correct]</span>").on("click", function () {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "{% url 'answer_submit' %}",
|
url: "{% url 'answer_submit' %}",
|
||||||
data: {
|
data: {
|
||||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||||
//active: this.checked // true if checked else false
|
//active: this.checked // true if checked else false
|
||||||
question_type: "rapid",
|
question_type: "rapid",
|
||||||
qid: "{{question.pk}}",
|
qid: "{{question.pk}}",
|
||||||
status: 2,
|
status: 2,
|
||||||
answer: el.dataset.string,
|
answer: el.dataset.string,
|
||||||
},
|
},
|
||||||
type: "POST",
|
type: "POST",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
})
|
})
|
||||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||||
.done(function (data) {
|
.done(function (data) {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
toastr.info('Answer saved')
|
toastr.info('Answer saved')
|
||||||
$(el).find(".correct").remove()
|
$(el).find(".correct").remove()
|
||||||
}
|
}
|
||||||
// show some message according to the response.
|
// show some message according to the response.
|
||||||
// For eg. A message box showing that the status has been changed
|
// For eg. A message box showing that the status has been changed
|
||||||
})
|
})
|
||||||
.always(function () {
|
.always(function () {
|
||||||
console.log('[Done]');
|
console.log('[Done]');
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -324,63 +337,63 @@
|
|||||||
// Add button to confirm answer is correct
|
// Add button to confirm answer is correct
|
||||||
$(el).append($("<span class='confirm'>[Add 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: {
|
||||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||||
//active: this.checked // true if checked else false
|
//active: this.checked // true if checked else false
|
||||||
question_type: "rapid",
|
question_type: "rapid",
|
||||||
aid: el.dataset.aid,
|
aid: el.dataset.aid,
|
||||||
status: 2,
|
status: 2,
|
||||||
},
|
},
|
||||||
type: "POST",
|
type: "POST",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
})
|
})
|
||||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||||
.done(function (data) {
|
.done(function (data) {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
toastr.info('Answer saved')
|
toastr.info('Answer saved')
|
||||||
$(el).find(".confirm").remove()
|
$(el).find(".confirm").remove()
|
||||||
$(el).removeClass("proposed-answer")
|
$(el).removeClass("proposed-answer")
|
||||||
}
|
}
|
||||||
// show some message according to the response.
|
// show some message according to the response.
|
||||||
// For eg. A message box showing that the status has been changed
|
// For eg. A message box showing that the status has been changed
|
||||||
})
|
})
|
||||||
.always(function () {
|
.always(function () {
|
||||||
console.log('[Done]');
|
console.log('[Done]');
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Add button to confirm answer is incorrect
|
// Add button to confirm answer is incorrect
|
||||||
$(el).append($("<span class='confirm'>[Incorrect]</span>").on("click", function () {
|
$(el).append($("<span class='confirm'>[Incorrect]</span>").on("click", function () {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "{% url 'answer_suggestion_confirm' %}",
|
url: "{% url 'answer_suggestion_confirm' %}",
|
||||||
data: {
|
data: {
|
||||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||||
//active: this.checked // true if checked else false
|
//active: this.checked // true if checked else false
|
||||||
question_type: "rapid",
|
question_type: "rapid",
|
||||||
aid: el.dataset.aid,
|
aid: el.dataset.aid,
|
||||||
status: 0,
|
status: 0,
|
||||||
},
|
},
|
||||||
type: "POST",
|
type: "POST",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
})
|
})
|
||||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||||
.done(function (data) {
|
.done(function (data) {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
toastr.info('Answer saved')
|
toastr.info('Answer saved')
|
||||||
$(el).find(".confirm").remove()
|
$(el).find(".confirm").remove()
|
||||||
$(el).removeClass("proposed-answer")
|
$(el).removeClass("proposed-answer")
|
||||||
}
|
}
|
||||||
// show some message according to the response.
|
// show some message according to the response.
|
||||||
// For eg. A message box showing that the status has been changed
|
// For eg. A message box showing that the status has been changed
|
||||||
})
|
})
|
||||||
.always(function () {
|
.always(function () {
|
||||||
console.log('[Done]');
|
console.log('[Done]');
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,23 +6,26 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div id="view-filter-options">
|
<div id="view-filter-options">
|
||||||
<h3>Filter Rapids</h3>
|
<h3>Filter Rapids</h3>
|
||||||
<form action="" method="get">
|
<form action="" method="get">
|
||||||
{{ filter.form }}
|
{{ filter.form }}
|
||||||
<input class="btn btn-primary btn-sm mt-1 mb-1" type="submit" />
|
<input class="btn btn-primary btn-sm mt-1 mb-1" type="submit" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% render_table table %}
|
{% render_table table %}
|
||||||
|
|
||||||
<button id="button-select-add-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}}">Add selected questions to
|
<button id="button-select-add-exam"
|
||||||
exam</button>
|
data-exam_json_edit_url="{% url 'generic:generic_exam_json_edit' %}"
|
||||||
<div id="exam-options"></div>
|
data-exam_list_url="{% url 'api-1:rapid_user_exams' %}"
|
||||||
|
data-type="rapid" data-csrf="{{ csrf_token}}">Add selected questions to
|
||||||
|
exam</button>
|
||||||
|
<div id="exam-options"></div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
+23
-36
@@ -762,46 +762,33 @@ class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
|
|||||||
return Exam.objects.filter(author__id=user.id, archive=False).order_by('name')
|
return Exam.objects.filter(author__id=user.id, archive=False).order_by('name')
|
||||||
|
|
||||||
|
|
||||||
class QuestionAnswerViewSet(viewsets.ModelViewSet):
|
#class QuestionAnswerViewSet(viewsets.ModelViewSet):
|
||||||
queryset = Answer.objects.all() # .order_by('name')
|
# queryset = Answer.objects.all() # .order_by('name')
|
||||||
serializer_class = QuestionAnswerSerializer
|
# serializer_class = QuestionAnswerSerializer
|
||||||
permission_classes = [permissions.IsAuthenticated]
|
# permission_classes = [permissions.IsAuthenticated]
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#class UserAnswerViewSet(viewsets.ModelViewSet):
|
||||||
|
# queryset = CidUserAnswer.objects.all() # .order_by('name')
|
||||||
|
# serializer_class = UserAnswerSerializer
|
||||||
|
# permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
|
||||||
class UserAnswerViewSet(viewsets.ModelViewSet):
|
#class RapidViewSet(
|
||||||
queryset = CidUserAnswer.objects.all() # .order_by('name')
|
# RevisionMixin,
|
||||||
serializer_class = UserAnswerSerializer
|
# mixins.CreateModelMixin,
|
||||||
permission_classes = [permissions.IsAuthenticated]
|
# 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 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 AddSuggestedAnswer(LoginRequiredMixin, CreateView):
|
# class AddSuggestedAnswer(LoginRequiredMixin, CreateView):
|
||||||
# model = QuestionNote
|
# model = QuestionNote
|
||||||
|
|||||||
+2
-1
@@ -3,7 +3,6 @@ Django==4.1.4
|
|||||||
django_debug_toolbar
|
django_debug_toolbar
|
||||||
django_jquery
|
django_jquery
|
||||||
django_reversion
|
django_reversion
|
||||||
django_tagulous
|
|
||||||
Pillow
|
Pillow
|
||||||
django-cors-headers
|
django-cors-headers
|
||||||
plotly
|
plotly
|
||||||
@@ -35,3 +34,5 @@ django-htmx
|
|||||||
debugpy
|
debugpy
|
||||||
git+https://github.com/jazzband/django-cookie-consent#django-cookie-consent
|
git+https://github.com/jazzband/django-cookie-consent#django-cookie-consent
|
||||||
django-crispy-forms
|
django-crispy-forms
|
||||||
|
django-ninja
|
||||||
|
crispy-bootstrap4
|
||||||
@@ -3,8 +3,6 @@ from django.contrib import admin
|
|||||||
# Register your models here.
|
# Register your models here.
|
||||||
from .models import Question, Exam, CidUserAnswer, Category
|
from .models import Question, Exam, CidUserAnswer, Category
|
||||||
|
|
||||||
import tagulous.admin
|
|
||||||
|
|
||||||
from django.forms import ModelForm
|
from django.forms import ModelForm
|
||||||
|
|
||||||
from reversion.admin import VersionAdmin
|
from reversion.admin import VersionAdmin
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
|
||||||
|
|
||||||
<link href="{% static 'css/widgets.css' %}" type="text/css" media="all" rel="stylesheet">
|
<link href="{% static 'css/widgets.css' %}" type="text/css" media="all" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="{% static 'tagulous/lib/select2-3/select2.css' %}">
|
|
||||||
<link rel="stylesheet" href="{% static 'css/dicomViewer.css' %}">
|
<link rel="stylesheet" href="{% static 'css/dicomViewer.css' %}">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.css" integrity="sha512-6S2HWzVFxruDlZxI3sXOZZ4/eJ8AcxkQH1+JjSe/ONCEqR9L4Ysq5JdT5ipqtzU7WHalNwzwBv+iE51gNHJNqQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.css" integrity="sha512-6S2HWzVFxruDlZxI3sXOZZ4/eJ8AcxkQH1+JjSe/ONCEqR9L4Ysq5JdT5ipqtzU7WHalNwzwBv+iE51gNHJNqQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||||
|
|
||||||
@@ -43,7 +42,6 @@
|
|||||||
<script src="{% static 'autocomplete_light/i18n/en.js' %}"></script>
|
<script src="{% static 'autocomplete_light/i18n/en.js' %}"></script>
|
||||||
<script type="text/javascript" async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-AMS-MML_HTMLorMML"> </script>
|
<script type="text/javascript" async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-AMS-MML_HTMLorMML"> </script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/dexie/3.0.3/dexie.min.js" integrity="sha512-aEtNzq8X5E0ambgeM68H174SOXaANJ6wDqJ0TuVIx4R2J4fRdUA0nLzx0faA1mmViqb+r0VX7cOXkskxyJENUA==" crossorigin="anonymous"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/dexie/3.0.3/dexie.min.js" integrity="sha512-aEtNzq8X5E0ambgeM68H174SOXaANJ6wDqJ0TuVIx4R2J4fRdUA0nLzx0faA1mmViqb+r0VX7cOXkskxyJENUA==" crossorigin="anonymous"></script>
|
||||||
<!-- <script src="{% static 'tagulous/lib/jquery.js' %}"></script> -->
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5sortable/0.10.0/html5sortable.min.js" integrity="sha512-tBlVMq89XaEC9iU5LyRjP2Vxs8SmVhEHGbv2Co6SbGa14Wsxy2qZN0jadrN+Xn5AifORaUbvZcG21/ExcNfWDA==" crossorigin="anonymous"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5sortable/0.10.0/html5sortable.min.js" integrity="sha512-tBlVMq89XaEC9iU5LyRjP2Vxs8SmVhEHGbv2Co6SbGa14Wsxy2qZN0jadrN+Xn5AifORaUbvZcG21/ExcNfWDA==" crossorigin="anonymous"></script>
|
||||||
<script src="{% static 'tesseract.min.js' %}"></script>
|
<script src="{% static 'tesseract.min.js' %}"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.js" integrity="sha512-lbwH47l/tPXJYG9AcFNoJaTMhGvYWhVM9YI43CT+uteTRRaiLCui8snIgyAN8XWgNjNhCqlAUdzZptso6OCoFQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.js" integrity="sha512-lbwH47l/tPXJYG9AcFNoJaTMhGvYWhVM9YI43CT+uteTRRaiLCui8snIgyAN8XWgNjNhCqlAUdzZptso6OCoFQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
|||||||
@@ -20,8 +20,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{% render_table table %}
|
{% render_table table %}
|
||||||
|
|
||||||
<button id="button-select-add-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}}">Add selected questions to
|
<button id="button-select-add-exam"
|
||||||
exam</button>
|
data-exam_json_edit_url="{% url 'generic:generic_exam_json_edit' %}"
|
||||||
<div id="exam-options"></div>
|
data-exam_list_url="{% url 'api-1:'|add:app_name|add:'_user_exams' %}"
|
||||||
|
data-type={{app_name}} data-csrf="{{ csrf_token}}">Add selected questions to
|
||||||
|
exam</button>
|
||||||
|
<div id="exam-options"></div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user