diff --git a/lung_cancer_screening/core/tests/acceptance/helpers/user_interaction_helpers.py b/lung_cancer_screening/core/tests/acceptance/helpers/user_interaction_helpers.py index 6e5daa84..5289b416 100644 --- a/lung_cancer_screening/core/tests/acceptance/helpers/user_interaction_helpers.py +++ b/lung_cancer_screening/core/tests/acceptance/helpers/user_interaction_helpers.py @@ -52,3 +52,11 @@ def fill_in_and_submit_weight_imperial(page, stone, pounds): page.get_by_label("Pounds").fill(str(pounds)) page.click("text=Continue") + +def fill_in_and_submit_sex_at_birth(page, sex): + expect(page.locator("legend")).to_have_text( + "What was your sex at birth?") + + page.get_by_label(sex, exact=True).check() + + page.click("text=Continue") diff --git a/lung_cancer_screening/core/tests/acceptance/test_cannot_change_answers_after_submission.py b/lung_cancer_screening/core/tests/acceptance/test_cannot_change_answers_after_submission.py index f8ac92ac..2536419a 100644 --- a/lung_cancer_screening/core/tests/acceptance/test_cannot_change_answers_after_submission.py +++ b/lung_cancer_screening/core/tests/acceptance/test_cannot_change_answers_after_submission.py @@ -9,7 +9,8 @@ fill_in_and_submit_weight_metric, fill_in_and_submit_participant_id, fill_in_and_submit_smoking_eligibility, - fill_in_and_submit_date_of_birth + fill_in_and_submit_date_of_birth, + fill_in_and_submit_sex_at_birth ) class TestQuestionnaire(StaticLiveServerTestCase): @@ -40,6 +41,7 @@ def test_cannot_change_responses_once_checked_and_submitted(self): fill_in_and_submit_date_of_birth(page, age) fill_in_and_submit_height_metric(page, "170") fill_in_and_submit_weight_metric(page, "25.4") + fill_in_and_submit_sex_at_birth(page, "Male") page.click("text=Submit") diff --git a/lung_cancer_screening/core/tests/acceptance/test_questionnaire.py b/lung_cancer_screening/core/tests/acceptance/test_questionnaire.py index dba5c871..d92f7975 100644 --- a/lung_cancer_screening/core/tests/acceptance/test_questionnaire.py +++ b/lung_cancer_screening/core/tests/acceptance/test_questionnaire.py @@ -11,7 +11,8 @@ fill_in_and_submit_smoking_eligibility, fill_in_and_submit_date_of_birth, fill_in_and_submit_weight_metric, - fill_in_and_submit_weight_imperial + fill_in_and_submit_weight_imperial, + fill_in_and_submit_sex_at_birth ) from .helpers.assertion_helpers import expect_back_link_to_have_url @@ -49,43 +50,45 @@ def test_full_questionnaire_user_journey(self): expect(page).to_have_url( f"{self.live_server_url}/have-you-ever-smoked") - + expect_back_link_to_have_url(page, "/start") fill_in_and_submit_smoking_eligibility(page, smoking_status) expect(page).to_have_url(f"{self.live_server_url}/date-of-birth") expect_back_link_to_have_url(page, "/have-you-ever-smoked") - fill_in_and_submit_date_of_birth(page, age) expect(page).to_have_url(f"{self.live_server_url}/height") - + expect_back_link_to_have_url(page, "/date-of-birth") fill_in_and_submit_height_metric(page, height) - expect(page).to_have_url(f"{self.live_server_url}/weight") - page.click("text=Back") - expect(page).to_have_url(f"{self.live_server_url}/height") - page.click("text=Switch to imperial") - fill_in_and_submit_height_imperial(page, feet, inches) expect(page).to_have_url(f"{self.live_server_url}/weight") - + expect_back_link_to_have_url(page, "/height") fill_in_and_submit_weight_metric(page, weight_metric) - - expect(page).to_have_url(f"{self.live_server_url}/responses") page.click("text=Back") + + expect(page).to_have_url(f"{self.live_server_url}/weight") page.get_by_role("link", name="Switch to stone and pounds").click() fill_in_and_submit_weight_imperial(page, weight_stone, weight_pound) + + expect(page).to_have_url(f"{self.live_server_url}/sex-at-birth") + expect_back_link_to_have_url(page, "/weight") + fill_in_and_submit_sex_at_birth(page, "Male") + expect(page).to_have_url(f"{self.live_server_url}/responses") + expect_back_link_to_have_url(page, "/sex-at-birth") + responses = page.locator(".responses") expect(responses).to_contain_text("Have you ever smoked? Yes, I used to smoke regularly") expect(responses).to_contain_text( age.strftime("What is your date of birth? %Y-%m-%d")) expect(responses).to_contain_text(f"What is your height? {feet} feet {inches} inches") expect(responses).to_contain_text(f"What is your weight? {weight_stone} stone {weight_pound} pound") + expect(responses).to_contain_text("What was your sex at birth? Male") page.click("text=Submit") diff --git a/lung_cancer_screening/nhsuk_forms/choice_field.py b/lung_cancer_screening/nhsuk_forms/choice_field.py index 45db783a..cc9a2eab 100644 --- a/lung_cancer_screening/nhsuk_forms/choice_field.py +++ b/lung_cancer_screening/nhsuk_forms/choice_field.py @@ -1,8 +1,6 @@ from django import forms from django.forms import widgets -from .bound_choice_field import BoundChoiceField - class ChoiceField(forms.ChoiceField): """ A ChoiceField that renders using NHS.UK design system radios/select @@ -10,7 +8,6 @@ class ChoiceField(forms.ChoiceField): """ widget = widgets.RadioSelect - bound_field_class = BoundChoiceField def __init__( self, diff --git a/lung_cancer_screening/nhsuk_forms/jinja2/radios.jinja b/lung_cancer_screening/nhsuk_forms/jinja2/radios.jinja index 80e212d5..a97855fc 100644 --- a/lung_cancer_screening/nhsuk_forms/jinja2/radios.jinja +++ b/lung_cancer_screening/nhsuk_forms/jinja2/radios.jinja @@ -5,20 +5,12 @@ {% endif %} {% set ns = namespace(items=[]) %} {% for value, text in unbound_field.choices %} - {% set conditional_html = field.conditional_html(value) %} {% set ns.items = ns.items + [{ "id": field.auto_id if loop.first, "value": value, "text": text, - "checked": field.value() == value, - "conditional": { - "html": conditional_html - } if conditional_html else undefined + "checked": field.value() == value }] %} - {% set divider = field.get_divider_after(value) %} - {% if divider %} - {% set ns.items = ns.items + [{"divider": divider}] %} - {% endif %} {% endfor %} {{ radios({ "name": field.html_name, diff --git a/lung_cancer_screening/nhsuk_forms/tests/unit/test_choice_field.py b/lung_cancer_screening/nhsuk_forms/tests/unit/test_choice_field.py index d8ae7f62..50fd7ea5 100644 --- a/lung_cancer_screening/nhsuk_forms/tests/unit/test_choice_field.py +++ b/lung_cancer_screening/nhsuk_forms/tests/unit/test_choice_field.py @@ -38,41 +38,3 @@ def test_renders_nhs_radios(self): """, ) - - def test_renders_radios_with_conditional_html(self): - form = TestForm() - form["field"].add_conditional_html("b", "

Hello

") - - self.assertHTMLEqual( - form["field"].as_field_group(), - """ -
-
- - Abc - -
- Pick either one -
-
-
- - -
-
- - -
-
-

Hello

-
-
-
-
- """, - ) - - def test_adding_dividers_via_boundfield(self): - bound_field = TestForm()["field"] - bound_field.add_divider_after("a", "or") - assert bound_field.get_divider_after("a") == "or" diff --git a/lung_cancer_screening/nhsuk_forms/tests/unit/test_typed_choice_field.py b/lung_cancer_screening/nhsuk_forms/tests/unit/test_typed_choice_field.py index 433eb1be..76ccff9a 100644 --- a/lung_cancer_screening/nhsuk_forms/tests/unit/test_typed_choice_field.py +++ b/lung_cancer_screening/nhsuk_forms/tests/unit/test_typed_choice_field.py @@ -3,7 +3,6 @@ from ...typed_choice_field import TypedChoiceField - class TestForm(Form): field = TypedChoiceField( label="Abc", @@ -39,41 +38,3 @@ def test_renders_nhs_radios(self): """, ) - - def test_renders_radios_with_conditional_html(self): - form = TestForm() - form["field"].add_conditional_html("b", "

Hello

") - - self.assertHTMLEqual( - form["field"].as_field_group(), - """ -
-
- - Abc - -
- Pick either one -
-
-
- - -
-
- - -
-
-

Hello

-
-
-
-
- """, - ) - - def test_adding_dividers_via_boundfield(self): - bound_field = TestForm()["field"] - bound_field.add_divider_after("a", "or") - assert bound_field.get_divider_after("a") == "or" diff --git a/lung_cancer_screening/questions/forms/sex_at_birth_form.py b/lung_cancer_screening/questions/forms/sex_at_birth_form.py new file mode 100644 index 00000000..97e4617b --- /dev/null +++ b/lung_cancer_screening/questions/forms/sex_at_birth_form.py @@ -0,0 +1,26 @@ +from django import forms + +from ...nhsuk_forms.typed_choice_field import TypedChoiceField +from ..models.response_set import ResponseSet, SexAtBirthValues + +class SexAtBirthForm(forms.ModelForm): + + def __init__(self, *args, **kwargs): + self.participant = kwargs.pop('participant') + super().__init__(*args, **kwargs) + self.instance.participant = self.participant + + self.fields["sex_at_birth"] = TypedChoiceField( + choices=SexAtBirthValues.choices, + widget=forms.RadioSelect, + label="What was your sex at birth?", + label_classes="nhsuk-fieldset__legend--m", + hint="Your sex may impact your chances of developing lung cancer.", + error_messages={ + 'required': 'Select your sex at birth.' + } + ) + + class Meta: + model = ResponseSet + fields = ['sex_at_birth'] diff --git a/lung_cancer_screening/questions/jinja2/have_you_ever_smoked.jinja b/lung_cancer_screening/questions/jinja2/have_you_ever_smoked.jinja index 978b2c40..a1678a81 100644 --- a/lung_cancer_screening/questions/jinja2/have_you_ever_smoked.jinja +++ b/lung_cancer_screening/questions/jinja2/have_you_ever_smoked.jinja @@ -1,6 +1,16 @@ {% extends 'layout.jinja' %} {% from 'nhsuk/components/button/macro.jinja' import button %} {% from 'nhsuk/components/radios/macro.jinja' import radios %} +{% from 'nhsuk/components/back-link/macro.jinja' import backLink %} + +{% block beforeContent %} + {{ + backLink({ + "href": url("questions:start"), + "text": "Back" + }) + }} +{% endblock beforeContent %} {% block content %}
diff --git a/lung_cancer_screening/questions/jinja2/responses.jinja b/lung_cancer_screening/questions/jinja2/responses.jinja index bff46631..c2a8c339 100644 --- a/lung_cancer_screening/questions/jinja2/responses.jinja +++ b/lung_cancer_screening/questions/jinja2/responses.jinja @@ -5,7 +5,7 @@ {% block beforeContent %} {{ backLink({ - "href": url("questions:weight"), + "href": url("questions:sex_at_birth"), "text": "Back" }) }} @@ -20,6 +20,7 @@
  • What is your date of birth? {{ response_set.date_of_birth }}
  • What is your height? {{ response_set.formatted_height }}
  • What is your weight? {{ response_set.formatted_weight }}
  • +
  • What was your sex at birth? {{ response_set.get_sex_at_birth_display() }}
  • diff --git a/lung_cancer_screening/questions/jinja2/sex_at_birth.jinja b/lung_cancer_screening/questions/jinja2/sex_at_birth.jinja new file mode 100644 index 00000000..95ce5044 --- /dev/null +++ b/lung_cancer_screening/questions/jinja2/sex_at_birth.jinja @@ -0,0 +1,28 @@ +{% extends 'layout.jinja' %} +{% from 'nhsuk/components/button/macro.jinja' import button %} +{% from 'nhsuk/components/back-link/macro.jinja' import backLink %} + +{% block beforeContent %} + {{ + backLink({ + "href": url("questions:weight"), + "text": "Back" + }) + }} +{% endblock beforeContent %} + +{% block page_content %} +
    +
    + + {{ csrf_input }} + + {{ form }} + + {{ button({ + "text": "Continue" + }) }} + +
    +
    +{% endblock %} diff --git a/lung_cancer_screening/questions/migrations/0013_responseset_sex_at_birth.py b/lung_cancer_screening/questions/migrations/0013_responseset_sex_at_birth.py new file mode 100644 index 00000000..adf0c720 --- /dev/null +++ b/lung_cancer_screening/questions/migrations/0013_responseset_sex_at_birth.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-10-28 11:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('questions', '0012_responseset_weight_imperial_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='responseset', + name='sex_at_birth', + field=models.CharField(blank=True, choices=[('F', 'Female'), ('M', 'Male')], max_length=1, null=True), + ), + ] diff --git a/lung_cancer_screening/questions/models/response_set.py b/lung_cancer_screening/questions/models/response_set.py index e838f9e9..13993fde 100644 --- a/lung_cancer_screening/questions/models/response_set.py +++ b/lung_cancer_screening/questions/models/response_set.py @@ -14,6 +14,10 @@ class HaveYouEverSmokedValues(models.IntegerChoices): YES_BUT_ONLY_A_FEW_TIMES = 2, 'Yes, but only a few times' NO_I_HAVE_NEVER_SMOKED = 3, 'No, I have never smoked' +class SexAtBirthValues(models.TextChoices): + FEMALE = "F", 'Female' + MALE = "M", 'Male' + class ResponseSet(BaseModel): participant = models.ForeignKey(Participant, on_delete=models.CASCADE) @@ -54,6 +58,14 @@ class ResponseSet(BaseModel): MaxValueValidator( MAX_WEIGHT_IMPERIAL, message="Weight must be between 4 stone and 50 stone"), ]) + + sex_at_birth = models.CharField( + max_length=1, + choices=SexAtBirthValues.choices, + null=True, + blank=True + ) + submitted_at = models.DateTimeField(null=True, blank=True) class Meta: diff --git a/lung_cancer_screening/questions/tests/unit/forms/test_sex_at_birth_form.py b/lung_cancer_screening/questions/tests/unit/forms/test_sex_at_birth_form.py new file mode 100644 index 00000000..c553cd40 --- /dev/null +++ b/lung_cancer_screening/questions/tests/unit/forms/test_sex_at_birth_form.py @@ -0,0 +1,48 @@ +from django.test import TestCase + +from ....models.response_set import SexAtBirthValues +from ....models.participant import Participant +from ....forms.sex_at_birth_form import SexAtBirthForm + +class TestSexAtBirthForm(TestCase): + def setUp(self): + self.participant = Participant.objects.create(unique_id="1234567890") + + def test_is_valid_with_a_valid_value(self): + form = SexAtBirthForm( + participant=self.participant, + data={ + "sex_at_birth": SexAtBirthValues.FEMALE + } + ) + self.assertTrue(form.is_valid()) + self.assertEqual( + form.cleaned_data["sex_at_birth"], + SexAtBirthValues.FEMALE.value + ) + + def test_is_invalid_with_an_invalid_value(self): + form = SexAtBirthForm( + participant=self.participant, + data={ + "sex_at_birth": "invalid" + } + ) + self.assertFalse(form.is_valid()) + self.assertEqual( + form.errors["sex_at_birth"], + ["Select a valid choice. invalid is not one of the available choices."] + ) + + def test_is_invalid_when_no_option_is_selected(self): + form = SexAtBirthForm( + participant=self.participant, + data={ + "sex_at_birth": None + } + ) + self.assertFalse(form.is_valid()) + self.assertEqual( + form.errors["sex_at_birth"], + ["Select your sex at birth."] + ) diff --git a/lung_cancer_screening/questions/tests/unit/models/test_response_set.py b/lung_cancer_screening/questions/tests/unit/models/test_response_set.py index 160ee5b4..869a46ef 100644 --- a/lung_cancer_screening/questions/tests/unit/models/test_response_set.py +++ b/lung_cancer_screening/questions/tests/unit/models/test_response_set.py @@ -5,16 +5,16 @@ from django.core.exceptions import ValidationError -from ....models.response_set import ResponseSet +from ....models.response_set import ResponseSet, HaveYouEverSmokedValues, SexAtBirthValues from ....models.participant import Participant -from ....models.response_set import HaveYouEverSmokedValues - class TestResponseSet(TestCase): def setUp(self): participant = Participant.objects.create(unique_id="12345") self.response_set = participant.responseset_set.create() + # FIELDS + def test_has_have_you_ever_smoked_as_an_enum(self): self.response_set.have_you_ever_smoked = HaveYouEverSmokedValues.YES_I_USED_TO_SMOKE_REGULARLY self.response_set.save() @@ -91,6 +91,17 @@ def test_has_submitted_at_as_a_datetime(self): datetime ) + def test_has_sex_at_birth_as_string(self): + self.response_set.sex_at_birth = SexAtBirthValues.MALE + self.response_set.save() + + self.assertIsInstance( + self.response_set.sex_at_birth, + str + ) + + # VALIDATIONS + def test_is_invalid_if_another_unsubmitted_response_set_exists(self): participant = Participant.objects.create(unique_id="56789") participant.responseset_set.create(submitted_at=None) @@ -200,3 +211,23 @@ def test_is_invalid_if_weight_metric_is_above_upper_bound(self): "Weight must be between 25.4kg and 317.5kg", context.exception.messages ) + + def test_is_valid_if_sex_at_birth_is_null(self): + self.response_set.sex_at_birth = None + self.response_set.save() + + self.assertIsNone( + self.response_set.sex_at_birth + ) + + + def test_is_invalid_if_sex_at_birth_is_not_an_accepted_value(self): + self.response_set.sex_at_birth = "X" + + with self.assertRaises(ValidationError) as context: + self.response_set.full_clean() + + self.assertEqual( + context.exception.messages[0], + "Value 'X' is not a valid choice." + ) diff --git a/lung_cancer_screening/questions/tests/unit/views/test_sex_at_birth.py b/lung_cancer_screening/questions/tests/unit/views/test_sex_at_birth.py new file mode 100644 index 00000000..002fb5c5 --- /dev/null +++ b/lung_cancer_screening/questions/tests/unit/views/test_sex_at_birth.py @@ -0,0 +1,91 @@ +from django.test import TestCase +from django.urls import reverse + +from lung_cancer_screening.questions.models.participant import Participant +from lung_cancer_screening.questions.models.response_set import SexAtBirthValues + +class TestSexAtBirth(TestCase): + def setUp(self): + self.participant = Participant.objects.create(unique_id="12345") + self.participant.responseset_set.create() + self.valid_params = { "sex_at_birth": SexAtBirthValues.FEMALE } + + session = self.client.session + session['participant_id'] = self.participant.unique_id + session.save() + + def test_get_redirects_if_the_participant_does_not_exist(self): + session = self.client.session + session['participant_id'] = "somebody none existant participant" + session.save() + + response = self.client.get( + reverse("questions:sex_at_birth") + ) + + self.assertRedirects(response, reverse("questions:start")) + + def test_get_responds_successfully(self): + response = self.client.get(reverse("questions:sex_at_birth")) + + self.assertEqual(response.status_code, 200) + + def test_get_contains_the_correct_form_fields(self): + response = self.client.get(reverse("questions:sex_at_birth")) + + self.assertContains(response, "What was your sex at birth?") + + def test_post_redirects_if_the_participant_does_not_exist(self): + session = self.client.session + session['participant_id'] = "somebody none existant participant" + session.save() + + response = self.client.post( + reverse("questions:sex_at_birth"), + self.valid_params + ) + + self.assertRedirects(response, reverse("questions:start")) + + def test_post_stores_a_valid_response_for_the_participant(self): + self.client.post( + reverse("questions:sex_at_birth"), + self.valid_params + ) + + response_set = self.participant.responseset_set.first() + self.assertEqual(response_set.sex_at_birth, self.valid_params["sex_at_birth"]) + self.assertEqual(response_set.participant, self.participant) + + def test_post_sets_the_participant_id_in_session(self): + self.client.post( + reverse("questions:sex_at_birth"), + self.valid_params + ) + + self.assertEqual(self.client.session["participant_id"], "12345") + + def test_post_redirects_to_the_responses_path(self): + response = self.client.post( + reverse("questions:sex_at_birth"), + self.valid_params + ) + + self.assertRedirects(response, reverse("questions:responses")) + + def test_post_responds_with_422_if_the_date_response_fails_to_create(self): + response = self.client.post( + reverse("questions:sex_at_birth"), + {"sex_at_birth": "something not in list"} + ) + + self.assertEqual(response.status_code, 422) + + def test_post_renders_the_sex_at_birth_page_with_an_error_if_the_form_is_invalid(self): + response = self.client.post( + reverse("questions:sex_at_birth"), + {"sex_at_birth": "something not in list"} + ) + + self.assertContains(response, "What was your sex at birth?", status_code=422) + self.assertContains(response, "nhsuk-error-message", status_code=422) diff --git a/lung_cancer_screening/questions/tests/unit/views/test_weight.py b/lung_cancer_screening/questions/tests/unit/views/test_weight.py index c4b8c56c..35c02f97 100644 --- a/lung_cancer_screening/questions/tests/unit/views/test_weight.py +++ b/lung_cancer_screening/questions/tests/unit/views/test_weight.py @@ -72,7 +72,7 @@ def test_post_redirects_if_the_weight_is_valid(self): self.valid_params ) - self.assertRedirects(response, reverse("questions:responses")) + self.assertRedirects(response, reverse("questions:sex_at_birth")) def test_post_valid_weight_added_to_response_set(self): self.client.post( diff --git a/lung_cancer_screening/questions/urls.py b/lung_cancer_screening/questions/urls.py index 0bef04d6..6d3f50dd 100644 --- a/lung_cancer_screening/questions/urls.py +++ b/lung_cancer_screening/questions/urls.py @@ -24,6 +24,7 @@ from .views.your_results import your_results from .views.height import height from .views.weight import weight +from .views.sex_at_birth import sex_at_birth urlpatterns = [ path('start', start, name='start'), @@ -31,6 +32,7 @@ path('date-of-birth', date_of_birth, name='date_of_birth'), path('height', height, name='height'), path('weight', weight, name='weight'), + path('sex-at-birth', sex_at_birth, name='sex_at_birth'), path('responses', responses, name='responses'), path('age-range-exit', age_range_exit, name='age_range_exit'), path('non-smoker-exit', non_smoker_exit, name='non_smoker_exit'), diff --git a/lung_cancer_screening/questions/views/sex_at_birth.py b/lung_cancer_screening/questions/views/sex_at_birth.py new file mode 100644 index 00000000..3ace1dee --- /dev/null +++ b/lung_cancer_screening/questions/views/sex_at_birth.py @@ -0,0 +1,32 @@ +from django.shortcuts import render, redirect +from django.urls import reverse + +from .decorators.participant_decorators import require_participant +from ..forms.sex_at_birth_form import SexAtBirthForm + +@require_participant +def sex_at_birth(request): + if request.method == "POST": + form = SexAtBirthForm( + participant=request.participant, + data=request.POST + ) + + if form.is_valid(): + response_set = request.participant.responseset_set.last() + response_set.sex_at_birth = form.cleaned_data["sex_at_birth"] + response_set.save() + return redirect(reverse("questions:responses")) + else: + return render( + request, + "sex_at_birth.jinja", + { "form": form }, + status=422 + ) + + return render( + request, + "sex_at_birth.jinja", + { "form": SexAtBirthForm(participant=request.participant) } + ) diff --git a/lung_cancer_screening/questions/views/weight.py b/lung_cancer_screening/questions/views/weight.py index ebd40576..07716074 100644 --- a/lung_cancer_screening/questions/views/weight.py +++ b/lung_cancer_screening/questions/views/weight.py @@ -17,7 +17,7 @@ def weight(request): ) if form.is_valid(): form.save() - return redirect("questions:responses") + return redirect("questions:sex_at_birth") else: return render( request, diff --git a/scripts/tests/unit.sh b/scripts/tests/unit.sh index 3012b098..493a7e55 100755 --- a/scripts/tests/unit.sh +++ b/scripts/tests/unit.sh @@ -18,6 +18,11 @@ cd "$(git rev-parse --show-toplevel)" # tasks in scripts/test.mk. if [[ -n "${TEST_MODULE:-}" ]]; then + if [[ "$TEST_MODULE" == *\/* ]]; then + # Modify paths to point to modules + TEST_MODULE="${TEST_MODULE%.py}" + TEST_MODULE=${TEST_MODULE//\//\.} + fi docker compose run --rm --remove-orphans web poetry run python manage.py test $TEST_MODULE --settings=lung_cancer_screening.settings_test else docker compose run --rm --remove-orphans web poetry run python manage.py test --settings=lung_cancer_screening.settings_test