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"""
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):
+18 -2
View File
@@ -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}}),
<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>
+5 -1
View File
@@ -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
View File
@@ -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()
-2
View File
@@ -15,8 +15,6 @@ from .models import (
Presentation,
)
import tagulous.admin
from django.forms import ModelForm
from reversion.admin import VersionAdmin
+1 -1
View File
@@ -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'),
),
]
-2
View File
@@ -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
View File
@@ -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)
+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.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
View File
@@ -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):
+22 -1
View File
@@ -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
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.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
View File
@@ -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):
+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 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
View File
@@ -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
View File
@@ -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
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.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
View File
@@ -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")),
+2
View File
@@ -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
-4
View File
@@ -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"]
+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 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
View File
@@ -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
@@ -34,7 +34,7 @@
{% 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>
@@ -49,12 +49,25 @@
{% 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 %}
<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 }} ({{answer.status}}),
{{ answer }}
</span>
<td>
<td>{{answer.status}}</td>
</tr>
{% endfor %}
</table>
</details>
</p>
</div>
<div>
+4 -1
View File
@@ -15,7 +15,10 @@
</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
<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>
+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')
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
+2 -1
View File
@@ -3,7 +3,6 @@ Django==4.1.4
django_debug_toolbar
django_jquery
django_reversion
django_tagulous
Pillow
django-cors-headers
plotly
@@ -35,3 +34,5 @@ django-htmx
debugpy
git+https://github.com/jazzband/django-cookie-consent#django-cookie-consent
django-crispy-forms
django-ninja
crispy-bootstrap4
-2
View File
@@ -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
-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="{% 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>
+4 -1
View File
@@ -20,7 +20,10 @@
</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
<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>