From 0ae4849c3ed235147e4b49b8de191fe709d25f45 Mon Sep 17 00:00:00 2001 From: Andy Mitchell <326561+Themitchell@users.noreply.github.com> Date: Thu, 12 Feb 2026 09:24:49 +0000 Subject: [PATCH 1/6] PPHA-585: Add NHS login given_name and family_name to user --- .coverage | Bin 77824 -> 73728 bytes lung_cancer_screening/questions/auth.py | 22 +++++- ..._add_given_name_and_family_name_to_user.py | 23 +++++++ .../questions/models/user.py | 2 + .../questions/tests/factories/user_factory.py | 2 + .../questions/tests/unit/models/test_user.py | 23 +++++++ .../questions/tests/unit/test_auth.py | 63 ++++++++++++------ 7 files changed, 114 insertions(+), 21 deletions(-) create mode 100644 lung_cancer_screening/questions/migrations/0054_add_given_name_and_family_name_to_user.py diff --git a/.coverage b/.coverage index 117fc844745e3c2ce73936f0522c85db5ec87f8c..0298cbf104324f1105734f6aa23e26d4bc9853bb 100644 GIT binary patch delta 4811 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 U#RyjGd>&n>J&k||=>^UEe}W>1mjD0& diff --git a/lung_cancer_screening/questions/auth.py b/lung_cancer_screening/questions/auth.py index 9c753ab2..98d29b59 100644 --- a/lung_cancer_screening/questions/auth.py +++ b/lung_cancer_screening/questions/auth.py @@ -39,16 +39,36 @@ def create_user(self, claims): raise ValueError("Missing 'nhs_number' claim in OIDC token") email = claims.get('email') + given_name = claims.get('given_name') + family_name = claims.get('family_name') return user_class.objects.create_user( nhs_number=nhs_number, - email=email + email=email, + given_name=given_name, + family_name=family_name, ) def update_user(self, user, claims): + changed = False email = claims.get('email') + given_name = claims.get("given_name") + family_name = claims.get("family_name") + if email and user.email != email: user.email = email + changed = True + + if given_name and user.given_name != given_name: + user.given_name = given_name + changed = True + + if family_name and user.family_name != family_name: + user.family_name = family_name + changed = True + + if changed: user.save() + return user def _create_client_assertion(self): diff --git a/lung_cancer_screening/questions/migrations/0054_add_given_name_and_family_name_to_user.py b/lung_cancer_screening/questions/migrations/0054_add_given_name_and_family_name_to_user.py new file mode 100644 index 00000000..9cffd303 --- /dev/null +++ b/lung_cancer_screening/questions/migrations/0054_add_given_name_and_family_name_to_user.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('questions', '0053_smokedamountresponse'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='given_name', + field=models.CharField(blank=False, null=False, max_length=255), + ), + migrations.AddField( + model_name='user', + name='family_name', + field=models.CharField(blank=False, null=False, max_length=255), + ), + ] diff --git a/lung_cancer_screening/questions/models/user.py b/lung_cancer_screening/questions/models/user.py index 84b43206..eed7f1f0 100644 --- a/lung_cancer_screening/questions/models/user.py +++ b/lung_cancer_screening/questions/models/user.py @@ -19,6 +19,8 @@ def create_user(self, nhs_number, **extra_fields): class User(AbstractBaseUser): nhs_number = models.CharField(max_length=10, unique=True) + given_name = models.CharField(max_length=255, blank=False, null=False) + family_name = models.CharField(max_length=255, blank=False, null=False) email = models.EmailField(blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) diff --git a/lung_cancer_screening/questions/tests/factories/user_factory.py b/lung_cancer_screening/questions/tests/factories/user_factory.py index fa8495c9..57c0c54c 100644 --- a/lung_cancer_screening/questions/tests/factories/user_factory.py +++ b/lung_cancer_screening/questions/tests/factories/user_factory.py @@ -9,3 +9,5 @@ class Meta: nhs_number = factory.Sequence(lambda n: f"9{str(n).zfill(9)}") password = factory.django.Password(None) + given_name = factory.Faker("first_name") + family_name = factory.Faker("last_name") diff --git a/lung_cancer_screening/questions/tests/unit/models/test_user.py b/lung_cancer_screening/questions/tests/unit/models/test_user.py index 7463d47c..14508baf 100644 --- a/lung_cancer_screening/questions/tests/unit/models/test_user.py +++ b/lung_cancer_screening/questions/tests/unit/models/test_user.py @@ -79,6 +79,29 @@ def test_raises_a_validation_error_if_nhs_number_is_duplicate(self): ) + def test_is_invalid_without_a_given_name(self): + self.user.given_name = None + + with self.assertRaises(ValidationError) as context: + self.user.full_clean() + + self.assertIn( + "This field cannot be null.", + context.exception.messages + ) + + + def test_is_invalid_without_a_family_name(self): + self.user.family_name = None + + with self.assertRaises(ValidationError) as context: + self.user.full_clean() + + self.assertIn( + "This field cannot be null.", + context.exception.messages[0] + ) + def test_has_recently_submitted_responses_returns_true_if_has_recently_submitted_response_set(self): ResponseSetFactory.create( user=self.user, diff --git a/lung_cancer_screening/questions/tests/unit/test_auth.py b/lung_cancer_screening/questions/tests/unit/test_auth.py index 7ac42315..ef4f19fe 100644 --- a/lung_cancer_screening/questions/tests/unit/test_auth.py +++ b/lung_cancer_screening/questions/tests/unit/test_auth.py @@ -6,6 +6,7 @@ from cryptography.hazmat.primitives import serialization from ...auth import NHSLoginOIDCBackend +from ...tests.factories.user_factory import UserFactory User = get_user_model() @@ -31,44 +32,45 @@ def setUp(self): encryption_algorithm=serialization.NoEncryption() ).decode('utf-8') + self.claims = { + "nhs_number": "1234567890", + "email": "test@example.com", + "given_name": "Jane", + "family_name": "Smith", + } + def test_filter_users_by_claims_for_existing_user(self): - user = User.objects.create_user(nhs_number='1234567890') + user = User.objects.create_user(**self.claims) - claims = {'nhs_number': '1234567890'} - result = self.backend.filter_users_by_claims(claims) + result = self.backend.filter_users_by_claims(self.claims) self.assertEqual(result.count(), 1) self.assertEqual(result.first(), user) def test_filter_users_by_claims_for_non_existent_user(self): - claims = {'nhs_number': '1111111111'} + claims = {**self.claims, "nhs_number": "1111111111"} result = self.backend.filter_users_by_claims(claims) self.assertEqual(result.count(), 0) def test_filter_users_by_claims_when_no_claim_is_provided(self): - claims = {} - result = self.backend.filter_users_by_claims(claims) + result = self.backend.filter_users_by_claims({}) self.assertEqual(result.count(), 0) def test_create_user_when_nhs_number_claim_is_provided(self): - claims = {'nhs_number': '1234567890'} + user = self.backend.create_user(self.claims) - user = self.backend.create_user(claims) + self.assertEqual(user.nhs_number, self.claims["nhs_number"]) + self.assertEqual(user.email, self.claims["email"]) - self.assertEqual(user.nhs_number, '1234567890') - def test_create_user_with_email_claim(self): - claims = { - 'nhs_number': '1234567890', - 'email': 'test@example.com' - } + def test_create_user_with_name_claims_sets_given_name_and_family_name(self): + user = self.backend.create_user(self.claims) - user = self.backend.create_user(claims) + self.assertEqual(user.given_name, 'Jane') + self.assertEqual(user.family_name, 'Smith') - self.assertEqual(user.nhs_number, '1234567890') - self.assertEqual(user.email, 'test@example.com') def test_create_user_without_nhs_number_raises_error(self): claims = {} @@ -80,7 +82,7 @@ def test_create_user_without_nhs_number_raises_error(self): def test_update_user_returns_user(self): - user = User.objects.create_user(nhs_number='1234567890') + user = UserFactory.create(nhs_number='1234567890') claims = {'nhs_number': '1234567890', 'email': 'test@example.com'} result = self.backend.update_user(user, claims) @@ -89,7 +91,7 @@ def test_update_user_returns_user(self): self.assertEqual(user.email, 'test@example.com') def test_update_user_updates_email_when_provided(self): - user = User.objects.create_user( + user = UserFactory.create( nhs_number='1234567890', email='old@example.com' ) @@ -102,7 +104,7 @@ def test_update_user_updates_email_when_provided(self): self.assertEqual(result, user) def test_update_user_does_not_update_email_when_not_provided(self): - user = User.objects.create_user( + user = UserFactory.create( nhs_number='1234567890', email='existing@example.com' ) @@ -114,6 +116,27 @@ def test_update_user_does_not_update_email_when_not_provided(self): self.assertEqual(user.email, 'existing@example.com') self.assertEqual(result, user) + + def test_update_user_updates_given_name_and_family_name_when_provided(self): + user = UserFactory.create( + nhs_number='1234567890', + given_name='Old', + family_name='Name', + ) + claims = { + 'nhs_number': '1234567890', + 'given_name': 'Jane', + 'family_name': 'Smith', + } + + result = self.backend.update_user(user, claims) + + user.refresh_from_db() + self.assertEqual(user.given_name, 'Jane') + self.assertEqual(user.family_name, 'Smith') + self.assertEqual(result, user) + + @patch('lung_cancer_screening.questions.auth.requests.post') def test_get_token_success(self, mock_post): settings.OIDC_RP_CLIENT_PRIVATE_KEY = self.test_private_key_pem From 9f8c35cb382921d0d2e58a1d9b48554efec62573 Mon Sep 17 00:00:00 2001 From: Andy Mitchell <326561+Themitchell@users.noreply.github.com> Date: Thu, 12 Feb 2026 09:49:18 +0000 Subject: [PATCH 2/6] PPHA-585: Display current users full name in header --- lung_cancer_screening/core/jinja2/layout.jinja | 6 +++++- lung_cancer_screening/questions/models/user.py | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lung_cancer_screening/core/jinja2/layout.jinja b/lung_cancer_screening/core/jinja2/layout.jinja index 0268f944..fbdde69e 100644 --- a/lung_cancer_screening/core/jinja2/layout.jinja +++ b/lung_cancer_screening/core/jinja2/layout.jinja @@ -18,11 +18,15 @@ }, "account": { "items": [ + { + "text": request.user.full_name, + "icon": True + }, { "text": "Log out", "href": url("questions:logout") } - ] + ] if request.user.is_authenticated else [] } }) }} {% endblock header %} diff --git a/lung_cancer_screening/questions/models/user.py b/lung_cancer_screening/questions/models/user.py index eed7f1f0..36957f2e 100644 --- a/lung_cancer_screening/questions/models/user.py +++ b/lung_cancer_screening/questions/models/user.py @@ -48,3 +48,7 @@ def has_recently_submitted_responses(self, excluding=None): def most_recent_response_set(self): return self.responseset_set.order_by('-submitted_at').first() + + @property + def full_name(self): + return f"{self.given_name} {self.family_name}" From 6462831937606598f0d8718cce3e5d484ee01e61 Mon Sep 17 00:00:00 2001 From: Andy Mitchell <326561+Themitchell@users.noreply.github.com> Date: Thu, 12 Feb 2026 09:53:59 +0000 Subject: [PATCH 3/6] PPHA-585: Link to NHS login settings url in header --- .env.example | 1 + lung_cancer_screening/core/jinja2/layout.jinja | 1 + lung_cancer_screening/jinja2_env.py | 7 ++++++- lung_cancer_screening/settings.py | 1 + 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 265942af..60f344c1 100644 --- a/.env.example +++ b/.env.example @@ -15,3 +15,4 @@ POSTGRES_PASSWORD=password OIDC_RP_CLIENT_PRIVATE_KEY="MYSUPERSECRETPRIVATEKEY" OIDC_RP_CLIENT_ID="lcrc" OIDC_OP_FQDN="https://example.com" +NHS_LOGIN_SETTINGS_URL="https://settings.example.com" diff --git a/lung_cancer_screening/core/jinja2/layout.jinja b/lung_cancer_screening/core/jinja2/layout.jinja index fbdde69e..f26fbdc8 100644 --- a/lung_cancer_screening/core/jinja2/layout.jinja +++ b/lung_cancer_screening/core/jinja2/layout.jinja @@ -20,6 +20,7 @@ "items": [ { "text": request.user.full_name, + "href": NHS_LOGIN_SETTINGS_URL, "icon": True }, { diff --git a/lung_cancer_screening/jinja2_env.py b/lung_cancer_screening/jinja2_env.py index 9df59df2..5f021072 100644 --- a/lung_cancer_screening/jinja2_env.py +++ b/lung_cancer_screening/jinja2_env.py @@ -26,7 +26,12 @@ def environment(**options): ) env.globals.update( - {"static": static, "url": reverse, "STATIC_URL": settings.STATIC_URL} + { + "static": static, + "url": reverse, + "STATIC_URL": settings.STATIC_URL, + "NHS_LOGIN_SETTINGS_URL": settings.NHS_LOGIN_SETTINGS_URL, + } ) env.filters.update( diff --git a/lung_cancer_screening/settings.py b/lung_cancer_screening/settings.py index 59556710..aa63abba 100644 --- a/lung_cancer_screening/settings.py +++ b/lung_cancer_screening/settings.py @@ -235,6 +235,7 @@ def list_env(key): OIDC_RP_SIGN_ALGO = "RS512" OIDC_RP_SCOPES = "openid profile" OIDC_RP_REDIRECT_URI = "/oidc/callback/" +NHS_LOGIN_SETTINGS_URL = environ.get("NHS_LOGIN_SETTINGS_URL") # Authentication backends AUTHENTICATION_BACKENDS = [ From c4d2373329fd733ce76060a9120496dbc97ef637 Mon Sep 17 00:00:00 2001 From: Andy Mitchell <326561+Themitchell@users.noreply.github.com> Date: Thu, 12 Feb 2026 09:54:54 +0000 Subject: [PATCH 4/6] fixup! PPHA-585: Add NHS login given_name and family_name to user --- lung_cancer_screening/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lung_cancer_screening/settings.py b/lung_cancer_screening/settings.py index aa63abba..36f1d1dd 100644 --- a/lung_cancer_screening/settings.py +++ b/lung_cancer_screening/settings.py @@ -233,7 +233,7 @@ def list_env(key): # NHS Login requires RS512 for token endpoint authentication # See: https://auth.sandpit.signin.nhs.uk/.well-known/openid-configuration OIDC_RP_SIGN_ALGO = "RS512" -OIDC_RP_SCOPES = "openid profile" +OIDC_RP_SCOPES = "openid profile profile_extended" OIDC_RP_REDIRECT_URI = "/oidc/callback/" NHS_LOGIN_SETTINGS_URL = environ.get("NHS_LOGIN_SETTINGS_URL") From 9e7a9f0409994e8b4029dac6f7b112d8caf6c665 Mon Sep 17 00:00:00 2001 From: Andy Mitchell <326561+Themitchell@users.noreply.github.com> Date: Thu, 12 Feb 2026 11:24:40 +0000 Subject: [PATCH 5/6] Enforce presence of all user fields from NHS login --- .coverage | Bin 73728 -> 77824 bytes lung_cancer_screening/questions/auth.py | 44 ++++------- ...ter_smokedamountresponse_value_and_more.py | 23 ++++++ .../questions/models/user.py | 18 +++-- .../questions/tests/factories/user_factory.py | 2 + .../questions/tests/unit/models/test_user.py | 59 ++++++++++++--- .../questions/tests/unit/test_auth.py | 71 ++++-------------- 7 files changed, 116 insertions(+), 101 deletions(-) create mode 100644 lung_cancer_screening/questions/migrations/0055_user_sub_alter_smokedamountresponse_value_and_more.py diff --git a/.coverage b/.coverage index 0298cbf104324f1105734f6aa23e26d4bc9853bb..6d679bb331d815be018d8827c08bb6d8a57afa28 100644 GIT binary patch delta 4756 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 C{K{4U diff --git a/lung_cancer_screening/questions/auth.py b/lung_cancer_screening/questions/auth.py index 98d29b59..6f27a36f 100644 --- a/lung_cancer_screening/questions/auth.py +++ b/lung_cancer_screening/questions/auth.py @@ -18,7 +18,7 @@ class NHSLoginOIDCBackend(OIDCAuthenticationBackend): """ Custom OIDC authentication backend that uses private key JWT for client authentication instead of client secret. - Uses NHS number as the username field. + Uses sub as the username field. """ def filter_users_by_claims(self, claims): @@ -34,40 +34,26 @@ def filter_users_by_claims(self, claims): def create_user(self, claims): user_class = get_user_model() - nhs_number = claims.get('nhs_number') - if not nhs_number: - raise ValueError("Missing 'nhs_number' claim in OIDC token") + sub = claims.get('sub') + if not sub: + raise ValueError("Missing 'sub' claim in OIDC token") - email = claims.get('email') - given_name = claims.get('given_name') - family_name = claims.get('family_name') return user_class.objects.create_user( - nhs_number=nhs_number, - email=email, - given_name=given_name, - family_name=family_name, + sub=claims.get('sub'), + nhs_number=claims.get('nhs_number'), + email=claims.get('email'), + given_name=claims.get('given_name'), + family_name=claims.get('family_name'), ) def update_user(self, user, claims): - changed = False - email = claims.get('email') - given_name = claims.get("given_name") - family_name = claims.get("family_name") - - if email and user.email != email: - user.email = email - changed = True - - if given_name and user.given_name != given_name: - user.given_name = given_name - changed = True - - if family_name and user.family_name != family_name: - user.family_name = family_name - changed = True + user.sub = claims.get('sub'), + user.nhs_number = claims.get('nhs_number') + user.email = claims.get('email') + user.given_name = claims.get('given_name') + user.family_name = claims.get('family_name') - if changed: - user.save() + user.save() return user diff --git a/lung_cancer_screening/questions/migrations/0055_user_sub_alter_smokedamountresponse_value_and_more.py b/lung_cancer_screening/questions/migrations/0055_user_sub_alter_smokedamountresponse_value_and_more.py new file mode 100644 index 00000000..6b036979 --- /dev/null +++ b/lung_cancer_screening/questions/migrations/0055_user_sub_alter_smokedamountresponse_value_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.11 on 2026-02-12 11:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('questions', '0054_add_given_name_and_family_name_to_user'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='sub', + field=models.CharField(max_length=255, unique=True) + ), + migrations.AlterField( + model_name='user', + name='email', + field=models.EmailField(max_length=254), + ), + ] diff --git a/lung_cancer_screening/questions/models/user.py b/lung_cancer_screening/questions/models/user.py index 36957f2e..8bb5fd0e 100644 --- a/lung_cancer_screening/questions/models/user.py +++ b/lung_cancer_screening/questions/models/user.py @@ -7,10 +7,11 @@ class UserManager(BaseUserManager): - def create_user(self, nhs_number, **extra_fields): - if not nhs_number: - raise ValueError('The NHS number must be set') - user = self.model(nhs_number=nhs_number, **extra_fields) + def create_user(self, sub, **extra_fields): + if not sub: + raise ValueError('The sub must be set') + + user = self.model(sub=sub, **extra_fields) # Set an unusable password since AbstractBaseUser requires it user.set_unusable_password() user.save(using=self._db) @@ -18,16 +19,17 @@ def create_user(self, nhs_number, **extra_fields): class User(AbstractBaseUser): + sub = models.CharField(max_length=255, unique=True) nhs_number = models.CharField(max_length=10, unique=True) - given_name = models.CharField(max_length=255, blank=False, null=False) - family_name = models.CharField(max_length=255, blank=False, null=False) - email = models.EmailField(blank=True, null=True) + given_name = models.CharField(max_length=255) + family_name = models.CharField(max_length=255) + email = models.EmailField() created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) objects = UserManager() - USERNAME_FIELD = 'nhs_number' + USERNAME_FIELD = 'sub' REQUIRED_FIELDS = [] def save(self, *args, **kwargs): diff --git a/lung_cancer_screening/questions/tests/factories/user_factory.py b/lung_cancer_screening/questions/tests/factories/user_factory.py index 57c0c54c..9328736d 100644 --- a/lung_cancer_screening/questions/tests/factories/user_factory.py +++ b/lung_cancer_screening/questions/tests/factories/user_factory.py @@ -7,7 +7,9 @@ class UserFactory(factory.django.DjangoModelFactory): class Meta: model = User + sub = factory.Sequence(lambda n: f"nhs-login-sub-{n}") nhs_number = factory.Sequence(lambda n: f"9{str(n).zfill(9)}") password = factory.django.Password(None) + email = factory.Faker("email") given_name = factory.Faker("first_name") family_name = factory.Faker("last_name") diff --git a/lung_cancer_screening/questions/tests/unit/models/test_user.py b/lung_cancer_screening/questions/tests/unit/models/test_user.py index 14508baf..36d4b523 100644 --- a/lung_cancer_screening/questions/tests/unit/models/test_user.py +++ b/lung_cancer_screening/questions/tests/unit/models/test_user.py @@ -17,6 +17,9 @@ def test_has_a_valid_factory(self): model.full_clean() + def test_has_sub_as_a_string(self): + self.assertIsInstance(self.user.sub, str) + def test_has_nhs_number_as_a_string(self): self.assertIsInstance( self.user.nhs_number, @@ -45,23 +48,37 @@ def test_has_updated_at_as_a_datetime(self): ) - def test_nhs_number_has_a_max_length_of_10(self): + def test_has_many_response_sets(self): + response_set = self.user.responseset_set.create() + self.assertIn(response_set, list(self.user.responseset_set.all())) + + + def test_is_invalid_without_a_sub(self): + self.user.sub = None + with self.assertRaises(ValidationError) as context: - UserFactory(nhs_number="1"*11) + self.user.full_clean() self.assertIn( - "Ensure this value has at most 10 characters (it has 11).", + "This field cannot be null.", context.exception.messages ) - def test_has_many_response_sets(self): - response_set = self.user.responseset_set.create() - self.assertIn(response_set, list(self.user.responseset_set.all())) + def test_in_invalid_if_sub_is_not_unique(self): + with self.assertRaises(ValidationError) as context: + UserFactory(sub=self.user.sub) + self.assertIn( + "User with this Sub already exists.", + context.exception.messages + ) + + + def test_is_invalid_without_nhs_number(self): + self.user.nhs_number = None - def test_raises_a_validation_error_if_nhs_number_is_null(self): with self.assertRaises(ValidationError) as context: - UserFactory(nhs_number=None) + self.user.full_clean() self.assertIn( "This field cannot be null.", @@ -79,6 +96,18 @@ def test_raises_a_validation_error_if_nhs_number_is_duplicate(self): ) + def test_nhs_number_has_a_max_length_of_10(self): + self.user.nhs_number = "1"*11 + + with self.assertRaises(ValidationError) as context: + self.user.full_clean() + + self.assertIn( + "Ensure this value has at most 10 characters (it has 11).", + context.exception.messages + ) + + def test_is_invalid_without_a_given_name(self): self.user.given_name = None @@ -90,7 +119,6 @@ def test_is_invalid_without_a_given_name(self): context.exception.messages ) - def test_is_invalid_without_a_family_name(self): self.user.family_name = None @@ -102,6 +130,19 @@ def test_is_invalid_without_a_family_name(self): context.exception.messages[0] ) + + def test_is_invalid_without_an_email(self): + self.user.email = None + + with self.assertRaises(ValidationError) as context: + self.user.full_clean() + + self.assertIn( + "This field cannot be null.", + context.exception.messages[0] + ) + + def test_has_recently_submitted_responses_returns_true_if_has_recently_submitted_response_set(self): ResponseSetFactory.create( user=self.user, diff --git a/lung_cancer_screening/questions/tests/unit/test_auth.py b/lung_cancer_screening/questions/tests/unit/test_auth.py index ef4f19fe..21306cbc 100644 --- a/lung_cancer_screening/questions/tests/unit/test_auth.py +++ b/lung_cancer_screening/questions/tests/unit/test_auth.py @@ -33,6 +33,7 @@ def setUp(self): ).decode('utf-8') self.claims = { + "sub": "nhs-login-sub-123", "nhs_number": "1234567890", "email": "test@example.com", "given_name": "Jane", @@ -48,7 +49,7 @@ def test_filter_users_by_claims_for_existing_user(self): self.assertEqual(result.first(), user) def test_filter_users_by_claims_for_non_existent_user(self): - claims = {**self.claims, "nhs_number": "1111111111"} + claims = {**self.claims, "sub": "other-sub-456"} result = self.backend.filter_users_by_claims(claims) self.assertEqual(result.count(), 0) @@ -58,83 +59,43 @@ def test_filter_users_by_claims_when_no_claim_is_provided(self): self.assertEqual(result.count(), 0) - def test_create_user_when_nhs_number_claim_is_provided(self): + + def test_create_user_creates_a_valid_user(self): user = self.backend.create_user(self.claims) + self.assertEqual(user.sub, self.claims["sub"]) self.assertEqual(user.nhs_number, self.claims["nhs_number"]) self.assertEqual(user.email, self.claims["email"]) + self.assertEqual(user.given_name, self.claims["given_name"]) + self.assertEqual(user.family_name, self.claims["family_name"]) - def test_create_user_with_name_claims_sets_given_name_and_family_name(self): - user = self.backend.create_user(self.claims) - - self.assertEqual(user.given_name, 'Jane') - self.assertEqual(user.family_name, 'Smith') - - - def test_create_user_without_nhs_number_raises_error(self): + def test_create_user_without_sub_raises_error(self): claims = {} with self.assertRaises(ValueError) as context: self.backend.create_user(claims) - self.assertIn("Missing 'nhs_number' claim", str(context.exception)) - - - def test_update_user_returns_user(self): - user = UserFactory.create(nhs_number='1234567890') - claims = {'nhs_number': '1234567890', 'email': 'test@example.com'} - - result = self.backend.update_user(user, claims) - - self.assertEqual(result, user) - self.assertEqual(user.email, 'test@example.com') - - def test_update_user_updates_email_when_provided(self): - user = UserFactory.create( - nhs_number='1234567890', - email='old@example.com' - ) - claims = {'nhs_number': '1234567890', 'email': 'new@example.com'} - - result = self.backend.update_user(user, claims) - - user.refresh_from_db() - self.assertEqual(user.email, 'new@example.com') - self.assertEqual(result, user) - - def test_update_user_does_not_update_email_when_not_provided(self): - user = UserFactory.create( - nhs_number='1234567890', - email='existing@example.com' - ) - claims = {'nhs_number': '1234567890'} + self.assertIn("Missing 'sub' claim", str(context.exception)) - result = self.backend.update_user(user, claims) - user.refresh_from_db() - self.assertEqual(user.email, 'existing@example.com') - self.assertEqual(result, user) - - - def test_update_user_updates_given_name_and_family_name_when_provided(self): - user = UserFactory.create( - nhs_number='1234567890', - given_name='Old', - family_name='Name', - ) + def test_update_user_updates_the_user(self): + user = UserFactory.create(sub='sub-123', nhs_number='1234567890') claims = { + 'sub': 'sub-123', 'nhs_number': '1234567890', + 'email': 'test@example.com', 'given_name': 'Jane', 'family_name': 'Smith', } result = self.backend.update_user(user, claims) - user.refresh_from_db() + self.assertEqual(result, user) + self.assertEqual(user.email, 'test@example.com') self.assertEqual(user.given_name, 'Jane') self.assertEqual(user.family_name, 'Smith') - self.assertEqual(result, user) + self.assertEqual(user.nhs_number, '1234567890') @patch('lung_cancer_screening.questions.auth.requests.post') From 4b9f3009c9d92db8f8d99cb7e7a47bb4759a7524 Mon Sep 17 00:00:00 2001 From: Andy Mitchell <326561+Themitchell@users.noreply.github.com> Date: Thu, 12 Feb 2026 11:39:14 +0000 Subject: [PATCH 6/6] Squash migrations into a schema bootstrap --- .../questions/migrations/0001_initial.py | 305 +++++++++++++++++- .../migrations/0002_booleanresponse.py | 27 -- .../0003_booleanresponse_question_and_more.py | 25 -- ...e_questionnaireresponse_to_dateresponse.py | 18 -- .../questions/migrations/0005_responseset.py | 25 -- ...emove_dateresponse_participant_and_more.py | 19 -- .../0007_responseset_submitted_and_more.py | 28 -- ...8_remove_responseset_submitted_and_more.py | 22 -- .../0009_responseset_height_and_more.py | 18 -- ...erial_alter_responseset_height_and_more.py | 28 -- ...0011_responseset_weight_metric_and_more.py | 24 -- ...12_responseset_weight_imperial_and_more.py | 24 -- .../0013_responseset_sex_at_birth.py | 18 -- .../migrations/0014_responseset_gender.py | 18 -- ...eset_ethnicity_alter_responseset_gender.py | 23 -- .../0016_responseset_asbestos_exposure.py | 18 -- ...017_alter_responseset_asbestos_exposure.py | 18 -- ...0018_responseset_respiratory_conditions.py | 19 -- .../questions/migrations/0019_user.py | 28 -- .../0020_responseset_user_and_more.py | 33 -- ...ponseset_participant_delete_participant.py | 20 -- ...itted_response_per_participant_and_more.py | 21 -- ...rename_height_responseset_height_metric.py | 18 -- .../migrations/0024_add_email_to_user.py | 18 -- .../0025_haveyoueversmokedresponse.py | 53 --- .../0026_asbestosexposureresponse.py | 53 --- .../migrations/0027_dateofbirthresponse.py | 53 --- .../migrations/0028_ethnicityresponse.py | 53 --- .../migrations/0029_genderresponse.py | 53 --- .../migrations/0030_heightresponse.py | 60 ---- .../0031_respiratoryconditionsresponse.py | 55 ---- .../migrations/0032_sexatbirthresponse.py | 53 --- .../migrations/0033_weightresponse.py | 60 ---- ..._haveyoueversmokedresponse_response_set.py | 19 -- .../0035_cancerdiagnosisresponse.py | 27 -- .../0036_familyhistorylungcancerresponse.py | 27 -- ...7_familyhistoryagewhendiagnosedresponse.py | 27 -- ...ativesagewhendiagnosedresponse_and_more.py | 30 -- .../0039_checkneedappointmentresponse.py | 27 -- .../migrations/0040_educationresponse.py | 27 -- .../0041_alter_educationresponse_value.py | 20 -- .../0042_alter_educationresponse_value.py | 20 -- .../0043_agewhenstartedsmokingresponse.py | 27 -- ...44_periodswhenyoustoppedsmokingresponse.py | 25 -- ...oustoppedsmokingresponse_duration_years.py | 18 -- ..._alter_ethnicityresponse_value_and_more.py | 29 -- ...enstartedsmokingresponse_value_and_more.py | 35 -- .../0048_tobaccosmokinghistory_and_more.py | 30 -- .../0049_smokedtotalyearsresponse_and_more.py | 40 --- ...smokingresponse_duration_years_and_more.py | 24 -- .../migrations/0051_smokingcurrentresponse.py | 27 -- .../0052_smokingfrequencyresponse.py | 27 -- .../migrations/0053_smokedamountresponse.py | 30 -- ..._add_given_name_and_family_name_to_user.py | 23 -- ...ter_smokedamountresponse_value_and_more.py | 23 -- 55 files changed, 300 insertions(+), 1590 deletions(-) delete mode 100644 lung_cancer_screening/questions/migrations/0002_booleanresponse.py delete mode 100644 lung_cancer_screening/questions/migrations/0003_booleanresponse_question_and_more.py delete mode 100644 lung_cancer_screening/questions/migrations/0004_rename_questionnaireresponse_to_dateresponse.py delete mode 100644 lung_cancer_screening/questions/migrations/0005_responseset.py delete mode 100644 lung_cancer_screening/questions/migrations/0006_remove_dateresponse_participant_and_more.py delete mode 100644 lung_cancer_screening/questions/migrations/0007_responseset_submitted_and_more.py delete mode 100644 lung_cancer_screening/questions/migrations/0008_remove_responseset_submitted_and_more.py delete mode 100644 lung_cancer_screening/questions/migrations/0009_responseset_height_and_more.py delete mode 100644 lung_cancer_screening/questions/migrations/0010_responseset_height_imperial_alter_responseset_height_and_more.py delete mode 100644 lung_cancer_screening/questions/migrations/0011_responseset_weight_metric_and_more.py delete mode 100644 lung_cancer_screening/questions/migrations/0012_responseset_weight_imperial_and_more.py delete mode 100644 lung_cancer_screening/questions/migrations/0013_responseset_sex_at_birth.py delete mode 100644 lung_cancer_screening/questions/migrations/0014_responseset_gender.py delete mode 100644 lung_cancer_screening/questions/migrations/0015_responseset_ethnicity_alter_responseset_gender.py delete mode 100644 lung_cancer_screening/questions/migrations/0016_responseset_asbestos_exposure.py delete mode 100644 lung_cancer_screening/questions/migrations/0017_alter_responseset_asbestos_exposure.py delete mode 100644 lung_cancer_screening/questions/migrations/0018_responseset_respiratory_conditions.py delete mode 100644 lung_cancer_screening/questions/migrations/0019_user.py delete mode 100644 lung_cancer_screening/questions/migrations/0020_responseset_user_and_more.py delete mode 100644 lung_cancer_screening/questions/migrations/0021_remove_responseset_participant_delete_participant.py delete mode 100644 lung_cancer_screening/questions/migrations/0022_remove_responseset_unique_unsubmitted_response_per_participant_and_more.py delete mode 100644 lung_cancer_screening/questions/migrations/0023_rename_height_responseset_height_metric.py delete mode 100644 lung_cancer_screening/questions/migrations/0024_add_email_to_user.py delete mode 100644 lung_cancer_screening/questions/migrations/0025_haveyoueversmokedresponse.py delete mode 100644 lung_cancer_screening/questions/migrations/0026_asbestosexposureresponse.py delete mode 100644 lung_cancer_screening/questions/migrations/0027_dateofbirthresponse.py delete mode 100644 lung_cancer_screening/questions/migrations/0028_ethnicityresponse.py delete mode 100644 lung_cancer_screening/questions/migrations/0029_genderresponse.py delete mode 100644 lung_cancer_screening/questions/migrations/0030_heightresponse.py delete mode 100644 lung_cancer_screening/questions/migrations/0031_respiratoryconditionsresponse.py delete mode 100644 lung_cancer_screening/questions/migrations/0032_sexatbirthresponse.py delete mode 100644 lung_cancer_screening/questions/migrations/0033_weightresponse.py delete mode 100644 lung_cancer_screening/questions/migrations/0034_alter_haveyoueversmokedresponse_response_set.py delete mode 100644 lung_cancer_screening/questions/migrations/0035_cancerdiagnosisresponse.py delete mode 100644 lung_cancer_screening/questions/migrations/0036_familyhistorylungcancerresponse.py delete mode 100644 lung_cancer_screening/questions/migrations/0037_familyhistoryagewhendiagnosedresponse.py delete mode 100644 lung_cancer_screening/questions/migrations/0038_relativesagewhendiagnosedresponse_and_more.py delete mode 100644 lung_cancer_screening/questions/migrations/0039_checkneedappointmentresponse.py delete mode 100644 lung_cancer_screening/questions/migrations/0040_educationresponse.py delete mode 100644 lung_cancer_screening/questions/migrations/0041_alter_educationresponse_value.py delete mode 100644 lung_cancer_screening/questions/migrations/0042_alter_educationresponse_value.py delete mode 100644 lung_cancer_screening/questions/migrations/0043_agewhenstartedsmokingresponse.py delete mode 100644 lung_cancer_screening/questions/migrations/0044_periodswhenyoustoppedsmokingresponse.py delete mode 100644 lung_cancer_screening/questions/migrations/0045_periodswhenyoustoppedsmokingresponse_duration_years.py delete mode 100644 lung_cancer_screening/questions/migrations/0046_alter_ethnicityresponse_value_and_more.py delete mode 100644 lung_cancer_screening/questions/migrations/0047_alter_agewhenstartedsmokingresponse_value_and_more.py delete mode 100644 lung_cancer_screening/questions/migrations/0048_tobaccosmokinghistory_and_more.py delete mode 100644 lung_cancer_screening/questions/migrations/0049_smokedtotalyearsresponse_and_more.py delete mode 100644 lung_cancer_screening/questions/migrations/0050_alter_periodswhenyoustoppedsmokingresponse_duration_years_and_more.py delete mode 100644 lung_cancer_screening/questions/migrations/0051_smokingcurrentresponse.py delete mode 100644 lung_cancer_screening/questions/migrations/0052_smokingfrequencyresponse.py delete mode 100644 lung_cancer_screening/questions/migrations/0053_smokedamountresponse.py delete mode 100644 lung_cancer_screening/questions/migrations/0054_add_given_name_and_family_name_to_user.py delete mode 100644 lung_cancer_screening/questions/migrations/0055_user_sub_alter_smokedamountresponse_value_and_more.py diff --git a/lung_cancer_screening/questions/migrations/0001_initial.py b/lung_cancer_screening/questions/migrations/0001_initial.py index 2ca81bd6..2b392bc0 100644 --- a/lung_cancer_screening/questions/migrations/0001_initial.py +++ b/lung_cancer_screening/questions/migrations/0001_initial.py @@ -1,6 +1,10 @@ -# Generated by Django 5.2.4 on 2025-09-03 11:05 +# Generated by Django 5.2.11 on 2026-02-12 11:31 +import django.contrib.postgres.fields +import django.core.validators import django.db.models.deletion +import lung_cancer_screening.questions.models.validators.singleton_option +from django.conf import settings from django.db import migrations, models @@ -13,22 +17,313 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Participant', + name='User', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('unique_id', models.CharField(max_length=255, unique=True)), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('sub', models.CharField(max_length=255, unique=True)), + ('nhs_number', models.CharField(max_length=10, unique=True)), + ('given_name', models.CharField(max_length=255)), + ('family_name', models.CharField(max_length=255)), + ('email', models.EmailField(max_length=254)), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + }, ), migrations.CreateModel( - name='QuestionnaireResponse', + name='ResponseSet', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('submitted_at', models.DateTimeField(blank=True, null=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='RespiratoryConditionsResponse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('value', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('P', 'Pneumonia'), ('E', 'Emphysema'), ('B', 'Bronchitis'), ('T', 'Tuberculosis (TB)'), ('C', 'Chronic obstructive pulmonary disease (COPD)'), ('N', 'No, I have not had any of these respiratory conditions')], max_length=1), size=None, validators=[lung_cancer_screening.questions.models.validators.singleton_option.validate_singleton_option])), + ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='respiratory_conditions_response', to='questions.responseset')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='RelativesAgeWhenDiagnosedResponse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('value', models.CharField(choices=[('Y', 'Yes, they were younger than 60'), ('N', 'No, they were 60 or older'), ('U', 'I do not know')], max_length=1)), + ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='relatives_age_when_diagnosed', to='questions.responseset')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PeriodsWhenYouStoppedSmokingResponse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('value', models.BooleanField()), + ('duration_years', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1, message='The number of years you stopped smoking for must be at least 1')])), + ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='periods_when_you_stopped_smoking_response', to='questions.responseset')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='HeightResponse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('metric', models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1397, message='Height must be between 139.7cm and 243.8 cm'), django.core.validators.MaxValueValidator(2438, message='Height must be between 139.7cm and 243.8 cm')])), + ('imperial', models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(55, message='Height must be between 4 feet 7 inches and 8 feet'), django.core.validators.MaxValueValidator(96, message='Height must be between 4 feet 7 inches and 8 feet')])), + ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='height_response', to='questions.responseset')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='HaveYouEverSmokedResponse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('value', models.IntegerField(choices=[(0, 'Yes, I currently smoke'), (1, 'Yes, I used to smoke'), (2, 'Yes, but I have smoked fewer than 100 cigarettes in my lifetime'), (3, 'No, I have never smoked')])), + ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='have_you_ever_smoked_response', to='questions.responseset')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='GenderResponse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('value', models.CharField(choices=[('F', 'Female'), ('M', 'Male'), ('N', 'Non-binary'), ('P', 'Prefer not to say'), ('G', 'How I describe myself may not match my GP record')], max_length=1)), + ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='gender_response', to='questions.responseset')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='FamilyHistoryLungCancerResponse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('value', models.CharField(choices=[('Y', 'Yes'), ('N', 'No'), ('U', 'I do not know')], max_length=1)), + ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='family_history_lung_cancer', to='questions.responseset')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='EthnicityResponse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('value', models.CharField(choices=[('A', 'Asian or Asian British'), ('B', 'Black, African, Caribbean or Black British'), ('M', 'Mixed or multiple ethnic groups'), ('W', 'White'), ('O', 'Other ethnic group'), ('N', 'Prefer not to say')], max_length=1)), + ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='ethnicity_response', to='questions.responseset')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='EducationResponse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('value', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('X', 'I finished school before the age of 15'), ('G', 'GCSEs'), ('A', 'A-levels'), ('F', 'Further education'), ('B', "Bachelor's degree"), ('P', 'Postgraduate degree'), ('N', 'Prefer not to say')], max_length=1), size=None, validators=[lung_cancer_screening.questions.models.validators.singleton_option.validate_singleton_option])), + ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='education_response', to='questions.responseset')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='DateOfBirthResponse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), ('value', models.DateField()), + ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='date_of_birth_response', to='questions.responseset')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='CheckNeedAppointmentResponse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('value', models.BooleanField()), + ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='check_need_appointment_response', to='questions.responseset')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='CancerDiagnosisResponse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('value', models.BooleanField()), + ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='cancer_diagnosis_response', to='questions.responseset')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='AsbestosExposureResponse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('value', models.BooleanField()), + ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='asbestos_exposure_response', to='questions.responseset')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='AgeWhenStartedSmokingResponse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('value', models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1, message='The age you started smoking must be between 1 and your current age')])), + ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='age_when_started_smoking_response', to='questions.responseset')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SexAtBirthResponse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('value', models.CharField(choices=[('F', 'Female'), ('M', 'Male'), ('I', 'Intersex')], max_length=1)), + ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='sex_at_birth_response', to='questions.responseset')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TobaccoSmokingHistory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), - ('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='questions.participant')), + ('type', models.CharField(choices=[('Cigarettes', 'Cigarettes'), ('RolledCigarettes', 'Rolled cigarettes, or roll-ups'), ('Pipe', 'Pipe'), ('Cigars', 'Cigars'), ('Cigarillos', 'Cigarillos'), ('Shisha', 'Shisha')])), + ('response_set', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tobacco_smoking_history', to='questions.responseset')), ], ), + migrations.CreateModel( + name='SmokingFrequencyResponse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('value', models.CharField(choices=[('D', 'Daily'), ('W', 'Weekly'), ('M', 'Monthly')], max_length=1)), + ('tobacco_smoking_history', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='smoking_frequency_response', to='questions.tobaccosmokinghistory')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SmokingCurrentResponse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('value', models.BooleanField()), + ('tobacco_smoking_history', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='smoking_current_response', to='questions.tobaccosmokinghistory')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SmokedTotalYearsResponse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('value', models.IntegerField(validators=[django.core.validators.MinValueValidator(1, message='The number of years you smoked cigarettes must be at least 1')])), + ('tobacco_smoking_history', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='smoked_total_years_response', to='questions.tobaccosmokinghistory')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SmokedAmountResponse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('value', models.IntegerField(validators=[django.core.validators.MinValueValidator(1)])), + ('tobacco_smoking_history', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='smoked_amount_response', to='questions.tobaccosmokinghistory')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='WeightResponse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('metric', models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(254, message='Weight must be between 25.4kg and 317.5kg'), django.core.validators.MaxValueValidator(3175, message='Weight must be between 25.4kg and 317.5kg')])), + ('imperial', models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(56, message='Weight must be between 4 stone and 50 stone'), django.core.validators.MaxValueValidator(700, message='Weight must be between 4 stone and 50 stone')])), + ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='weight_response', to='questions.responseset')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddConstraint( + model_name='responseset', + constraint=models.UniqueConstraint(condition=models.Q(('submitted_at__isnull', True)), fields=('user',), name='unique_unsubmitted_response_per_user', violation_error_message='An unsubmitted response set already exists for this user'), + ), + migrations.AddConstraint( + model_name='tobaccosmokinghistory', + constraint=models.UniqueConstraint(fields=('response_set', 'type'), name='unique_tobacco_smoking_history_per_response_set', violation_error_message='A tobacco smoking history already exists for this response set and type'), + ), ] diff --git a/lung_cancer_screening/questions/migrations/0002_booleanresponse.py b/lung_cancer_screening/questions/migrations/0002_booleanresponse.py deleted file mode 100644 index f856a39a..00000000 --- a/lung_cancer_screening/questions/migrations/0002_booleanresponse.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.2.4 on 2025-09-03 16:03 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='BooleanResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('value', models.BooleanField()), - ('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='questions.participant')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0003_booleanresponse_question_and_more.py b/lung_cancer_screening/questions/migrations/0003_booleanresponse_question_and_more.py deleted file mode 100644 index 81050d0a..00000000 --- a/lung_cancer_screening/questions/migrations/0003_booleanresponse_question_and_more.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 5.2.4 on 2025-09-04 10:10 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0002_booleanresponse'), - ] - - operations = [ - migrations.AddField( - model_name='BooleanResponse', - name='question', - field=models.CharField(default='Have you ever smoked?', max_length=255), - preserve_default=False, - ), - migrations.AddField( - model_name='QuestionnaireResponse', - name='question', - field=models.CharField(default='What is your date of birth?', max_length=255), - preserve_default=False, - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0004_rename_questionnaireresponse_to_dateresponse.py b/lung_cancer_screening/questions/migrations/0004_rename_questionnaireresponse_to_dateresponse.py deleted file mode 100644 index 73fc8174..00000000 --- a/lung_cancer_screening/questions/migrations/0004_rename_questionnaireresponse_to_dateresponse.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.4 on 2025-09-04 10:10 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0003_booleanresponse_question_and_more'), - ] - - operations = [ - migrations.RenameModel( - old_name='QuestionnaireResponse', - new_name='DateResponse', - ), - - ] diff --git a/lung_cancer_screening/questions/migrations/0005_responseset.py b/lung_cancer_screening/questions/migrations/0005_responseset.py deleted file mode 100644 index 86290ecf..00000000 --- a/lung_cancer_screening/questions/migrations/0005_responseset.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 5.2.6 on 2025-09-08 13:27 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0004_rename_questionnaireresponse_to_dateresponse'), - ] - - operations = [ - migrations.CreateModel( - name='ResponseSet', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('have_you_ever_smoked', models.IntegerField(choices=[(0, 'Yes, I currently smoke'), (1, 'Yes, I used to smoke regularly'), (2, 'Yes, but only a few times'), (3, 'No, I have never smoked')], null=True)), - ('date_of_birth', models.DateField(null=True)), - ('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='questions.participant')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ], - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0006_remove_dateresponse_participant_and_more.py b/lung_cancer_screening/questions/migrations/0006_remove_dateresponse_participant_and_more.py deleted file mode 100644 index 74f89cbb..00000000 --- a/lung_cancer_screening/questions/migrations/0006_remove_dateresponse_participant_and_more.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.2.6 on 2025-09-08 14:02 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0005_responseset'), - ] - - operations = [ - migrations.DeleteModel( - name='BooleanResponse', - ), - migrations.DeleteModel( - name='DateResponse', - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0007_responseset_submitted_and_more.py b/lung_cancer_screening/questions/migrations/0007_responseset_submitted_and_more.py deleted file mode 100644 index 02633af4..00000000 --- a/lung_cancer_screening/questions/migrations/0007_responseset_submitted_and_more.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 5.2.6 on 2025-09-08 15:57 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0006_remove_dateresponse_participant_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='responseset', - name='submitted', - field=models.BooleanField(default=False), - ), - migrations.AlterField( - model_name='responseset', - name='date_of_birth', - field=models.DateField(blank=True, null=True), - ), - migrations.AlterField( - model_name='responseset', - name='have_you_ever_smoked', - field=models.IntegerField(blank=True, choices=[(0, 'Yes, I currently smoke'), (1, 'Yes, I used to smoke regularly'), (2, 'Yes, but only a few times'), (3, 'No, I have never smoked')], null=True), - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0008_remove_responseset_submitted_and_more.py b/lung_cancer_screening/questions/migrations/0008_remove_responseset_submitted_and_more.py deleted file mode 100644 index 19c7c766..00000000 --- a/lung_cancer_screening/questions/migrations/0008_remove_responseset_submitted_and_more.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 5.2.6 on 2025-09-09 09:34 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0007_responseset_submitted_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='responseset', - name='submitted', - ), - migrations.AddField( - model_name='responseset', - name='submitted_at', - field=models.DateTimeField(blank=True, null=True), - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0009_responseset_height_and_more.py b/lung_cancer_screening/questions/migrations/0009_responseset_height_and_more.py deleted file mode 100644 index e890a04e..00000000 --- a/lung_cancer_screening/questions/migrations/0009_responseset_height_and_more.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-06 15:16 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0008_remove_responseset_submitted_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='responseset', - name='height', - field=models.PositiveIntegerField(blank=True, null=True), - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0010_responseset_height_imperial_alter_responseset_height_and_more.py b/lung_cancer_screening/questions/migrations/0010_responseset_height_imperial_alter_responseset_height_and_more.py deleted file mode 100644 index af74cddb..00000000 --- a/lung_cancer_screening/questions/migrations/0010_responseset_height_imperial_alter_responseset_height_and_more.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-17 13:52 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0009_responseset_height_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='responseset', - name='height_imperial', - field=models.PositiveIntegerField(blank=True, null=True), - ), - migrations.AlterField( - model_name='responseset', - name='height', - field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1397, message='Height must be between 139.7cm and 243.8 cm'), django.core.validators.MaxValueValidator(2438, message='Height must be between 139.7cm and 243.8 cm')]), - ), - migrations.AddConstraint( - model_name='responseset', - constraint=models.UniqueConstraint(condition=models.Q(('submitted_at__isnull', True)), fields=('participant',), name='unique_unsubmitted_response_per_participant', violation_error_message='An unsubmitted response set already exists for this participant'), - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0011_responseset_weight_metric_and_more.py b/lung_cancer_screening/questions/migrations/0011_responseset_weight_metric_and_more.py deleted file mode 100644 index f0b854e5..00000000 --- a/lung_cancer_screening/questions/migrations/0011_responseset_weight_metric_and_more.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-21 13:13 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0010_responseset_height_imperial_alter_responseset_height_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='responseset', - name='weight_metric', - field=models.PositiveIntegerField(blank=True, null=True), - ), - migrations.AlterField( - model_name='responseset', - name='height_imperial', - field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(55, message='Height must be between 4 feet 7 inches and 8 feet'), django.core.validators.MaxValueValidator(96, message='Height must be between 4 feet 7 inches and 8 feet')]), - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0012_responseset_weight_imperial_and_more.py b/lung_cancer_screening/questions/migrations/0012_responseset_weight_imperial_and_more.py deleted file mode 100644 index a3c8297f..00000000 --- a/lung_cancer_screening/questions/migrations/0012_responseset_weight_imperial_and_more.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-27 12:36 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0011_responseset_weight_metric_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='responseset', - name='weight_imperial', - field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(56, message='Weight must be between 4 stone and 50 stone'), django.core.validators.MaxValueValidator(700, message='Weight must be between 4 stone and 50 stone')]), - ), - migrations.AlterField( - model_name='responseset', - name='weight_metric', - field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(254, message='Weight must be between 25.4kg and 317.5kg'), django.core.validators.MaxValueValidator(3175, message='Weight must be between 25.4kg and 317.5kg')]), - ), - ] 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 deleted file mode 100644 index adf0c720..00000000 --- a/lung_cancer_screening/questions/migrations/0013_responseset_sex_at_birth.py +++ /dev/null @@ -1,18 +0,0 @@ -# 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/migrations/0014_responseset_gender.py b/lung_cancer_screening/questions/migrations/0014_responseset_gender.py deleted file mode 100644 index d3703839..00000000 --- a/lung_cancer_screening/questions/migrations/0014_responseset_gender.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-31 10:56 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0013_responseset_sex_at_birth'), - ] - - operations = [ - migrations.AddField( - model_name='responseset', - name='gender', - field=models.CharField(blank=True, choices=[('M', 'Male'), ('F', 'Female'), ('N', 'Non-binary'), ('P', 'Prefer not to say'), ('G', 'How I describe myself may not match my GP record')], max_length=1, null=True), - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0015_responseset_ethnicity_alter_responseset_gender.py b/lung_cancer_screening/questions/migrations/0015_responseset_ethnicity_alter_responseset_gender.py deleted file mode 100644 index cc15d032..00000000 --- a/lung_cancer_screening/questions/migrations/0015_responseset_ethnicity_alter_responseset_gender.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-03 14:11 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0014_responseset_gender'), - ] - - operations = [ - migrations.AddField( - model_name='responseset', - name='ethnicity', - field=models.CharField(blank=True, choices=[('A', 'Asian or Asian British'), ('B', 'Black, African, Caribbean or Black British'), ('M', 'Mixed or multiple ethnic groups'), ('W', 'White'), ('O', 'Other ethnic group'), ('N', "I'd prefer not to say")], max_length=1, null=True), - ), - migrations.AlterField( - model_name='responseset', - name='gender', - field=models.CharField(blank=True, choices=[('F', 'Female'), ('M', 'Male'), ('N', 'Non-binary'), ('P', 'Prefer not to say'), ('G', 'How I describe myself may not match my GP record')], max_length=1, null=True), - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0016_responseset_asbestos_exposure.py b/lung_cancer_screening/questions/migrations/0016_responseset_asbestos_exposure.py deleted file mode 100644 index 7afaef79..00000000 --- a/lung_cancer_screening/questions/migrations/0016_responseset_asbestos_exposure.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.8 on 2025-11-12 08:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0015_responseset_ethnicity_alter_responseset_gender'), - ] - - operations = [ - migrations.AddField( - model_name='responseset', - name='asbestos_exposure', - field=models.CharField(blank=True, choices=[('Y', 'Yes'), ('N', 'No')], max_length=1, null=True), - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0017_alter_responseset_asbestos_exposure.py b/lung_cancer_screening/questions/migrations/0017_alter_responseset_asbestos_exposure.py deleted file mode 100644 index e76f04cd..00000000 --- a/lung_cancer_screening/questions/migrations/0017_alter_responseset_asbestos_exposure.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-12 11:00 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0016_responseset_asbestos_exposure'), - ] - - operations = [ - migrations.AlterField( - model_name='responseset', - name='asbestos_exposure', - field=models.BooleanField(blank=True, null=True), - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0018_responseset_respiratory_conditions.py b/lung_cancer_screening/questions/migrations/0018_responseset_respiratory_conditions.py deleted file mode 100644 index 18c0905f..00000000 --- a/lung_cancer_screening/questions/migrations/0018_responseset_respiratory_conditions.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.2.8 on 2025-11-14 11:20 - -import django.contrib.postgres.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0017_alter_responseset_asbestos_exposure'), - ] - - operations = [ - migrations.AddField( - model_name='responseset', - name='respiratory_conditions', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('P', 'Pneumonia'), ('E', 'Emphysema'), ('B', 'Chronic bronchitis'), ('T', 'Tuberculosis (TB)'), ('C', 'Chronic obstructive pulmonary disease (COPD)'), ('N', 'None of the above')], max_length=1), blank=True, null=True, size=None), - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0019_user.py b/lung_cancer_screening/questions/migrations/0019_user.py deleted file mode 100644 index d364295b..00000000 --- a/lung_cancer_screening/questions/migrations/0019_user.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 5.2.9 on 2025-12-03 16:51 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0018_responseset_respiratory_conditions'), - ] - - operations = [ - migrations.CreateModel( - name='User', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('nhs_number', models.CharField(max_length=10, unique=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ], - options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - }, - ) - ] diff --git a/lung_cancer_screening/questions/migrations/0020_responseset_user_and_more.py b/lung_cancer_screening/questions/migrations/0020_responseset_user_and_more.py deleted file mode 100644 index bff208c4..00000000 --- a/lung_cancer_screening/questions/migrations/0020_responseset_user_and_more.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 5.2.9 on 2025-12-09 16:06 - -import django.contrib.postgres.fields -import django.db.models.deletion -import lung_cancer_screening.questions.models.response_set -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0019_user'), - ] - - operations = [ - migrations.AddField( - model_name='responseset', - name='user', - field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), - preserve_default=False, - ), - migrations.AlterField( - model_name='responseset', - name='have_you_ever_smoked', - field=models.IntegerField(blank=True, choices=[(0, 'Yes, I currently smoke'), (1, 'Yes, I used to smoke'), (2, 'Yes, but I have smoked fewer than 100 cigarettes in my lifetime'), (3, 'No, I have never smoked')], null=True), - ), - migrations.AlterField( - model_name='responseset', - name='respiratory_conditions', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('P', 'Pneumonia'), ('E', 'Emphysema'), ('B', 'Bronchitis'), ('T', 'Tuberculosis (TB)'), ('C', 'Chronic obstructive pulmonary disease (COPD)'), ('N', 'No, I have not had any of these respiratory conditions')], max_length=1), blank=True, null=True, size=None, validators=[lung_cancer_screening.questions.models.validators.singleton_option.validate_singleton_option]), - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0021_remove_responseset_participant_delete_participant.py b/lung_cancer_screening/questions/migrations/0021_remove_responseset_participant_delete_participant.py deleted file mode 100644 index a91f28aa..00000000 --- a/lung_cancer_screening/questions/migrations/0021_remove_responseset_participant_delete_participant.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.2.9 on 2025-12-09 16:21 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0020_responseset_user_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='responseset', - name='participant', - ), - migrations.DeleteModel( - name='Participant', - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0022_remove_responseset_unique_unsubmitted_response_per_participant_and_more.py b/lung_cancer_screening/questions/migrations/0022_remove_responseset_unique_unsubmitted_response_per_participant_and_more.py deleted file mode 100644 index 576515dd..00000000 --- a/lung_cancer_screening/questions/migrations/0022_remove_responseset_unique_unsubmitted_response_per_participant_and_more.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 5.2.9 on 2025-12-09 16:27 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0021_remove_responseset_participant_delete_participant'), - ] - - operations = [ - migrations.RemoveConstraint( - model_name='responseset', - name='unique_unsubmitted_response_per_participant', - ), - migrations.AddConstraint( - model_name='responseset', - constraint=models.UniqueConstraint(condition=models.Q(('submitted_at__isnull', True)), fields=('user',), name='unique_unsubmitted_response_per_user', violation_error_message='An unsubmitted response set already exists for this user'), - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0023_rename_height_responseset_height_metric.py b/lung_cancer_screening/questions/migrations/0023_rename_height_responseset_height_metric.py deleted file mode 100644 index 8482e558..00000000 --- a/lung_cancer_screening/questions/migrations/0023_rename_height_responseset_height_metric.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.9 on 2025-12-11 16:01 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0022_remove_responseset_unique_unsubmitted_response_per_participant_and_more'), - ] - - operations = [ - migrations.RenameField( - model_name='responseset', - old_name='height', - new_name='height_metric', - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0024_add_email_to_user.py b/lung_cancer_screening/questions/migrations/0024_add_email_to_user.py deleted file mode 100644 index 8bfbc5cc..00000000 --- a/lung_cancer_screening/questions/migrations/0024_add_email_to_user.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.9 on 2025-12-17 15:37 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0023_rename_height_responseset_height_metric'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='email', - field=models.EmailField(blank=True, max_length=254, null=True), - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0025_haveyoueversmokedresponse.py b/lung_cancer_screening/questions/migrations/0025_haveyoueversmokedresponse.py deleted file mode 100644 index 9a4f75ec..00000000 --- a/lung_cancer_screening/questions/migrations/0025_haveyoueversmokedresponse.py +++ /dev/null @@ -1,53 +0,0 @@ -# Generated by Django 5.2.9 on 2025-12-18 17:15 - -import django.db.models.deletion -from django.db import migrations, models - - -def copy_have_you_ever_smoked_data(apps, schema_editor): - with schema_editor.connection.cursor() as cursor: - cursor.execute(""" - INSERT INTO questions_haveyoueversmokedresponse (created_at, updated_at, value, response_set_id) - SELECT NOW(), NOW(), have_you_ever_smoked, id - FROM questions_responseset - WHERE have_you_ever_smoked IS NOT NULL - ON CONFLICT (response_set_id) DO NOTHING - """) - - -def reverse_copy_have_you_ever_smoked_data(apps, schema_editor): - with schema_editor.connection.cursor() as cursor: - cursor.execute(""" - UPDATE questions_responseset rs - SET have_you_ever_smoked = h.value - FROM questions_haveyoueversmokedresponse h - WHERE rs.id = h.response_set_id - """) - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0024_add_email_to_user'), - ] - - operations = [ - migrations.CreateModel( - name='HaveYouEverSmokedResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('value', models.IntegerField(choices=[(0, 'Yes, I currently smoke'), (1, 'Yes, I used to smoke'), (2, 'Yes, but I have smoked fewer than 100 cigarettes in my lifetime'), (3, 'No, I have never smoked')])), - ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='questions.responseset')), - ], - options={ - 'abstract': False, - }, - ), - migrations.RunPython(copy_have_you_ever_smoked_data, reverse_copy_have_you_ever_smoked_data), - migrations.RemoveField( - model_name='responseset', - name='have_you_ever_smoked', - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0026_asbestosexposureresponse.py b/lung_cancer_screening/questions/migrations/0026_asbestosexposureresponse.py deleted file mode 100644 index 01c3fbf3..00000000 --- a/lung_cancer_screening/questions/migrations/0026_asbestosexposureresponse.py +++ /dev/null @@ -1,53 +0,0 @@ -# Generated manually to create AsbestosExposureResponse and copy data - -import django.db.models.deletion -from django.db import migrations, models - - -def copy_asbestos_exposure_data(apps, schema_editor): - with schema_editor.connection.cursor() as cursor: - cursor.execute(""" - INSERT INTO questions_asbestosexposureresponse (created_at, updated_at, value, response_set_id) - SELECT NOW(), NOW(), asbestos_exposure, id - FROM questions_responseset - WHERE asbestos_exposure IS NOT NULL - ON CONFLICT (response_set_id) DO NOTHING - """) - - -def reverse_copy_asbestos_exposure_data(apps, schema_editor): - with schema_editor.connection.cursor() as cursor: - cursor.execute(""" - UPDATE questions_responseset rs - SET asbestos_exposure = a.value - FROM questions_asbestosexposureresponse a - WHERE rs.id = a.response_set_id - """) - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0025_haveyoueversmokedresponse'), - ] - - operations = [ - migrations.CreateModel( - name='AsbestosExposureResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('value', models.BooleanField()), - ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='asbestos_exposure_response', to='questions.responseset')), - ], - options={ - 'abstract': False, - }, - ), - migrations.RunPython(copy_asbestos_exposure_data, reverse_copy_asbestos_exposure_data), - migrations.RemoveField( - model_name='responseset', - name='asbestos_exposure', - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0027_dateofbirthresponse.py b/lung_cancer_screening/questions/migrations/0027_dateofbirthresponse.py deleted file mode 100644 index 7440245a..00000000 --- a/lung_cancer_screening/questions/migrations/0027_dateofbirthresponse.py +++ /dev/null @@ -1,53 +0,0 @@ -# Generated manually to create DateOfBirthResponse and copy data - -import django.db.models.deletion -from django.db import migrations, models - - -def copy_date_of_birth_data(apps, schema_editor): - with schema_editor.connection.cursor() as cursor: - cursor.execute(""" - INSERT INTO questions_dateofbirthresponse (created_at, updated_at, value, response_set_id) - SELECT NOW(), NOW(), date_of_birth, id - FROM questions_responseset - WHERE date_of_birth IS NOT NULL - ON CONFLICT (response_set_id) DO NOTHING - """) - - -def reverse_copy_date_of_birth_data(apps, schema_editor): - with schema_editor.connection.cursor() as cursor: - cursor.execute(""" - UPDATE questions_responseset rs - SET date_of_birth = d.value - FROM questions_dateofbirthresponse d - WHERE rs.id = d.response_set_id - """) - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0026_asbestosexposureresponse'), - ] - - operations = [ - migrations.CreateModel( - name='DateOfBirthResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('value', models.DateField()), - ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='date_of_birth_response', to='questions.responseset')), - ], - options={ - 'abstract': False, - }, - ), - migrations.RunPython(copy_date_of_birth_data, reverse_copy_date_of_birth_data), - migrations.RemoveField( - model_name='responseset', - name='date_of_birth', - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0028_ethnicityresponse.py b/lung_cancer_screening/questions/migrations/0028_ethnicityresponse.py deleted file mode 100644 index 914b7fb9..00000000 --- a/lung_cancer_screening/questions/migrations/0028_ethnicityresponse.py +++ /dev/null @@ -1,53 +0,0 @@ -# Generated manually to create EthnicityResponse and copy data - -import django.db.models.deletion -from django.db import migrations, models - - -def copy_ethnicity_data(apps, schema_editor): - with schema_editor.connection.cursor() as cursor: - cursor.execute(""" - INSERT INTO questions_ethnicityresponse (created_at, updated_at, value, response_set_id) - SELECT NOW(), NOW(), ethnicity, id - FROM questions_responseset - WHERE ethnicity IS NOT NULL - ON CONFLICT (response_set_id) DO NOTHING - """) - - -def reverse_copy_ethnicity_data(apps, schema_editor): - with schema_editor.connection.cursor() as cursor: - cursor.execute(""" - UPDATE questions_responseset rs - SET ethnicity = e.value - FROM questions_ethnicityresponse e - WHERE rs.id = e.response_set_id - """) - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0027_dateofbirthresponse'), - ] - - operations = [ - migrations.CreateModel( - name='EthnicityResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('value', models.CharField(choices=[('A', 'Asian or Asian British'), ('B', 'Black, African, Caribbean or Black British'), ('M', 'Mixed or multiple ethnic groups'), ('W', 'White'), ('O', 'Other ethnic group'), ('N', "I'd prefer not to say")], max_length=1)), - ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='ethnicity_response', to='questions.responseset')), - ], - options={ - 'abstract': False, - }, - ), - migrations.RunPython(copy_ethnicity_data, reverse_copy_ethnicity_data), - migrations.RemoveField( - model_name='responseset', - name='ethnicity', - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0029_genderresponse.py b/lung_cancer_screening/questions/migrations/0029_genderresponse.py deleted file mode 100644 index 5fc534f3..00000000 --- a/lung_cancer_screening/questions/migrations/0029_genderresponse.py +++ /dev/null @@ -1,53 +0,0 @@ -# Generated manually to create GenderResponse and copy data - -import django.db.models.deletion -from django.db import migrations, models - - -def copy_gender_data(apps, schema_editor): - with schema_editor.connection.cursor() as cursor: - cursor.execute(""" - INSERT INTO questions_genderresponse (created_at, updated_at, value, response_set_id) - SELECT NOW(), NOW(), gender, id - FROM questions_responseset - WHERE gender IS NOT NULL - ON CONFLICT (response_set_id) DO NOTHING - """) - - -def reverse_copy_gender_data(apps, schema_editor): - with schema_editor.connection.cursor() as cursor: - cursor.execute(""" - UPDATE questions_responseset rs - SET gender = g.value - FROM questions_genderresponse g - WHERE rs.id = g.response_set_id - """) - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0028_ethnicityresponse'), - ] - - operations = [ - migrations.CreateModel( - name='GenderResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('value', models.CharField(choices=[('F', 'Female'), ('M', 'Male'), ('N', 'Non-binary'), ('P', 'Prefer not to say'), ('G', 'How I describe myself may not match my GP record')], max_length=1)), - ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='gender_response', to='questions.responseset')), - ], - options={ - 'abstract': False, - }, - ), - migrations.RunPython(copy_gender_data, reverse_copy_gender_data), - migrations.RemoveField( - model_name='responseset', - name='gender', - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0030_heightresponse.py b/lung_cancer_screening/questions/migrations/0030_heightresponse.py deleted file mode 100644 index 9177cdd5..00000000 --- a/lung_cancer_screening/questions/migrations/0030_heightresponse.py +++ /dev/null @@ -1,60 +0,0 @@ -# Generated manually to create HeightResponse and copy data - -import django.core.validators -import django.db.models.deletion -from django.db import migrations, models - - -def copy_height_data(apps, schema_editor): - with schema_editor.connection.cursor() as cursor: - cursor.execute(""" - INSERT INTO questions_heightresponse (created_at, updated_at, metric, imperial, response_set_id) - SELECT NOW(), NOW(), height_metric, height_imperial, id - FROM questions_responseset - WHERE height_metric IS NOT NULL OR height_imperial IS NOT NULL - ON CONFLICT (response_set_id) DO NOTHING - """) - - -def reverse_copy_height_data(apps, schema_editor): - with schema_editor.connection.cursor() as cursor: - cursor.execute(""" - UPDATE questions_responseset rs - SET height_metric = h.metric, - height_imperial = h.imperial - FROM questions_heightresponse h - WHERE rs.id = h.response_set_id - """) - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0029_genderresponse'), - ] - - operations = [ - migrations.CreateModel( - name='HeightResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('metric', models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1397, message='Height must be between 139.7cm and 243.8 cm'), django.core.validators.MaxValueValidator(2438, message='Height must be between 139.7cm and 243.8 cm')])), - ('imperial', models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(55, message='Height must be between 4 feet 7 inches and 8 feet'), django.core.validators.MaxValueValidator(96, message='Height must be between 4 feet 7 inches and 8 feet')])), - ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='height_response', to='questions.responseset')), - ], - options={ - 'abstract': False, - }, - ), - migrations.RunPython(copy_height_data, reverse_copy_height_data), - migrations.RemoveField( - model_name='responseset', - name='height_metric', - ), - migrations.RemoveField( - model_name='responseset', - name='height_imperial', - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0031_respiratoryconditionsresponse.py b/lung_cancer_screening/questions/migrations/0031_respiratoryconditionsresponse.py deleted file mode 100644 index fdb3805c..00000000 --- a/lung_cancer_screening/questions/migrations/0031_respiratoryconditionsresponse.py +++ /dev/null @@ -1,55 +0,0 @@ -# Generated manually to create RespiratoryConditionsResponse and copy data - -import django.contrib.postgres.fields -import django.db.models.deletion -import lung_cancer_screening.questions.models.respiratory_conditions_response -from django.db import migrations, models - - -def copy_respiratory_conditions_data(apps, schema_editor): - with schema_editor.connection.cursor() as cursor: - cursor.execute(""" - INSERT INTO questions_respiratoryconditionsresponse (created_at, updated_at, value, response_set_id) - SELECT NOW(), NOW(), respiratory_conditions, id - FROM questions_responseset - WHERE respiratory_conditions IS NOT NULL - ON CONFLICT (response_set_id) DO NOTHING - """) - - -def reverse_copy_respiratory_conditions_data(apps, schema_editor): - with schema_editor.connection.cursor() as cursor: - cursor.execute(""" - UPDATE questions_responseset rs - SET respiratory_conditions = r.value - FROM questions_respiratoryconditionsresponse r - WHERE rs.id = r.response_set_id - """) - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0030_heightresponse'), - ] - - operations = [ - migrations.CreateModel( - name='RespiratoryConditionsResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('value', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('P', 'Pneumonia'), ('E', 'Emphysema'), ('B', 'Bronchitis'), ('T', 'Tuberculosis (TB)'), ('C', 'Chronic obstructive pulmonary disease (COPD)'), ('N', 'No, I have not had any of these respiratory conditions')], max_length=1), size=None, validators=[lung_cancer_screening.questions.models.validators.singleton_option.validate_singleton_option])), - ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='respiratory_conditions_response', to='questions.responseset')), - ], - options={ - 'abstract': False, - }, - ), - migrations.RunPython(copy_respiratory_conditions_data, reverse_copy_respiratory_conditions_data), - migrations.RemoveField( - model_name='responseset', - name='respiratory_conditions', - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0032_sexatbirthresponse.py b/lung_cancer_screening/questions/migrations/0032_sexatbirthresponse.py deleted file mode 100644 index 32a4fdb7..00000000 --- a/lung_cancer_screening/questions/migrations/0032_sexatbirthresponse.py +++ /dev/null @@ -1,53 +0,0 @@ -# Generated manually to create SexAtBirthResponse and copy data - -import django.db.models.deletion -from django.db import migrations, models - - -def copy_sex_at_birth_data(apps, schema_editor): - with schema_editor.connection.cursor() as cursor: - cursor.execute(""" - INSERT INTO questions_sexatbirthresponse (created_at, updated_at, value, response_set_id) - SELECT NOW(), NOW(), sex_at_birth, id - FROM questions_responseset - WHERE sex_at_birth IS NOT NULL - ON CONFLICT (response_set_id) DO NOTHING - """) - - -def reverse_copy_sex_at_birth_data(apps, schema_editor): - with schema_editor.connection.cursor() as cursor: - cursor.execute(""" - UPDATE questions_responseset rs - SET sex_at_birth = s.value - FROM questions_sexatbirthresponse s - WHERE rs.id = s.response_set_id - """) - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0031_respiratoryconditionsresponse'), - ] - - operations = [ - migrations.CreateModel( - name='SexAtBirthResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('value', models.CharField(choices=[('F', 'Female'), ('M', 'Male')], max_length=1)), - ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='sex_at_birth_response', to='questions.responseset')), - ], - options={ - 'abstract': False, - }, - ), - migrations.RunPython(copy_sex_at_birth_data, reverse_copy_sex_at_birth_data), - migrations.RemoveField( - model_name='responseset', - name='sex_at_birth', - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0033_weightresponse.py b/lung_cancer_screening/questions/migrations/0033_weightresponse.py deleted file mode 100644 index e050512f..00000000 --- a/lung_cancer_screening/questions/migrations/0033_weightresponse.py +++ /dev/null @@ -1,60 +0,0 @@ -# Generated manually to create WeightResponse and copy data - -import django.core.validators -import django.db.models.deletion -from django.db import migrations, models - - -def copy_weight_data(apps, schema_editor): - with schema_editor.connection.cursor() as cursor: - cursor.execute(""" - INSERT INTO questions_weightresponse (created_at, updated_at, metric, imperial, response_set_id) - SELECT NOW(), NOW(), weight_metric, weight_imperial, id - FROM questions_responseset - WHERE weight_metric IS NOT NULL OR weight_imperial IS NOT NULL - ON CONFLICT (response_set_id) DO NOTHING - """) - - -def reverse_copy_weight_data(apps, schema_editor): - with schema_editor.connection.cursor() as cursor: - cursor.execute(""" - UPDATE questions_responseset rs - SET weight_metric = w.metric, - weight_imperial = w.imperial - FROM questions_weightresponse w - WHERE rs.id = w.response_set_id - """) - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0032_sexatbirthresponse'), - ] - - operations = [ - migrations.CreateModel( - name='WeightResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('metric', models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(254, message='Weight must be between 25.4kg and 317.5kg'), django.core.validators.MaxValueValidator(3175, message='Weight must be between 25.4kg and 317.5kg')])), - ('imperial', models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(56, message='Weight must be between 4 stone and 50 stone'), django.core.validators.MaxValueValidator(700, message='Weight must be between 4 stone and 50 stone')])), - ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='weight_response', to='questions.responseset')), - ], - options={ - 'abstract': False, - }, - ), - migrations.RunPython(copy_weight_data, reverse_copy_weight_data), - migrations.RemoveField( - model_name='responseset', - name='weight_metric', - ), - migrations.RemoveField( - model_name='responseset', - name='weight_imperial', - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0034_alter_haveyoueversmokedresponse_response_set.py b/lung_cancer_screening/questions/migrations/0034_alter_haveyoueversmokedresponse_response_set.py deleted file mode 100644 index e66a2035..00000000 --- a/lung_cancer_screening/questions/migrations/0034_alter_haveyoueversmokedresponse_response_set.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.2.9 on 2025-12-30 09:30 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0033_weightresponse'), - ] - - operations = [ - migrations.AlterField( - model_name='haveyoueversmokedresponse', - name='response_set', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='have_you_ever_smoked_response', to='questions.responseset'), - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0035_cancerdiagnosisresponse.py b/lung_cancer_screening/questions/migrations/0035_cancerdiagnosisresponse.py deleted file mode 100644 index 73f7d9a9..00000000 --- a/lung_cancer_screening/questions/migrations/0035_cancerdiagnosisresponse.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.2.9 on 2025-12-30 09:41 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0034_alter_haveyoueversmokedresponse_response_set'), - ] - - operations = [ - migrations.CreateModel( - name='CancerDiagnosisResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('value', models.BooleanField()), - ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='cancer_diagnosis_response', to='questions.responseset')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0036_familyhistorylungcancerresponse.py b/lung_cancer_screening/questions/migrations/0036_familyhistorylungcancerresponse.py deleted file mode 100644 index 69a88ccb..00000000 --- a/lung_cancer_screening/questions/migrations/0036_familyhistorylungcancerresponse.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.2.9 on 2026-01-02 09:15 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0035_cancerdiagnosisresponse'), - ] - - operations = [ - migrations.CreateModel( - name='FamilyHistoryLungCancerResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('value', models.CharField(choices=[('Y', 'Yes'), ('N', 'No'), ('U', 'I do not know')], max_length=1)), - ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='family_history_lung_cancer', to='questions.responseset')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0037_familyhistoryagewhendiagnosedresponse.py b/lung_cancer_screening/questions/migrations/0037_familyhistoryagewhendiagnosedresponse.py deleted file mode 100644 index 79d41feb..00000000 --- a/lung_cancer_screening/questions/migrations/0037_familyhistoryagewhendiagnosedresponse.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.2.9 on 2026-01-05 12:20 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0036_familyhistorylungcancerresponse'), - ] - - operations = [ - migrations.CreateModel( - name='FamilyHistoryAgeWhenDiagnosedResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('value', models.CharField(choices=[('Y', 'Yes, they were younger than 60'), ('N', 'No, they were 60 or older'), ('U', 'I do not know')], max_length=1)), - ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='family_history_age_when_diagnosed', to='questions.responseset')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0038_relativesagewhendiagnosedresponse_and_more.py b/lung_cancer_screening/questions/migrations/0038_relativesagewhendiagnosedresponse_and_more.py deleted file mode 100644 index dfbf8c0a..00000000 --- a/lung_cancer_screening/questions/migrations/0038_relativesagewhendiagnosedresponse_and_more.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 5.2.9 on 2026-01-05 15:23 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0037_familyhistoryagewhendiagnosedresponse'), - ] - - operations = [ - migrations.CreateModel( - name='RelativesAgeWhenDiagnosedResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('value', models.CharField(choices=[('Y', 'Yes, they were younger than 60'), ('N', 'No, they were 60 or older'), ('U', 'I do not know')], max_length=1)), - ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='relatives_age_when_diagnosed', to='questions.responseset')), - ], - options={ - 'abstract': False, - }, - ), - migrations.DeleteModel( - name='FamilyHistoryAgeWhenDiagnosedResponse', - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0039_checkneedappointmentresponse.py b/lung_cancer_screening/questions/migrations/0039_checkneedappointmentresponse.py deleted file mode 100644 index 1b217bcf..00000000 --- a/lung_cancer_screening/questions/migrations/0039_checkneedappointmentresponse.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.2.9 on 2026-01-08 12:11 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0038_relativesagewhendiagnosedresponse_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='CheckNeedAppointmentResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('value', models.BooleanField()), - ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='check_need_appointment_response', to='questions.responseset')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0040_educationresponse.py b/lung_cancer_screening/questions/migrations/0040_educationresponse.py deleted file mode 100644 index cc1cde9d..00000000 --- a/lung_cancer_screening/questions/migrations/0040_educationresponse.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.2.9 on 2026-01-09 13:33 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0039_checkneedappointmentresponse'), - ] - - operations = [ - migrations.CreateModel( - name='EducationResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('value', models.CharField(choices=[('X', 'I finished school before the age of 15'), ('G', 'GCSEs'), ('A', 'A-levels'), ('B', "Bachelor's degree"), ('P', 'Postgraduate degree'), ('N', "I'd prefer not to say")], max_length=1)), - ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='education_response', to='questions.responseset')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0041_alter_educationresponse_value.py b/lung_cancer_screening/questions/migrations/0041_alter_educationresponse_value.py deleted file mode 100644 index 5f4051e3..00000000 --- a/lung_cancer_screening/questions/migrations/0041_alter_educationresponse_value.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.2.10 on 2026-01-15 16:41 - -import django.contrib.postgres.fields -import lung_cancer_screening.questions.models.validators.singleton_option -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0040_educationresponse'), - ] - - operations = [ - migrations.AlterField( - model_name='educationresponse', - name='value', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('X', 'I finished school before the age of 15'), ('G', 'GCSEs'), ('A', 'A-levels'), ('B', "Bachelor's degree"), ('P', 'Postgraduate degree'), ('N', "I'd prefer not to say")], max_length=1), size=None, validators=[lung_cancer_screening.questions.models.validators.singleton_option.validate_singleton_option]), - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0042_alter_educationresponse_value.py b/lung_cancer_screening/questions/migrations/0042_alter_educationresponse_value.py deleted file mode 100644 index e85a18b5..00000000 --- a/lung_cancer_screening/questions/migrations/0042_alter_educationresponse_value.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.2.10 on 2026-01-19 10:47 - -import django.contrib.postgres.fields -import lung_cancer_screening.questions.models.validators.singleton_option -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0041_alter_educationresponse_value'), - ] - - operations = [ - migrations.AlterField( - model_name='educationresponse', - name='value', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('X', 'I finished school before the age of 15'), ('G', 'GCSEs'), ('A', 'A-levels'), ('F', 'Further education'), ('B', "Bachelor's degree"), ('P', 'Postgraduate degree'), ('N', 'Prefer not to say')], max_length=1), size=None, validators=[lung_cancer_screening.questions.models.validators.singleton_option.validate_singleton_option]), - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0043_agewhenstartedsmokingresponse.py b/lung_cancer_screening/questions/migrations/0043_agewhenstartedsmokingresponse.py deleted file mode 100644 index 0866db59..00000000 --- a/lung_cancer_screening/questions/migrations/0043_agewhenstartedsmokingresponse.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.2.10 on 2026-01-15 08:59 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0042_alter_educationresponse_value'), - ] - - operations = [ - migrations.CreateModel( - name='AgeWhenStartedSmokingResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('value', models.PositiveIntegerField()), - ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='age_when_started_smoking_response', to='questions.responseset')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0044_periodswhenyoustoppedsmokingresponse.py b/lung_cancer_screening/questions/migrations/0044_periodswhenyoustoppedsmokingresponse.py deleted file mode 100644 index 94231637..00000000 --- a/lung_cancer_screening/questions/migrations/0044_periodswhenyoustoppedsmokingresponse.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ("questions", "0043_agewhenstartedsmokingresponse"), - ] - - operations = [ - migrations.CreateModel( - name='PeriodsWhenYouStoppedSmokingResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('value', models.BooleanField()), - ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='periods_when_you_stopped_smoking_response', to='questions.responseset')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0045_periodswhenyoustoppedsmokingresponse_duration_years.py b/lung_cancer_screening/questions/migrations/0045_periodswhenyoustoppedsmokingresponse_duration_years.py deleted file mode 100644 index 366cc48e..00000000 --- a/lung_cancer_screening/questions/migrations/0045_periodswhenyoustoppedsmokingresponse_duration_years.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.10 on 2026-01-21 11:01 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0044_periodswhenyoustoppedsmokingresponse'), - ] - - operations = [ - migrations.AddField( - model_name='periodswhenyoustoppedsmokingresponse', - name='duration_years', - field=models.IntegerField(blank=True, null=True), - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0046_alter_ethnicityresponse_value_and_more.py b/lung_cancer_screening/questions/migrations/0046_alter_ethnicityresponse_value_and_more.py deleted file mode 100644 index 0ed41559..00000000 --- a/lung_cancer_screening/questions/migrations/0046_alter_ethnicityresponse_value_and_more.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 5.2.10 on 2026-01-21 16:57 - -import django.contrib.postgres.fields -import django.db.models.deletion -import lung_cancer_screening.questions.models.validators.singleton_option -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("questions", "0045_periodswhenyoustoppedsmokingresponse_duration_years"), - ] - - operations = [ - migrations.CreateModel( - name='TypesTobaccoSmokingResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('value', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('C', 'Cigarettes'), ('G', 'Cigars'), ('P', 'Pipes'), ('E', 'E-cigarettes or vaping'), ('N', 'None of the above')], max_length=1), size=None, validators=[lung_cancer_screening.questions.models.validators.singleton_option.validate_singleton_option])), - ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='types_tobacco_smoking_response', to='questions.responseset')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0047_alter_agewhenstartedsmokingresponse_value_and_more.py b/lung_cancer_screening/questions/migrations/0047_alter_agewhenstartedsmokingresponse_value_and_more.py deleted file mode 100644 index 92bb6aaf..00000000 --- a/lung_cancer_screening/questions/migrations/0047_alter_agewhenstartedsmokingresponse_value_and_more.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 5.2.10 on 2026-01-27 14:59 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0046_alter_ethnicityresponse_value_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='agewhenstartedsmokingresponse', - name='value', - field=models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1, message='The age you started smoking must be between 1 and your current age')]), - ), - migrations.AlterField( - model_name='ethnicityresponse', - name='value', - field=models.CharField(choices=[('A', 'Asian or Asian British'), ('B', 'Black, African, Caribbean or Black British'), ('M', 'Mixed or multiple ethnic groups'), ('W', 'White'), ('O', 'Other ethnic group'), ('N', 'Prefer not to say')], max_length=1), - ), - migrations.AlterField( - model_name='sexatbirthresponse', - name='value', - field=models.CharField(choices=[('F', 'Female'), ('M', 'Male'), ('I', 'Intersex')], max_length=1), - ), - migrations.AlterField( - model_name='typestobaccosmokingresponse', - name='value', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('C', 'Cigarettes'), ('R', 'Rolled cigarettes, or roll-ups'), ('P', 'Pipe'), ('G', 'Cigars'), ('E', 'Cigarillos'), ('S', 'Shisha')], max_length=1), size=None), - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0048_tobaccosmokinghistory_and_more.py b/lung_cancer_screening/questions/migrations/0048_tobaccosmokinghistory_and_more.py deleted file mode 100644 index e48661f7..00000000 --- a/lung_cancer_screening/questions/migrations/0048_tobaccosmokinghistory_and_more.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 5.2.10 on 2026-01-28 10:46 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0047_alter_agewhenstartedsmokingresponse_value_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='TobaccoSmokingHistory', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('type', models.CharField(choices=[('Cigarettes', 'Cigarettes'), ('RolledCigarettes', 'Rolled cigarettes, or roll-ups'), ('Pipe', 'Pipe'), ('Cigars', 'Cigars'), ('Cigarillos', 'Cigarillos'), ('Shisha', 'Shisha')])), - ('response_set', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='types_tobacco_smoking', to='questions.responseset')), - ], - options={ - 'abstract': False, - }, - ), - migrations.DeleteModel( - name='TypesTobaccoSmokingResponse', - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0049_smokedtotalyearsresponse_and_more.py b/lung_cancer_screening/questions/migrations/0049_smokedtotalyearsresponse_and_more.py deleted file mode 100644 index b2e3662a..00000000 --- a/lung_cancer_screening/questions/migrations/0049_smokedtotalyearsresponse_and_more.py +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by Django 5.2.10 on 2026-01-28 15:38 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0048_tobaccosmokinghistory_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='SmokedTotalYearsResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('value', models.IntegerField()), - ], - options={ - 'abstract': False, - }, - ), - migrations.AlterField( - model_name='tobaccosmokinghistory', - name='response_set', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tobacco_smoking_history', to='questions.responseset'), - ), - migrations.AddConstraint( - model_name='tobaccosmokinghistory', - constraint=models.UniqueConstraint(fields=('response_set', 'type'), name='unique_tobacco_smoking_history_per_response_set', violation_error_message='A tobacco smoking history already exists for this response set and type'), - ), - migrations.AddField( - model_name='smokedtotalyearsresponse', - name='tobacco_smoking_history', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='smoked_total_years_response', to='questions.tobaccosmokinghistory'), - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0050_alter_periodswhenyoustoppedsmokingresponse_duration_years_and_more.py b/lung_cancer_screening/questions/migrations/0050_alter_periodswhenyoustoppedsmokingresponse_duration_years_and_more.py deleted file mode 100644 index 28172bcf..00000000 --- a/lung_cancer_screening/questions/migrations/0050_alter_periodswhenyoustoppedsmokingresponse_duration_years_and_more.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.2.10 on 2026-02-03 16:30 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0049_smokedtotalyearsresponse_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='periodswhenyoustoppedsmokingresponse', - name='duration_years', - field=models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1, message='The number of years you stopped smoking for must be at least 1')]), - ), - migrations.AlterField( - model_name='smokedtotalyearsresponse', - name='value', - field=models.IntegerField(validators=[django.core.validators.MinValueValidator(1, message='The number of years you smoked cigarettes must be at least 1')]), - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0051_smokingcurrentresponse.py b/lung_cancer_screening/questions/migrations/0051_smokingcurrentresponse.py deleted file mode 100644 index 240ec805..00000000 --- a/lung_cancer_screening/questions/migrations/0051_smokingcurrentresponse.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.2.11 on 2026-02-04 13:59 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0050_alter_periodswhenyoustoppedsmokingresponse_duration_years_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='SmokingCurrentResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('value', models.BooleanField()), - ('tobacco_smoking_history', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='smoking_current_response', to='questions.tobaccosmokinghistory')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0052_smokingfrequencyresponse.py b/lung_cancer_screening/questions/migrations/0052_smokingfrequencyresponse.py deleted file mode 100644 index 53ae82b2..00000000 --- a/lung_cancer_screening/questions/migrations/0052_smokingfrequencyresponse.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.2.11 on 2026-02-08 18:16 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0051_smokingcurrentresponse'), - ] - - operations = [ - migrations.CreateModel( - name='SmokingFrequencyResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('value', models.CharField(choices=[('D', 'Daily'), ('W', 'Weekly'), ('M', 'Monthly')], max_length=1)), - ('tobacco_smoking_history', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='smoking_frequency_response', to='questions.tobaccosmokinghistory')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0053_smokedamountresponse.py b/lung_cancer_screening/questions/migrations/0053_smokedamountresponse.py deleted file mode 100644 index 7b4deb83..00000000 --- a/lung_cancer_screening/questions/migrations/0053_smokedamountresponse.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 5.2.10 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ( - "questions", - "0052_smokingfrequencyresponse", - ), - ] - - operations = [ - migrations.CreateModel( - name='SmokedAmountResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('value', models.IntegerField()), - ('tobacco_smoking_history', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='smoked_amount_response', to='questions.tobaccosmokinghistory')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0054_add_given_name_and_family_name_to_user.py b/lung_cancer_screening/questions/migrations/0054_add_given_name_and_family_name_to_user.py deleted file mode 100644 index 9cffd303..00000000 --- a/lung_cancer_screening/questions/migrations/0054_add_given_name_and_family_name_to_user.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.10 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0053_smokedamountresponse'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='given_name', - field=models.CharField(blank=False, null=False, max_length=255), - ), - migrations.AddField( - model_name='user', - name='family_name', - field=models.CharField(blank=False, null=False, max_length=255), - ), - ] diff --git a/lung_cancer_screening/questions/migrations/0055_user_sub_alter_smokedamountresponse_value_and_more.py b/lung_cancer_screening/questions/migrations/0055_user_sub_alter_smokedamountresponse_value_and_more.py deleted file mode 100644 index 6b036979..00000000 --- a/lung_cancer_screening/questions/migrations/0055_user_sub_alter_smokedamountresponse_value_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.11 on 2026-02-12 11:06 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('questions', '0054_add_given_name_and_family_name_to_user'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='sub', - field=models.CharField(max_length=255, unique=True) - ), - migrations.AlterField( - model_name='user', - name='email', - field=models.EmailField(max_length=254), - ), - ]