Compare commits
10
Commits
81d4dc72cc
...
3381ce6dc9
| 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"""
|
||||
|
||||
images = []
|
||||
@@ -291,6 +291,9 @@ class AnatomyQuestion(models.Model):
|
||||
if answers:
|
||||
json["answers"] = list(self.get_correct_unstripped_answers())
|
||||
|
||||
#if feedback:
|
||||
# json["feedback"] = self.feedback
|
||||
|
||||
return json
|
||||
|
||||
def get_image_url(self):
|
||||
|
||||
@@ -40,9 +40,25 @@
|
||||
</div>
|
||||
<h1>{{ question.get_primary_answer }}</h1>
|
||||
<h2>{{question.question_type}}</h2>
|
||||
Answers (score): {% for answer in question.answers.all %}
|
||||
{{ answer }} ({{answer.status}}),
|
||||
{% endfor %}
|
||||
<details>
|
||||
<summary>
|
||||
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>
|
||||
Description: {{ question.description }}
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
</div>
|
||||
{% 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>
|
||||
<div id="exam-options"></div>
|
||||
|
||||
|
||||
+15
-15
@@ -775,20 +775,20 @@ class ExamDelete(AuthorOrCheckerRequiredMixin, ExamDeleteBase):
|
||||
|
||||
|
||||
|
||||
class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
|
||||
# queryset = Exam.objects.all().order_by('name')
|
||||
serializer_class = ExamSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
This view should return a list exams available to a user.
|
||||
"""
|
||||
user = self.request.user
|
||||
if user.groups.filter(name="anatomy_checker").exists():
|
||||
return Exam.objects.all()
|
||||
|
||||
return Exam.objects.filter(author__id=user.id)
|
||||
#class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
|
||||
# # queryset = Exam.objects.all().order_by('name')
|
||||
# serializer_class = ExamSerializer
|
||||
# permission_classes = [permissions.IsAuthenticated]
|
||||
#
|
||||
# def get_queryset(self):
|
||||
# """
|
||||
# This view should return a list exams available to a user.
|
||||
# """
|
||||
# user = self.request.user
|
||||
# if user.groups.filter(name="anatomy_checker").exists():
|
||||
# return Exam.objects.all()
|
||||
#
|
||||
# return Exam.objects.filter(author__id=user.id)
|
||||
|
||||
|
||||
class ExamAuthorUpdate(RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView):
|
||||
@@ -815,7 +815,7 @@ class StructureAutocomplete(autocomplete.Select2QuerySetView):
|
||||
if self.q:
|
||||
# This raises a fielderror which breaks creating a new item if not caught
|
||||
try:
|
||||
qs = qs.filter(name__istartswith=self.q)
|
||||
qs = qs.filter(structure__icontains=self.q)
|
||||
except FieldError:
|
||||
return Structure.objects.none()
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@ from .models import (
|
||||
Presentation,
|
||||
)
|
||||
|
||||
import tagulous.admin
|
||||
|
||||
from django.forms import ModelForm
|
||||
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ from atlas.models import (
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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.shortcuts import get_object_or_404
|
||||
from django.utils import timezone
|
||||
import tagulous
|
||||
import tagulous.models
|
||||
|
||||
|
||||
import pydicom
|
||||
|
||||
+4
-4
@@ -5,8 +5,8 @@ from django.contrib import admin
|
||||
from .models import (
|
||||
CidUserGroup,
|
||||
Examination,
|
||||
Sign,
|
||||
Condition,
|
||||
#Sign,
|
||||
#Condition,
|
||||
Plane,
|
||||
Contrast,
|
||||
QuestionNote,
|
||||
@@ -22,9 +22,9 @@ from .models import (
|
||||
# from .models import Examination, Sign, Site, Condition
|
||||
|
||||
admin.site.register(Examination)
|
||||
admin.site.register(Sign)
|
||||
#admin.site.register(Sign)
|
||||
admin.site.register(Site)
|
||||
admin.site.register(Condition)
|
||||
#admin.site.register(Condition)
|
||||
admin.site.register(Plane)
|
||||
admin.site.register(Contrast)
|
||||
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.http import Http404
|
||||
from .constants import Group
|
||||
|
||||
from functools import wraps
|
||||
|
||||
def user_is_cid_user_manager(function):
|
||||
def wrap(request, *args, **kwargs):
|
||||
@@ -14,3 +18,21 @@ def user_is_cid_user_manager(function):
|
||||
wrap.__doc__ = function.__doc__
|
||||
wrap.__name__ = function.__name__
|
||||
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 reversion.views import RevisionMixin
|
||||
|
||||
import tagulous
|
||||
import tagulous.models
|
||||
|
||||
from sortedm2m.fields import SortedManyToManyField
|
||||
from django.conf import settings
|
||||
|
||||
@@ -67,13 +64,6 @@ class Site(models.Model):
|
||||
return self.short_code
|
||||
|
||||
|
||||
class Condition(tagulous.models.TagModel):
|
||||
pass
|
||||
|
||||
|
||||
class Sign(tagulous.models.TagModel):
|
||||
pass
|
||||
|
||||
class QuestionBase(models.Model):
|
||||
|
||||
authors_only = models.BooleanField(
|
||||
@@ -203,6 +193,8 @@ class ExamBase(models.Model):
|
||||
def get_authors(self):
|
||||
"""Returns a comma seperated text list of authors"""
|
||||
authors = ", ".join([i.username for i in self.author.all()])
|
||||
if not authors:
|
||||
return "None"
|
||||
return authors
|
||||
|
||||
def get_author_objects(self):
|
||||
|
||||
@@ -13,7 +13,15 @@
|
||||
<h4 class="exam-number-title">{{filter.qs|length}} exams found.</h4>
|
||||
{% for exam in filter.qs %}
|
||||
<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 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 %}
|
||||
@@ -23,3 +31,16 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block css %}
|
||||
|
||||
<style>
|
||||
.authors {
|
||||
float: right;
|
||||
opacity: 50%;}
|
||||
|
||||
|
||||
</style>
|
||||
{% endblock css %}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<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 %}
|
||||
<div class="alert alert-info" role="alert">{{errors}}</a></div>
|
||||
{% 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.detail import DetailView
|
||||
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 reversion.views import RevisionMixin
|
||||
from atlas.models import CaseCollection
|
||||
@@ -195,10 +195,10 @@ def generic_exam_json_edit(request):
|
||||
if request.method == "POST":
|
||||
question_type = request.POST.get("type")
|
||||
|
||||
if question_type == "rapid":
|
||||
if question_type == "rapids":
|
||||
Exam = RapidExam
|
||||
Question = RapidQuestion
|
||||
elif question_type == "long":
|
||||
elif question_type == "longs":
|
||||
Exam = LongExam
|
||||
Question = LongQuestion
|
||||
elif question_type == "anatomy":
|
||||
@@ -320,7 +320,7 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
# group_map = {"rapids" : "rapid_checker", "anatomy" : "anatomy_checker", "longs":"long_checker"}
|
||||
# exam_group = group_map[self.app_name]
|
||||
|
||||
class ExamFilter(FilterSet):
|
||||
class BasicExamFilter(FilterSet):
|
||||
sort_order = OrderingFilter(
|
||||
fields=(("name", "name"), ("exam_mode", "exam_mode"))
|
||||
)
|
||||
@@ -333,22 +333,7 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
"active": ["exact"],
|
||||
"archive": ["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):
|
||||
# if filterset is bound, use initial values as defaults
|
||||
@@ -365,7 +350,13 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
|
||||
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
|
||||
def check_user_access(self, user: User, exam_id: int = None):
|
||||
@@ -1264,10 +1255,11 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
)
|
||||
):
|
||||
exams = self.Exam.objects.all()
|
||||
filter = self.ExtraExamFilter(request.GET, queryset=exams)
|
||||
else:
|
||||
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(
|
||||
request,
|
||||
|
||||
+2
-6
@@ -1,8 +1,6 @@
|
||||
from django.contrib import admin
|
||||
from .models import AnswerMarks, Long, LongSeries, LongSeriesImage, LongCreationDefault, Exam, CidUserAnswer
|
||||
|
||||
import tagulous.admin
|
||||
|
||||
from django.forms import ModelForm
|
||||
|
||||
from reversion.admin import VersionAdmin
|
||||
@@ -11,7 +9,7 @@ from django.forms.widgets import RadioSelect
|
||||
from tinymce.widgets import TinyMCE
|
||||
|
||||
# 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.
|
||||
# admin.site.register(Examination)
|
||||
@@ -44,7 +42,7 @@ class LongAdminForm(ModelForm):
|
||||
fields = [
|
||||
"description",
|
||||
"history",
|
||||
"sign",
|
||||
#"sign",
|
||||
"feedback",
|
||||
"mark_scheme",
|
||||
"model_observations",
|
||||
@@ -88,8 +86,6 @@ class LongSeriesAdmin(VersionAdmin):
|
||||
admin.site.register(LongSeries, LongSeriesAdmin)
|
||||
|
||||
admin.site.register(Exam, ExamAdmin)
|
||||
# tagulous.admin.register(Long.condition)
|
||||
|
||||
|
||||
#@admin.register(AnswerMarks)
|
||||
#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 generic.models import Examination, Condition, Sign
|
||||
from generic.models import Examination#, Condition, Sign
|
||||
|
||||
# from generic.models import Examination, Site, Condition, Sign
|
||||
|
||||
@@ -201,8 +201,8 @@ class LongForm(ModelForm):
|
||||
"description",
|
||||
"history",
|
||||
"feedback",
|
||||
"condition",
|
||||
"sign",
|
||||
#"condition",
|
||||
#"sign",
|
||||
"model_observations",
|
||||
"model_interpretation",
|
||||
"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.shortcuts import get_object_or_404
|
||||
from django.utils import timezone
|
||||
import tagulous
|
||||
import tagulous.models
|
||||
|
||||
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
@@ -34,8 +32,8 @@ from generic.models import (
|
||||
CidUserGroup,
|
||||
ExamUserStatus,
|
||||
Examination,
|
||||
Condition,
|
||||
Sign,
|
||||
#Condition,
|
||||
#Sign,
|
||||
ExamBase,
|
||||
Plane,
|
||||
Contrast,
|
||||
@@ -90,15 +88,16 @@ class Long(models.Model):
|
||||
|
||||
feedback = models.TextField(null=True, blank=True)
|
||||
|
||||
condition = tagulous.models.TagField(
|
||||
to=Condition,
|
||||
blank=True,
|
||||
help_text='Associated condition. Will allow searching / filtering and tips / hints to be displayed. Conditions with spaces must be enclosed in quotes "..."',
|
||||
)
|
||||
# TODO: merge with atlas condition / signs / finding
|
||||
#condition = tagulous.models.TagField(
|
||||
# to=Condition,
|
||||
# blank=True,
|
||||
# help_text='Associated condition. Will allow searching / filtering and tips / hints to be displayed. Conditions with spaces must be enclosed in quotes "..."',
|
||||
#)
|
||||
|
||||
sign = tagulous.models.TagField(
|
||||
to=Sign, blank=True, help_text="Radiological signs in the question"
|
||||
)
|
||||
#sign = tagulous.models.TagField(
|
||||
# to=Sign, blank=True, help_text="Radiological signs in the question"
|
||||
#)
|
||||
|
||||
# Model answers
|
||||
model_observations = models.TextField(null=True, blank=True)
|
||||
|
||||
+3
-3
@@ -1135,9 +1135,9 @@ class ExamAuthorUpdate(RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, U
|
||||
form_class = ExamAuthorForm
|
||||
|
||||
|
||||
class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
|
||||
queryset = Exam.objects.all().order_by("name")
|
||||
serializer_class = ExamSerializer
|
||||
#class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
|
||||
# queryset = Exam.objects.all().order_by("name")
|
||||
# serializer_class = ExamSerializer
|
||||
|
||||
|
||||
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.staticfiles",
|
||||
"debug_toolbar",
|
||||
"tagulous",
|
||||
"dbbackup",
|
||||
"rest_framework",
|
||||
"tinymce",
|
||||
"django_unused_media",
|
||||
"django_htmx",
|
||||
"crispy_forms",
|
||||
"crispy_bootstrap4",
|
||||
]
|
||||
|
||||
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
|
||||
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
||||
|
||||
@@ -193,15 +186,6 @@ MEDIA_ROOT = "media/"
|
||||
LOGIN_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"]
|
||||
|
||||
#LOGGING = {
|
||||
@@ -299,7 +283,8 @@ EMAIL_USE_SSL = True
|
||||
|
||||
ADMINS = [("Ross","ross@xkjq.uk")]
|
||||
|
||||
CRISPY_TEMPLATE_PACK = 'bootstrap'
|
||||
CRISPY_ALLOWED_TEMPLATE_PACK = 'bootstrap4'
|
||||
CRISPY_TEMPLATE_PACK = 'bootstrap4'
|
||||
|
||||
DEBUG_CONTAINER = False
|
||||
|
||||
|
||||
+21
-18
@@ -33,25 +33,28 @@ from rest_framework import routers
|
||||
|
||||
from django.conf.urls import handler400, handler403, handler404, handler500
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
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.register(
|
||||
r"rapids/answers",
|
||||
rapid_views.QuestionAnswerViewSet,
|
||||
basename="rapid-question-answers",
|
||||
)
|
||||
router.register(
|
||||
r"rapids/user_answer",
|
||||
rapid_views.UserAnswerViewSet,
|
||||
basename="rapid-question-answers",
|
||||
)
|
||||
router.register(r"rapids", rapid_views.RapidViewSet, basename="rapid")
|
||||
# router.register(r'rapids/laterality', rapid_views.RapidLateralityViewSet, basename="rapid")
|
||||
from .api import api
|
||||
|
||||
|
||||
router_drf = routers.DefaultRouter()
|
||||
#router_drf.register(r"rapids/exams", rapid_views.ExamViewSet, basename="rapid-exam")
|
||||
#router_drf.register(r"anatomy/exams", anatomy_views.ExamViewSet, basename="anatomy-exam")
|
||||
#router_drf.register(r"longs/exams", long_views.ExamViewSet, basename="long-exam")
|
||||
#router_drf.register(
|
||||
# r"rapids/answers",
|
||||
# rapid_views.QuestionAnswerViewSet,
|
||||
# basename="rapid-question-answers",
|
||||
#)
|
||||
#router_drf.register(
|
||||
# r"rapids/user_answer",
|
||||
# rapid_views.UserAnswerViewSet,
|
||||
# basename="rapid-question-answers",
|
||||
#)
|
||||
#router_drf.register(r"rapids", rapid_views.RapidViewSet, basename="rapid")
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls, name="admin"),
|
||||
path("api/", api.urls),
|
||||
path("anatomy/", include("anatomy.urls"), name="anatomy"),
|
||||
path("physics/", include("physics.urls"), name="physics"),
|
||||
# path("sbas/", include("sbas.urls"), name="sbas"),
|
||||
@@ -139,8 +142,8 @@ urlpatterns = [
|
||||
name="answer_suggestion_confirm",
|
||||
),
|
||||
path("feedback/view", views.view_feedback, name="view_feedback"),
|
||||
path("api/", include(router.urls)),
|
||||
path("api-auth/", include("rest_framework.urls")),
|
||||
#path("api/", include(router_drf.urls)),
|
||||
#path("api-auth/", include("rest_framework.urls")),
|
||||
path("tinymce/", include("tinymce.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
|
||||
return context
|
||||
|
||||
|
||||
class DeleteUserView(CidManagerRequiredMixin, DeleteView):
|
||||
model = User
|
||||
template_name: str = "confirm_delete.html"
|
||||
success_url = reverse_lazy("accounts_list")
|
||||
|
||||
|
||||
class UpdateUserView(CidManagerRequiredMixin, UpdateView):
|
||||
model = User
|
||||
fields = ["first_name", "last_name", "email"] # Keep listing whatever fields
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from django.contrib import admin
|
||||
from .models import Rapid, RapidImage, Examination, Site, Abnormality, Region, RapidCreationDefault, Answer, Exam, CidUserAnswer
|
||||
|
||||
import tagulous.admin
|
||||
|
||||
from django.forms import ModelForm
|
||||
|
||||
from reversion.admin import VersionAdmin
|
||||
@@ -73,8 +71,6 @@ class ExamAdmin(VersionAdmin, admin.ModelAdmin):
|
||||
admin.site.register(Rapid, RapidAdmin)
|
||||
admin.site.register(Exam, ExamAdmin)
|
||||
|
||||
#tagulous.admin.register(Rapid.condition)
|
||||
|
||||
class CidUserAnswerAdmin(admin.ModelAdmin):
|
||||
exclude = []
|
||||
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 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():
|
||||
return function(request, *args, **kwargs)
|
||||
else:
|
||||
|
||||
+3
-5
@@ -6,8 +6,6 @@ from django.db import models
|
||||
from django.http.response import JsonResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone
|
||||
import tagulous
|
||||
import tagulous.models
|
||||
|
||||
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
@@ -32,8 +30,8 @@ from generic.models import (
|
||||
CidUserGroup,
|
||||
ExamUserStatus,
|
||||
Site,
|
||||
Condition,
|
||||
Sign,
|
||||
#Condition,
|
||||
#Sign,
|
||||
ExamBase,
|
||||
QuestionNote,
|
||||
UserUserGroup,
|
||||
@@ -513,7 +511,7 @@ class Rapid(models.Model):
|
||||
}
|
||||
|
||||
if annotations:
|
||||
json["annotations"] = self.annotations
|
||||
json["annotations"] = annotations
|
||||
|
||||
if history:
|
||||
json["history"] = self.history
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<p class="pre-whitespace"><b>Rapid:</b> {{ question }}</p>
|
||||
<p class="pre-whitespace"><b>Normal:</b> {{ question.normal }} <button id="toggle-normal-button"
|
||||
class="toggle-button">toggle</button></p>
|
||||
class="toggle-button">toggle</button></p>
|
||||
<p class="pre-whitespace"><b>Region:</b> {{ question.get_regions }}</p>
|
||||
<p class="pre-whitespace"><b>Examination:</b> {{ question.get_examinations }}</p>
|
||||
<p class="pre-whitespace"><b>Laterality:</b> {{ question.laterality }}
|
||||
@@ -21,20 +21,20 @@
|
||||
<p class="pre-whitespace"><b>Abnormality:</b> {{ question.get_abnormalities }}</p>
|
||||
<div class="pre-whitespace multi-image-block"><b>Images:</b>
|
||||
{% for image in question.images.all %}
|
||||
<span class="image-block">
|
||||
Image {{ forloop.counter }}{% if image.description %} ({{image.description}}){% endif %}{% if image.feedback_image %} [feedback image]{% 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>
|
||||
</span>
|
||||
<span class="image-block">
|
||||
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 %}"
|
||||
data-url="https://www.penracourses.org.uk{{ image.image.url}}"></div>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
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 %}
|
||||
|
||||
<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>
|
||||
<button id="cancel-add-to-exam" style="display:none">Cancel</button>
|
||||
<span id="exam-options"></span>
|
||||
@@ -43,36 +43,49 @@
|
||||
<p class="pre-whitespace"><b>Feedback:</b> {{ question.feedback }}</p>
|
||||
<p class="pre-whitespace"><b>History:</b> {{ question.history }}</p>
|
||||
<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
|
||||
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
|
||||
href="{% url 'rapids:rapid_scrap' pk=question.pk %}">(toggle)</a></p> {% endcomment %}
|
||||
<p class="pre-whitespace">
|
||||
Answers (score): {% for answer in question.answers.all %}
|
||||
<span {% if answer.proposed %}class="proposed-answer" data-aid="{{answer.pk}}" data-question-type="rapid"
|
||||
{% endif %}>
|
||||
{{ answer }} ({{answer.status}}),
|
||||
</span>
|
||||
{% endfor %}
|
||||
<details>
|
||||
<summary>
|
||||
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="rapid"
|
||||
{% endif %}>
|
||||
{{ answer }}
|
||||
</span>
|
||||
<td>
|
||||
<td>{{answer.status}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</details>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
{% if view_feedback %}
|
||||
{% include 'question_notes.html' %}
|
||||
{% if not question.normal %}
|
||||
<details>
|
||||
<summary>
|
||||
Suggested answers
|
||||
</summary>
|
||||
<ul class="suggested_answers">
|
||||
{% for ans in question.get_suggested_answers %}
|
||||
<li data-string="{{ans}}">{{ans}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
{% endif %}
|
||||
{% include 'question_notes.html' %}
|
||||
{% if not question.normal %}
|
||||
<details>
|
||||
<summary>
|
||||
Suggested answers
|
||||
</summary>
|
||||
<ul class="suggested_answers">
|
||||
{% for ans in question.get_suggested_answers %}
|
||||
<li data-string="{{ans}}">{{ans}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -91,13 +104,13 @@
|
||||
Image annotations:
|
||||
</summary>
|
||||
{% for image in question.images.all %}
|
||||
<span class="image-block">
|
||||
Image {{ forloop.counter }}: {{image.image_annotations}}<br />
|
||||
</span>
|
||||
<span class="image-block">
|
||||
Image {{ forloop.counter }}: {{image.image_annotations}}<br />
|
||||
</span>
|
||||
{% endfor %}
|
||||
</details>
|
||||
<a href="{% url 'rapids:question_anonymise_dicom' pk=question.pk %}"
|
||||
title="Anonymise dicom images">Anonymise dicoms</a><br />
|
||||
<a href="{% url 'rapids:question_anonymise_dicom' pk=question.pk %}"
|
||||
title="Anonymise dicom images">Anonymise dicoms</a><br />
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$("#edit-button").click(() => {
|
||||
@@ -127,34 +140,34 @@
|
||||
console.log("json_toolstates", json_toolstates)
|
||||
|
||||
$.ajax({
|
||||
url: "{% url 'rapids:question_save_annotation' pk=question.pk %}",
|
||||
data: {
|
||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||
// Yes we do double encode the json....
|
||||
annotation: JSON.stringify(json_toolstates),
|
||||
},
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
error: function (e) {
|
||||
toastr.warning(`Error saving annotations`)
|
||||
console.log(e);
|
||||
},
|
||||
url: "{% url 'rapids:question_save_annotation' pk=question.pk %}",
|
||||
data: {
|
||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||
// Yes we do double encode the json....
|
||||
annotation: JSON.stringify(json_toolstates),
|
||||
},
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
error: function (e) {
|
||||
toastr.warning(`Error saving annotations`)
|
||||
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) {
|
||||
console.log(data);
|
||||
// show some message according to the response.
|
||||
// For eg. A message box showing that the status has been changed
|
||||
if (data.status == "success") {
|
||||
toastr.info('Annotations saved')
|
||||
} else {
|
||||
toastr.warning('Error saving annotations')
|
||||
}
|
||||
})
|
||||
console.log(data);
|
||||
// show some message according to the response.
|
||||
// For eg. A message box showing that the status has been changed
|
||||
if (data.status == "success") {
|
||||
toastr.info('Annotations saved')
|
||||
} else {
|
||||
toastr.warning('Error saving annotations')
|
||||
}
|
||||
})
|
||||
.always(function () {
|
||||
console.log('[Done]');
|
||||
})
|
||||
console.log('[Done]');
|
||||
})
|
||||
})
|
||||
|
||||
$("#cancel-add-to-exam").click(function (evt) {
|
||||
@@ -168,28 +181,28 @@
|
||||
$("#cancel-add-to-exam").toggle();
|
||||
|
||||
var jqxhr = $.get(evt.target.dataset.exam_list_url, function (data) {
|
||||
console.log(data);
|
||||
$("#exam-options").empty();
|
||||
data.forEach((obj, n) => {
|
||||
$("#exam-options").append($(document.createElement('button')).prop({
|
||||
type: 'button',
|
||||
innerHTML: obj.name,
|
||||
class: '',
|
||||
console.log(data);
|
||||
$("#exam-options").empty();
|
||||
data.forEach((obj, n) => {
|
||||
$("#exam-options").append($(document.createElement('button')).prop({
|
||||
type: 'button',
|
||||
innerHTML: obj.name,
|
||||
class: '',
|
||||
|
||||
}).data("exam-id", obj.id).click((evt) => {
|
||||
addToExam(evt)
|
||||
}))
|
||||
})
|
||||
}).data("exam-id", obj.id).click((evt) => {
|
||||
addToExam(evt)
|
||||
}))
|
||||
})
|
||||
})
|
||||
.done(function () {
|
||||
//alert( "second success" );
|
||||
})
|
||||
//alert( "second success" );
|
||||
})
|
||||
.fail(function () {
|
||||
//alert( "error" );
|
||||
})
|
||||
//alert( "error" );
|
||||
})
|
||||
.always(function () {
|
||||
//alert( "finished" );
|
||||
});
|
||||
//alert( "finished" );
|
||||
});
|
||||
|
||||
return;
|
||||
|
||||
@@ -200,7 +213,7 @@
|
||||
let ids = [];
|
||||
ids.push(exam_button.dataset.qid);
|
||||
|
||||
// if no question selected
|
||||
// if no question selected
|
||||
if (ids.length < 1) {
|
||||
toastr.info('No question selected.');
|
||||
return
|
||||
@@ -209,28 +222,28 @@
|
||||
let type = exam_button.dataset.type;
|
||||
let csrf = exam_button.dataset.csrf;
|
||||
$.ajax({
|
||||
url: post_url,
|
||||
data: {
|
||||
csrfmiddlewaretoken: csrf,
|
||||
add_exam_questions: JSON.stringify(ids),
|
||||
type: type,
|
||||
exam_id: $(evt.target).data("exam-id"),
|
||||
},
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
})
|
||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||
url: post_url,
|
||||
data: {
|
||||
csrfmiddlewaretoken: csrf,
|
||||
add_exam_questions: JSON.stringify(ids),
|
||||
type: type,
|
||||
exam_id: $(evt.target).data("exam-id"),
|
||||
},
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
})
|
||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||
.done(function (data) {
|
||||
console.log(data);
|
||||
console.log(data);
|
||||
|
||||
if (data.status == "success") {
|
||||
toastr.info('Questions added to exams.')
|
||||
}
|
||||
})
|
||||
if (data.status == "success") {
|
||||
toastr.info('Questions added to exams.')
|
||||
}
|
||||
})
|
||||
.always(function () {
|
||||
console.log('[Done]');
|
||||
$("#exam-options").empty();
|
||||
})
|
||||
console.log('[Done]');
|
||||
$("#exam-options").empty();
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
@@ -241,6 +254,29 @@
|
||||
|
||||
$("#toggle-normal-button").click(function () {
|
||||
$.ajax({
|
||||
url: "{% url 'rapid-detail' question.id %}",
|
||||
type: 'PATCH',
|
||||
headers: {
|
||||
"X-CSRFToken": "{{ csrf_token }}"
|
||||
},
|
||||
timeout: 3000,
|
||||
data: {
|
||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||
normal: n,
|
||||
}
|
||||
})
|
||||
.done(function (data) {
|
||||
console.log(data);
|
||||
toastr.info('Answer updated')
|
||||
})
|
||||
.fail(function () {
|
||||
alert('Error updating this model instance.');
|
||||
});
|
||||
});
|
||||
|
||||
$(".toggle-laterality-button").each((n, el) => {
|
||||
$(el).click(function () {
|
||||
$.ajax({
|
||||
url: "{% url 'rapid-detail' question.id %}",
|
||||
type: 'PATCH',
|
||||
headers: {
|
||||
@@ -249,71 +285,48 @@
|
||||
timeout: 3000,
|
||||
data: {
|
||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||
normal: n,
|
||||
laterality: el.dataset.lat,
|
||||
}
|
||||
})
|
||||
.done(function (data) {
|
||||
.done(function (data) {
|
||||
console.log(data);
|
||||
toastr.info('Answer updated')
|
||||
})
|
||||
.fail(function () {
|
||||
.fail(function () {
|
||||
alert('Error updating this model instance.');
|
||||
});
|
||||
});
|
||||
|
||||
$(".toggle-laterality-button").each((n, el) => {
|
||||
$(el).click(function () {
|
||||
$.ajax({
|
||||
url: "{% url 'rapid-detail' question.id %}",
|
||||
type: 'PATCH',
|
||||
headers: {
|
||||
"X-CSRFToken": "{{ csrf_token }}"
|
||||
},
|
||||
timeout: 3000,
|
||||
data: {
|
||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||
laterality: el.dataset.lat,
|
||||
}
|
||||
})
|
||||
.done(function (data) {
|
||||
console.log(data);
|
||||
toastr.info('Answer updated')
|
||||
})
|
||||
.fail(function () {
|
||||
alert('Error updating this model instance.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$(".suggested_answers li").each((n, el) => {
|
||||
$(el).append($("<span class='correct'>[Add Correct]</span>").on("click", function () {
|
||||
$.ajax({
|
||||
url: "{% url 'answer_submit' %}",
|
||||
data: {
|
||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||
//active: this.checked // true if checked else false
|
||||
question_type: "rapid",
|
||||
qid: "{{question.pk}}",
|
||||
status: 2,
|
||||
answer: el.dataset.string,
|
||||
},
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
})
|
||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||
url: "{% url 'answer_submit' %}",
|
||||
data: {
|
||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||
//active: this.checked // true if checked else false
|
||||
question_type: "rapid",
|
||||
qid: "{{question.pk}}",
|
||||
status: 2,
|
||||
answer: el.dataset.string,
|
||||
},
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
})
|
||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||
.done(function (data) {
|
||||
console.log(data);
|
||||
console.log(data);
|
||||
|
||||
if (data.success) {
|
||||
toastr.info('Answer saved')
|
||||
$(el).find(".correct").remove()
|
||||
}
|
||||
// show some message according to the response.
|
||||
// For eg. A message box showing that the status has been changed
|
||||
})
|
||||
if (data.success) {
|
||||
toastr.info('Answer saved')
|
||||
$(el).find(".correct").remove()
|
||||
}
|
||||
// show some message according to the response.
|
||||
// For eg. A message box showing that the status has been changed
|
||||
})
|
||||
.always(function () {
|
||||
console.log('[Done]');
|
||||
})
|
||||
console.log('[Done]');
|
||||
})
|
||||
}))
|
||||
|
||||
});
|
||||
@@ -324,63 +337,63 @@
|
||||
// Add button to confirm answer is correct
|
||||
$(el).append($("<span class='confirm'>[Add Correct]</span>").on("click", function () {
|
||||
$.ajax({
|
||||
url: "{% url 'answer_suggestion_confirm' %}",
|
||||
data: {
|
||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||
//active: this.checked // true if checked else false
|
||||
question_type: "rapid",
|
||||
aid: el.dataset.aid,
|
||||
status: 2,
|
||||
},
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
})
|
||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||
url: "{% url 'answer_suggestion_confirm' %}",
|
||||
data: {
|
||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||
//active: this.checked // true if checked else false
|
||||
question_type: "rapid",
|
||||
aid: el.dataset.aid,
|
||||
status: 2,
|
||||
},
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
})
|
||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||
.done(function (data) {
|
||||
console.log(data);
|
||||
console.log(data);
|
||||
|
||||
if (data.success) {
|
||||
toastr.info('Answer saved')
|
||||
$(el).find(".confirm").remove()
|
||||
$(el).removeClass("proposed-answer")
|
||||
}
|
||||
// show some message according to the response.
|
||||
// For eg. A message box showing that the status has been changed
|
||||
})
|
||||
if (data.success) {
|
||||
toastr.info('Answer saved')
|
||||
$(el).find(".confirm").remove()
|
||||
$(el).removeClass("proposed-answer")
|
||||
}
|
||||
// show some message according to the response.
|
||||
// For eg. A message box showing that the status has been changed
|
||||
})
|
||||
.always(function () {
|
||||
console.log('[Done]');
|
||||
})
|
||||
console.log('[Done]');
|
||||
})
|
||||
}))
|
||||
|
||||
// Add button to confirm answer is incorrect
|
||||
$(el).append($("<span class='confirm'>[Incorrect]</span>").on("click", function () {
|
||||
$.ajax({
|
||||
url: "{% url 'answer_suggestion_confirm' %}",
|
||||
data: {
|
||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||
//active: this.checked // true if checked else false
|
||||
question_type: "rapid",
|
||||
aid: el.dataset.aid,
|
||||
status: 0,
|
||||
},
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
})
|
||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||
url: "{% url 'answer_suggestion_confirm' %}",
|
||||
data: {
|
||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||
//active: this.checked // true if checked else false
|
||||
question_type: "rapid",
|
||||
aid: el.dataset.aid,
|
||||
status: 0,
|
||||
},
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
})
|
||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||
.done(function (data) {
|
||||
console.log(data);
|
||||
console.log(data);
|
||||
|
||||
if (data.success) {
|
||||
toastr.info('Answer saved')
|
||||
$(el).find(".confirm").remove()
|
||||
$(el).removeClass("proposed-answer")
|
||||
}
|
||||
// show some message according to the response.
|
||||
// For eg. A message box showing that the status has been changed
|
||||
})
|
||||
if (data.success) {
|
||||
toastr.info('Answer saved')
|
||||
$(el).find(".confirm").remove()
|
||||
$(el).removeClass("proposed-answer")
|
||||
}
|
||||
// show some message according to the response.
|
||||
// For eg. A message box showing that the status has been changed
|
||||
})
|
||||
.always(function () {
|
||||
console.log('[Done]');
|
||||
})
|
||||
console.log('[Done]');
|
||||
})
|
||||
}))
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,23 +6,26 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div id="view-filter-options">
|
||||
<h3>Filter Rapids</h3>
|
||||
<form action="" method="get">
|
||||
{{ filter.form }}
|
||||
<input class="btn btn-primary btn-sm mt-1 mb-1" type="submit" />
|
||||
</form>
|
||||
</div>
|
||||
{% render_table table %}
|
||||
<div id="view-filter-options">
|
||||
<h3>Filter Rapids</h3>
|
||||
<form action="" method="get">
|
||||
{{ filter.form }}
|
||||
<input class="btn btn-primary btn-sm mt-1 mb-1" type="submit" />
|
||||
</form>
|
||||
</div>
|
||||
{% 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
|
||||
exam</button>
|
||||
<div id="exam-options"></div>
|
||||
<button id="button-select-add-exam"
|
||||
data-exam_json_edit_url="{% url 'generic:generic_exam_json_edit' %}"
|
||||
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">
|
||||
$(document).ready(function () {
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function () {
|
||||
|
||||
|
||||
});
|
||||
</script>
|
||||
});
|
||||
</script>
|
||||
{% 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')
|
||||
|
||||
|
||||
class QuestionAnswerViewSet(viewsets.ModelViewSet):
|
||||
queryset = Answer.objects.all() # .order_by('name')
|
||||
serializer_class = QuestionAnswerSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
#class QuestionAnswerViewSet(viewsets.ModelViewSet):
|
||||
# queryset = Answer.objects.all() # .order_by('name')
|
||||
# serializer_class = QuestionAnswerSerializer
|
||||
# permission_classes = [permissions.IsAuthenticated]
|
||||
#
|
||||
#
|
||||
#class UserAnswerViewSet(viewsets.ModelViewSet):
|
||||
# queryset = CidUserAnswer.objects.all() # .order_by('name')
|
||||
# serializer_class = UserAnswerSerializer
|
||||
# permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
|
||||
class UserAnswerViewSet(viewsets.ModelViewSet):
|
||||
queryset = CidUserAnswer.objects.all() # .order_by('name')
|
||||
serializer_class = UserAnswerSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
#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 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):
|
||||
# model = QuestionNote
|
||||
|
||||
+3
-2
@@ -3,7 +3,6 @@ Django==4.1.4
|
||||
django_debug_toolbar
|
||||
django_jquery
|
||||
django_reversion
|
||||
django_tagulous
|
||||
Pillow
|
||||
django-cors-headers
|
||||
plotly
|
||||
@@ -34,4 +33,6 @@ bs4
|
||||
django-htmx
|
||||
debugpy
|
||||
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.
|
||||
from .models import Question, Exam, CidUserAnswer, Category
|
||||
|
||||
import tagulous.admin
|
||||
|
||||
from django.forms import ModelForm
|
||||
|
||||
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="{% 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="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 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="{% 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="{% 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>
|
||||
|
||||
@@ -20,8 +20,11 @@
|
||||
</div>
|
||||
{% 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
|
||||
exam</button>
|
||||
<div id="exam-options"></div>
|
||||
<button id="button-select-add-exam"
|
||||
data-exam_json_edit_url="{% url 'generic:generic_exam_json_edit' %}"
|
||||
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 %}
|
||||
Reference in New Issue
Block a user