diff --git a/isic/core/templates/core/widgets/diagnosis_picker.html b/isic/core/templates/core/widgets/diagnosis_picker.html
new file mode 100644
index 00000000..bac8d813
--- /dev/null
+++ b/isic/core/templates/core/widgets/diagnosis_picker.html
@@ -0,0 +1,258 @@
+
+
+
+ {{ diagnosis_values|json_script:"diagnosis-values" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/isic/studies/forms.py b/isic/studies/forms.py
index 3d91d36c..3cf93089 100644
--- a/isic/studies/forms.py
+++ b/isic/studies/forms.py
@@ -1,10 +1,11 @@
from django import forms
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
+from django.db.models import Count
from django.db.models.query import QuerySet
from django.db.models.query_utils import Q
-from isic.studies.models import Question, Study
+from isic.studies.models import Question, Response, Study
class StudyTaskForm(forms.Form):
@@ -22,6 +23,42 @@ def __init__(self, *args, **kwargs):
# Note: questions must be annotated with a required attribute
questions: QuerySet[Question] = kwargs.pop("questions")
self.questions = {x.pk: x for x in questions}
+
+ num_diagnosis_questions = len(
+ [question for question in questions if question.type == Question.QuestionType.DIAGNOSIS]
+ )
+ if num_diagnosis_questions > 1:
+ # this is a hack because passing a per-question version of most frequent diagnoses is
+ # unreasonably difficult.
+ raise ValueError("Only one diagnosis question is allowed per study.")
+ elif num_diagnosis_questions == 1: # noqa: RET506
+ # the study and user are necessary for diagnosis questions in order to compute
+ # the most frequently used diagnosis.
+ self.study = kwargs.pop("study")
+ self.user = kwargs.pop("user")
+
+ self.most_frequent_diagnoses = list(
+ Response.objects.filter(
+ question=next(
+ question
+ for question in questions
+ if question.type == Question.QuestionType.DIAGNOSIS
+ ),
+ annotation__study=self.study,
+ annotation__annotator=self.user,
+ )
+ .values("choice", "choice__text")
+ .alias(count=Count("choice"))
+ .order_by("-count")
+ )
+
+ # remove study/user from kwargs before passing to super
+ if "study" in kwargs:
+ del kwargs["study"]
+
+ if "user" in kwargs:
+ del kwargs["user"]
+
super().__init__(*args, **kwargs)
for question in questions:
# field names for django forms must be strings
diff --git a/isic/studies/migrations/0002_alter_question_type.py b/isic/studies/migrations/0002_alter_question_type.py
new file mode 100644
index 00000000..e70c94af
--- /dev/null
+++ b/isic/studies/migrations/0002_alter_question_type.py
@@ -0,0 +1,25 @@
+# Generated by Django 5.1.2 on 2024-11-13 17:33
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("studies", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="question",
+ name="type",
+ field=models.CharField(
+ choices=[
+ ("select", "Select"),
+ ("number", "Number"),
+ ("diagnosis", "Diagnosis"),
+ ],
+ default="select",
+ max_length=9,
+ ),
+ ),
+ ]
diff --git a/isic/studies/migrations/0003_alter_questionchoice_text.py b/isic/studies/migrations/0003_alter_questionchoice_text.py
new file mode 100644
index 00000000..9d82bd9f
--- /dev/null
+++ b/isic/studies/migrations/0003_alter_questionchoice_text.py
@@ -0,0 +1,17 @@
+# Generated by Django 5.1.3 on 2024-11-21 14:20
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("studies", "0002_alter_question_type"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="questionchoice",
+ name="text",
+ field=models.CharField(max_length=255),
+ ),
+ ]
diff --git a/isic/studies/models.py b/isic/studies/models.py
index 41dac2d3..40c05cf9 100644
--- a/isic/studies/models.py
+++ b/isic/studies/models.py
@@ -26,15 +26,17 @@
from isic.core.models import Image
from isic.core.models.collection import Collection
from isic.core.storages.utils import generate_upload_to
+from isic.studies.widgets import DiagnosisPicker
class Question(TimeStampedModel):
class QuestionType(models.TextChoices):
SELECT = "select", "Select"
NUMBER = "number", "Number"
+ DIAGNOSIS = "diagnosis", "Diagnosis"
prompt = models.CharField(max_length=400)
- type = models.CharField(max_length=6, choices=QuestionType.choices, default=QuestionType.SELECT)
+ type = models.CharField(max_length=9, choices=QuestionType.choices, default=QuestionType.SELECT)
official = models.BooleanField()
# TODO: maybe add a default field
@@ -51,7 +53,7 @@ class Meta(TimeStampedModel.Meta):
def __str__(self) -> str:
return self.prompt
- def to_form_field(self, *, required: bool):
+ def to_form_field(self, *, required: bool) -> ChoiceField | FormCharField:
if self.type == self.QuestionType.SELECT:
return ChoiceField(
required=required,
@@ -59,12 +61,19 @@ def to_form_field(self, *, required: bool):
label=self.prompt,
widget=RadioSelect,
)
- if self.type == self.QuestionType.NUMBER:
+ elif self.type == self.QuestionType.NUMBER: # noqa: RET505
# TODO: Use floatfield/intfield
return FormCharField(
required=required,
label=self.prompt,
)
+ elif self.type == self.QuestionType.DIAGNOSIS:
+ return ChoiceField(
+ required=required,
+ choices=[(choice.pk, choice.text) for choice in self.choices.all()],
+ label=self.prompt,
+ widget=DiagnosisPicker,
+ )
def save(self, **kwargs):
from isic.studies.models import Annotation
@@ -83,7 +92,7 @@ class Meta(TimeStampedModel.Meta):
unique_together = [["question", "text"]]
question = models.ForeignKey(Question, related_name="choices", on_delete=models.CASCADE)
- text = models.CharField(max_length=100)
+ text = models.CharField(max_length=255)
def __str__(self) -> str:
return self.text
diff --git a/isic/studies/templates/studies/study_task_detail.html b/isic/studies/templates/studies/study_task_detail.html
index 69b42b35..dfc0d15c 100644
--- a/isic/studies/templates/studies/study_task_detail.html
+++ b/isic/studies/templates/studies/study_task_detail.html
@@ -38,8 +38,15 @@
{% if study_task.complete %}
You've finished this task.
{% else %}
+ {% if form.most_frequent_diagnoses %}
+ {{ form.most_frequent_diagnoses|json_script:"most-frequent-diagnoses" }}
+ {% endif %}
+
{% endfor %}
-