Merge branch 'master' of ssh://git.xkjq.uk:30001/ross/rad
This commit is contained in:
@@ -156,7 +156,6 @@ AnswerFormSet = inlineformset_factory(
|
||||
can_delete=True,
|
||||
extra=1,
|
||||
max_num=10,
|
||||
field_classes="testing",
|
||||
)
|
||||
|
||||
AnswerUpdateFormSet = inlineformset_factory(
|
||||
@@ -172,7 +171,6 @@ AnswerUpdateFormSet = inlineformset_factory(
|
||||
can_delete=True,
|
||||
extra=0,
|
||||
max_num=10,
|
||||
field_classes="testing",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.1.2 on 2021-01-18 13:39
|
||||
|
||||
import anatomy.models
|
||||
import django.core.files.storage
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('anatomy', '0018_auto_20210112_1726'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='anatomyquestion',
|
||||
name='image',
|
||||
field=models.ImageField(storage=django.core.files.storage.FileSystemStorage(base_url='/media/anatomy/', location='/home/ross/sites/rad/media//anatomy/'), upload_to=anatomy.models.image_directory_path),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.1.2 on 2021-01-18 14:02
|
||||
|
||||
import anatomy.models
|
||||
import django.core.files.storage
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('anatomy', '0019_auto_20210118_1339'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='anatomyquestion',
|
||||
name='image',
|
||||
field=models.ImageField(storage=django.core.files.storage.FileSystemStorage(base_url='http://penracourses.org.uk//media/anatomy/', location='/home/ross/sites/rad/media//anatomy/'), upload_to=anatomy.models.image_directory_path),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.1.3 on 2021-01-20 22:06
|
||||
|
||||
import anatomy.models
|
||||
import django.core.files.storage
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('anatomy', '0020_auto_20210118_1402'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='anatomyquestion',
|
||||
name='image',
|
||||
field=models.ImageField(storage=django.core.files.storage.FileSystemStorage(base_url='http://penracourses.org.uk//media/anatomy/', location='/home/ross/scripts/sites/rad/media//anatomy/'), upload_to=anatomy.models.image_directory_path),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.1.3 on 2021-01-21 18:06
|
||||
|
||||
import anatomy.models
|
||||
import django.core.files.storage
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('anatomy', '0021_auto_20210120_2206'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='anatomyquestion',
|
||||
name='image',
|
||||
field=models.ImageField(storage=django.core.files.storage.FileSystemStorage(base_url='/media/anatomy/', location='/home/ross/scripts/sites/rad/media//anatomy/'), upload_to=anatomy.models.image_directory_path),
|
||||
),
|
||||
]
|
||||
@@ -396,4 +396,44 @@ td.user-answer-score-2::after {
|
||||
|
||||
.answer-0 {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
.selector {
|
||||
border-color: #05668d;
|
||||
}
|
||||
|
||||
.selector input {
|
||||
color: gray;
|
||||
background-color: white;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.selector .selector-available h2, .selector .selector-chosen h2 {
|
||||
margin-bottom: 0px;
|
||||
border: none;
|
||||
/* border-color: #05668d; */
|
||||
background-color: #ebf2fa;
|
||||
color: rgb(28, 184, 65);
|
||||
|
||||
}
|
||||
|
||||
.selector .selector-available select, .selector .selector-chosen select {
|
||||
border-color: #05668d;
|
||||
}
|
||||
|
||||
#id_laterality {
|
||||
margin-top: 30px;
|
||||
}
|
||||
#id_laterality li{
|
||||
display: inline-block;
|
||||
border-radius: 4px;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
|
||||
background: rgb(28, 184, 65);
|
||||
padding-right: 10px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.add-popup {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,82 +39,13 @@ $(document).ready(function () {
|
||||
});
|
||||
}
|
||||
|
||||
if ($("#dicom-image").length) {
|
||||
dicom_images = document.getElementsByClassName("dicom-image");
|
||||
if (dicom_images.length) {
|
||||
|
||||
console.log("Dicom element found ", $("#dicom-image"));
|
||||
for (let element of dicom_images) {
|
||||
setUpDicom(element)
|
||||
}
|
||||
|
||||
$('#dicom-image').bind('contextmenu', function (e) {
|
||||
return false;
|
||||
});
|
||||
|
||||
cornerstoneBase64ImageLoader.external.cornerstone = cornerstone;
|
||||
cornerstoneWebImageLoader.external.cornerstone = cornerstone;
|
||||
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
||||
|
||||
cornerstoneTools.init();
|
||||
const PanTool = cornerstoneTools.PanTool;
|
||||
const ZoomTool = cornerstoneTools.ZoomTool;
|
||||
const ZoomMouseWheelTool = cornerstoneTools.ZoomMouseWheelTool;
|
||||
const WwwcTool = cornerstoneTools.WwwcTool;
|
||||
const WwwcRegionTool = cornerstoneTools.WwwcRegionTool;
|
||||
const RotateTool = cornerstoneTools.RotateTool;
|
||||
const StackScrollTool = cornerstoneTools.StackScrollTool;
|
||||
const MagnifyTool = cornerstoneTools.MagnifyTool;
|
||||
const ArrowAnnotateTool = cornerstoneTools.ArrowAnnotateTool;
|
||||
|
||||
|
||||
const element = document.getElementById('dicom-image');
|
||||
const imageId = element.dataset.url;
|
||||
|
||||
console.log("Dicom - load imageId: ", imageId);
|
||||
|
||||
cornerstone.enable(element);
|
||||
cornerstone.loadAndCacheImage(imageId).then(function (image) {
|
||||
cornerstone.displayImage(element, image);
|
||||
|
||||
cornerstoneTools.addTool(PanTool);
|
||||
cornerstoneTools.addTool(ZoomTool);
|
||||
cornerstoneTools.addTool(ZoomMouseWheelTool);
|
||||
cornerstoneTools.addTool(WwwcTool);
|
||||
cornerstoneTools.addTool(WwwcRegionTool);
|
||||
cornerstoneTools.addTool(RotateTool);
|
||||
cornerstoneTools.addTool(StackScrollTool);
|
||||
cornerstoneTools.addTool(MagnifyTool);
|
||||
|
||||
cornerstoneTools.addTool(ArrowAnnotateTool, {
|
||||
configuration: {
|
||||
getTextCallback: () => { },
|
||||
changeTextCallback: () => { },
|
||||
allowEmptyLabel: true,
|
||||
renderDashed: false,
|
||||
drawHandles: false,
|
||||
drawHandlesOnHover: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Enable our tools
|
||||
// Avoid incorrect aspect ratio
|
||||
cornerstoneTools.setToolActive("Pan", { mouseButtonMask: 1 });
|
||||
cornerstoneTools.setToolActive("Wwwc", { mouseButtonMask: 2 });
|
||||
cornerstoneTools.setToolActive("ZoomMouseWheel", { mouseButtonMask: 3 });
|
||||
cornerstoneTools.setToolActive("Zoom", { mouseButtonMask: 4 });
|
||||
|
||||
if (element.dataset.edit_annotation == "true") {
|
||||
cornerstoneTools.setToolActive("ArrowAnnotate", { mouseButtonMask: 2 });
|
||||
} else {
|
||||
cornerstoneTools.setToolEnabled("ArrowAnnotate");
|
||||
}
|
||||
|
||||
if (element.dataset.annotations) {
|
||||
loadJsonToolStateOnCurrentImage(element.dataset.annotations)
|
||||
}
|
||||
|
||||
cornerstone.resize(element);
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
if ($("#question-mark-list").length) {
|
||||
@@ -207,4 +138,86 @@ function loadJsonToolStateOnCurrentImage(json) {
|
||||
return tool_state;
|
||||
|
||||
|
||||
}
|
||||
|
||||
function setUpDicom(element) {
|
||||
|
||||
console.log("Dicom element found ", $(element));
|
||||
|
||||
$(element).bind('contextmenu', function (e) {
|
||||
return false;
|
||||
});
|
||||
|
||||
$(element).dblclick((evt) => {
|
||||
element.requestFullscreen();
|
||||
})
|
||||
|
||||
cornerstoneBase64ImageLoader.external.cornerstone = cornerstone;
|
||||
cornerstoneWebImageLoader.external.cornerstone = cornerstone;
|
||||
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
||||
|
||||
cornerstoneTools.init();
|
||||
const PanTool = cornerstoneTools.PanTool;
|
||||
const ZoomTool = cornerstoneTools.ZoomTool;
|
||||
const ZoomMouseWheelTool = cornerstoneTools.ZoomMouseWheelTool;
|
||||
const WwwcTool = cornerstoneTools.WwwcTool;
|
||||
const WwwcRegionTool = cornerstoneTools.WwwcRegionTool;
|
||||
const RotateTool = cornerstoneTools.RotateTool;
|
||||
const StackScrollTool = cornerstoneTools.StackScrollTool;
|
||||
const MagnifyTool = cornerstoneTools.MagnifyTool;
|
||||
const ArrowAnnotateTool = cornerstoneTools.ArrowAnnotateTool;
|
||||
|
||||
|
||||
const imageId = element.dataset.url;
|
||||
|
||||
console.log("Dicom - load imageId: ", imageId);
|
||||
|
||||
cornerstone.enable(element);
|
||||
cornerstone.loadAndCacheImage(imageId).then(function (image) {
|
||||
cornerstone.displayImage(element, image);
|
||||
|
||||
cornerstoneTools.addTool(PanTool);
|
||||
cornerstoneTools.addTool(ZoomTool);
|
||||
cornerstoneTools.addTool(ZoomMouseWheelTool);
|
||||
cornerstoneTools.addTool(WwwcTool);
|
||||
cornerstoneTools.addTool(WwwcRegionTool);
|
||||
cornerstoneTools.addTool(RotateTool);
|
||||
cornerstoneTools.addTool(StackScrollTool);
|
||||
cornerstoneTools.addTool(MagnifyTool);
|
||||
|
||||
cornerstoneTools.addTool(ArrowAnnotateTool, {
|
||||
configuration: {
|
||||
getTextCallback: () => { },
|
||||
changeTextCallback: () => { },
|
||||
allowEmptyLabel: true,
|
||||
renderDashed: false,
|
||||
drawHandles: false,
|
||||
drawHandlesOnHover: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Enable our tools
|
||||
// Avoid incorrect aspect ratio
|
||||
cornerstoneTools.setToolActive("Pan", { mouseButtonMask: 1 });
|
||||
cornerstoneTools.setToolActive("Wwwc", { mouseButtonMask: 2 });
|
||||
cornerstoneTools.setToolActive("ZoomMouseWheel", { mouseButtonMask: 3 });
|
||||
cornerstoneTools.setToolActive("Zoom", { mouseButtonMask: 4 });
|
||||
|
||||
if (element.dataset.edit_annotation == "true") {
|
||||
cornerstoneTools.setToolActive("ArrowAnnotate", { mouseButtonMask: 2 });
|
||||
} else {
|
||||
cornerstoneTools.setToolEnabled("ArrowAnnotate");
|
||||
}
|
||||
|
||||
if (element.dataset.annotations) {
|
||||
loadJsonToolStateOnCurrentImage(element.dataset.annotations)
|
||||
}
|
||||
|
||||
cornerstone.resize(element);
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
@@ -28,6 +28,9 @@
|
||||
<span id="physics-link">
|
||||
<a href="{% url 'physics:index' %}">Physics</a>
|
||||
</span>
|
||||
<span id="rapids-link">
|
||||
<a href="{% url 'rapids:index' %}">Rapids</a>
|
||||
</span>
|
||||
<span id="logout-link">
|
||||
<a href="{% url 'logout' %}">Logout</a>
|
||||
</span>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<div id="dicom-image" data-url="{{ question.image.url}}" data-annotations='{{question.image_annotations}}'>
|
||||
<div id="dicom-image" class="dicom-image" data-url="{{ question.image.url}}" data-annotations='{{question.image_annotations}}'>
|
||||
</div>
|
||||
|
||||
<form method="POST" class="post-form">{% csrf_token %}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<a href="{% url 'anatomy:anatomy_question_update' question.id %}" title="Edit the Question">Edit</a> <a href="{% url 'admin:anatomy_anatomyquestion_change' question.id %}" title="Edit the Question using the admin interface">Admin Edit</a>
|
||||
<h3>{{ question.question_type }}</h3>
|
||||
|
||||
<div id="dicom-image" class="marking-dicom" data-url="http://penracourses.org.uk{{ question.image.url}}" data-annotations='{{question.image_annotations}}'>
|
||||
<div id="dicom-image" class="marking-dicom dicom-image" data-url="http://penracourses.org.uk{{ question.image.url}}" data-annotations='{{question.image_annotations}}'>
|
||||
</div>
|
||||
<div class="marking">
|
||||
<form method="POST" class="post-form">{% csrf_token %}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<button id="save-annotations">Save Annotations</button>
|
||||
<div id="dicom-image" data-url="http://penracourses.org.uk{{ question.image.url}}" data-annotations='{{question.image_annotations}}' data-edit_annotation=true></div>
|
||||
<div id="dicom-image" class="dicom-image" data-url="http://penracourses.org.uk{{ question.image.url}}" data-annotations='{{question.image_annotations}}' data-edit_annotation=true></div>
|
||||
<!-- testing -->
|
||||
<!--<div id="dicom-image" data-url="http://localhost:8000/static/abdoct.jpg"
|
||||
data-annotations='{{question.image_annotations}}' data-edit_annotation=true></div>-->
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
<span id="anatomy-link">
|
||||
<a href="{% url 'anatomy:index' %}">Anatomy</a>
|
||||
</span>
|
||||
<span id="rapids-link">
|
||||
<a href="{% url 'rapids:index' %}">Rapids</a>
|
||||
</span>
|
||||
<span id="logout-link">
|
||||
<a href="{% url 'logout' %}">Logout</a>
|
||||
</span>
|
||||
|
||||
+18
-17
@@ -23,7 +23,7 @@ with open('/etc/secret_key') as f:
|
||||
SECRET_KEY = f.read().strip()
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = False
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ["localhost", "127.0.0.1", "161.35.163.87", "penracourses.org.uk"]
|
||||
|
||||
@@ -32,6 +32,7 @@ ALLOWED_HOSTS = ["localhost", "127.0.0.1", "161.35.163.87", "penracourses.org.uk
|
||||
INSTALLED_APPS = [
|
||||
'anatomy',
|
||||
'physics',
|
||||
'rapids',
|
||||
"reversion",
|
||||
"django_tables2",
|
||||
"django_filters",
|
||||
@@ -46,7 +47,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'debug_toolbar',
|
||||
#"tagulous",
|
||||
"tagulous",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@@ -120,12 +121,12 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
},
|
||||
]
|
||||
|
||||
#SERIALIZATION_MODULES = {
|
||||
# "xml": "tagulous.serializers.xml_serializer",
|
||||
# "json": "tagulous.serializers.json",
|
||||
# "python": "tagulous.serializers.python",
|
||||
# "yaml": "tagulous.serializers.pyyaml",
|
||||
#}
|
||||
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/
|
||||
@@ -154,20 +155,20 @@ REMOTE_URL = "http://penracourses.org.uk"
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
|
||||
|
||||
if DEBUG:
|
||||
MEDIA_URL = REMOTE_URL + MEDIA_URL
|
||||
#if DEBUG:
|
||||
# MEDIA_URL = REMOTE_URL + MEDIA_URL
|
||||
|
||||
# Redirect to home URL after login (Default redirects to /accounts/profile/)
|
||||
LOGIN_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_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"]}
|
||||
TAGULOUS_AUTOCOMPLETE_CSS = {"all": ["tagulous/lib/select2-3/select2.css"]}
|
||||
|
||||
INTERNAL_IPS = ["localhost", "127.0.0.1"]
|
||||
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ urlpatterns = [
|
||||
path("anatomy/", include("anatomy.urls"), name="anatomy"),
|
||||
path("physics/", include("physics.urls"), name="physics"),
|
||||
#path("sbas/", include("sbas.urls"), name="sbas"),
|
||||
#path("rapids/", include("rapids.urls"), name="rapids"),
|
||||
path("rapids/", include("rapids.urls"), name="rapids"),
|
||||
path("accounts/", include("django.contrib.auth.urls")),
|
||||
path("accounts/profile", views.profile, name="profile"),
|
||||
path("", TemplateView.as_view(template_name="index.html"), name="home"),
|
||||
|
||||
Executable
Executable
+73
@@ -0,0 +1,73 @@
|
||||
from django.contrib import admin
|
||||
from .models import Rapid, RapidImage, Examination, Site, Abnormality, Region, Note, RapidCreationDefault, Answer, Exam
|
||||
|
||||
import tagulous.admin
|
||||
|
||||
from django.forms import ModelForm
|
||||
|
||||
from reversion.admin import VersionAdmin
|
||||
from django.forms.widgets import RadioSelect
|
||||
|
||||
# Register your models here.
|
||||
admin.site.register(Examination)
|
||||
admin.site.register(Site)
|
||||
admin.site.register(Exam)
|
||||
admin.site.register(Abnormality)
|
||||
admin.site.register(Region)
|
||||
admin.site.register(Note)
|
||||
admin.site.register(RapidCreationDefault)
|
||||
admin.site.register(Answer)
|
||||
|
||||
class RapidAnswersInline(admin.TabularInline):
|
||||
model = Answer
|
||||
extra = 1
|
||||
|
||||
|
||||
class RapidImageInline(admin.TabularInline):
|
||||
model = RapidImage
|
||||
extra = 1
|
||||
readonly_fields = [
|
||||
"image_tag",
|
||||
]
|
||||
|
||||
class ExamInline(admin.TabularInline):
|
||||
model = Rapid.exams.through
|
||||
extra = 1
|
||||
|
||||
|
||||
class RapidAdminForm(ModelForm):
|
||||
class Meta:
|
||||
model = Rapid
|
||||
|
||||
fields = [
|
||||
"normal", "abnormality", "region", "laterality", "examination",
|
||||
"site", "sign", "condition", "feedback", "author"
|
||||
]
|
||||
|
||||
widgets = {
|
||||
"laterality": RadioSelect(attrs={'style': 'display: inline-flex'}),
|
||||
}
|
||||
|
||||
|
||||
class RapidAdmin(VersionAdmin):
|
||||
form = RapidAdminForm
|
||||
|
||||
filter_horizontal = (
|
||||
"abnormality",
|
||||
"region",
|
||||
#"examination",
|
||||
)
|
||||
save_on_top = True
|
||||
save_as = True
|
||||
view_on_site = True
|
||||
|
||||
inlines = [
|
||||
RapidImageInline,
|
||||
RapidAnswersInline,
|
||||
ExamInline,
|
||||
]
|
||||
|
||||
|
||||
admin.site.register(Rapid, RapidAdmin)
|
||||
|
||||
#tagulous.admin.register(Rapid.condition)
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class RapidsConfig(AppConfig):
|
||||
name = 'rapids'
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from .models import Rapid
|
||||
|
||||
def user_is_author_or_rapid_checker(function):
|
||||
def wrap(request, *args, **kwargs):
|
||||
rapid = Rapid.objects.get(pk=kwargs['pk'])
|
||||
if request.user in rapid.author.all() or request.user.groups.filter(name='rapid_checker').exists():
|
||||
return function(request, *args, **kwargs)
|
||||
else:
|
||||
raise PermissionDenied
|
||||
wrap.__doc__ = function.__doc__
|
||||
wrap.__name__ = function.__name__
|
||||
return wrap
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
import django_filters
|
||||
|
||||
from .models import Rapid
|
||||
|
||||
|
||||
class RapidFilter(django_filters.FilterSet):
|
||||
class Meta:
|
||||
model = Rapid
|
||||
fields = ("normal", "abnormality", "region", "examination",
|
||||
"laterality", "site", "created_date", "author")
|
||||
Executable
+132
@@ -0,0 +1,132 @@
|
||||
from django.forms import ModelForm, ModelMultipleChoiceField, ModelChoiceField, ChoiceField
|
||||
from django.forms import inlineformset_factory
|
||||
|
||||
from rapids.models import Abnormality, Examination, Note, Rapid, RapidCreationDefault, RapidImage, Region, Answer
|
||||
|
||||
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||
from django.forms.widgets import RadioSelect, TextInput, Textarea
|
||||
|
||||
|
||||
class ExaminationForm(ModelForm):
|
||||
class Meta:
|
||||
model = Examination
|
||||
fields = ['examination']
|
||||
|
||||
|
||||
class NoteForm(ModelForm):
|
||||
class Meta:
|
||||
model = Note
|
||||
fields = ['note']
|
||||
|
||||
|
||||
class RegionForm(ModelForm):
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class AbnormalityForm(ModelForm):
|
||||
class Meta:
|
||||
model = Abnormality
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class RapidCreationDefaultForm(ModelForm):
|
||||
class Meta:
|
||||
model = RapidCreationDefault
|
||||
fields = ['site']
|
||||
exclude = ["author"]
|
||||
|
||||
|
||||
class RapidForm(ModelForm):
|
||||
class Media:
|
||||
# Django also includes a few javascript files necessary
|
||||
# for the operation of this form element. You need to
|
||||
# include <script src="/admin/jsi18n"></script>
|
||||
# in the template.
|
||||
css = {
|
||||
'all': ['css/widgets.css'],
|
||||
}
|
||||
# Adding this javascript is crucial
|
||||
js = ['jsi18n.js', "tesseract.min.js"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RapidForm, self).__init__(*args, **kwargs)
|
||||
#self.fields['question'].widget.attrs = {'class': 'question-form', 'rows': 10, 'cols': 100}
|
||||
#self.fields['feedback'].widget.attrs = {'class': 'feedback-form', 'rows': 10, 'cols': 100}
|
||||
self.fields['abnormality'] = ModelMultipleChoiceField(
|
||||
required=False,
|
||||
queryset=Abnormality.objects.all(),
|
||||
widget=FilteredSelectMultiple(verbose_name="Abnormality",
|
||||
is_stacked=False))
|
||||
|
||||
self.fields['region'] = ModelMultipleChoiceField(
|
||||
required=False,
|
||||
queryset=Region.objects.all(),
|
||||
widget=FilteredSelectMultiple(verbose_name="Region",
|
||||
is_stacked=False))
|
||||
|
||||
self.fields['examination'] = ModelMultipleChoiceField(
|
||||
queryset=Examination.objects.all(),
|
||||
widget=FilteredSelectMultiple(verbose_name="Examination",
|
||||
is_stacked=False))
|
||||
|
||||
self.fields['laterality'] = ChoiceField(
|
||||
choices=Rapid.LATERALITY_CHOICES,
|
||||
required=True,
|
||||
widget=RadioSelect())
|
||||
|
||||
class Meta:
|
||||
model = Rapid
|
||||
#fields = ['due_back']
|
||||
#fields = '__all__'
|
||||
fields = [
|
||||
"normal", "abnormality", "region", "laterality", "examination",
|
||||
"site", "feedback"
|
||||
]
|
||||
#fields = ['question', 'feedback', 'subspecialty', 'references']
|
||||
widgets = {
|
||||
# "normal": RadioSelect(
|
||||
# choices=[(True, 'Yes'),
|
||||
# (False, 'No')])
|
||||
}
|
||||
|
||||
|
||||
ImageFormSet = inlineformset_factory(Rapid,
|
||||
RapidImage,
|
||||
fields=['image', 'feedback_image'],
|
||||
exclude=[],
|
||||
can_delete=True,
|
||||
extra=4,
|
||||
max_num=10,
|
||||
field_classes="testing")
|
||||
|
||||
AnswerFormSet = inlineformset_factory(
|
||||
Rapid,
|
||||
Answer,
|
||||
fields=["answer", "status"],
|
||||
widgets={
|
||||
"answer": Textarea(
|
||||
attrs={"required": "true", "minlength": 2, "maxlength": 500, "rows": 3}
|
||||
)
|
||||
},
|
||||
exclude=[],
|
||||
can_delete=True,
|
||||
extra=1,
|
||||
max_num=10,
|
||||
)
|
||||
|
||||
AnswerUpdateFormSet = inlineformset_factory(
|
||||
Rapid,
|
||||
Answer,
|
||||
fields=["answer", "status"],
|
||||
widgets={
|
||||
"answer": Textarea(
|
||||
attrs={"required": "true", "minlength": 2, "maxlength": 500, "rows": 3}
|
||||
)
|
||||
},
|
||||
exclude=[],
|
||||
can_delete=True,
|
||||
extra=0,
|
||||
max_num=10,
|
||||
)
|
||||
@@ -0,0 +1,122 @@
|
||||
# Generated by Django 3.1.2 on 2021-01-18 13:39
|
||||
|
||||
from django.conf import settings
|
||||
import django.core.files.storage
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import rapids.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Abnormality',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(help_text='Primary abnormality on the film(s)', max_length=200, unique=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ('name',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Examination',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('examination', models.CharField(max_length=200)),
|
||||
],
|
||||
options={
|
||||
'ordering': ('examination',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Rapid',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('question', models.TextField(blank=True, null=True)),
|
||||
('feedback', models.TextField(blank=True, null=True)),
|
||||
('normal', models.BooleanField(default=False, help_text='Tick if true')),
|
||||
('laterality', models.CharField(choices=[('NONE', 'None'), ('LEFT', 'Left'), ('RIGHT', 'Right'), ('BILAT', 'Bilateral')], default='NONE', help_text='Applies to the answer, not the examination', max_length=20)),
|
||||
('verified', models.BooleanField(default=False)),
|
||||
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('published_date', models.DateTimeField(blank=True, null=True)),
|
||||
('scrapped', models.BooleanField(default=False, help_text='Question has been scrapped and will not be shown')),
|
||||
('abnormality', models.ManyToManyField(blank=True, help_text='The abnormality (laterality and region independent)', to='rapids.Abnormality')),
|
||||
('author', models.ManyToManyField(blank=True, help_text='Author of question', related_name='rapid_authored_questions', to=settings.AUTH_USER_MODEL)),
|
||||
('examination', models.ManyToManyField(help_text='Name of the (primary) examination', to='rapids.Examination')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Region',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(blank=True, help_text='Region of the abnormality (not including lateralitiy)', max_length=200, unique=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ('name',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Site',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('site', models.CharField(max_length=200)),
|
||||
('initials', models.CharField(max_length=200)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RapidImage',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('image', models.ImageField(storage=django.core.files.storage.FileSystemStorage(base_url='/media/rapids/', location='/home/ross/sites/rad/media//rapids/'), upload_to=rapids.models.image_directory_path)),
|
||||
('feedback_image', models.BooleanField(default=False)),
|
||||
('rapid', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='rapids.rapid')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RapidCreationDefault',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('author', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='rapid_default', to=settings.AUTH_USER_MODEL)),
|
||||
('site', models.ManyToManyField(blank=True, default=1, help_text='Default site to use when creating a new rapid', to='rapids.Site')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rapid',
|
||||
name='region',
|
||||
field=models.ManyToManyField(blank=True, help_text='Region of the abnormality (laterality independent)', to='rapids.Region'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rapid',
|
||||
name='site',
|
||||
field=models.ManyToManyField(blank=True, default=1, help_text='If we know the source of the image (should possibly be removed?)', to='rapids.Site'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Note',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('note', models.TextField()),
|
||||
('created_on', models.DateTimeField(auto_now_add=True)),
|
||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rapid_notes', to=settings.AUTH_USER_MODEL)),
|
||||
('rapid', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='rapid_notes', to='rapids.rapid')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Answer',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('answer', models.TextField(max_length=500)),
|
||||
('answer_compare', models.TextField(max_length=500)),
|
||||
('status', models.CharField(choices=[('', 'Unmarked'), ('0', 'Incorrect'), ('1', 'Half mark'), ('2', 'Correct')], default='', max_length=1)),
|
||||
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='rapids.rapid')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,52 @@
|
||||
# Generated by Django 3.1.2 on 2021-01-18 14:02
|
||||
|
||||
import django.core.files.storage
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import rapids.models
|
||||
import sortedm2m.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rapids', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='rapid',
|
||||
name='abnormality',
|
||||
field=models.ManyToManyField(blank=True, help_text='The abnormality (laterality and region independent). Used for categorisation but does not affect the answer', to='rapids.Abnormality'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rapidimage',
|
||||
name='image',
|
||||
field=models.ImageField(storage=django.core.files.storage.FileSystemStorage(base_url='http://penracourses.org.uk//media/rapids/', location='/home/ross/sites/rad/media//rapids/'), upload_to=rapids.models.image_directory_path),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Exam',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200)),
|
||||
('active', models.BooleanField(default=True, help_text='If an exam should be available')),
|
||||
('publish_results', models.BooleanField(default=True, help_text='If an exams results should be available')),
|
||||
('recreate_json', models.BooleanField(default=False, help_text='If the json cache needs updating')),
|
||||
('time_limit', models.IntegerField(default=5400, help_text='Exam time limit (in seconds)')),
|
||||
('exam_questions', sortedm2m.fields.SortedManyToManyField(blank='true', help_text=None, related_name='exams', to='rapids.Rapid')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CidUserAnswer',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('answer', models.TextField(blank=True, max_length=500)),
|
||||
('answer_compare', models.TextField(blank=True, max_length=500)),
|
||||
('cid', models.BigIntegerField(blank=True, help_text='Candidate ID (limitied by BigIntegerField size)', null=True)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('updated', models.DateTimeField(auto_now=True)),
|
||||
('exam', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='cid_user_answers', to='rapids.exam')),
|
||||
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cid_user_answers', to='rapids.rapid')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,69 @@
|
||||
# Generated by Django 3.1.3 on 2021-01-20 22:06
|
||||
|
||||
import django.core.files.storage
|
||||
from django.db import migrations, models
|
||||
import rapids.models
|
||||
import tagulous.models.fields
|
||||
import tagulous.models.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rapids', '0002_auto_20210118_1402'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='rapid',
|
||||
name='site',
|
||||
field=models.ManyToManyField(blank=True, default=1, help_text='If we know the source of the image', to='rapids.Site'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rapidimage',
|
||||
name='image',
|
||||
field=models.ImageField(storage=django.core.files.storage.FileSystemStorage(base_url='http://penracourses.org.uk//media/rapids/', location='/home/ross/scripts/sites/rad/media//rapids/'), upload_to=rapids.models.image_directory_path),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Sign',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, unique=True)),
|
||||
('slug', models.SlugField()),
|
||||
('count', models.IntegerField(default=0, help_text='Internal counter of how many times this tag is in use')),
|
||||
('protected', models.BooleanField(default=False, help_text='Will not be deleted when the count reaches 0')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('name',),
|
||||
'abstract': False,
|
||||
'unique_together': {('slug',)},
|
||||
},
|
||||
bases=(tagulous.models.models.BaseTagModel, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Condition',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, unique=True)),
|
||||
('slug', models.SlugField()),
|
||||
('count', models.IntegerField(default=0, help_text='Internal counter of how many times this tag is in use')),
|
||||
('protected', models.BooleanField(default=False, help_text='Will not be deleted when the count reaches 0')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('name',),
|
||||
'abstract': False,
|
||||
'unique_together': {('slug',)},
|
||||
},
|
||||
bases=(tagulous.models.models.BaseTagModel, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rapid',
|
||||
name='condition',
|
||||
field=tagulous.models.fields.TagField(_set_tag_meta=True, blank=True, help_text='Associated condition. Will allow searching / filtering and tips / hints to be displayed. Conditions with spaces must be enclosed in quotes "..."', to='rapids.Condition'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rapid',
|
||||
name='sign',
|
||||
field=tagulous.models.fields.TagField(_set_tag_meta=True, blank=True, help_text='Radiological signs in the question', to='rapids.Sign'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 3.1.3 on 2021-01-20 22:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rapids', '0003_auto_20210120_2206'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='rapid',
|
||||
name='condition',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rapid',
|
||||
name='condition',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='rapid',
|
||||
name='sign',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rapid',
|
||||
name='sign',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 3.1.3 on 2021-01-21 18:06
|
||||
|
||||
import django.core.files.storage
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import rapids.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rapids', '0004_auto_20210120_2214'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='rapid',
|
||||
name='examination',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rapid',
|
||||
name='examination',
|
||||
field=models.ForeignKey(help_text='Name of the (primary) examination', null=True, on_delete=django.db.models.deletion.SET_NULL, to='rapids.examination'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rapidimage',
|
||||
name='image',
|
||||
field=models.ImageField(storage=django.core.files.storage.FileSystemStorage(base_url='/media/rapids/', location='/home/ross/scripts/sites/rad/media//rapids/'), upload_to=rapids.models.image_directory_path),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 3.1.3 on 2021-01-21 18:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rapids', '0005_auto_20210121_1806'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='rapid',
|
||||
name='examination',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rapid',
|
||||
name='examination',
|
||||
field=models.ManyToManyField(help_text='Name of the (primary) examination', to='rapids.Examination'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,436 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
import tagulous
|
||||
import tagulous.models
|
||||
|
||||
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
from django.conf import settings
|
||||
from django.utils.html import format_html
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.html import mark_safe
|
||||
|
||||
from sortedm2m.fields import SortedManyToManyField
|
||||
|
||||
import string
|
||||
|
||||
image_storage = FileSystemStorage(
|
||||
# Physical file location ROOT
|
||||
location=u"{0}/rapids/".format(settings.MEDIA_ROOT),
|
||||
# Url for file
|
||||
base_url=u"{0}rapids/".format(settings.MEDIA_URL),
|
||||
)
|
||||
|
||||
def image_directory_path(instance, filename):
|
||||
return u"picture/{0}".format(filename)
|
||||
|
||||
|
||||
|
||||
class Abnormality(models.Model):
|
||||
name = models.CharField(max_length=200,
|
||||
unique=True,
|
||||
help_text="Primary abnormality on the film(s)")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
ordering = ('name', )
|
||||
|
||||
|
||||
class Region(models.Model):
|
||||
name = models.CharField(
|
||||
max_length=200,
|
||||
unique=True,
|
||||
help_text="Region of the abnormality (not including lateralitiy)",
|
||||
blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
ordering = ('name', )
|
||||
|
||||
|
||||
class Examination(models.Model):
|
||||
examination = models.CharField(max_length=200)
|
||||
|
||||
def __str__(self):
|
||||
return self.examination
|
||||
|
||||
class Meta:
|
||||
ordering = ('examination', )
|
||||
|
||||
|
||||
class Site(models.Model):
|
||||
site = models.CharField(max_length=200)
|
||||
initials = models.CharField(max_length=200)
|
||||
|
||||
def __str__(self):
|
||||
return self.site
|
||||
|
||||
|
||||
class Condition(tagulous.models.TagModel):
|
||||
pass
|
||||
|
||||
class Sign(tagulous.models.TagModel):
|
||||
pass
|
||||
|
||||
class Answer(models.Model):
|
||||
question = models.ForeignKey(
|
||||
"Rapid", related_name="answers", on_delete=models.CASCADE
|
||||
)
|
||||
answer = models.TextField(max_length=500)
|
||||
answer_compare = models.TextField(max_length=500)
|
||||
|
||||
class MarkOptions(models.TextChoices):
|
||||
UNMARKED = "", _("Unmarked")
|
||||
INCORRECT = "0", _("Incorrect")
|
||||
HALF_MARK = "1", _("Half mark")
|
||||
CORRECT = "2", _("Correct")
|
||||
|
||||
status = models.CharField(
|
||||
max_length=1, choices=MarkOptions.choices, default=MarkOptions.UNMARKED
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.answer
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.clean()
|
||||
return super(Answer, self).save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
if self.answer:
|
||||
self.answer = self.answer.strip()
|
||||
|
||||
s = self.answer.lower()
|
||||
s = s.translate(str.maketrans('', '', string.punctuation))
|
||||
|
||||
self.answer_compare = s
|
||||
|
||||
|
||||
class Rapid(models.Model):
|
||||
#author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
|
||||
#image = models.ImageField()
|
||||
question = models.TextField(null=True, blank=True)
|
||||
feedback = models.TextField(null=True, blank=True)
|
||||
|
||||
normal = models.BooleanField(default=False, help_text="Tick if true")
|
||||
|
||||
NONE = "NONE"
|
||||
LEFT = "LEFT"
|
||||
RIGHT = "RIGHT"
|
||||
BILATERAL = "BILAT"
|
||||
|
||||
LATERALITY_CHOICES = (
|
||||
(NONE, "None"),
|
||||
(LEFT, "Left"),
|
||||
(RIGHT, "Right"),
|
||||
(BILATERAL, "Bilateral"),
|
||||
)
|
||||
|
||||
abnormality = models.ManyToManyField(
|
||||
Abnormality,
|
||||
blank=True,
|
||||
help_text="The abnormality (laterality and region independent). Used for categorisation but does not affect the answer",
|
||||
)
|
||||
region = models.ManyToManyField(
|
||||
Region,
|
||||
blank=True,
|
||||
help_text="Region of the abnormality (laterality independent)")
|
||||
examination = models.ManyToManyField(
|
||||
Examination, help_text="Name of the (primary) examination")
|
||||
laterality = models.CharField(
|
||||
max_length=20,
|
||||
choices=LATERALITY_CHOICES,
|
||||
default=NONE,
|
||||
help_text="Applies to the answer, not the examination")
|
||||
condition = models.CharField(max_length=255, 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 \"...\""
|
||||
#)
|
||||
|
||||
sign = models.CharField(max_length=255, blank=True)
|
||||
#sign = tagulous.models.TagField(
|
||||
# to=Sign, blank=True, help_text='Radiological signs in the question')
|
||||
|
||||
DEFAULT_SITE_ID = 1
|
||||
site = models.ManyToManyField(
|
||||
Site,
|
||||
blank=True,
|
||||
default=DEFAULT_SITE_ID,
|
||||
help_text=
|
||||
"If we know the source of the image")
|
||||
verified = models.BooleanField(default=False)
|
||||
created_date = models.DateTimeField(default=timezone.now)
|
||||
published_date = models.DateTimeField(blank=True, null=True)
|
||||
author = models.ManyToManyField(settings.AUTH_USER_MODEL,
|
||||
blank=True,
|
||||
help_text='Author of question',
|
||||
related_name="rapid_authored_questions")
|
||||
|
||||
scrapped = models.BooleanField(
|
||||
default=False,
|
||||
help_text='Question has been scrapped and will not be shown')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('rapids:rapid_detail', kwargs={'pk': self.pk})
|
||||
|
||||
def get_authors(self):
|
||||
"""Returns a comma seperated text list of authors"""
|
||||
authors = ", ".join([i.username for i in self.author.all()])
|
||||
return authors
|
||||
|
||||
def get_regions(self):
|
||||
"""Returns a comma seperated text list of regions"""
|
||||
regions = ", ".join([i.name for i in self.region.all()])
|
||||
return regions
|
||||
|
||||
def get_abnormalities(self):
|
||||
"""Returns a comma seperated text list of regions"""
|
||||
abnormalities = ", ".join([i.name for i in self.abnormality.all()])
|
||||
return abnormalities
|
||||
|
||||
def get_examinations(self):
|
||||
"""Returns a comma seperated text list of regions"""
|
||||
examinations = ", ".join([i.examination for i in self.examination.all()])
|
||||
return examinations
|
||||
|
||||
def __str__(self):
|
||||
n = "Normal"
|
||||
if not self.normal:
|
||||
#n = self.answers.first()
|
||||
n = "{} / {}".format(self.abnormality.first(), self.region.first())
|
||||
exams = self.get_examinations()
|
||||
lat = ""
|
||||
if self.laterality != "NONE":
|
||||
lat = self.laterality
|
||||
return "{} / {} {}".format(exams, lat, n)
|
||||
|
||||
def GetPrimaryAnswer(self):
|
||||
if len(self.answers.all()) > 0:
|
||||
return self.answers.all().first().answer
|
||||
else:
|
||||
return "None yet..."
|
||||
|
||||
def GetExams(self):
|
||||
e = self.exams.all().values_list("name", flat=True)
|
||||
exams = ", ".join(e)
|
||||
return exams
|
||||
|
||||
def GetUnmarkedAnswersString(self):
|
||||
unmarked_answers = self.GetUnmarkedAnswers()
|
||||
|
||||
if not unmarked_answers:
|
||||
return "No answers to mark"
|
||||
return format_html(
|
||||
"<span class='warn'>{} answer unmarked:</span> {}".format(
|
||||
len(unmarked_answers), ", ".join(unmarked_answers)
|
||||
)
|
||||
)
|
||||
|
||||
def GetUnmarkedAnswers(self):
|
||||
user_answers = set(
|
||||
[i.answer_compare for i in self.cid_user_answers.all()]
|
||||
)
|
||||
unmarked_answers = user_answers - self.GetMarkedAnswers()
|
||||
|
||||
return unmarked_answers
|
||||
|
||||
def GetUnmarkedAnswerCount(self):
|
||||
return len(self.GetUnmarkedAnswers())
|
||||
|
||||
def GetMarkedAnswers(self):
|
||||
return set(
|
||||
[
|
||||
i.answer_compare
|
||||
for i in self.answers.all()
|
||||
if i.status != i.MarkOptions.UNMARKED
|
||||
]
|
||||
)
|
||||
correct_answers = set(
|
||||
[i.answer.get_compare_string() for i in self.answers.all()]
|
||||
)
|
||||
half_mark_answers = set(
|
||||
[i.answer.get_compare_string() for i in self.half_mark_answers.all()]
|
||||
)
|
||||
incorrect_answers = set(
|
||||
[i.answer.get_compare_string() for i in self.incorrect_answers.all()]
|
||||
)
|
||||
|
||||
marked_answers = correct_answers | half_mark_answers | incorrect_answers
|
||||
return marked_answers
|
||||
|
||||
|
||||
class RapidImage(models.Model):
|
||||
rapid = models.ForeignKey(Rapid,
|
||||
related_name="images",
|
||||
on_delete=models.CASCADE)
|
||||
image = models.ImageField(upload_to=image_directory_path, storage=image_storage)
|
||||
|
||||
feedback_image = models.BooleanField(default=False)
|
||||
|
||||
def image_tag(self):
|
||||
if self.image:
|
||||
return mark_safe(
|
||||
'<img src="{}" class="admin-rapid-image" /><span class="admin-rapid-image-info">Click and hold to zoom<span>'
|
||||
.format(self.image.url))
|
||||
else:
|
||||
return ""
|
||||
|
||||
image_tag.short_description = 'Image'
|
||||
|
||||
|
||||
class Note(models.Model):
|
||||
rapid = models.ForeignKey(Rapid,
|
||||
related_name="rapid_notes",
|
||||
on_delete=models.CASCADE,
|
||||
null=True)
|
||||
#author = models.ForeignKey(User,
|
||||
author = models.ForeignKey( settings.AUTH_USER_MODEL,
|
||||
related_name="rapid_notes",
|
||||
on_delete=models.CASCADE)
|
||||
note = models.TextField()
|
||||
created_on = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def get_absolute_url(self):
|
||||
self.pk = self.rapid_id
|
||||
return reverse('rapids:rapid_detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return "{} [{}] {}".format(self.rapid, self.author, self.created_on)
|
||||
|
||||
|
||||
class RapidCreationDefault(models.Model):
|
||||
#author = models.OneToOneField(User,
|
||||
author = models.OneToOneField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
related_name="rapid_default",
|
||||
on_delete=models.CASCADE)
|
||||
|
||||
site = models.ManyToManyField(
|
||||
Site,
|
||||
blank=True,
|
||||
default=1,
|
||||
help_text="Default site to use when creating a new rapid")
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('rapids:rapid_create')
|
||||
|
||||
|
||||
class Exam(models.Model):
|
||||
name = models.CharField(max_length=200)
|
||||
exam_questions = SortedManyToManyField(
|
||||
Rapid, related_name="exams", blank="true"
|
||||
)
|
||||
|
||||
active = models.BooleanField(
|
||||
help_text="If an exam should be available", default=True
|
||||
)
|
||||
|
||||
publish_results = models.BooleanField(
|
||||
help_text="If an exams results should be available", default=True
|
||||
)
|
||||
|
||||
recreate_json = models.BooleanField(
|
||||
help_text="If the json cache needs updating", default=False
|
||||
)
|
||||
|
||||
time_limit = models.IntegerField(
|
||||
help_text="Exam time limit (in seconds)", default=5400
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("rapids:exam_overview", kwargs={"pk": self.pk})
|
||||
|
||||
def get_exam_name(self):
|
||||
return self.name
|
||||
|
||||
def get_json_url(self):
|
||||
return reverse("rapids:exam_json", args=(self.pk,))
|
||||
|
||||
def get_question_index(self, question):
|
||||
return list(self.exam_questions.all()).index(question)
|
||||
|
||||
class CidUserAnswer(models.Model):
|
||||
"""User answers by candidate"""
|
||||
|
||||
question = models.ForeignKey(
|
||||
Rapid, related_name="cid_user_answers", on_delete=models.CASCADE
|
||||
)
|
||||
answer = models.TextField(max_length=500, blank=True)
|
||||
|
||||
answer_compare = models.TextField(max_length=500, blank=True)
|
||||
|
||||
cid = models.BigIntegerField(blank=True, null=True, help_text="Candidate ID (limitied by BigIntegerField size)")
|
||||
|
||||
# Each user answer is associated with a particular exam
|
||||
exam = models.ForeignKey(
|
||||
Exam, related_name="cid_user_answers", on_delete=models.CASCADE, null=True
|
||||
)
|
||||
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
updated = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
try:
|
||||
exam = self.exam
|
||||
except (Exam.DoesNotExist, KeyError) as e:
|
||||
exam = "None"
|
||||
return "{}/{}/{}: {}".format(
|
||||
exam, self.cid, self.question.GetPrimaryAnswer(), self.answer
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.clean()
|
||||
return super(CidUserAnswer, self).save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
if self.answer:
|
||||
self.answer = self.answer.strip()
|
||||
|
||||
s = self.answer.lower()
|
||||
s = s.translate(str.maketrans('', '', string.punctuation))
|
||||
|
||||
self.answer_compare = s
|
||||
|
||||
# def get_compare_string(self):
|
||||
# # strip here should be unneccasry (providing clean is now working)
|
||||
# s = self.answer.lower().strip()
|
||||
# s = s.translate(str.maketrans('', '', string.punctuation))
|
||||
# return s
|
||||
|
||||
def get_answer_score(self):
|
||||
q = self.question
|
||||
ans = self.answer_compare
|
||||
if (
|
||||
q.answers.filter(
|
||||
answer_compare__iexact=ans, status=Answer.MarkOptions.CORRECT
|
||||
).first()
|
||||
is not None
|
||||
):
|
||||
mark = 2
|
||||
elif (
|
||||
q.answers.filter(
|
||||
answer_compare__iexact=ans, status=Answer.MarkOptions.HALF_MARK
|
||||
).first()
|
||||
is not None
|
||||
):
|
||||
mark = 1
|
||||
elif q.answers.filter(
|
||||
answer_compare__iexact=ans, status=Answer.MarkOptions.INCORRECT
|
||||
).first():
|
||||
mark = 0
|
||||
else:
|
||||
mark = "unmarked"
|
||||
return mark
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import A
|
||||
|
||||
from .models import Rapid
|
||||
|
||||
from django.utils.html import format_html
|
||||
|
||||
from easy_thumbnails.files import get_thumbnailer
|
||||
|
||||
from easy_thumbnails.exceptions import InvalidImageFormatError
|
||||
|
||||
|
||||
class ImageColumn(tables.Column):
|
||||
def render(self, value):
|
||||
image_object = value.first().image
|
||||
try:
|
||||
thumbnailer = get_thumbnailer(image_object)
|
||||
return format_html('<img src="/media/{}" />', thumbnailer["exam-list"])
|
||||
except InvalidImageFormatError:
|
||||
return format_html('<span title="{}">Invalid image url<span>', image_object)
|
||||
|
||||
|
||||
class RapidTable(tables.Table):
|
||||
edit = tables.LinkColumn('rapids:rapid_update',
|
||||
text='Edit',
|
||||
args=[A('pk')],
|
||||
orderable=False)
|
||||
view = tables.LinkColumn('rapids:rapid_detail',
|
||||
text='View',
|
||||
args=[A('pk')],
|
||||
orderable=False)
|
||||
clone = tables.LinkColumn('rapids:rapid_clone',
|
||||
text='Clone',
|
||||
args=[A('pk')],
|
||||
orderable=False)
|
||||
images = ImageColumn("images", orderable=False)
|
||||
|
||||
class Meta:
|
||||
model = Rapid
|
||||
template_name = "django_tables2/bootstrap4.html"
|
||||
fields = ("normal", "abnormality", "region", "examination",
|
||||
"laterality", "site", "created_date", "author")
|
||||
sequence = ("view", "images")
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
{% load static %}
|
||||
{% block extrahead %}
|
||||
<link rel="stylesheet" href="{% static 'css/admin.css' %}">
|
||||
{% endblock %}
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
{% extends 'rapids/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% for author in authors %}
|
||||
<p>Author: <a href="{% url 'rapids:author_detail' pk=author.pk %}">{{author.username}}</a>, </p>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
{% load static %}
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Rapids</title>
|
||||
{% block css %}
|
||||
{% endblock %}
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="{% static 'tagulous/lib/select2-3/select2.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/anatomy.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/toastr.min.css' %}">
|
||||
|
||||
{% comment %} <script src="{% static 'tagulous/lib/jquery.js' %}"></script>
|
||||
<script src="{% static 'tagulous/lib/select2-3/select2.min.js' %}"></script>
|
||||
<script src="{% static 'tagulous/tagulous.js' %}"></script>
|
||||
<script src="{% static 'tagulous/adaptor/select2-3.js' %}"></script> {% endcomment %}
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
|
||||
<script src="{% static 'js/cornerstone/hammer.js' %}"></script>
|
||||
<script src="{% static 'js/cornerstone/cornerstone.min.js' %}"></script>
|
||||
<script src="{% static 'js/cornerstone/dicomParser.min.js' %}"></script>
|
||||
<script src="{% static 'js/cornerstone/cornerstoneMath.min.js' %}"></script>
|
||||
<script src="{% static 'js/cornerstone/cornerstoneTools.min.js' %}"></script>
|
||||
<script src="{% static 'js/cornerstone/cornerstoneWebImageLoader.min.js' %}"></script>
|
||||
<script src="{% static 'js/cornerstone/cornerstoneWADOImageLoader.js' %}"></script>
|
||||
<script src="{% static 'js/cornerstone/cornerstone-base64-image-loader.umd.js' %}"></script>
|
||||
<script src="{% static 'js/anatomy.js' %}" defer="defer"></script>
|
||||
{% block js %}
|
||||
{% endblock %}
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="page-header">
|
||||
{% if request.user.is_authenticated %}
|
||||
<span id="physics-link">
|
||||
<a href="{% url 'physics:index' %}">Physics</a>
|
||||
</span>
|
||||
<span id="anatomy-link">
|
||||
<a href="{% url 'anatomy:index' %}">Anatomy</a>
|
||||
</span>
|
||||
<span id="logout-link">
|
||||
<a href="{% url 'logout' %}">Logout</a>
|
||||
</span>
|
||||
<span id="profile-link">
|
||||
<a href="{% url 'profile' %}">Profile</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if request.user.is_staff %}
|
||||
<span id="admin-link">
|
||||
<a href="{% url 'admin:index' %}">Admin</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="content container">
|
||||
{% if request.user.is_staff %}<span id="admin-link"><a
|
||||
href="{% url 'admin:index' %}">Admin</a></span>{% endif %}
|
||||
<span id="view-link"><a href="{% url 'rapids:rapid_view' %}">View questions</a></span>
|
||||
<span id="create-link"><a href="{% url 'rapids:rapid_create' %}">Add new rapid</a></span>
|
||||
</br>
|
||||
Questions by:
|
||||
<span id="authors-link"><a href="{% url 'rapids:author_list' %}">author</a></span>
|
||||
{% block navigation %}
|
||||
{% endblock %}
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
{% extends 'rapids/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
{{category}}:
|
||||
<ul>
|
||||
{% for rapid in rapids %}
|
||||
<li><a href="{% url 'rapids:rapid_detail' pk=rapid.pk %}">{{rapid}}
|
||||
[{{rapid.get_authors}}]</a></li>
|
||||
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% endblock %}
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
<h2>Add new {{name}}</h2>
|
||||
<form method="POST" class="post-form">{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="save btn btn-default">Save</button>
|
||||
</form>
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
{% extends 'rapids/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
My Questions:
|
||||
<ul>
|
||||
{% for rapid in user_rapids %}
|
||||
<li{% if rapid.scrapped %} class='rapid-scrapped' {% endif %}><a
|
||||
href="{% url 'rapids:rapid_detail' pk=rapid.pk %}">{{rapid}}
|
||||
[{{rapid.get_authors}}]</a></li>
|
||||
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
Other Questions:
|
||||
<ul>
|
||||
{% for rapid in other_rapids %}
|
||||
<li{% if rapid.scrapped %} class='rapid-scrapped' {% endif %}><a
|
||||
href="{% url 'rapids:rapid_detail' pk=rapid.pk %}">{{rapid}} [{{rapid.get_authors}}]</a></li>
|
||||
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% endblock %}
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
{% extends "rapids/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Add Note</h2>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
{{ form.as_table }}
|
||||
</table>
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
{% endblock %}
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
{% extends 'rapids/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<a href="{% url 'rapids:rapid_update' pk=rapid.pk %}" title="Edit the Rapid">Edit</a>
|
||||
<a href="{% url 'rapids:rapid_clone' pk=rapid.pk %}" title="Clone the Rapid (duplicate everything but the images)">Clone</a>
|
||||
<a href="{% url 'rapids:rapid_add_note' pk=rapid.pk %}"> Add Note</a>
|
||||
{% if request.user.is_superuser %}
|
||||
<a href="{% url 'admin:rapids_rapid_change' rapid.id %}" title="Edit the Rapid using the admin interface">Admin Edit</a>
|
||||
{% endif %}
|
||||
{% include 'rapids/rapid_display_block.html' %}
|
||||
{% endblock %}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<div class="rapid {% if rapid.scrapped %}rapid-scrapped{% endif %}">
|
||||
<div class="date">
|
||||
{{ rapid.created_date }}
|
||||
</div>
|
||||
|
||||
<p class="pre-whitespace"><b>Rapid:</b> {{ rapid }}</p>
|
||||
<p class="pre-whitespace"><b>Region:</b> {{ rapid.get_regions }}</p>
|
||||
<p class="pre-whitespace"><b>Examination:</b> {{ rapid.get_examinations }}</p>
|
||||
<p class="pre-whitespace"><b>Laterality:</b> {{ rapid.laterality }}</p>
|
||||
<p class="pre-whitespace"><b>Abnormality:</b> {{ rapid.get_abnormalities }}</p>
|
||||
<p class="pre-whitespace"><b>Images:</b>
|
||||
{% for image in rapid.images.all %}
|
||||
Image {{ forloop.counter }}{% if image.feedback_image %} [feedback image]{% endif %}:
|
||||
<!-- <img class="rapid-img {% if image.feedback_image %}feedback-img{% endif %}" src="{{ image.image.url }}"><br/> -->
|
||||
<div class="dicom-image rapid-img {% if image.feedback_image %}feedback-img{% endif %}" data-url="http://penracourses.org.uk{{ image.image.url}}"></div>
|
||||
{% endfor %}</p>
|
||||
<p class="pre-whitespace"><b>Feedback:</b> {{ rapid.feedback }}</p>
|
||||
<p><b>Author(s):</b> {% for author in rapid.author.all %} <a
|
||||
href="{% url 'rapids:author_detail' pk=author.pk %}">{{author}}</a>, {% endfor %}</p>
|
||||
<p><b>Checked by:</b> {% for verified in rapid.verified.all %} <a
|
||||
href="{% url 'rapids:verified_detail' pk=verified.pk %}">{{verified}}</a>, {% endfor %}</p>
|
||||
<p><b>Scrapped:</b> {{ rapid.scrapped }} <a href="{% url 'rapids:rapid_scrap' pk=rapid.pk %}">(toggle)</a>
|
||||
<p class="pre-whitespace"><b>Answers:</b> {{ rapid.GetMarkedAnswers }}</p>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
Notes:
|
||||
<ul>
|
||||
{% for note in rapid.rapid_notes.all %}
|
||||
<li>
|
||||
{{ note.created_on }} by {{ note.author }}: {{ note.note }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
Executable
+275
@@ -0,0 +1,275 @@
|
||||
{% extends "rapids/base.html" %}
|
||||
{% load static from static %}
|
||||
|
||||
{% block js %}
|
||||
<!--<script type="text/javascript" src="/admin/jsi18n/"></script>-->
|
||||
|
||||
<script type="text/javascript">
|
||||
function showEditPopup(url) {
|
||||
var win = window.open(url, "Edit",
|
||||
'height=500,width=800,resizable=yes,scrollbars=yes');
|
||||
return false;
|
||||
}
|
||||
function showAddPopup(triggeringLink) {
|
||||
var name = triggeringLink.id.replace(/^add_/, '');
|
||||
href = triggeringLink.href;
|
||||
var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes');
|
||||
win.focus();
|
||||
return false;
|
||||
}
|
||||
function closePopup(win, newID, newRepr, id) {
|
||||
console.log(id)
|
||||
$(id + "_to").append('<option value=' + newID + ' title=' + newRepr + ' >' + newRepr + '</option>')
|
||||
win.close();
|
||||
}
|
||||
|
||||
document.addEventListener('drop', function (e) { e.preventDefault(); }, false);
|
||||
|
||||
function add_input_form() {
|
||||
var form_idx = $('#id_images-TOTAL_FORMS').val();
|
||||
$('#image_form_set').append($('#empty_form').html().replace(/__prefix__/g, form_idx));
|
||||
$('#id_images-TOTAL_FORMS').val(parseInt(form_idx) + 1);
|
||||
}
|
||||
|
||||
function add_answers_input_form() {
|
||||
var form_idx = $('#id_answers-TOTAL_FORMS').val();
|
||||
$('#answer_form_set').append($('#answer_empty_form').html().replace(/__prefix__/g, form_idx));
|
||||
$('#id_answers-TOTAL_FORMS').val(parseInt(form_idx) + 1);
|
||||
}
|
||||
|
||||
|
||||
$(document).ready(function () {
|
||||
$("#add_abnormality").appendTo($("label[for='id_abnormality']"));
|
||||
$("#add_examination").appendTo($("label[for='id_examination']"));
|
||||
$("#add_region").appendTo($("label[for='id_region']"));
|
||||
|
||||
dropContainer = document.getElementById("drop-container");
|
||||
|
||||
dropContainer.ondragover = function (evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
};
|
||||
|
||||
dropContainer.ondragenter = function (evt) {
|
||||
console.log("ENTER")
|
||||
console.log(evt)
|
||||
$(evt.target).addClass("drop-target-active")
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
};
|
||||
|
||||
dropContainer.ondragleave = function (evt) {
|
||||
console.log("ENTER")
|
||||
console.log(evt)
|
||||
$(evt.target).removeClass("drop-target-active")
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
};
|
||||
|
||||
dropContainer.ondrop = function (evt) {
|
||||
console.log("SHIT", evt);
|
||||
$(evt.target).removeClass("drop-target-active");
|
||||
|
||||
// Get all input elements
|
||||
inputs = $("#rapid-form input[type=file][id^=id_images-]");
|
||||
//fileInput = document.getElementById("id_images-0-image");
|
||||
|
||||
// Make sure we have enough input targets
|
||||
input_diff = (evt.dataTransfer.files.length - inputs.length)
|
||||
console.log("diff", input_diff)
|
||||
|
||||
if(input_diff > 0) {
|
||||
for(let j = 0; j < input_diff; j++) {
|
||||
add_input_form()
|
||||
}
|
||||
|
||||
// Need to make sure we include the new ones...
|
||||
inputs = $("#rapid-form input[type=file][id^=id_images-]");
|
||||
}
|
||||
|
||||
// Loop through each dropped file and try to assign to an
|
||||
// input element
|
||||
[...evt.dataTransfer.files].forEach((f) => {
|
||||
let dT = new DataTransfer();
|
||||
dT.clearData();
|
||||
dT.items.add(f);
|
||||
for(let i = 0; i < inputs.length; i++) {
|
||||
el = inputs.get(i)
|
||||
|
||||
if(el.files.length == 0) {
|
||||
el.files = dT.files;
|
||||
ocr(el)
|
||||
|
||||
if(evt.target.id == "feedback-drop-target") {
|
||||
arr = el.name.split("-");
|
||||
arr.splice(2, 1, "feedback_image");
|
||||
feedback_el_name = arr.join("-");
|
||||
|
||||
$(`[name=${feedback_el_name}]`).prop("checked", true);
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// // If you want to use some of the dropped files
|
||||
// const dT = new DataTransfer();
|
||||
// dT.items.add(evt.dataTransfer.files[0]);
|
||||
// dT.items.add(evt.dataTransfer.files[3]);
|
||||
// fileInput.files = dT.files;
|
||||
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
updateFileList();
|
||||
};
|
||||
|
||||
|
||||
$('#add_more_images').click(() => { add_input_form() });
|
||||
$('#add_more_answers').click(() => { add_answers_input_form() });
|
||||
|
||||
function updateFileList() {
|
||||
$("#drop-filenames").empty()
|
||||
$("#image_form_set input[type=file]").each((n, el) => {
|
||||
console.log(el);
|
||||
if(el.files.length > 0) {
|
||||
extra_class = " image-ident-loading";
|
||||
if($(el).hasClass("image-ident-warning")) {
|
||||
extra_class = " image-ident-warning"
|
||||
} else if($(el).hasClass("image-ident-ok")) {
|
||||
extra_class = " image-ident-ok"
|
||||
}
|
||||
$("#drop-filenames").append(
|
||||
`<span>${n}: ${el.files[0].name}</span><img class='uploading${extra_class}' data-input-id='${el.id}' src=${URL.createObjectURL(el.files[0])}>`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
$("input[type=file]").on('change', function () {
|
||||
$(this).removeClass("image-ident-warning");
|
||||
$(this).removeClass("image-ident-ok");
|
||||
updateFileList();
|
||||
console.log("input change1");
|
||||
console.log("input change", this);
|
||||
ocr(this);
|
||||
});
|
||||
|
||||
});
|
||||
function ocr(input) {
|
||||
// Tesseract.recognize(f, "eng",{ logger: m => console.log(m) }
|
||||
// ).then(({ data: { text } }) => {
|
||||
// console.log(text);
|
||||
// l = text.toLowerCase();
|
||||
// if (l.includes("accesion") || l.includes("number") || l.search(/(REF|RK9|RH8|RA9)\d+/g)) {
|
||||
// console.log("SHIT", this);
|
||||
// }
|
||||
// })
|
||||
|
||||
console.log('{% static "worker.min.js" %}');
|
||||
console.log('{{ STATIC_URL }}/tesseract-core.wasm.js %}');
|
||||
|
||||
const worker = Tesseract.createWorker({
|
||||
workerPath: '{% static "worker.min.js" %}',
|
||||
langPath: '{% static "" %}',
|
||||
//langPath: '{{ STATIC_URL }}',
|
||||
corePath: '{% static "tesseract-core.wasm.js" %}',
|
||||
});
|
||||
|
||||
const url = URL.createObjectURL(input.files[0]);
|
||||
let img = new Image;
|
||||
img.src = url;
|
||||
|
||||
img.onload = function () {
|
||||
|
||||
|
||||
width = img.width;
|
||||
|
||||
// const rectangle = { left: 0, top: 0, width: width, height: 10 }
|
||||
|
||||
|
||||
(async () => {
|
||||
await worker.load();
|
||||
await worker.loadLanguage('eng');
|
||||
await worker.initialize('eng');
|
||||
// const { data: { text } } = await worker.recognize(input.files[0], { rectangle });
|
||||
const { data: { text } } = await worker.recognize(input.files[0]);
|
||||
//console.log(text);
|
||||
l = text.toLowerCase();
|
||||
$(`img[data-input-id='${input.id}'`).removeClass("image-ident-loading");
|
||||
console.log(l);
|
||||
if(l.includes("accesion") || l.includes("number") || l.search(/(ref|rk9|rh8|ra9|rbz)\d+/g) > -1) {
|
||||
console.log("SHIT", input);
|
||||
$(input).addClass("image-ident-warning");
|
||||
$(`img[data-input-id='${input.id}'`).addClass("image-ident-warning");
|
||||
} else {
|
||||
$(input).addClass("image-ident-ok");
|
||||
|
||||
}
|
||||
|
||||
await worker.terminate();
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{{ form.media }}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Submit Rapid</h2>
|
||||
<a href="{% url 'rapids:rapid_create_defaults' %}">Edit defaults</a>
|
||||
<form action="" method="post" enctype="multipart/form-data" id="rapid-form">
|
||||
{% csrf_token %}
|
||||
<a href="/rapids/abnormality/create" id="add_abnormality" class="add-popup"
|
||||
onclick="return showAddPopup(this);"><img src="{% static '/img/icon-addlink.svg' %}"></a>
|
||||
<a href="/rapids/examination/create" id="add_examination" class="add-popup"
|
||||
onclick="return showAddPopup(this);"><img src="{% static '/img/icon-addlink.svg' %}"></a>
|
||||
<a href="/rapids/region/create" id="add_region" class="add-popup" onclick="return showAddPopup(this);"><img
|
||||
src="{% static '/img/icon-addlink.svg' %}"></a>
|
||||
|
||||
<table>
|
||||
{{ form.as_table }}
|
||||
</table>
|
||||
<h3>Answers:</h3>
|
||||
<input type="button" value="Add More Answers" id="add_more_answers">
|
||||
<div id="answer_form_set">
|
||||
{% for form in answer_formset %}
|
||||
<ul class="no-error answer-formset">
|
||||
{{form.non_field_errors}}
|
||||
{{form.errors}}
|
||||
{{ form.as_ul }}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ answer_formset.management_form }}
|
||||
<h3>Images:</h3>
|
||||
<input type="button" value="Add More Images" id="add_more_images">
|
||||
<div id="drop-container" class="drop-target">Drop images here (or use the buttons below)<div
|
||||
id="feedback-drop-target">Feedback image?<br />drop those here</div>
|
||||
<div id="drop-filenames"></div>
|
||||
</div>
|
||||
<div id="image_form_set">
|
||||
{% for form in image_formset %}
|
||||
<ul class="no-error image-formset">
|
||||
{{form.non_field_errors}}
|
||||
{{form.errors}}
|
||||
{{ form.as_ul }}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ image_formset.management_form }}
|
||||
<input type="submit" class="submit-button" value="Submit" name="submit">
|
||||
<input type="submit" class="submit-button" value="Submit and Clone" name="submit-clone">
|
||||
</form>
|
||||
<div id="empty_form" style="display:none">
|
||||
<ul class='no_error image-formset'>
|
||||
{{ image_formset.empty_form.as_ul }}
|
||||
</ul>
|
||||
</div>
|
||||
<div id="answer_empty_form" style="display:none">
|
||||
<ul class='no_error answer-formset'>
|
||||
{{ answer_formset.empty_form.as_ul }}
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
{% extends "rapids/base.html" %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<h2>Defaults for creating new rapids</h2>
|
||||
<form method="POST" class="post-form">{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="save btn btn-primary">Save</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
{% extends 'rapids/base.html' %}
|
||||
|
||||
{% load render_table from django_tables2 %}
|
||||
{% block css %}
|
||||
{% endblock %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% endblock %}
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
from django.urls import path, include
|
||||
from . import views
|
||||
|
||||
app_name = "rapids"
|
||||
|
||||
urlpatterns = [
|
||||
# path('', views.question_list, name='question_list'),
|
||||
path("", views.index, name="index"),
|
||||
path("author/<int:pk>/", views.author_detail, name="author_detail"),
|
||||
path("author/", views.author_list, name="author_list"),
|
||||
#path("unchecked/", views.unchecked_list, name="unchecked_list"),
|
||||
#path("verified/<int:pk>/", views.verified_detail, name="verified_detail"),
|
||||
path("<int:pk>/", views.rapid_detail, name="rapid_detail"),
|
||||
path("<int:pk>/split", views.rapid_split, name="rapid_split"),
|
||||
path("<int:pk>/clone", views.RapidClone.as_view(), name="rapid_clone"),
|
||||
#path("verified/", views.verified, name="verified"),
|
||||
#path("all_questions/", views.all_questions, name="all_questions"),
|
||||
path("<int:pk>/scrap", views.rapid_scrap, name="rapid_scrap"),
|
||||
path("create/", views.RapidCreate.as_view(), name="rapid_create"),
|
||||
path("create/defaults",
|
||||
views.RapidCreationDefaultView.as_view(),
|
||||
name="rapid_create_defaults"),
|
||||
path("view/", views.RapidView.as_view(), name="rapid_view"),
|
||||
path("region/create/", views.create_region, name="create_region"),
|
||||
path("region/ajax/get_region_id",
|
||||
views.get_region_id,
|
||||
name="get_region_id"),
|
||||
path("abnormality/create/",
|
||||
views.create_abnormality,
|
||||
name="create_abnormality"),
|
||||
path("abnormality/ajax/get_abnormality_id",
|
||||
views.get_abnormality_id,
|
||||
name="get_abnormality_id"),
|
||||
path("examination/create/",
|
||||
views.create_examination,
|
||||
name="create_examination"),
|
||||
path("examination/ajax/get_examination_id",
|
||||
views.get_examination_id,
|
||||
name="get_examination_id"),
|
||||
path(
|
||||
"<int:pk>/update",
|
||||
views.RapidUpdate.as_view(),
|
||||
name="rapid_update",
|
||||
),
|
||||
path("<int:pk>/add_note", views.AddNote.as_view(), name="rapid_add_note"),
|
||||
]
|
||||
Executable
+484
@@ -0,0 +1,484 @@
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django import forms
|
||||
|
||||
# from django.contrib.auth.models import User
|
||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
|
||||
from django.views.generic.edit import CreateView, UpdateView, DeleteView
|
||||
from django.views.generic import ListView
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from django.urls import reverse_lazy, reverse
|
||||
|
||||
from django.http import Http404, JsonResponse
|
||||
from django.http import HttpResponseRedirect, HttpResponse
|
||||
|
||||
from .forms import (
|
||||
RapidForm,
|
||||
ImageFormSet,
|
||||
NoteForm,
|
||||
RegionForm,
|
||||
AbnormalityForm,
|
||||
ExaminationForm,
|
||||
AnswerFormSet,
|
||||
AnswerUpdateFormSet,
|
||||
)
|
||||
from .models import Rapid, Note, Abnormality, Region, Examination
|
||||
from .tables import RapidTable
|
||||
from .filters import RapidFilter
|
||||
|
||||
from django_tables2 import SingleTableView, SingleTableMixin
|
||||
from django_filters.views import FilterView
|
||||
|
||||
from .decorators import user_is_author_or_rapid_checker
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from django.forms.models import model_to_dict
|
||||
from rapids.forms import RapidCreationDefaultForm
|
||||
from rapids.models import RapidCreationDefault
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AuthorOrCheckerRequiredMixin(object):
|
||||
def get_object(self, *args, **kwargs):
|
||||
obj = super(UpdateView, self).get_object(*args, **kwargs)
|
||||
if self.request.user.groups.filter(name="rapid_checker").exists():
|
||||
return obj
|
||||
if self.request.user not in obj.author.all():
|
||||
raise PermissionDenied() # or Http404
|
||||
return obj
|
||||
|
||||
|
||||
@login_required
|
||||
def index(request):
|
||||
other_rapids = Rapid.objects.exclude(author=request.user.pk)
|
||||
user_rapids = Rapid.objects.filter(author=request.user.pk)
|
||||
return render(
|
||||
request,
|
||||
"rapids/index.html",
|
||||
{"other_rapids": other_rapids, "user_rapids": user_rapids},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def rapid_detail(request, pk):
|
||||
rapid = get_object_or_404(Rapid, pk=pk)
|
||||
|
||||
# if request.user not in rapid.author.all():
|
||||
# raise PermissionDenied
|
||||
|
||||
# logging.debug(rapid.rapid_notes.first())
|
||||
# logging.debug(rapid.subspecialty.first().name.all())
|
||||
return render(request, "rapids/rapid_detail.html", {"rapid": rapid})
|
||||
|
||||
|
||||
@login_required
|
||||
def rapid_split(request, pk):
|
||||
rapid = get_object_or_404(Rapid, pk=pk)
|
||||
|
||||
images = rapid.images.all()
|
||||
|
||||
old_abnormality = rapid.abnormality.all()
|
||||
old_region = rapid.region.all()
|
||||
old_examination = rapid.examination.all()
|
||||
old_site = rapid.site.all()
|
||||
old_author = rapid.author.all()
|
||||
|
||||
if not images:
|
||||
raise Http404
|
||||
|
||||
for n in range(len(images) - 1):
|
||||
rapid.pk = None
|
||||
|
||||
rapid.save()
|
||||
|
||||
rapid.abnormality.set(old_abnormality)
|
||||
rapid.region.set(old_region)
|
||||
rapid.examination.set(old_examination)
|
||||
rapid.site.set(old_site)
|
||||
rapid.author.set(old_author)
|
||||
images[n].rapid = rapid
|
||||
images[n].save()
|
||||
|
||||
# images[-1].rapid
|
||||
|
||||
# if request.user not in rapid.author.all():
|
||||
# raise PermissionDenied
|
||||
|
||||
# logging.debug(rapid.rapid_notes.first())
|
||||
# logging.debug(rapid.subspecialty.first().name.all())
|
||||
return render(request, "rapids/rapid_detail.html", {"rapid": rapid})
|
||||
|
||||
|
||||
@login_required
|
||||
def author_detail(request, pk):
|
||||
# logging.debug(Author.objects.all())
|
||||
# author = get_object_or_404(Author, pk=pk)
|
||||
author = User.objects.get(pk=pk)
|
||||
|
||||
rapids = Rapid.objects.filter(author=pk)
|
||||
|
||||
return render(
|
||||
request, "rapids/category_detail.html", {"category": author, "rapids": rapids}
|
||||
)
|
||||
|
||||
|
||||
def author_list(request):
|
||||
authors = User.objects.all()
|
||||
|
||||
return render(request, "rapids/author_list.html", {"authors": authors})
|
||||
|
||||
|
||||
class RapidCreationDefaultView(LoginRequiredMixin, UpdateView):
|
||||
model = RapidCreationDefault
|
||||
form_class = RapidCreationDefaultForm
|
||||
|
||||
# fields = '__all__'
|
||||
# #fields = [ 'condition' ]
|
||||
# #initial = {'date_of_death': '05/01/2018'}
|
||||
# exclude = [ 'created_date', 'published_date' ]
|
||||
# def dispatch(self, request, *args, **kwargs):
|
||||
# """
|
||||
# Overridden so we can make sure the `Rapid` instance exists
|
||||
# before going any further.
|
||||
# """
|
||||
# self.pk = get_object_or_404(Rapid, pk=kwargs['pk'])
|
||||
# return super().dispatch(request, *args, **kwargs)
|
||||
def get_object(self, queryset=None):
|
||||
obj, create = RapidCreationDefault.objects.get_or_create(
|
||||
author=self.request.user
|
||||
)
|
||||
|
||||
return obj
|
||||
|
||||
def form_valid(self, form):
|
||||
|
||||
model = form.save(commit=False)
|
||||
|
||||
model.author = self.request.user
|
||||
model.save()
|
||||
|
||||
response = super().form_valid(form)
|
||||
|
||||
return response
|
||||
|
||||
# form.instance.author.add(self.request.user.id)
|
||||
|
||||
|
||||
class AddNote(LoginRequiredMixin, CreateView):
|
||||
model = Note
|
||||
form_class = NoteForm
|
||||
|
||||
# fields = '__all__'
|
||||
# #fields = [ 'condition' ]
|
||||
# #initial = {'date_of_death': '05/01/2018'}
|
||||
# exclude = [ 'created_date', 'published_date' ]
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
"""
|
||||
Overridden so we can make sure the `Rapid` instance exists
|
||||
before going any further.
|
||||
"""
|
||||
self.pk = get_object_or_404(Rapid, pk=kwargs["pk"])
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
|
||||
note = form.save(commit=False)
|
||||
|
||||
note.rapid = self.pk
|
||||
|
||||
note.author = self.request.user
|
||||
note.save()
|
||||
|
||||
response = super().form_valid(form)
|
||||
|
||||
return response
|
||||
|
||||
# form.instance.author.add(self.request.user.id)
|
||||
|
||||
|
||||
@login_required
|
||||
def rapid_clone(request, pk):
|
||||
new_item = get_object_or_404(Rapid, pk=pk)
|
||||
new_item.pk = None # autogen a new pk (item_id)
|
||||
# new_item.name = "Copy of " + new_item.name #need to change uniques
|
||||
|
||||
form = RapidForm(request.POST or None, instance=new_item)
|
||||
|
||||
image_formset = ImageFormSet()
|
||||
answer_formset = AnswerFormSet()
|
||||
|
||||
if form.is_valid():
|
||||
form.instance.author.add(request.user.id)
|
||||
|
||||
# logger.debug(formset.is_valid())
|
||||
if image_formset.is_valid() and answer_formset.is_valid():
|
||||
response = super().form_valid(form)
|
||||
image_formset.instance = obj
|
||||
image_formset.save()
|
||||
|
||||
answer_formset.instance = obj
|
||||
answer_formset.save()
|
||||
return response
|
||||
else:
|
||||
return super().form_invalid(form)
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"image_formset": image_formset,
|
||||
"answer_formset": answer_formset
|
||||
# other context
|
||||
}
|
||||
|
||||
return render(request, "rapids/rapid_form.html", context)
|
||||
|
||||
|
||||
class RapidCreateBase(LoginRequiredMixin, CreateView):
|
||||
model = Rapid
|
||||
form_class = RapidForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(RapidCreateBase, self).get_context_data(**kwargs)
|
||||
if self.request.POST:
|
||||
context["image_formset"] = ImageFormSet(
|
||||
self.request.POST, self.request.FILES
|
||||
)
|
||||
context["image_formset"].full_clean()
|
||||
context["answer_formset"] = AnswerFormSet(
|
||||
self.request.POST
|
||||
)
|
||||
context["answer_formset"].full_clean()
|
||||
else:
|
||||
context["image_formset"] = ImageFormSet()
|
||||
context["answer_formset"] = AnswerFormSet()
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
|
||||
self.object = form.save(commit=False)
|
||||
self.object.save()
|
||||
|
||||
form.instance.author.add(self.request.user.id)
|
||||
|
||||
context = self.get_context_data(form=form)
|
||||
image_formset = context["image_formset"]
|
||||
answer_formset = context["answer_formset"]
|
||||
if image_formset.is_valid() and answer_formset.is_valid():
|
||||
response = super().form_valid(form)
|
||||
image_formset.instance = self.object
|
||||
image_formset.save()
|
||||
answer_formset.instance = self.object
|
||||
answer_formset.save()
|
||||
# If the normal submit button is pressed we save as normal
|
||||
if "submit" in self.request.POST:
|
||||
return response
|
||||
# else we redirect to the clone url
|
||||
else:
|
||||
return redirect("rapids:rapid_clone", pk=self.object.pk)
|
||||
|
||||
else:
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
# @login_required
|
||||
class RapidCreate(RapidCreateBase):
|
||||
|
||||
initial = {"laterality": Rapid.NONE}
|
||||
|
||||
def get_initial(self):
|
||||
# There has to be a better way...
|
||||
try:
|
||||
s = (i.pk for i in self.request.user.rapid_default.site.all())
|
||||
self.initial.update({"site": s})
|
||||
except AttributeError:
|
||||
pass
|
||||
return self.initial
|
||||
|
||||
# fields = '__all__'
|
||||
# #fields = [ 'condition' ]
|
||||
# #initial = {'date_of_death': '05/01/2018'}
|
||||
# exclude = [ 'created_date', 'published_date' ]
|
||||
|
||||
# self.object = form.save(commit=False)
|
||||
# self.object.save()
|
||||
|
||||
# form.instance.author.add(self.request.user.id)
|
||||
# return super().form_valid(form)
|
||||
|
||||
|
||||
class RapidUpdate(LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView):
|
||||
model = Rapid
|
||||
form_class = RapidForm
|
||||
|
||||
# fields = '__all__'
|
||||
# #fields = [ 'condition' ]
|
||||
# #initial = {'date_of_death': '05/01/2018'}
|
||||
# exclude = [ 'created_date', 'published_date' ]
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(RapidUpdate, self).get_context_data(**kwargs)
|
||||
if self.request.POST:
|
||||
context["image_formset"] = ImageFormSet(
|
||||
self.request.POST, self.request.FILES, instance=self.object
|
||||
)
|
||||
context["image_formset"].full_clean()
|
||||
context["answer_formset"] = AnswerFormSet(
|
||||
self.request.POST, instance=self.object
|
||||
)
|
||||
context["answer_formset"].full_clean()
|
||||
else:
|
||||
context["image_formset"] = ImageFormSet(instance=self.object)
|
||||
context["answer_formset"] = AnswerFormSet(instance=self.object)
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
|
||||
self.object = form.save(commit=False)
|
||||
self.object.save()
|
||||
|
||||
form.instance.author.add(self.request.user.id)
|
||||
|
||||
context = self.get_context_data(form=form)
|
||||
image_formset = context["image_formset"]
|
||||
answer_formset = context["answer_formset"]
|
||||
# logger.debug(formset.is_valid())
|
||||
if image_formset.is_valid() and answer_formset.is_valid():
|
||||
response = super().form_valid(form)
|
||||
image_formset.instance = self.object
|
||||
image_formset.save()
|
||||
answer_formset.instance = self.object
|
||||
answer_formset.save()
|
||||
return response
|
||||
else:
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class RapidClone(RapidCreateBase):
|
||||
"""Clones a existing rapid"""
|
||||
|
||||
# fields = '__all__'
|
||||
# #fields = [ 'condition' ]
|
||||
# #initial = {'date_of_death': '05/01/2018'}
|
||||
# exclude = [ 'created_date', 'published_date' ]
|
||||
def get_initial(self):
|
||||
# print(self.request)
|
||||
old_object = get_object_or_404(Rapid, pk=self.kwargs["pk"])
|
||||
initial_data = model_to_dict(old_object, exclude=["id"])
|
||||
|
||||
return initial_data
|
||||
|
||||
|
||||
@login_required
|
||||
@user_is_author_or_rapid_checker
|
||||
def rapid_scrap(request, pk):
|
||||
try:
|
||||
rapid = Rapid.objects.get(pk=pk)
|
||||
except Rapid.DoesNotExist:
|
||||
raise Http404("Rapid does not exist")
|
||||
|
||||
rapid.scrapped = not rapid.scrapped
|
||||
rapid.save()
|
||||
return HttpResponseRedirect(reverse("rapids:rapid_detail", args=(pk,)))
|
||||
|
||||
|
||||
# @login_required
|
||||
# def edit_abnormality_popup(request):
|
||||
# instance = get_object_or_404(Abnormality, pk = pk)
|
||||
# form = AbnormalityForm(request.POST or None)
|
||||
# if form.is_valid():
|
||||
# instance = form.save()
|
||||
# return HttpResponse('<script>opener.closePopup(window, "%s", "%s", "#id_abnormality");</script>' % (instance.pk, instance))
|
||||
# return render(request, "rapids/create_simple.html", {'form': form})
|
||||
|
||||
|
||||
@login_required
|
||||
def create_abnormality(request):
|
||||
form = AbnormalityForm(request.POST or None)
|
||||
if form.is_valid():
|
||||
instance = form.save()
|
||||
return HttpResponse(
|
||||
'<script>opener.closePopup(window, "%s", "%s", "#id_abnormality");</script>'
|
||||
% (instance.pk, instance)
|
||||
)
|
||||
return render(
|
||||
request, "rapids/create_simple.html", {"form": form, "name": "Abnormality"}
|
||||
)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def get_abnormality_id(request):
|
||||
if request.is_ajax():
|
||||
abnormality_name = request.GET["abnormality_name"]
|
||||
abnormality_id = Abnormality.objects.get(name=abnormality_name).id
|
||||
data = {
|
||||
"abnormality_id": abnormality_id,
|
||||
}
|
||||
return HttpResponse(json.dumps(data), content_type="application/json")
|
||||
return HttpResponse("/")
|
||||
|
||||
|
||||
@login_required
|
||||
def create_examination(request):
|
||||
form = ExaminationForm(request.POST or None)
|
||||
if form.is_valid():
|
||||
instance = form.save()
|
||||
return HttpResponse(
|
||||
'<script>opener.closePopup(window, "%s", "%s", "#id_examination");</script>'
|
||||
% (instance.pk, instance)
|
||||
)
|
||||
return render(
|
||||
request, "rapids/create_simple.html", {"form": form, "name": "Examination"}
|
||||
)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def get_examination_id(request):
|
||||
if request.is_ajax():
|
||||
examination_name = request.GET["examination_name"]
|
||||
examination_id = Examination.objects.get(name=examination_name).id
|
||||
data = {
|
||||
"examination_id": examination_id,
|
||||
}
|
||||
return HttpResponse(json.dumps(data), content_type="application/json")
|
||||
return HttpResponse("/")
|
||||
|
||||
|
||||
@login_required
|
||||
def create_region(request):
|
||||
form = RegionForm(request.POST or None)
|
||||
if form.is_valid():
|
||||
instance = form.save()
|
||||
return HttpResponse(
|
||||
'<script>opener.closePopup(window, "%s", "%s", "#id_region");</script>'
|
||||
% (instance.pk, instance)
|
||||
)
|
||||
return render(
|
||||
request, "rapids/create_simple.html", {"form": form, "name": "Region"}
|
||||
)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def get_region_id(request):
|
||||
if request.is_ajax():
|
||||
region_name = request.GET["region_name"]
|
||||
region_id = Region.objects.get(name=region_name).id
|
||||
data = {
|
||||
"region_id": region_id,
|
||||
}
|
||||
return HttpResponse(json.dumps(data), content_type="application/json")
|
||||
return HttpResponse("/")
|
||||
|
||||
|
||||
class RapidView(SingleTableMixin, FilterView):
|
||||
model = Rapid
|
||||
table_class = RapidTable
|
||||
template_name = "rapids/view.html"
|
||||
|
||||
filterset_class = RapidFilter
|
||||
@@ -5,6 +5,7 @@
|
||||
<h1>PENRA Courses</h1>
|
||||
<a href="{% url 'anatomy:index'%}">Anatomy</a>
|
||||
<a href="{% url 'physics:index'%}">Physics</a>
|
||||
<a href="{% url 'rapids:index'%}">Rapids</a>
|
||||
<a href="{% url 'login'%}">Log in</a>
|
||||
{% if request.user.is_staff %}<a href="{% url 'admin:index'%}">Admin</a>{% endif %}
|
||||
<a href="{% url 'password_reset'%}">Reset password</a>
|
||||
|
||||
Reference in New Issue
Block a user