This commit is contained in:
Ross
2025-01-06 11:14:34 +00:00
parent f0c5964a5a
commit d891c7b9ee
11 changed files with 52 additions and 19 deletions
+8 -2
View File
@@ -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 . .
+3
View File
@@ -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
+5 -5
View File
@@ -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' %}
+2
View File
@@ -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
View File
@@ -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
-2
View File
@@ -1,5 +1,3 @@
version: '3.8'
services: services:
web: web:
build: ./rad build: ./rad
+6 -2
View File
@@ -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)
+3
View File
@@ -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
View File
@@ -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
View File
@@ -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