.
This commit is contained in:
+8
-2
@@ -1,5 +1,6 @@
|
|||||||
# pull official base image
|
# pull official base image
|
||||||
FROM python:3.12.1-slim
|
FROM python:3.12.1-slim
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||||
|
|
||||||
# set work directory
|
# set work directory
|
||||||
WORKDIR /usr/src/rad
|
WORKDIR /usr/src/rad
|
||||||
@@ -17,9 +18,14 @@ RUN apt-get update && apt-get -y install postgresql postgresql-contrib git libca
|
|||||||
|
|
||||||
|
|
||||||
# install dependencies
|
# install dependencies
|
||||||
RUN pip install --upgrade pip
|
#RUN pip install --upgrade pip
|
||||||
COPY ./requirements.txt .
|
COPY ./requirements.txt .
|
||||||
RUN pip install -r requirements.txt
|
RUN uv venv /opt/venv
|
||||||
|
# Use the virtual environment automatically
|
||||||
|
ENV VIRTUAL_ENV=/opt/venv
|
||||||
|
# Place entry points in the environment at the front of the path
|
||||||
|
ENV PATH="/opt/venv/bin:$PATH"
|
||||||
|
RUN uv pip install -r requirements.txt
|
||||||
|
|
||||||
# copy project
|
# copy project
|
||||||
COPY . .
|
COPY . .
|
||||||
@@ -22,6 +22,7 @@ from collections import defaultdict
|
|||||||
from helpers.images import image_as_base64
|
from helpers.images import image_as_base64
|
||||||
|
|
||||||
import reversion
|
import reversion
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
image_storage = FileSystemStorage(
|
image_storage = FileSystemStorage(
|
||||||
# Physical file location ROOT
|
# Physical file location ROOT
|
||||||
@@ -540,6 +541,8 @@ class UserAnswer(UserAnswerBase):
|
|||||||
# s = self.answer.lower().strip()
|
# s = self.answer.lower().strip()
|
||||||
# s = s.translate(str.maketrans('', '', string.punctuation))
|
# s = s.translate(str.maketrans('', '', string.punctuation))
|
||||||
# return s
|
# return s
|
||||||
|
def get_answer_url_safe(self):
|
||||||
|
return urllib.parse.quote(self.answer_compare)
|
||||||
|
|
||||||
def get_answer_string(self):
|
def get_answer_string(self):
|
||||||
return self.answer
|
return self.answer
|
||||||
|
|||||||
@@ -38,22 +38,22 @@
|
|||||||
{% for answer in user_answers %}
|
{% for answer in user_answers %}
|
||||||
|
|
||||||
{% if answer in answer_suggest_incorrect %}
|
{% if answer in answer_suggest_incorrect %}
|
||||||
<li><pre><span class="answer incorrect suggest" title="{{answer}}" data-answerurl="{% url 'anatomy:question_user_answers_by_compare' question.pk answer %}">{{ answer }}</span></pre></li>
|
<li><pre><span class="answer incorrect suggest" title="{{answer}}" data-answerurl="{% url 'anatomy:question_user_answers_by_compare' question.pk answer.get_answer_url_safe %}">{{ answer }}</span></pre></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><pre><span class="answer not-marked" title="{{answer}}" data-answerurl="{% url 'anatomy:question_user_answers_by_compare' question.pk answer %}">{{ answer }}</span></pre></li>
|
<li><pre><span class="answer not-marked" title="{{answer}}" data-answerurl="{% url 'anatomy:question_user_answers_by_compare' question.pk answer.get_answer_url_safe %}">{{ answer }}</span></pre></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
Marked:
|
Marked:
|
||||||
<ul id="marked-answer-list" class="answer-list">
|
<ul id="marked-answer-list" class="answer-list">
|
||||||
{% for answer in correct_answers %}
|
{% for answer in correct_answers %}
|
||||||
<li><pre><span class="answer correct" title="{{answer}}" data-answerurl="{% url 'anatomy:question_user_answers_by_compare' question.pk answer %}">{{ answer }}</span></pre></li>
|
<li><pre><span class="answer correct" title="{{answer}}" data-answerurl="{% url 'anatomy:question_user_answers_by_compare' question.pk answer.get_answer_url_safe %}">{{ answer }}</span></pre></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for answer in half_mark_answers %}
|
{% for answer in half_mark_answers %}
|
||||||
<li><pre><span class="answer half-correct" title="{{answer}}" data-answerurl="{% url 'anatomy:question_user_answers_by_compare' question.pk answer %}">{{ answer }}</span></pre></li>
|
<li><pre><span class="answer half-correct" title="{{answer}}" data-answerurl="{% url 'anatomy:question_user_answers_by_compare' question.pk answer.get_answer_url_safe %}">{{ answer }}</span></pre></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for answer in incorrect_answers %}
|
{% for answer in incorrect_answers %}
|
||||||
<li><pre><span class="answer incorrect" title="{{answer}}" data-answerurl="{% url 'anatomy:question_user_answers_by_compare' question.pk answer %}">{{ answer }}</span></pre></li>
|
<li><pre><span class="answer incorrect" title="{{answer}}" data-answerurl="{% url 'anatomy:question_user_answers_by_compare' question.pk answer.get_answer_url_safe %}">{{ answer }}</span></pre></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
<div class="mark-buttons">
|
<div class="mark-buttons">
|
||||||
|
|||||||
@@ -199,7 +199,13 @@
|
|||||||
|
|
||||||
|
|
||||||
<p class="pre-whitespace"><b>Previous case:</b> {{ case.previous_case.get_link }}</p>
|
<p class="pre-whitespace"><b>Previous case:</b> {{ case.previous_case.get_link }}</p>
|
||||||
<p class="pre-whitespace"><b>Next case:</b> {{ case.next_case.get_link }}</p>
|
<p class="pre-whitespace"><b>Next case:</b>
|
||||||
|
{% if case.next_case %}
|
||||||
|
{{ case.next_case.get_link }}
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'atlas:case_clone_next' case.pk %}">Clone and add next case</a>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
<div>
|
<div>
|
||||||
{% include 'question_notes.html' %}
|
{% include 'question_notes.html' %}
|
||||||
|
|
||||||
|
|||||||
@@ -306,6 +306,7 @@ urlpatterns = [
|
|||||||
name="structure_update",
|
name="structure_update",
|
||||||
),
|
),
|
||||||
path("structure/create", views.StructureCreate.as_view(), name="structure_create"),
|
path("structure/create", views.StructureCreate.as_view(), name="structure_create"),
|
||||||
|
path("series/<int:pk>/thumbnail/fail", views.series_thumbnail_fail, name="series_thumbnail_fail"),
|
||||||
path("series/<int:pk>/thumbnail", views.series_thumbnail, name="series_thumbnail"),
|
path("series/<int:pk>/thumbnail", views.series_thumbnail, name="series_thumbnail"),
|
||||||
# TODO: case context series viewing (so that we can view series in the context of a case)
|
# TODO: case context series viewing (so that we can view series in the context of a case)
|
||||||
path("series/<int:pk>", views.series_detail, name="series_detail"),
|
path("series/<int:pk>", views.series_detail, name="series_detail"),
|
||||||
@@ -372,6 +373,7 @@ urlpatterns = [
|
|||||||
name="case_collection_form",
|
name="case_collection_form",
|
||||||
),
|
),
|
||||||
path("case/<int:pk>/clone", views.AtlasClone.as_view(), name="case_clone"),
|
path("case/<int:pk>/clone", views.AtlasClone.as_view(), name="case_clone"),
|
||||||
|
path("case/<int:pk>/clone/next", views.AtlasCloneNext.as_view(), name="case_clone_next"),
|
||||||
# path("verified/", views.verified, name="verified"),
|
# path("verified/", views.verified, name="verified"),
|
||||||
# path("all_questions/", views.all_questions, name="all_questions"),
|
# path("all_questions/", views.all_questions, name="all_questions"),
|
||||||
path("case/<int:pk>/scrap", views.atlas_scrap, name="case_scrap"),
|
path("case/<int:pk>/scrap", views.atlas_scrap, name="case_scrap"),
|
||||||
|
|||||||
+14
-4
@@ -197,13 +197,17 @@ def case_detail(request, pk):
|
|||||||
# logging.debug(atlas.subspecialty.first().name.all())
|
# logging.debug(atlas.subspecialty.first().name.all())
|
||||||
return render(request, "atlas/case_detail.html", {"case": case})
|
return render(request, "atlas/case_detail.html", {"case": case})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@user_is_author_or_atlas_series_checker_or_atlas_marker_or_open_access
|
||||||
|
def series_thumbnail_fail(request, pk):
|
||||||
|
return series_thumbnail(request, pk=pk, fail_loudly=True)
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@user_is_author_or_atlas_series_checker_or_atlas_marker_or_open_access
|
@user_is_author_or_atlas_series_checker_or_atlas_marker_or_open_access
|
||||||
def series_thumbnail(request, pk, finding_pk=None):
|
def series_thumbnail(request, pk, fail_loudly=False):
|
||||||
series = get_object_or_404(Series, pk=pk)
|
series = get_object_or_404(Series, pk=pk)
|
||||||
|
|
||||||
thumbnail, _ = series.get_thumbnail(recreate=True)
|
thumbnail, _ = series.get_thumbnail(recreate=True, fail_loudly=fail_loudly)
|
||||||
|
|
||||||
return HttpResponse(thumbnail)
|
return HttpResponse(thumbnail)
|
||||||
|
|
||||||
@@ -1115,7 +1119,6 @@ def add_collection_to_case_form(request, case_id):
|
|||||||
# pk = self.kwargs["pk"]
|
# pk = self.kwargs["pk"]
|
||||||
# return reverse("atlas:case_detail", kwargs={"pk": pk})
|
# return reverse("atlas:case_detail", kwargs={"pk": pk})
|
||||||
|
|
||||||
|
|
||||||
class AtlasClone(AtlasCreateBase, AuthorOrCheckerRequiredMixin, CreateView):
|
class AtlasClone(AtlasCreateBase, AuthorOrCheckerRequiredMixin, CreateView):
|
||||||
"""Clones a existing atlas"""
|
"""Clones a existing atlas"""
|
||||||
|
|
||||||
@@ -1138,7 +1141,14 @@ class AtlasClone(AtlasCreateBase, AuthorOrCheckerRequiredMixin, CreateView):
|
|||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
initial_data = model_to_dict(old_object, exclude=["id"])
|
initial_data = model_to_dict(old_object, exclude=["id", "previous_case"])
|
||||||
|
return initial_data
|
||||||
|
|
||||||
|
class AtlasCloneNext(AtlasClone):
|
||||||
|
"""Extends AtlasClone to set the previous_case field to the case being cloned"""
|
||||||
|
def get_initial(self):
|
||||||
|
initial_data = super().get_initial()
|
||||||
|
initial_data["previous_case"] = self.kwargs["pk"]
|
||||||
return initial_data
|
return initial_data
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
build: ./rad
|
build: ./rad
|
||||||
|
|||||||
+6
-2
@@ -42,6 +42,8 @@ from django.contrib.auth.models import User
|
|||||||
from django.forms.models import model_to_dict
|
from django.forms.models import model_to_dict
|
||||||
from django.forms.utils import from_current_timezone, to_current_timezone
|
from django.forms.utils import from_current_timezone, to_current_timezone
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
def findMiddle(input_list):
|
def findMiddle(input_list):
|
||||||
"""Returns the middle element of a list"""
|
"""Returns the middle element of a list"""
|
||||||
@@ -419,7 +421,7 @@ class SeriesBase(models.Model):
|
|||||||
else:
|
else:
|
||||||
return format_html('", "'.join(images))
|
return format_html('", "'.join(images))
|
||||||
|
|
||||||
def get_thumbnail(self, recreate=False):
|
def get_thumbnail(self, recreate=False, fail_loudly=False):
|
||||||
images = self.images.filter(removed=False)
|
images = self.images.filter(removed=False)
|
||||||
|
|
||||||
if len(images) < 1:
|
if len(images) < 1:
|
||||||
@@ -431,7 +433,9 @@ class SeriesBase(models.Model):
|
|||||||
if recreate:
|
if recreate:
|
||||||
thumbnailer.delete_thumbnails()
|
thumbnailer.delete_thumbnails()
|
||||||
thumbnail = thumbnailer["exam-list"]
|
thumbnail = thumbnailer["exam-list"]
|
||||||
except InvalidImageFormatError:
|
except InvalidImageFormatError as e:
|
||||||
|
if fail_loudly:
|
||||||
|
raise e
|
||||||
return format_html(
|
return format_html(
|
||||||
'<img title="{}" src="/static/not-found-image.jpg" />', img
|
'<img title="{}" src="/static/not-found-image.jpg" />', img
|
||||||
), len(images)
|
), len(images)
|
||||||
|
|||||||
@@ -33,8 +33,10 @@ from django.utils.decorators import method_decorator
|
|||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import urllib
|
import urllib
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.generic.edit import CreateView, UpdateView, DeleteView
|
from django.views.generic.edit import CreateView, UpdateView, DeleteView
|
||||||
@@ -2652,6 +2654,7 @@ class GenericViewBase:
|
|||||||
@method_decorator(login_required)
|
@method_decorator(login_required)
|
||||||
def question_user_answers_by_compare(self, request, pk, answer_compare):
|
def question_user_answers_by_compare(self, request, pk, answer_compare):
|
||||||
print(answer_compare)
|
print(answer_compare)
|
||||||
|
answer_compare = urllib.parse.unquote(answer_compare)
|
||||||
return self.question_user_answers(request, pk, answer_compare)
|
return self.question_user_answers(request, pk, answer_compare)
|
||||||
|
|
||||||
@method_decorator(login_required)
|
@method_decorator(login_required)
|
||||||
|
|||||||
+1
-1
@@ -62,7 +62,7 @@ def pil_dicom_image(source, exif_orientation=True, **options):
|
|||||||
#source = BytesIO(source.read())
|
#source = BytesIO(source.read())
|
||||||
|
|
||||||
# open file with pydicom
|
# open file with pydicom
|
||||||
ds = pydicom.read_file(source)
|
ds = pydicom.dcmread(source)
|
||||||
|
|
||||||
# return the image file
|
# return the image file
|
||||||
return get_PIL_image(ds)
|
return get_PIL_image(ds)
|
||||||
|
|||||||
+3
-2
@@ -1,5 +1,5 @@
|
|||||||
#Django==3.2.13
|
#Django==3.2.13
|
||||||
Django==5.0.2
|
Django==5.1.4
|
||||||
django_debug_toolbar
|
django_debug_toolbar
|
||||||
django_jquery
|
django_jquery
|
||||||
django_reversion
|
django_reversion
|
||||||
@@ -52,4 +52,5 @@ git+https://github.com/xkjq/django-jsonforms.git@bump-json-editor-lib
|
|||||||
django_svelte_jsoneditor
|
django_svelte_jsoneditor
|
||||||
psutil
|
psutil
|
||||||
django-psutil-dash
|
django-psutil-dash
|
||||||
django-template-partials
|
django-template-partials
|
||||||
|
easy_thumbnails
|
||||||
Reference in New Issue
Block a user