Compare commits

..
10 Commits
Author SHA1 Message Date
Ross 3381ce6dc9 remove tagulous 2023-04-03 10:22:20 +01:00
Ross e5ff3ed4dc start removing tagulous 2023-04-03 10:06:05 +01:00
Ross 344fd1695a remove more unused DRF stuff 2023-04-03 09:38:07 +01:00
Ross 2b6ac79439 FIx exam reviewing 2023-02-27 15:04:10 +00:00
Ross fcf22fdd73 start migrating from DRF to ninja 2023-02-27 14:49:25 +00:00
Ross 6b6d909038 upgrade cripsy forms 2023-02-27 14:49:04 +00:00
Ross bf670d355f stage starting django ninja 2023-02-27 11:24:08 +00:00
Ross 732ba1b25b Improve answers display on question pages 2023-02-27 10:02:03 +00:00
Ross b14b1c20bd Display and filter authors on index pages 2023-02-20 14:10:51 +00:00
Ross fc40aacd22 Fix structure autocompletion 2023-02-20 14:09:53 +00:00
38 changed files with 676 additions and 383 deletions
+53
View File
@@ -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
View File
@@ -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):
+19 -3
View File
@@ -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>
+5 -1
View File
@@ -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
View File
@@ -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()
-2
View File
@@ -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
View File
@@ -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'),
),
]
-2
View File
@@ -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
View File
@@ -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)
+7
View File
@@ -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"
+22
View File
@@ -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
View File
@@ -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):
+22 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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):
+53
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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")),
+2
View File
@@ -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
-4
View File
@@ -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"]
+54
View File
@@ -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')
+8 -1
View File
@@ -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
View File
@@ -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;
@@ -200,7 +213,7 @@
let ids = []; let ids = [];
ids.push(exam_button.dataset.qid); ids.push(exam_button.dataset.qid);
// if no question selected // if no question selected
if (ids.length < 1) { if (ids.length < 1) {
toastr.info('No question selected.'); toastr.info('No question selected.');
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]');
}) })
})) }))
}); });
}); });
+18 -15
View File
@@ -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
View File
@@ -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
+3 -2
View File
@@ -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
@@ -34,4 +33,6 @@ bs4
django-htmx 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
-2
View File
@@ -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
-2
View File
@@ -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>
+6 -3
View File
@@ -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 %}