diff --git a/.github/workflows/autodeploy-to-azure-epilepsy12-development.yml b/.github/workflows/autodeploy-to-azure-epilepsy12-development.yml index f8bf842f..ce59042e 100644 --- a/.github/workflows/autodeploy-to-azure-epilepsy12-development.yml +++ b/.github/workflows/autodeploy-to-azure-epilepsy12-development.yml @@ -16,21 +16,46 @@ on: jobs: build-and-deploy: runs-on: ubuntu-latest + + # this 'environment' refers to GitHub Environments, not environment variables environment: name: 'development' url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + # this 'env' refers to environment variables + env: + DJANGO_CSRF_TRUSTED_ORIGINS: https://localhost,https://0.0.0.0 + E12_POSTGRES_DB_HOST: 127.0.0.1 + E12_POSTGRES_DB_NAME: test_db + E12_POSTGRES_DB_PASSWORD: postgis + E12_POSTGRES_DB_PORT: 5432 + E12_POSTGRES_DB_USER: postgis + POSTCODES_IO_API_URL: https://api.postcodes.io + RCPCH_CENSUS_PLATFORM_TOKEN: ${{ secrets.RCPCH_CENSUS_PLATFORM_TOKEN }} + RCPCH_CENSUS_PLATFORM_URL: https://api.rcpch.ac.uk/deprivation/v1 + RCPCH_HERMES_SERVER_URL: http://rcpch-hermes.uksouth.azurecontainer.io:8080/v1/snomed + + # Sets up a Postgis container for the test db + services: + postgis: + image: postgis/postgis:15-3.3 + env: + POSTGRES_USER: postgis + POSTGRES_PASSWORD: postgis + POSTGRES_DB: test_db + ports: + - 5432:5432 + # needed because the postgis container does not provide a healthcheck + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + steps: - uses: actions/checkout@v3 - - - name: List all files - run: ls -al - + - name: Set up Python version uses: actions/setup-python@v4 with: python-version: '3.10' - + - name: Install PostGIS dependencies run: sudo apt-get install -y binutils libproj-dev gdal-bin libgdal-dev python3-gdal @@ -38,14 +63,23 @@ jobs: run: | python -m venv venv source venv/bin/activate - + # python dependencies - name: Install dependencies - run: pip install -r requirements.txt - + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + # run migrations + - name: Run migrations + run: python manage.py migrate + - name: collect static files run: python manage.py collectstatic --noinput + - name: Pytest Suite + run: pytest -rP + # Deploy to Azure - name: 'Deploy to Azure Web App' uses: azure/webapps-deploy@v2 diff --git a/.github/workflows/autodeploy-to-azure-epilepsy12-staging.yml b/.github/workflows/autodeploy-to-azure-epilepsy12-staging.yml index 2a720d68..72604882 100644 --- a/.github/workflows/autodeploy-to-azure-epilepsy12-staging.yml +++ b/.github/workflows/autodeploy-to-azure-epilepsy12-staging.yml @@ -14,9 +14,40 @@ on: workflow_dispatch: jobs: - build: + build-and-deploy: runs-on: ubuntu-latest + # this 'environment' refers to GitHub Environments, not environment variables + environment: + name: 'staging' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + + # this 'env' refers to environment variables + env: + DJANGO_CSRF_TRUSTED_ORIGINS: https://localhost,https://0.0.0.0 + E12_POSTGRES_DB_HOST: 127.0.0.1 + E12_POSTGRES_DB_NAME: test_db + E12_POSTGRES_DB_PASSWORD: postgis + E12_POSTGRES_DB_PORT: 5432 + E12_POSTGRES_DB_USER: postgis + POSTCODES_IO_API_URL: https://api.postcodes.io + RCPCH_CENSUS_PLATFORM_TOKEN: ${{ secrets.RCPCH_CENSUS_PLATFORM_TOKEN }} + RCPCH_CENSUS_PLATFORM_URL: https://api.rcpch.ac.uk/deprivation/v1 + RCPCH_HERMES_SERVER_URL: http://rcpch-hermes.uksouth.azurecontainer.io:8080/v1/snomed + + # Sets up a Postgis container for the test db + services: + postgis: + image: postgis/postgis:15-3.3 + env: + POSTGRES_USER: postgis + POSTGRES_PASSWORD: postgis + POSTGRES_DB: test_db + ports: + - 5432:5432 + # needed because the postgis container does not provide a healthcheck + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + steps: - uses: actions/checkout@v3 @@ -24,7 +55,7 @@ jobs: uses: actions/setup-python@v4 with: python-version: '3.10' - + - name: Install PostGIS dependencies run: sudo apt-get install -y binutils libproj-dev gdal-bin libgdal-dev python3-gdal @@ -32,37 +63,28 @@ jobs: run: | python -m venv venv source venv/bin/activate - + + # python dependencies - name: Install dependencies - run: pip install -r requirements.txt - + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: collect static files run: python manage.py collectstatic --noinput - - # Optional: Add step to run tests here (PyTest, Django test suites, etc.) - + + - name: Pytest Suite + run: pytest -rP + - name: Upload artifact for deployment jobs uses: actions/upload-artifact@v3 with: name: python-app path: | - . + . !venv/ - deploy: - runs-on: ubuntu-latest - needs: build - environment: - name: 'staging' - url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} - - steps: - - name: Download artifact from build job - uses: actions/download-artifact@v3 - with: - name: python-app - path: . - + # Deploy to Azure - name: 'Deploy to Azure Web App' uses: azure/webapps-deploy@v2 id: deploy-to-webapp diff --git a/.github/workflows/test-workflow-temp.yml b/.github/workflows/test-workflow-temp.yml new file mode 100644 index 00000000..2e1cbce6 --- /dev/null +++ b/.github/workflows/test-workflow-temp.yml @@ -0,0 +1,80 @@ +name: Temp workflow to test Pytest Suite + +on: + push: + branches: + - "add-cicd-auto-pytest" + workflow_dispatch: + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + # this 'env' refers to environment variables + env: + DJANGO_CSRF_TRUSTED_ORIGINS: https://localhost,https://0.0.0.0 + E12_POSTGRES_DB_HOST: 127.0.0.1 + E12_POSTGRES_DB_NAME: test_db + E12_POSTGRES_DB_PASSWORD: postgis + E12_POSTGRES_DB_PORT: 5432 + E12_POSTGRES_DB_USER: postgis + POSTCODES_IO_API_URL: https://api.postcodes.io + RCPCH_CENSUS_PLATFORM_TOKEN: ${{ secrets.RCPCH_CENSUS_PLATFORM_TOKEN }} + RCPCH_CENSUS_PLATFORM_URL: https://api.rcpch.ac.uk/deprivation/v1 + RCPCH_HERMES_SERVER_URL: http://rcpch-hermes.uksouth.azurecontainer.io:8080/v1/snomed + + # this 'environment' refers to GitHub Environments, not environment variables + environment: + name: "development" + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + + services: + postgis: + image: postgis/postgis:15-3.3 + env: + POSTGRES_USER: postgis + POSTGRES_PASSWORD: postgis + POSTGRES_DB: test_db + ports: + - 5432:5432 + # needed because the postgis container does not provide a healthcheck + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + + steps: + - uses: actions/checkout@v3 + + - name: List all files + run: ls -al + + - name: Set up Python version + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install PostGIS dependencies + run: sudo apt-get install -y binutils libproj-dev gdal-bin libgdal-dev python3-gdal + + - name: Create and start virtual environment + run: | + python -m venv venv + source venv/bin/activate + + # python dependencies + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + # run migrations + - name: Run migrations + run: python manage.py migrate + + - name: collect static files + run: python manage.py collectstatic --noinput + + # Run Pytest Suite + - name: print settings for debugging purposes + run: python manage.py diffsettings --all + + - name: Pytest Suite + run: pytest -rP diff --git a/docker-compose.dev-init.yml b/docker-compose.dev-init.yml index e3ba8bc1..d077c412 100644 --- a/docker-compose.dev-init.yml +++ b/docker-compose.dev-init.yml @@ -25,7 +25,7 @@ services: - DEBUG=True - DJANGO_ALLOWED_HOSTS=0.0.0.0 - DJANGO_CSRF_TRUSTED_ORIGINS=https://localhost,https://0.0.0.0 - - POSTCODES_IO_API_URL=https://api.postcodes.io/postcodes + - POSTCODES_IO_API_URL=https://api.postcodes.io - RCPCH_CENSUS_PLATFORM_URL=https://api.rcpch.ac.uk/deprivation/v1 # live # - RCPCH_CENSUS_PLATFORM_URL=http://0.0.0.0:8001 # development - RCPCH_HERMES_SERVER_URL=http://rcpch-hermes.uksouth.azurecontainer.io:8080/v1/snomed @@ -43,7 +43,7 @@ services: command: > sh -c "python manage.py collectstatic --noinput && python manage.py migrate && - python manage.py seed --mode=seed_dummy_cases && + python manage.py seed --mode=cases --cases 100 && python manage.py seed --mode=seed_registrations && python manage.py seed --mode=seed_groups_and_permissions && python manage.py createsuperuser --noinput && @@ -51,7 +51,7 @@ services: echo 'DEV SETUP SCRIPT: Development superuser password: pw' && python manage.py runserver 0.0.0.0:8000" - # db container - runs postgres + # db container - runs postgis db: image: postgis/postgis:15-3.3 volumes: diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 6a64d2dd..a310d154 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -36,6 +36,8 @@ services: # ensures that docker compose always displays log output tty: true stdin_open: true + env_file: + - .env # env file with the token for RCPCH Census Platform, not committed to Git # db container - runs postgres db: diff --git a/epilepsy12/admin.py b/epilepsy12/admin.py index 3fd830cb..f4dde848 100644 --- a/epilepsy12/admin.py +++ b/epilepsy12/admin.py @@ -2,6 +2,7 @@ from django.contrib.auth.admin import UserAdmin from semantic_admin import SemanticModelAdmin from simple_history.admin import SimpleHistoryAdmin + # Register your models here. from .models import * @@ -11,86 +12,118 @@ class Epilepsy12UserAdmin(UserAdmin, SimpleHistoryAdmin): add_form = Epilepsy12UserCreationForm form = Epilepsy12UserChangeForm - ordering = ['email'] + ordering = ["email"] model = Epilepsy12User - search_fields = ('email', 'surname', - 'role', 'organisation_employer', 'is_active',) - list_display = ('id', "email", "title", "first_name", "surname", - "is_active", "twitter_handle", "role", "organisation_employer", 'is_superuser', "is_rcpch_audit_team_member", "email_confirmed") - list_filter = ("is_active", "role", "organisation_employer",) + search_fields = ( + "email", + "surname", + "role", + "is_active", + ) + list_display = ( + "id", + "email", + "title", + "first_name", + "surname", + "is_active", + "role", + "organisation_employer", + "is_superuser", + "is_rcpch_audit_team_member", + "email_confirmed", + ) + list_filter = ( + "is_active", + "role", + "organisation_employer", + ) fieldsets = ( ( - None, { - 'fields': ( - 'title', - 'first_name', - 'surname', - ) - } - ), - ( - 'Epilepsy12 Centre', { - 'fields': ( - 'organisation_employer', - 'role' + None, + { + "fields": ( + "title", + "first_name", + "surname", ) - } + }, ), + ("Epilepsy12 Centre", {"fields": ("organisation_employer", "role")}), + ("Contacts", {"fields": ("email",)}), ( - 'Contacts', { - 'fields': ( - 'email', - 'twitter_handle' - ) - } - ), - ( - 'Permissions', { - 'fields': ( - 'is_active', - 'is_staff', - 'is_rcpch_audit_team_member', - 'is_superuser', + "Permissions", + { + "fields": ( + "is_active", + "is_staff", + "is_rcpch_staff", + "is_rcpch_audit_team_member", + "is_superuser", "email_confirmed", "view_preference", ) - } + }, ), ( - 'Group Permissions', { - 'classes': ('collapse',), - 'fields': ( - 'groups', 'user_permissions', + "Access", + { + "fields": ( + "last_login", + "date_joined", ) - } + }, ), ( - 'Personal', { - 'fields': ('bio',) - } + "Group Permissions", + { + "classes": ("collapse",), + "fields": ( + "groups", + "user_permissions", + ), + }, ), ) add_fieldsets = ( - (None, { - 'classes': ('wide',), - 'fields': ('email', 'title', 'first_name', 'surname', 'is_staff', 'is_active', 'is_rcpch_audit_team_member', 'role', 'organisation_employer', 'is_superuser', 'groups') - }), + ( + None, + { + "classes": ("wide",), + "fields": ( + "email", + "title", + "first_name", + "surname", + "is_staff", + "is_rcpch_staff", + "is_active", + "is_rcpch_audit_team_member", + "role", + "organisation_employer", + "is_superuser", + "groups", + ), + }, + ), ) def get_form(self, request, obj=None, **kwargs): form = super().get_form(request, obj, **kwargs) - form.base_fields['organisation_employer'].required = False + form.base_fields["organisation_employer"].required = False if not request.user.is_superuser: - self.exclude = ['is_superuser'] + self.exclude = ["is_superuser"] else: self.exclude = [] - if request.user.groups.filter(name='trust_audit_team_edit_access'): - form.base_fields['groups'].disabled = True - form.base_fields['first_name'].disabled = True - form.base_fields['surname'].disabled = True - form.base_fields['title'].disabled = True - form.base_fields['email'].disabled = True - form.base_fields['is_staff'].disabled = True + if request.user.groups.filter(name="trust_audit_team_edit_access"): + form.base_fields["groups"].disabled = True + form.base_fields["first_name"].disabled = True + form.base_fields["surname"].disabled = True + form.base_fields["title"].disabled = True + form.base_fields["email"].disabled = True + form.base_fields["is_staff"].disabled = True + form.base_fields["is_rcpch_staff"].disabled = True + form.base_fields["is_rcpch_audit_team_member"].disabled = True return form @@ -113,12 +146,18 @@ def get_form(self, request, obj=None, **kwargs): admin.site.register(Syndrome, SimpleHistoryAdmin) admin.site.register(SyndromeEntity, SimpleHistoryAdmin) admin.site.register(KPI) +admin.site.register(KPIAggregation) admin.site.register(VisitActivity) admin.site.register(EpilepsyCauseEntity) admin.site.register(ComorbidityEntity) admin.site.register(MedicineEntity) -admin.site.site_header = 'Epilepsy12 admin' -admin.site.site_title = 'Epilepsy12 admin' -admin.site.index_title = 'Epilepsy12' -admin.site.site_url = '/' +admin.site.register(CountryBoundaries) +admin.site.register(NHSEnglandRegionBoundaries) +admin.site.register(LocalHealthBoardBoundaries) +admin.site.register(IntegratedCareBoardBoundaries) + +admin.site.site_header = "Epilepsy12 admin" +admin.site.site_title = "Epilepsy12 admin" +admin.site.index_title = "Epilepsy12" +admin.site.site_url = "/" diff --git a/epilepsy12/common_view_functions/__init__.py b/epilepsy12/common_view_functions/__init__.py index fd4cef92..28516ec0 100644 --- a/epilepsy12/common_view_functions/__init__.py +++ b/epilepsy12/common_view_functions/__init__.py @@ -2,7 +2,7 @@ from .recalculate_form_generate_response import ( recalculate_form_generate_response, completed_fields, - test_fields_update_audit_progress, + update_audit_progress, trigger_client_event, count_episode_fields, expected_score_for_single_episode, @@ -22,3 +22,4 @@ ) from .sanction_user_access import return_selected_organisation, sanction_user from .group_for_group import group_for_role +from .tiles_for_region import return_tile_for_region diff --git a/epilepsy12/common_view_functions/aggregate_by.py b/epilepsy12/common_view_functions/aggregate_by.py index fc9cd30e..531d0f21 100644 --- a/epilepsy12/common_view_functions/aggregate_by.py +++ b/epilepsy12/common_view_functions/aggregate_by.py @@ -1,11 +1,33 @@ from typing import Literal + # Django imports -from django.contrib.gis.db.models import Q, F, Count, Sum, Avg, When, Value, CharField, PositiveSmallIntegerField, Case as DJANGO_CASE +from django.apps import apps +from django.contrib.gis.db.models import ( + Q, + F, + Count, + Sum, + Avg, + When, + Value, + CharField, + PositiveSmallIntegerField, + Case as DJANGO_CASE, +) # E12 imports from epilepsy12.constants import ETHNICITIES, SEX_TYPE -from ..models import Case -from .report_queries import get_all_organisations, get_all_trusts, get_all_icbs, get_all_nhs_regions, get_all_open_uk_regions, get_all_countries + +# from ..models import Case +from .report_queries import ( + get_all_organisations, + get_all_trusts, + get_all_icbs, + get_all_nhs_regions, + get_all_open_uk_regions, + get_all_countries, +) + """ Reporting """ @@ -14,21 +36,17 @@ def cases_aggregated_by_sex(selected_organisation): # aggregate queries on trust level cases - sex_long_list = [When(sex=k, then=Value(v)) - for k, v in SEX_TYPE] + Case = apps.get_model("epilepsy12", "Case") + + sex_long_list = [When(sex=k, then=Value(v)) for k, v in SEX_TYPE] cases_aggregated_by_sex = ( - Case.objects.filter( - organisations__OrganisationName__contains=selected_organisation) - .values('sex') - .annotate( - sex_display=DJANGO_CASE( - *sex_long_list, output_field=CharField() - ) - ) - .values('sex_display') - .annotate( - sexes=Count('sex')).order_by('sexes') + Case.objects.filter(organisations=selected_organisation) + .values("sex") + .annotate(sex_display=DJANGO_CASE(*sex_long_list, output_field=CharField())) + .values("sex_display") + .annotate(sexes=Count("sex")) + .order_by("sexes") ) return cases_aggregated_by_sex @@ -36,70 +54,78 @@ def cases_aggregated_by_sex(selected_organisation): def cases_aggregated_by_deprivation_score(selected_organisation): # aggregate queries on trust level cases + Case = apps.get_model("epilepsy12", "Case") - deprivation_quintiles = ( - (1, 1), - (2, 2), - (3, 3), - (4, 4), - (5, 5), - (None, 6) + cases_in_selected_organisation = Case.objects.filter( + organisations__OrganisationName__contains=selected_organisation ) - imd_long_list = [When(index_of_multiple_deprivation_quintile=k, then=Value(v)) - for k, v in deprivation_quintiles] - cases_aggregated_by_deprivation = ( - Case.objects.filter( - organisations__OrganisationName__contains=selected_organisation) - .values('index_of_multiple_deprivation_quintile') + # Filter just Cases in selected org + cases_in_selected_organisation + # Get list of IMD quintiles + .values("index_of_multiple_deprivation_quintile") + # Converting 'None' to 6 in a new index_of_multiple_deprivation_quintile_display "column" .annotate( index_of_multiple_deprivation_quintile_display=DJANGO_CASE( - *imd_long_list, output_field=PositiveSmallIntegerField() + When(index_of_multiple_deprivation_quintile=None, then=Value(6)), + default="index_of_multiple_deprivation_quintile", + output_field=PositiveSmallIntegerField(), ) ) - .values('index_of_multiple_deprivation_quintile_display') + # Keeps only the new column + .values("index_of_multiple_deprivation_quintile_display") + # Value count the new column .annotate( - cases_aggregated_by_deprivation=Count('index_of_multiple_deprivation_quintile')) - .order_by('index_of_multiple_deprivation_quintile') + cases_aggregated_by_deprivation=Count( + "index_of_multiple_deprivation_quintile_display" + ), + ) + .order_by('index_of_multiple_deprivation_quintile_display') + ) - - # map quintile num to string repr + deprivation_quintile_str_map = { - 1: '1st quintile', - 2: '2nd quintile', - 3: '3rd quintile', - 4: '4th quintile', - 5: '5th quintile', - 6: 'Not known' + 1: "1st quintile", + 2: "2nd quintile", + 3: "3rd quintile", + 4: "4th quintile", + 5: "5th quintile", + 6: "Not known", } - for index, q in enumerate(cases_aggregated_by_deprivation): - q['index_of_multiple_deprivation_quintile_display'] = deprivation_quintile_str_map.get( - q.get('index_of_multiple_deprivation_quintile_display')) + for aggregate in cases_aggregated_by_deprivation: + quintile = aggregate["index_of_multiple_deprivation_quintile_display"] + str_map = deprivation_quintile_str_map.get(quintile) + + aggregate.update( + {"index_of_multiple_deprivation_quintile_display_str": str_map} + ) + return cases_aggregated_by_deprivation def cases_aggregated_by_ethnicity(selected_organisation): - # aggregate queries on trust level cases - ethnicity_long_list = [When(ethnicity=k, then=Value(v)) - for k, v in ETHNICITIES] + Case = apps.get_model("epilepsy12", "Case") + + ethnicity_long_list = [When(ethnicity=k, then=Value(v)) for k, v in ETHNICITIES] cases_aggregated_by_ethnicity = ( Case.objects.filter( - organisations__OrganisationName__contains=selected_organisation) - .values('ethnicity') + organisations__OrganisationName__contains=selected_organisation + ) + .values("ethnicity") .annotate( ethnicity_display=DJANGO_CASE( *ethnicity_long_list, output_field=CharField() ) ) - .values('ethnicity_display') - .annotate( - ethnicities=Count('ethnicity')).order_by('ethnicities') + .values("ethnicity_display") + .annotate(ethnicities=Count("ethnicity")) + .order_by("ethnicities") ) return cases_aggregated_by_ethnicity @@ -107,36 +133,36 @@ def cases_aggregated_by_ethnicity(selected_organisation): def aggregate_all_eligible_kpi_fields(filtered_cases, kpi_measure=None): """ - Returns a dictionary of all KPI fields with aggregation for each measure ready to pass - into a related model. If an individual measure is passed in, only that measure will be aggregated. - It can only be used by a model which has a relationship with registration (but not registration itself) + Returns a dictionary of all KPI fields with aggregation for each measure ready to persist in KPIAggregations. + It accepts a list of cases filtered by a given level of abstraction (all cases in an organisation, trust, icb etc) + If an individual measure is passed in, only that measure will be aggregated. Returned fields include sum of all eligible KPI measures (identified as having an individual score of 1 or 0) for that registration as well as average score of the same and total number KPIs. A KPI score of 2 is excluded as not eligible for that measure. """ all_kpi_measures = [ - 'paediatrician_with_expertise_in_epilepsies', - 'epilepsy_specialist_nurse', - 'tertiary_input', - 'epilepsy_surgery_referral', - 'ecg', - 'mri', - 'assessment_of_mental_health_issues', - 'mental_health_support', - 'sodium_valproate', - 'comprehensive_care_planning_agreement', - 'patient_held_individualised_epilepsy_document', - 'patient_carer_parent_agreement_to_the_care_planning', - 'care_planning_has_been_updated_when_necessary', - 'comprehensive_care_planning_content', - 'parental_prolonged_seizures_care_plan', - 'water_safety', - 'first_aid', - 'general_participation_and_risk', - 'service_contact_details', - 'sudep', - 'school_individual_healthcare_plan' + "paediatrician_with_expertise_in_epilepsies", + "epilepsy_specialist_nurse", + "tertiary_input", + "epilepsy_surgery_referral", + "ecg", + "mri", + "assessment_of_mental_health_issues", + "mental_health_support", + "sodium_valproate", + "comprehensive_care_planning_agreement", + "patient_held_individualised_epilepsy_document", + "patient_carer_parent_agreement_to_the_care_planning", + "care_planning_has_been_updated_when_necessary", + "comprehensive_care_planning_content", + "parental_prolonged_seizures_care_plan", + "water_safety", + "first_aid", + "general_participation_and_risk", + "service_contact_details", + "sudep", + "school_individual_healthcare_plan", ] aggregation_fields = {} @@ -144,80 +170,82 @@ def aggregate_all_eligible_kpi_fields(filtered_cases, kpi_measure=None): if kpi_measure: # a single measure selected for aggregation - q_objects = Q( - **{f'registration__kpi__{kpi_measure}__lt': 2} - ) & Q( - **{f'registration__kpi__{kpi_measure}__isnull': False} - ) - f_objects = F(f'registration__kpi__{kpi_measure}') + q_objects = Q(**{f"registration__kpi__{kpi_measure}__lt": 2}) & Q( + **{f"registration__kpi__{kpi_measure}__isnull": False} + ) + f_objects = F(f"registration__kpi__{kpi_measure}") # sum this measure - aggregation_fields[f'{kpi_measure}'] = Sum( - DJANGO_CASE(When(q_objects, - then=f_objects), default=None) + aggregation_fields[f"{kpi_measure}"] = Sum( + DJANGO_CASE(When(q_objects, then=f_objects), default=None) ) # average of the sum of this measure - aggregation_fields[f'{kpi_measure}_average'] = Avg( - DJANGO_CASE(When(q_objects, - then=f_objects), default=None)) + aggregation_fields[f"{kpi_measure}_average"] = Avg( + DJANGO_CASE(When(q_objects, then=f_objects), default=None) + ) # total cases scored for this measure - aggregation_fields['total_number_of_cases'] = Count( - DJANGO_CASE(When(q_objects, - then=f_objects), default=None)) + aggregation_fields["total_number_of_cases"] = Count( + DJANGO_CASE(When(q_objects, then=f_objects), default=None) + ) else: # aggregate all measures for measure in all_kpi_measures: # filter cases for all kpi with a score < 2 - q_objects = Q( - **{f'registration__kpi__{measure}__lt': 2} - ) & Q( - **{f'registration__kpi__{measure}__isnull': False} - ) & Q(**{f'registration__kpi__{measure}__isnull': False}) - f_objects = F(f'registration__kpi__{measure}') + q_objects = Q(**{f"registration__kpi__{measure}__lt": 2}) & Q( + **{f"registration__kpi__{measure}__isnull": False} + ) # & Q(**{f'registration__kpi__{measure}__isnull': False}) + f_objects = F(f"registration__kpi__{measure}") # sum this measure - aggregation_fields[f'{measure}'] = Sum( - DJANGO_CASE(When(q_objects, - then=f_objects), default=0)) + aggregation_fields[f"{measure}"] = Sum( + DJANGO_CASE(When(q_objects, then=f_objects), default=0) + ) # average of the sum of this measure - aggregation_fields[f'{measure}_average'] = Avg( - DJANGO_CASE(When(q_objects, - then=f_objects), default=None)) + aggregation_fields[f"{measure}_average"] = Avg( + DJANGO_CASE(When(q_objects, then=f_objects), default=None) + ) # total cases scored for this measure - aggregation_fields[f'{measure}_total'] = Count( - DJANGO_CASE(When(q_objects, - then=f_objects), default=None)) + aggregation_fields[f"{measure}_total"] = Count( + DJANGO_CASE(When(q_objects, then=f_objects), default=None) + ) # total_cases scored for all measures - aggregation_fields['total_number_of_cases'] = Count( - DJANGO_CASE(When(q_objects, - then=f_objects), default=None)) + aggregation_fields["total_number_of_cases"] = Count( + "registration__pk", default=None + ) return filtered_cases.aggregate(**aggregation_fields) -def return_all_aggregated_kpis_for_cohort_and_abstraction_level_annotated_by_sublevel(cohort, abstraction_level: Literal['organisation', 'trust', 'icb', 'nhs_region', 'open_uk', 'country', 'national'] = 'organisation', kpi_measure=None): +def return_all_aggregated_kpis_for_cohort_and_abstraction_level_annotated_by_sublevel( + cohort, + abstraction_level: Literal[ + "organisation", "trust", "icb", "nhs_region", "open_uk", "country", "national" + ] = "organisation", + kpi_measure=None, +): """ - Returns aggregated KPIS for given cohort against sublevel of abstraction (eg all NHS England regions) + Returns aggregated KPIS for given cohort annotated by sublevel of abstraction (eg kpis in each NHS England region, labelled by region) """ + Case = apps.get_model("epilepsy12", "Case") - if abstraction_level == 'organisation': + if abstraction_level == "organisation": abstraction_sublevels = get_all_organisations() - if abstraction_level == 'trust': + if abstraction_level == "trust": abstraction_sublevels = get_all_trusts() - if abstraction_level == 'icb': + if abstraction_level == "icb": abstraction_sublevels = get_all_icbs() - if abstraction_level == 'nhs_region': + if abstraction_level == "nhs_region": abstraction_sublevels = get_all_nhs_regions() - if abstraction_level == 'open_uk': + if abstraction_level == "open_uk": abstraction_sublevels = get_all_open_uk_regions() - if abstraction_level == 'country': + if abstraction_level == "country": abstraction_sublevels = get_all_countries() # if abstraction_level == 'national': @@ -227,45 +255,53 @@ def return_all_aggregated_kpis_for_cohort_and_abstraction_level_annotated_by_sub final_object = [] for abstraction_sublevel in abstraction_sublevels: - - if abstraction_level == 'organisation': + if abstraction_level == "organisation": abstraction_sublevel_Q = Q( - site__organisation__ODSCode=abstraction_sublevel.ODSCode) + site__organisation__ODSCode=abstraction_sublevel.ODSCode + ) label = abstraction_sublevel.ODSCode - if abstraction_level == 'trust': + if abstraction_level == "trust": abstraction_sublevel_Q = Q( - site__organisation__ParentOrganisation_ODSCode=abstraction_sublevel.ParentOrganisation_ODSCode) + site__organisation__ParentOrganisation_ODSCode=abstraction_sublevel.ParentOrganisation_ODSCode + ) label = abstraction_sublevel.ParentOrganisation_OrganisationName - if abstraction_level == 'icb': + if abstraction_level == "icb": abstraction_sublevel_Q = Q( - site__organisation__integrated_care_board__ODS_ICB_Code=abstraction_sublevel.ODS_ICB_Code) + site__organisation__integrated_care_board__ODS_ICB_Code=abstraction_sublevel.ODS_ICB_Code + ) label = abstraction_sublevel.ICB_Name - if abstraction_level == 'nhs_region': + if abstraction_level == "nhs_region": abstraction_sublevel_Q = Q( - site__organisation__nhs_region__NHS_Region_Code=abstraction_sublevel.NHS_Region_Code) + site__organisation__nhs_region__NHS_Region_Code=abstraction_sublevel.NHS_Region_Code + ) label = abstraction_sublevel.NHS_Region - if abstraction_level == 'open_uk': + if abstraction_level == "open_uk": abstraction_sublevel_Q = Q( - site__organisation__openuk_network__OPEN_UK_Network_Code=abstraction_sublevel.OPEN_UK_Network_Code) + site__organisation__openuk_network__OPEN_UK_Network_Code=abstraction_sublevel.OPEN_UK_Network_Code + ) label = abstraction_sublevel.OPEN_UK_Network_Name - if abstraction_level == 'country': + if abstraction_level == "country": abstraction_sublevel_Q = Q( - site__organisation__ons_region__ons_country__Country_ONS_Code=abstraction_sublevel.Country_ONS_Code) + site__organisation__ons_region__ons_country__Country_ONS_Code=abstraction_sublevel.Country_ONS_Code + ) label = abstraction_sublevel.Country_ONS_Name filtered_cases = Case.objects.filter( - Q(site__site_is_actively_involved_in_epilepsy_care=True) & - Q(site__site_is_primary_centre_of_epilepsy_care=True) & - abstraction_sublevel_Q & - Q(registration__cohort=cohort) + Q(site__site_is_actively_involved_in_epilepsy_care=True) + & Q(site__site_is_primary_centre_of_epilepsy_care=True) + & abstraction_sublevel_Q + & Q(registration__cohort=cohort) ) aggregated_kpis = aggregate_all_eligible_kpi_fields( - filtered_cases, kpi_measure=kpi_measure) + filtered_cases, kpi_measure=kpi_measure + ) final_object.append( { "region": label, "aggregated_kpis": aggregated_kpis, - 'color' : '#808080' if aggregated_kpis[kpi_measure] is None else '#000000', + "color": "#808080" + if aggregated_kpis[kpi_measure] is None + else "#000000", } ) diff --git a/epilepsy12/common_view_functions/calculate_kpi_functions/__init__.py b/epilepsy12/common_view_functions/calculate_kpi_functions/__init__.py new file mode 100644 index 00000000..2dbc5731 --- /dev/null +++ b/epilepsy12/common_view_functions/calculate_kpi_functions/__init__.py @@ -0,0 +1,12 @@ +from .score_kpi_1 import score_kpi_1 +from .score_kpi_2 import score_kpi_2 +from .score_kpi_3 import score_kpi_3, score_kpi_3b +from .score_kpi_4 import score_kpi_4 +from .score_kpi_5 import score_kpi_5 +from .score_kpi_6 import score_kpi_6 +from .score_kpi_7 import score_kpi_7 +from .score_kpi_8 import score_kpi_8 +from .score_kpi_9 import score_kpi_9A,score_kpi_9Ai,score_kpi_9Aii,score_kpi_9Aiii,score_kpi_9B,score_kpi_9Bi,score_kpi_9Bii,score_kpi_9Biii,score_kpi_9Biv,score_kpi_9Bv,score_kpi_9Bvi +from .score_kpi_10 import score_kpi_10 +from .calculate_age_at_first_paediatric_assessment_in_years import calculate_age_at_first_paediatric_assessment_in_years +from .check_is_registered import check_is_registered \ No newline at end of file diff --git a/epilepsy12/common_view_functions/calculate_kpi_functions/calculate_age_at_first_paediatric_assessment_in_years.py b/epilepsy12/common_view_functions/calculate_kpi_functions/calculate_age_at_first_paediatric_assessment_in_years.py new file mode 100644 index 00000000..0a37f45d --- /dev/null +++ b/epilepsy12/common_view_functions/calculate_kpi_functions/calculate_age_at_first_paediatric_assessment_in_years.py @@ -0,0 +1,18 @@ +# python imports +from dateutil.relativedelta import relativedelta + +# django imports + +# E12 imports + + +def calculate_age_at_first_paediatric_assessment_in_years(registration_instance) -> int: + """ + Helper fn returns age in years as int + """ + age_at_first_paediatric_assessment = relativedelta( + registration_instance.registration_date, + registration_instance.case.date_of_birth, + ) + + return age_at_first_paediatric_assessment.years diff --git a/epilepsy12/common_view_functions/calculate_kpi_functions/check_is_registered.py b/epilepsy12/common_view_functions/calculate_kpi_functions/check_is_registered.py new file mode 100644 index 00000000..631e820d --- /dev/null +++ b/epilepsy12/common_view_functions/calculate_kpi_functions/check_is_registered.py @@ -0,0 +1,15 @@ +# python imports + +# django imports + +# E12 imports + + +def check_is_registered(registration_instance) -> bool: + """ + Helper fn returns True if registered + """ + return ( + registration_instance.registration_date is not None + and registration_instance.eligibility_criteria_met + ) diff --git a/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_1.py b/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_1.py new file mode 100644 index 00000000..fd4a4ed8 --- /dev/null +++ b/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_1.py @@ -0,0 +1,68 @@ +# python imports +from dateutil.relativedelta import relativedelta + +# django imports + +# E12 imports +from epilepsy12.constants import KPI_SCORE + + +def score_kpi_1(registration_instance) -> int: + """ + 1. `paediatrician_with_expertise_in_epilepsies` + + % of children and young people with epilepsy, with input by a ‘consultant paediatrician with expertise in epilepsies’ within 2 weeks of initial referral + + Calculation Method + + Numerator = Number of children and young people [diagnosed with epilepsy] at first year AND (who had [input from a paediatrician with expertise in epilepsy] OR a [input from a paediatric neurologist] within 2 weeks of initial referral. (initial referral to mean first paediatric assessment) + + Denominator = Number of and young people [diagnosed with epilepsy] at first year + """ + + assessment = registration_instance.assessment + + # never saw a consultant OR neurologist! + if (assessment.consultant_paediatrician_referral_made == False) and ( + assessment.paediatric_neurologist_referral_made == False + ): + return KPI_SCORE["FAIL"] + + # check all fields complete for either consultant or neurologist + all_consultant_paediatrician_fields_complete = ( + (assessment.consultant_paediatrician_referral_made is not None) + and (assessment.consultant_paediatrician_referral_date is not None) + and (assessment.consultant_paediatrician_input_date is not None) + ) + all_paediatric_neurologist_fields_complete = ( + (assessment.paediatric_neurologist_referral_made is not None) + and (assessment.paediatric_neurologist_referral_date is not None) + and (assessment.paediatric_neurologist_input_date is not None) + ) + + # incomplete + if (not all_consultant_paediatrician_fields_complete) and ( + not all_paediatric_neurologist_fields_complete + ): + return KPI_SCORE["NOT_SCORED"] + + # score KPI + if all_consultant_paediatrician_fields_complete: + passed_metric = ( + assessment.consultant_paediatrician_input_date + - assessment.consultant_paediatrician_referral_date + ).days <= 14 + if passed_metric: + return KPI_SCORE["PASS"] + else: + return KPI_SCORE["FAIL"] + + elif all_paediatric_neurologist_fields_complete: + passed_metric = ( + assessment.paediatric_neurologist_input_date + - assessment.paediatric_neurologist_referral_date + ).days <= 14 + if passed_metric: + return KPI_SCORE["PASS"] + else: + return KPI_SCORE["FAIL"] diff --git a/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_10.py b/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_10.py new file mode 100644 index 00000000..2f2b81bf --- /dev/null +++ b/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_10.py @@ -0,0 +1,30 @@ +# python imports + +# django imports + +# E12 imports +from epilepsy12.constants import KPI_SCORE + +def score_kpi_10(registration_instance, age_at_first_paediatric_assessment) -> int: + """10. school_individual_healthcare_plan + Percentage of children and young people with epilepsy aged 5 years and above with evidence of a school individual healthcare plan by 1 year after first paediatric assessment. + Calculation Method + Numerator = Number of children and young people aged 5 years and above diagnosed with epilepsy at first year AND with evidence of EHCP + Denominator =Number of children and young people aged 5 years and above diagnosed with epilepsy at first year + """ + + management = registration_instance.management + + # ineligible + if age_at_first_paediatric_assessment < 5: + return KPI_SCORE["INELIGIBLE"] + + # unscored + if management.individualised_care_plan_includes_ehcp is None: + return KPI_SCORE["NOT_SCORED"] + + # score kpi + if management.individualised_care_plan_includes_ehcp is True: + return KPI_SCORE["PASS"] + else: + return KPI_SCORE["FAIL"] diff --git a/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_2.py b/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_2.py new file mode 100644 index 00000000..04a48d97 --- /dev/null +++ b/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_2.py @@ -0,0 +1,46 @@ +# python imports + +# django imports + +# E12 imports +from epilepsy12.constants import KPI_SCORE + + +def score_kpi_2(registration_instance) -> int: + """2. epilepsy_specialist_nurse + + % of children and young people with epilepsy, with input by epilepsy specialist nurse within the first year of care + + Calculation Method + + Numerator= Number of children and young people [diagnosed with epilepsy] AND who had [input from or referral to an Epilepsy Specialist Nurse] by first year + + Denominator = Number of children and young people [diagnosed with epilepsy] at first year + """ + + assessment = registration_instance.assessment + + # no nurse referral, fail + if assessment.epilepsy_specialist_nurse_referral_made is False: + return KPI_SCORE["FAIL"] + + # if not all filled, incomplete form + if ( + assessment.epilepsy_specialist_nurse_referral_made is None + or assessment.epilepsy_specialist_nurse_referral_date is None + or assessment.epilepsy_specialist_nurse_input_date is None + ): + return KPI_SCORE["NOT_SCORED"] + + # score check + has_seen_nurse_before_close_date = ( + assessment.epilepsy_specialist_nurse_input_date + <= registration_instance.registration_close_date + or assessment.epilepsy_specialist_nurse_referral_date + <= registration_instance.registration_close_date + ) + + if has_seen_nurse_before_close_date: + return KPI_SCORE["PASS"] + else: + return KPI_SCORE["FAIL"] diff --git a/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_3.py b/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_3.py new file mode 100644 index 00000000..9eb34dca --- /dev/null +++ b/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_3.py @@ -0,0 +1,112 @@ +# python imports + +# django imports +from django.contrib.gis.db.models import Q +from django.apps import apps + +# E12 imports +from epilepsy12.constants import KPI_SCORE + + +def score_kpi_3(registration_instance, age_at_first_paediatric_assessment) -> int: + """3. tertiary_input + % of children and young people meeting defined criteria for paediatric neurology referral, with input of tertiary care and/or CESS referral within the first year of care + + Calculation Method + + Numerator = Number of children ([less than 3 years old at first assessment] AND [diagnosed with epilepsy] OR (number of children and young people diagnosed with epilepsy who had [3 or more maintenance AEDS] at first year) OR (Number of children less than 4 years old at first assessment with epilepsy AND myoclonic seizures) OR (number of children and young people diagnosed with epilepsy who met [CESS criteria] ) AND had [evidence of referral or involvement of a paediatric neurologist] OR [evidence of referral or involvement of CESS] + + Denominator = Number of children [less than 3 years old at first assessment] AND [diagnosed with epilepsy] OR (number of children and young people diagnosed with epilepsy who had [3 or more maintenance AEDS] at first year )OR (number of children and young people diagnosed with epilepsy who met [CESS criteria] OR (Number of children less than 4 years old at first assessment with epilepsy AND [myoclonic seizures]) + """ + + assessment = registration_instance.assessment + + AntiEpilepsyMedicine = apps.get_model("epilepsy12", "AntiEpilepsyMedicine") + Episode = apps.get_model("epilepsy12", "Episode") + + # EVALUATE ELIGIBILITY CRITERIA + + # first gather relevant data + aems_count = AntiEpilepsyMedicine.objects.filter( + management=registration_instance.management, + is_rescue_medicine=False, + antiepilepsy_medicine_start_date__lt=registration_instance.registration_close_date, + ).count() + has_myoclonic_epilepsy_episode = Episode.objects.filter( + Q(multiaxial_diagnosis=registration_instance.multiaxialdiagnosis) + & Q(epilepsy_or_nonepilepsy_status="E") + & Q(epileptic_generalised_onset="MyC") + ).exists() + + # List of True/False assessing if meets any of criteria + eligibility_criteria = [ + (age_at_first_paediatric_assessment <= 3), + (age_at_first_paediatric_assessment < 4 and has_myoclonic_epilepsy_episode), + (aems_count >= 3), + ( + assessment.childrens_epilepsy_surgical_service_referral_criteria_met + ), # NOTE: CESS_referral_criteria_met is only one that could be None (rest must be True/False), however, only checking whether it is True or not True here so doesn't matter + ] + + # None of eligibility criteria are True -> set ineligible with guard clause + if not any(eligibility_criteria): + return KPI_SCORE["INELIGIBLE"] + + # Eligible for measure - EVALUATE IF AT LEAST REFERRED FROM NEUROLOGIST OR CESS. NOTE: technically the Assessment model allows a referral_date & input_date filled WITHOUT referral_made, but this is a rare edge case just for API-use. The UI does not allow you to enter either date, if referral_made is False. If the API has an endpoint for this measure, need to ensure referral_made==True, if dates are both valid. + + # first evaluate relevant fields complete + tertiary_input_complete = ( + assessment.paediatric_neurologist_referral_made is not None + ) or (assessment.childrens_epilepsy_surgical_service_referral_made is not None) + + if not tertiary_input_complete: + return KPI_SCORE["NOT_SCORED"] + + pass_criteria = [ + (assessment.paediatric_neurologist_referral_made is True), + (assessment.childrens_epilepsy_surgical_service_referral_made is True), + ] + + # if referral made to either neurologist or CESS, they pass + if any(pass_criteria): + return KPI_SCORE["PASS"] + else: + return KPI_SCORE["FAIL"] + + +def score_kpi_3b(registration_instance) -> int: + """3b. epilepsy_surgery_referral + + % of ongoing children and young people meeting defined epilepsy surgery referral criteria with evidence of epilepsy surgery referral + Calculation Method + + Numerator = Number of children and young people diagnosed with epilepsy AND met [CESS criteria] at first year AND had [evidence of referral or involvement of CESS] + + Denominator = Number of children and young people diagnosed with epilepsy AND met CESS criteria at first year + """ + + assessment = registration_instance.assessment + + # not scored + if assessment.childrens_epilepsy_surgical_service_referral_criteria_met is None: + return KPI_SCORE["NOT_SCORED"] + + # ineligible + if assessment.childrens_epilepsy_surgical_service_referral_criteria_met is False: + return KPI_SCORE["INELIGIBLE"] + + # not scored + if ( + assessment.childrens_epilepsy_surgical_service_referral_made is None + and assessment.paediatric_neurologist_referral_made is None + ): + return KPI_SCORE["NOT_SCORED"] + + # score KPI + if ( + assessment.childrens_epilepsy_surgical_service_referral_made + or assessment.paediatric_neurologist_referral_made + ): + return KPI_SCORE["PASS"] + else: + return KPI_SCORE["FAIL"] diff --git a/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_4.py b/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_4.py new file mode 100644 index 00000000..656420eb --- /dev/null +++ b/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_4.py @@ -0,0 +1,37 @@ +# python imports + +# django imports + +# E12 imports +from epilepsy12.constants import KPI_SCORE + +def score_kpi_4(registration_instance) -> int: + """4. ECG + + % of children and young people with convulsive seizures and epilepsy, with an ECG at first year + + Calculation Method + + Numerator = Number of children and young people diagnosed with epilepsy at first year AND with convulsive episodes at first year AND who have [12 lead ECG obtained] + + Denominator = Number of children and young people diagnosed with epilepsy at first year AND with convulsive episodes at first year + """ + epilepsy_context = registration_instance.epilepsycontext + investigations = registration_instance.investigations + + # ineligible + if epilepsy_context.were_any_of_the_epileptic_seizures_convulsive is False: + return KPI_SCORE["INELIGIBLE"] + + # not scored / ineligible guard clauses + if (epilepsy_context.were_any_of_the_epileptic_seizures_convulsive is None) or ( + investigations.twelve_lead_ecg_status is None + ): + return KPI_SCORE["NOT_SCORED"] + + # Convulsive seizure - score ECG status + + if investigations.twelve_lead_ecg_status is True: + return KPI_SCORE["PASS"] + else: + return KPI_SCORE["FAIL"] diff --git a/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_5.py b/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_5.py new file mode 100644 index 00000000..be8e60e4 --- /dev/null +++ b/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_5.py @@ -0,0 +1,77 @@ +# python imports + +# django imports +from django.contrib.gis.db.models import Q +from django.apps import apps + +# E12 imports +# from epilepsy12.models import Syndrome +from epilepsy12.constants import KPI_SCORE + + +def score_kpi_5(registration_instance, age_at_first_paediatric_assessment) -> int: + """5. MRI + + Calculation Method + + Numerator = Number of children and young people diagnosed with epilepsy at first year AND who are NOT JME or JAE or CAE or CECTS/Rolandic OR number of children aged under 2 years at first assessment with a diagnosis of epilepsy at first year AND who had an MRI within 6 weeks of request + + Denominator = Number of children and young people diagnosed with epilepsy at first year AND ((who are NOT JME or JAE or CAE or BECTS) OR (number of children aged under 2 years at first assessment with a diagnosis of epilepsy at first year)) + """ + multiaxial_diagnosis = registration_instance.multiaxialdiagnosis + investigations = registration_instance.investigations + + Syndrome = apps.get_model("epilepsy12", "Syndrome") + + # not scored + if (age_at_first_paediatric_assessment >= 2) and ( + multiaxial_diagnosis.syndrome_present is None + ): + return KPI_SCORE["NOT_SCORED"] + + # define eligibility criteria 1 + ineligible_syndrome_present = Syndrome.objects.filter( + Q(multiaxial_diagnosis=multiaxial_diagnosis) + & + # ELECTROCLINICAL SYNDROMES: BECTS/JME/JAE/CAE currently not included + Q( + syndrome__syndrome_name__in=[ + "Self-limited epilepsy with centrotemporal spikes", + "Juvenile myoclonic epilepsy", + "Juvenile absence epilepsy", + "Childhood absence epilepsy", + ] + ) + ).exists() + + # check eligibility criteria 1 & 2 + # 1 = none of the specified syndromes present + # 2 = age in years < 2 + if (not ineligible_syndrome_present) or (age_at_first_paediatric_assessment < 2): + # not scored + mri_dates_are_none = [ + (investigations.mri_brain_requested_date is None), + (investigations.mri_brain_reported_date is None), + ] + if any(mri_dates_are_none): + return KPI_SCORE["NOT_SCORED"] + + # eligible for this measure - score kpi + passing_criteria_met = ( + abs( + ( + investigations.mri_brain_requested_date + - investigations.mri_brain_reported_date + ).days + ) + <= 42 + ) + + if passing_criteria_met: + return KPI_SCORE["PASS"] + else: + return KPI_SCORE["FAIL"] + + # ineligible + else: + return KPI_SCORE["INELIGIBLE"] diff --git a/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_6.py b/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_6.py new file mode 100644 index 00000000..d9a72ee5 --- /dev/null +++ b/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_6.py @@ -0,0 +1,32 @@ +# python imports + +# django imports + +# E12 imports +from epilepsy12.constants import KPI_SCORE + +def score_kpi_6(registration_instance, age_at_first_paediatric_assessment) -> int: + """6. assessment_of_mental_health_issues + Calculation Method + + Numerator = Number of children and young people over 5 years diagnosed with epilepsy AND who had documented evidence of enquiry or screening for their mental health + + Denominator = = Number of children and young people over 5 years diagnosed with epilepsy + denominator""" + + multiaxial_diagnosis = registration_instance.multiaxialdiagnosis + + # ineligible + if age_at_first_paediatric_assessment < 5: + return KPI_SCORE["INELIGIBLE"] + + # not scored + if multiaxial_diagnosis.mental_health_screen is None: + return KPI_SCORE["NOT_SCORED"] + + + # score kpi + if multiaxial_diagnosis.mental_health_screen: + return KPI_SCORE["PASS"] + else: + return KPI_SCORE["FAIL"] diff --git a/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_7.py b/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_7.py new file mode 100644 index 00000000..4b5e5d71 --- /dev/null +++ b/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_7.py @@ -0,0 +1,39 @@ +# python imports + +# django imports + +# E12 imports +from epilepsy12.constants import KPI_SCORE + +def score_kpi_7(registration_instance) -> int: + """7. mental_health_support + + Percentage of children with epilepsy and a mental health problem who have evidence of mental health support + + Calculation Method + + Numerator = Number of children and young people diagnosed with epilepsy AND had a mental health issue identified AND had evidence of mental health support received + + Denominator= Number of children and young people diagnosed with epilepsy AND had a mental health issue identified + """ + + multiaxial_diagnosis = registration_instance.multiaxialdiagnosis + management = registration_instance.management + + # not scored + if multiaxial_diagnosis.mental_health_issue_identified is None: + return KPI_SCORE["NOT_SCORED"] + + # ineligible + if multiaxial_diagnosis.mental_health_issue_identified is False: + return KPI_SCORE["INELIGIBLE"] + + # not scored + if management.has_support_for_mental_health_support is None: + return KPI_SCORE["NOT_SCORED"] + + # score kpi + if management.has_support_for_mental_health_support: + return KPI_SCORE["PASS"] + else: + return KPI_SCORE["FAIL"] diff --git a/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_8.py b/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_8.py new file mode 100644 index 00000000..01e98e63 --- /dev/null +++ b/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_8.py @@ -0,0 +1,53 @@ +# python imports + +# django imports +from django.apps import apps + +# E12 imports +from epilepsy12.constants import KPI_SCORE + + +def score_kpi_8(registration_instance, age_at_first_paediatric_assessment) -> int: + AntiEpilepsyMedicine = apps.get_model("epilepsy12", "AntiEpilepsyMedicine") + MedicineEntity = apps.get_model("epilepsy12", "MedicineEntity") + + """8. Sodium Valproate + + Percentage of all females 12 years and above currently on valproate treatment with annual risk acknowledgement form completed + + Calculation Method + + Numerator = Number of females aged 12 and above diagnosed with epilepsy at first year AND on valproate AND annual risk acknowledgement forms completed AND pregnancy prevention programme in place + + Denominator = Number of females aged 12 and above diagnosed with epilepsy at first year AND on valproate + """ + # ineligible - < 12yo or male + if age_at_first_paediatric_assessment < 12 or registration_instance.case.sex != 2: + return KPI_SCORE["INELIGIBLE"] + + # not scored + if registration_instance.management.has_an_aed_been_given is None: + return KPI_SCORE["NOT_SCORED"] + + # ineligible + if not AntiEpilepsyMedicine.objects.filter( + management=registration_instance.management, + medicine_entity=MedicineEntity.objects.get(medicine_name="Sodium valproate"), + ).exists(): + return KPI_SCORE["INELIGIBLE"] + + # get valproate assigned + valproate = AntiEpilepsyMedicine.objects.filter( + management=registration_instance.management, + medicine_entity=MedicineEntity.objects.filter( + medicine_name="Sodium valproate" + ).first(), + ).first() + + if ( + valproate.is_a_pregnancy_prevention_programme_needed + and valproate.has_a_valproate_annual_risk_acknowledgement_form_been_completed + ): + return KPI_SCORE["PASS"] + else: + return KPI_SCORE["FAIL"] diff --git a/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_9.py b/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_9.py new file mode 100644 index 00000000..024eeb0c --- /dev/null +++ b/epilepsy12/common_view_functions/calculate_kpi_functions/score_kpi_9.py @@ -0,0 +1,305 @@ +# python imports + +# django imports + +# E12 imports +from epilepsy12.constants import KPI_SCORE + + +def score_kpi_9A(registration_instance) -> int: + """9A. comprehensive_care_planning_agreement + + % of children and young people with epilepsy after 12 months where there is evidence of a comprehensive care plan that is agreed between the person, their family and/or carers and primary and secondary care providers, and the care plan has been updated where necessary + + Calculation Method + + Numerator = Number of children and young people diagnosed with epilepsy at first year AND( with an individualised epilepsy document or copy clinic letter that includes care planning information )AND evidence of agreement AND care plan is up to date including elements where appropriate as below + + Denominator = Number of children and young people diagnosed with epilepsy at first year + """ + + management = registration_instance.management + + fields_not_filled = [ + (management.individualised_care_plan_in_place is None), + (management.has_individualised_care_plan_been_updated_in_the_last_year is None), + ] + + # unscored + if any(fields_not_filled): + return KPI_SCORE["NOT_SCORED"] + + # score kpi + pass_criteria = [ + (management.individualised_care_plan_in_place is True), + (management.has_individualised_care_plan_been_updated_in_the_last_year is True), + ] + + if all(pass_criteria): + return KPI_SCORE["PASS"] + else: + return KPI_SCORE["FAIL"] + + +def score_kpi_9Ai(registration_instance) -> int: + """i. patient_held_individualised_epilepsy_document + + % of children and young people with epilepsy after 12 months that had an individualised epilepsy document with individualised epilepsy document or a copy clinic letter that includes care planning information + + Calculation Method + + Numerator = Number of children and young people diagnosed with epilepsy at first year AND( with individualised epilepsy document or copy clinic letter that includes care planning information ) + + Denominator = Number of children and young people diagnosed with epilepsy at first year + """ + + # not scored + + management = registration_instance.management + + fields_not_filled = [ + (management.individualised_care_plan_in_place is None), + (management.individualised_care_plan_has_parent_carer_child_agreement is None), + (management.has_individualised_care_plan_been_updated_in_the_last_year is None), + ] + + # unscored + if any(fields_not_filled): + return KPI_SCORE["NOT_SCORED"] + + # score kpi + pass_criteria = [ + (management.individualised_care_plan_in_place), + (management.individualised_care_plan_has_parent_carer_child_agreement), + (management.has_individualised_care_plan_been_updated_in_the_last_year), + ] + + if all(pass_criteria): + return KPI_SCORE["PASS"] + else: + return KPI_SCORE["FAIL"] + + +def score_kpi_9Aii(registration_instance) -> int: + """ii patient_carer_parent_agreement_to_the_care_planning + % of children and young people with epilepsy after 12 months where there was evidence of agreement between the person, their family and/or carers as appropriate + Numerator = Number of children and young people diagnosed with epilepsy at first year AND with evidence of agreement + Denominator = Number of children and young people diagnosed with epilepsy at first year + """ + + management = registration_instance.management + + # unscored + if management.individualised_care_plan_has_parent_carer_child_agreement is None: + return KPI_SCORE["NOT_SCORED"] + + # score kpi + if management.individualised_care_plan_has_parent_carer_child_agreement is True: + return KPI_SCORE["PASS"] + else: + return KPI_SCORE["FAIL"] + + +def score_kpi_9Aiii(registration_instance) -> int: + """iii. care_planning_has_been_updated_when_necessary + Calculation Method + Numerator = Number of children and young people diagnosed with epilepsy at first year AND with care plan which is updated where necessary + Denominator = Number of children and young people diagnosed with epilepsy at first year + """ + + management = registration_instance.management + + # unscored + if management.has_individualised_care_plan_been_updated_in_the_last_year is None: + return KPI_SCORE["NOT_SCORED"] + + # score kpi + if management.has_individualised_care_plan_been_updated_in_the_last_year is True: + return KPI_SCORE["PASS"] + else: + return KPI_SCORE["FAIL"] + + +def score_kpi_9B(registration_instance) -> int: + """9B. comprehensive_care_planning_content + Percentage of children diagnosed with epilepsy with documented evidence of communication regarding core elements of care planning. + Calculation Method + Numerator = Number of children and young people diagnosed with epilepsy at first year AND evidence of written prolonged seizures plan if prescribed rescue medication AND evidence of discussion regarding water safety AND first aid AND participation and risk AND service contact details AND SUDEP + Denominator = Number of children and young people diagnosed with epilepsy at first year + """ + + management = registration_instance.management + + fields_not_filled = [ + (management.has_rescue_medication_been_prescribed is None), + (management.individualised_care_plan_parental_prolonged_seizure_care is None), + (management.individualised_care_plan_include_first_aid is None), + (management.individualised_care_plan_addresses_water_safety is None), + (management.individualised_care_plan_includes_service_contact_details is None), + ( + management.individualised_care_plan_includes_general_participation_risk + is None + ), + (management.individualised_care_plan_addresses_sudep is None), + ] + + # unscored + if any(fields_not_filled): + return KPI_SCORE["NOT_SCORED"] + + # score kpi + pass_criteria = [ + (management.has_rescue_medication_been_prescribed is True), + (management.individualised_care_plan_parental_prolonged_seizure_care is True), + (management.individualised_care_plan_include_first_aid is True), + (management.individualised_care_plan_addresses_water_safety is True), + (management.individualised_care_plan_includes_service_contact_details is True), + ( + management.individualised_care_plan_includes_general_participation_risk + is True + ), + (management.individualised_care_plan_addresses_sudep is True), + ] + + if all(pass_criteria): + return KPI_SCORE["PASS"] + else: + return KPI_SCORE["FAIL"] + + +def score_kpi_9Bi(registration_instance) -> int: + """9Bi. parental_prolonged_seizures_care_plan + Calculation Method + Numerator = Number of children and young people diagnosed with epilepsy at first year AND prescribed rescue medication AND evidence of a written prolonged seizures plan + Denominator = Number of children and young people diagnosed with epilepsy at first year AND prescribed rescue medication + """ + + management = registration_instance.management + + fields_not_filled = [ + (management.has_rescue_medication_been_prescribed is None), + (management.individualised_care_plan_parental_prolonged_seizure_care is None), + ] + + # unscored + if any(fields_not_filled): + return KPI_SCORE["NOT_SCORED"] + + # ineligible + if management.has_rescue_medication_been_prescribed is False: + return KPI_SCORE["INELIGIBLE"] + + # score kpi + if management.individualised_care_plan_parental_prolonged_seizure_care is True: + return KPI_SCORE["PASS"] + else: + return KPI_SCORE["FAIL"] + + +def score_kpi_9Bii(registration_instance) -> int: + """ii. water_safety + Calculation Method + Numerator = Number of children and young people diagnosed with epilepsy at first year AND with evidence of discussion regarding water safety + Denominator = Number of children and young people diagnosed with epilepsy at first year + """ + + management = registration_instance.management + + # unscored + if management.individualised_care_plan_addresses_water_safety is None: + return KPI_SCORE["NOT_SCORED"] + + # score kpi + if management.individualised_care_plan_addresses_water_safety is True: + return KPI_SCORE["PASS"] + else: + return KPI_SCORE["FAIL"] + + +def score_kpi_9Biii(registration_instance) -> int: + """# iii. first_aid + # Calculation Method + # Numerator = Number of children and young people diagnosed with epilepsy at first year AND with evidence of discussion regarding first aid + # Denominator = Number of children and young people diagnosed with epilepsy at first year + """ + + management = registration_instance.management + + # unscored + if management.individualised_care_plan_include_first_aid is None: + return KPI_SCORE["NOT_SCORED"] + + # score kpi + if management.individualised_care_plan_include_first_aid is True: + return KPI_SCORE["PASS"] + else: + return KPI_SCORE["FAIL"] + + +def score_kpi_9Biv(registration_instance) -> int: + """iv. general_participation_and_risk + Calculation Method + Numerator = Number of children and young people diagnosed with epilepsy at first year AND with evidence of discussion regarding general participation and risk + Denominator = Number of children and young people diagnosed with epilepsy at first year + """ + + management = registration_instance.management + + # unscored + if management.individualised_care_plan_includes_general_participation_risk is None: + return KPI_SCORE["NOT_SCORED"] + + # score kpi + if management.individualised_care_plan_includes_general_participation_risk is True: + return KPI_SCORE["PASS"] + else: + return KPI_SCORE["FAIL"] + + +def score_kpi_9Bv(registration_instance) -> int: + """v. service_contact_details + Calculation Method + Numerator = Number of children and young people diagnosed with epilepsy at first year AND with evidence of discussion of been given service contact details + Denominator = Number of children and young people diagnosed with epilepsy at first year + """ + + management = registration_instance.management + + # unscored + if management.individualised_care_plan_includes_service_contact_details is None: + return KPI_SCORE["NOT_SCORED"] + + # score kpi + if management.individualised_care_plan_includes_service_contact_details is True: + return KPI_SCORE["PASS"] + else: + return KPI_SCORE["FAIL"] + + +def score_kpi_9Bvi(registration_instance) -> int: + """vi. sudep + Calculation Method + Numerator = Number of children diagnosed with epilepsy AND had evidence of discussions regarding SUDEP AND evidence of a written prolonged seizures plan at first year + Denominator = Number of children diagnosed with epilepsy at first year""" + + management = registration_instance.management + + fields_not_filled = [ + (management.individualised_care_plan_parental_prolonged_seizure_care is None), + (management.individualised_care_plan_addresses_sudep is None), + ] + + # unscored + if any(fields_not_filled): + return KPI_SCORE["NOT_SCORED"] + + # score kpi + pass_criteria = [ + (management.individualised_care_plan_parental_prolonged_seizure_care is True), + (management.individualised_care_plan_addresses_sudep is True), + ] + + if all(pass_criteria): + return KPI_SCORE["PASS"] + else: + return KPI_SCORE["FAIL"] diff --git a/epilepsy12/common_view_functions/calculate_kpis.py b/epilepsy12/common_view_functions/calculate_kpis.py index b91474ba..2f9d82fc 100644 --- a/epilepsy12/common_view_functions/calculate_kpis.py +++ b/epilepsy12/common_view_functions/calculate_kpis.py @@ -1,11 +1,39 @@ # python imports -from dateutil.relativedelta import relativedelta -from typing import Literal + # django imports -from django.contrib.gis.db.models import Q, Sum +from django.contrib.gis.db.models import Sum +from django.apps import apps + # E12 imports -from ..models import * -from ..constants import ALL_NHS_TRUSTS +from ..general_functions import has_all_attributes +from .calculate_kpi_functions import ( + score_kpi_1, + score_kpi_2, + score_kpi_3, + score_kpi_3b, + score_kpi_4, + score_kpi_5, + score_kpi_6, + score_kpi_7, + score_kpi_8, + score_kpi_9A, + score_kpi_9Ai, + score_kpi_9Aii, + score_kpi_9Aiii, + score_kpi_9B, + score_kpi_9Bi, + score_kpi_9Bii, + score_kpi_9Biii, + score_kpi_9Biv, + score_kpi_9Bv, + score_kpi_9Bvi, + score_kpi_10, + calculate_age_at_first_paediatric_assessment_in_years, + check_is_registered, +) + +# from epilepsy12.models import KPI +from epilepsy12.constants import KPI_SCORE def calculate_kpis(registration_instance): @@ -20,526 +48,149 @@ def calculate_kpis(registration_instance): None - measure not scored yet """ + KPI = apps.get_model("epilepsy12", "KPI") + + # first set default value 'NOT_SCORED' to all KPIs + paediatrician_with_expertise_in_epilepsies = KPI_SCORE["NOT_SCORED"] + epilepsy_specialist_nurse = KPI_SCORE["NOT_SCORED"] + tertiary_input = KPI_SCORE["NOT_SCORED"] + epilepsy_surgery_referral = KPI_SCORE["NOT_SCORED"] + ecg = KPI_SCORE["NOT_SCORED"] + mri = KPI_SCORE["NOT_SCORED"] + assessment_of_mental_health_issues = KPI_SCORE["NOT_SCORED"] + mental_health_support = KPI_SCORE["NOT_SCORED"] + sodium_valproate = KPI_SCORE["NOT_SCORED"] + comprehensive_care_planning_agreement = KPI_SCORE["NOT_SCORED"] + patient_held_individualised_epilepsy_document = KPI_SCORE["NOT_SCORED"] + patient_carer_parent_agreement_to_the_care_planning = KPI_SCORE["NOT_SCORED"] + care_planning_has_been_updated_when_necessary = KPI_SCORE["NOT_SCORED"] + comprehensive_care_planning_content = KPI_SCORE["NOT_SCORED"] + parental_prolonged_seizures_care_plan = KPI_SCORE["NOT_SCORED"] + water_safety = KPI_SCORE["NOT_SCORED"] + first_aid = KPI_SCORE["NOT_SCORED"] + general_participation_and_risk = KPI_SCORE["NOT_SCORED"] + service_contact_details = KPI_SCORE["NOT_SCORED"] + sudep = KPI_SCORE["NOT_SCORED"] + school_individual_healthcare_plan = KPI_SCORE["NOT_SCORED"] + # important metric for calculations that follow - age_at_first_paediatric_assessment = relativedelta( - registration_instance.registration_date, registration_instance.case.date_of_birth).years + age_at_first_paediatric_assessment = ( + calculate_age_at_first_paediatric_assessment_in_years(registration_instance) + ) # child must be registered in the audit for the KPI to be counted - is_registered = ( - registration_instance.registration_date is not None and registration_instance.eligibility_criteria_met) == True - - if not is_registered: + if not check_is_registered(registration_instance): # cannot proceed any further if registration incomplete. # In theory it should not be possible to get this far. - return - - # 1. paediatrician_with_expertise_in_epilepsies - # % of children and young people with epilepsy, with input by a ‘consultant paediatrician with expertise in epilepsies’ within 2 weeks of initial referral - # Calculation Method - # Numerator = Number of children and young people [diagnosed with epilepsy] at first year AND (who had [input from a paediatrician with expertise in epilepsy] OR a [input from a paediatric neurologist] within 2 weeks of initial referral. (initial referral to mean first paediatric assessment) - # Denominator = Number of and young people [diagnosed with epilepsy] at first year - - paediatrician_with_expertise_in_epilepsies = 0 - if hasattr(registration_instance, 'assessment'): - if registration_instance.assessment.consultant_paediatrician_referral_made and registration_instance.assessment.consultant_paediatrician_input_date is not None and registration_instance.assessment.consultant_paediatrician_referral_date is not None: - if ( - registration_instance.assessment.consultant_paediatrician_input_date <= ( - registration_instance.assessment.consultant_paediatrician_referral_date + relativedelta(days=+14)) - ): - paediatrician_with_expertise_in_epilepsies = 1 - elif registration_instance.assessment.paediatric_neurologist_referral_made and registration_instance.assessment.paediatric_neurologist_input_date is not None: - if ( - registration_instance.assessment.paediatric_neurologist_input_date <= ( - registration_instance.assessment.paediatric_neurologist_referral_date + relativedelta(days=+14)) - ): - paediatrician_with_expertise_in_epilepsies = 1 - - # 2. epilepsy_specialist_nurse - # % of children and young people with epilepsy, with input by epilepsy specialist nurse within the first year of care - # Calculation Method - # Numerator= Number of children and young people [diagnosed with epilepsy] AND who had [input from or referral to an Epilepsy Specialist Nurse] by first year - # Denominator = Number of children and young people [diagnosed with epilepsy] at first year - - epilepsy_specialist_nurse = 0 - if hasattr(registration_instance, 'assessment'): - if registration_instance.assessment.epilepsy_specialist_nurse_referral_made and registration_instance.assessment.epilepsy_specialist_nurse_input_date is not None: - if ( - registration_instance.assessment.epilepsy_specialist_nurse_input_date <= registration_instance.registration_close_date - ) or ( - registration_instance.assessment.epilepsy_specialist_nurse_referral_date <= registration_instance.registration_close_date - ): - epilepsy_specialist_nurse = 1 - - # 3. tertiary_input - # % of children and young people meeting defined criteria for paediatric neurology referral, with input of tertiary care and/or CESS referral within the first year of care - # Calculation Method - # Numerator = Number of children ([less than 3 years old at first assessment] AND [diagnosed with epilepsy] OR (number of children and young people diagnosed with epilepsy who had [3 or more maintenance AEDS] at first year) OR (Number of children less than 4 years old at first assessment with epilepsy AND myoclonic seizures) OR (number of children and young people diagnosed with epilepsy who met [CESS criteria] ) AND had [evidence of referral or involvement of a paediatric neurologist] OR [evidence of referral or involvement of CESS] - # Denominator = Number of children [less than 3 years old at first assessment] AND [diagnosed with epilepsy] OR (number of children and young people diagnosed with epilepsy who had [3 or more maintenance AEDS] at first year )OR (number of children and young people diagnosed with epilepsy who met [CESS criteria] OR (Number of children less than 4 years old at first assessment with epilepsy AND [myoclonic seizures]) - - tertiary_input = None - if hasattr(registration_instance, 'management') and hasattr(registration_instance, 'assessment') and hasattr(registration_instance, 'multiaxialdiagnosis'): - - # denominator - if ( - age_at_first_paediatric_assessment <= 3 - ) or ( - # (number of children and young people diagnosed with epilepsy who had [3 or more maintenance AEDS] at first year) - AntiEpilepsyMedicine.objects.filter( - management=registration_instance.management, - is_rescue_medicine=False, - antiepilepsy_medicine_start_date__lt=registration_instance.registration_close_date - ).count() >= 3 - ) or ( - # number of children and young people diagnosed with epilepsy who met [CESS criteria] - registration_instance.assessment.childrens_epilepsy_surgical_service_referral_criteria_met - ) or ( - # Number of children less than 4 years old at first assessment with epilepsy AND [myoclonic seizures] - age_at_first_paediatric_assessment <= 4 and - Episode.objects.filter( - Q(multiaxial_diagnosis=registration_instance.multiaxialdiagnosis) & - Q(epilepsy_or_nonepilepsy_status='E') & - Q(epileptic_generalised_onset='MyC') - ).exists() - ): - # eligible for this measure - tertiary_input = 0 - if ( - # Number of children ([less than 3 years old at first assessment] AND [diagnosed with epilepsy] - age_at_first_paediatric_assessment <= 3 - ) or ( - # (number of children and young people diagnosed with epilepsy who had [3 or more maintenance AEDS] at first year) - AntiEpilepsyMedicine.objects.filter( - management=registration_instance.management, - is_rescue_medicine=False, - antiepilepsy_medicine_start_date__lt=registration_instance.registration_close_date - ).count() >= 3 - ) or ( - # (Number of children less than 4 years old at first assessment with epilepsy AND myoclonic seizures) - age_at_first_paediatric_assessment <= 4 and - Episode.objects.filter( - Q(multiaxial_diagnosis=registration_instance.multiaxialdiagnosis) & - Q(epilepsy_or_nonepilepsy_status='E') & - Q(epileptic_generalised_onset='MyC') - ).exists() - ) or ( - # (number of children and young people diagnosed with epilepsy who met [CESS criteria] ) AND had [evidence of referral or involvement of a paediatric neurologist] - (registration_instance.assessment.childrens_epilepsy_surgical_service_referral_criteria_met == registration_instance.assessment.paediatric_neurologist_referral_made) or - (registration_instance.assessment.paediatric_neurologist_input_date is not None and registration_instance.assessment.childrens_epilepsy_surgical_service_referral_criteria_met) - ) or ( - # [evidence of referral or involvement of CESS] - registration_instance.assessment.childrens_epilepsy_surgical_service_referral_made is not None or - registration_instance.assessment.childrens_epilepsy_surgical_service_referral_date is not None or - registration_instance.assessment.childrens_epilepsy_surgical_service_input_date is not None - ): - # measure has been met - tertiary_input = 1 - - else: - # not eligible for this measure - tertiary_input = 2 - - # 3b. epilepsy_surgery_referral - - # % of ongoing children and young people meeting defined epilepsy surgery referral criteria with evidence of epilepsy surgery referral - # Calculation Method - # Numerator = Number of children and young people diagnosed with epilepsy AND met [CESS criteria] at first year AND had [evidence of referral or involvement of CESS] - # Denominator =Number of children and young people diagnosed with epilepsy AND met CESS criteria at first year - epilepsy_surgery_referral = None - if hasattr(registration_instance, 'assessment'): - - # denominator - if (registration_instance.assessment.childrens_epilepsy_surgical_service_referral_criteria_met): - # eligible for this measure - epilepsy_surgery_referral = 0 - if ( - registration_instance.assessment.childrens_epilepsy_surgical_service_referral_criteria_met and ( - registration_instance.assessment.childrens_epilepsy_surgical_service_referral_made is not None or - registration_instance.assessment.childrens_epilepsy_surgical_service_referral_date is not None or - registration_instance.assessment.childrens_epilepsy_surgical_service_input_date is not None - ) - ): - # criteria met - epilepsy_surgery_referral = 1 - else: - # not eligible for this measure - epilepsy_surgery_referral = 2 - - # 4. ECG - # % of children and young people with convulsive seizures and epilepsy, with an ECG at first year - # Calculation Method - # Numerator = Number of children and young people diagnosed with epilepsy at first year AND with convulsive episodes at first year AND who have [12 lead ECG obtained] - # Denominator = Number of children and young people diagnosed with epilepsy at first year AND with convulsive episodes at first year - ecg = None - if hasattr(registration_instance, 'epilepsycontext') and hasattr(registration_instance, 'investigations'): - - # denominator - if (registration_instance.epilepsycontext.were_any_of_the_epileptic_seizures_convulsive): - # eligible for this measure - ecg = 0 - if ( - registration_instance.epilepsycontext.were_any_of_the_epileptic_seizures_convulsive and - registration_instance.investigations.twelve_lead_ecg_status - ): - # criteria met - ecg = 1 - else: - # not eligible for this measure - ecg = 2 - - # 5. MRI - # Calculation Method - # Numerator = Number of children and young people diagnosed with epilepsy at first year AND who are NOT JME or JAE or CAE or CECTS/Rolandic OR number of children aged under 2 years at first assessment with a diagnosis of epilepsy at first year AND who had an MRI within 6 weeks of request - # Denominator = Number of children and young people diagnosed with epilepsy at first year AND ((who are NOT JME or JAE or CAE or BECTS) OR (number of children aged under 2 years at first assessment with a diagnosis of epilepsy at first year)) - mri = None - if hasattr(registration_instance, 'multiaxialdiagnosis') and hasattr(registration_instance, 'investigations'): - # denominator - if ( - ( - registration_instance.multiaxialdiagnosis.syndrome_present and - Syndrome.objects.filter( - Q(multiaxial_diagnosis=registration_instance.multiaxialdiagnosis) & - # ELECTROCLINICAL SYNDROMES: BECTS/JME/JAE/CAE currently not included - ~Q(syndrome__syndrome_name__in=['Self-limited epilepsy with centrotemporal spikes', - 'Juvenile myoclonic epilepsy', 'Juvenile absence epilepsy', 'Childhood absence epilepsy']) - ).exists() or - - age_at_first_paediatric_assessment <= 2 - ) and ( - registration_instance.investigations.mri_brain_requested_date is not None and - registration_instance.investigations.mri_brain_reported_date is not None - ) - ): - # eligible for this measure - mri = 0 - if ( - registration_instance.multiaxialdiagnosis.syndrome_present and - Syndrome.objects.filter( - Q(multiaxial_diagnosis=registration_instance.multiaxialdiagnosis) & - # ELECTROCLINICAL SYNDROMES: BECTS/JME/JAE/CAE currently not included - ~Q(syndrome__syndrome_name__in=['Self-limited epilepsy with centrotemporal spikes', - 'Juvenile myoclonic epilepsy', 'Juvenile absence epilepsy', 'Childhood absence epilepsy']) - ).exists() or - - age_at_first_paediatric_assessment <= 2 - ) and ( - registration_instance.investigations.mri_brain_reported_date <= ( - registration_instance.investigations.mri_brain_requested_date + relativedelta(days=42)) - ): - # criteria met - mri = 1 - - else: - # not eligible for this measure - mri = 2 - - # 6. assessment_of_mental_health_issues - # Calculation Method - # Numerator = Number of children and young people over 5 years diagnosed with epilepsy AND who had documented evidence of enquiry or screening for their mental health - # Denominator = = Number of children and young people over 5 years diagnosed with epilepsy - assessment_of_mental_health_issues = None - if hasattr(registration_instance, 'multiaxialdiagnosis'): - # denominator - if (age_at_first_paediatric_assessment >= 5): - # eligible for this measure - assessment_of_mental_health_issues = 0 - if ( - age_at_first_paediatric_assessment >= 5 - ) and ( - registration_instance.multiaxialdiagnosis.mental_health_screen - ): - # criteria met - assessment_of_mental_health_issues = 1 - else: - # not eligible for this measure - assessment_of_mental_health_issues = 2 - - # 7. mental_health_support - # Percentage of children with epilepsy and a mental health problem who have evidence of mental health support - # Calculation Method - # Numerator = Number of children and young people diagnosed with epilepsy AND had a mental health issue identified AND had evidence of mental health support received - # Denominator= Number of children and young people diagnosed with epilepsy AND had a mental health issue identified - - mental_health_support = None - if hasattr(registration_instance, 'multiaxialdiagnosis') and hasattr(registration_instance, 'management'): - # denominator - if ( - registration_instance.multiaxialdiagnosis.mental_health_issue_identified - ): - # eligible for this measure - mental_health_support = 0 - if ( - registration_instance.multiaxialdiagnosis.mental_health_issue_identified and - registration_instance.management.has_support_for_mental_health_support - ): - # criteria met - mental_health_support = 1 - else: - # not eligible for this measure - mental_health_support = 2 - - # 8. Sodium Valproate - # Percentage of all females 12 years and above currently on valproate treatment with annual risk acknowledgement form completed - # Calculation Method - # Numerator = Number of females aged 12 and above diagnosed with epilepsy at first year AND on valproate AND annual risk acknowledgement forms completed AND pregnancy prevention programme in place - # Denominator = Number of females aged 12 and above diagnosed with epilepsy at first year AND on valproate - sodium_valproate = None - - if hasattr(registration_instance, 'management'): - - # denominator - if ( - age_at_first_paediatric_assessment >= 12 and - registration_instance.case.sex == 2 - ) and ( - registration_instance.management.has_an_aed_been_given and - AntiEpilepsyMedicine.objects.filter( - management=registration_instance.management, - medicine_entity=MedicineEntity.objects.filter( - medicine_name__icontains='valproate').first() - ).exists() - ): - - # eligible for this measure - sodium_valproate = 0 - if( - age_at_first_paediatric_assessment >= 12 and - registration_instance.case.sex == 2 - ) and ( - registration_instance.management.has_an_aed_been_given and - AntiEpilepsyMedicine.objects.filter( - management=registration_instance.management, - medicine_entity=MedicineEntity.objects.filter( - medicine_name__icontains='valproate').first(), - is_a_pregnancy_prevention_programme_needed=True, - has_a_valproate_annual_risk_acknowledgement_form_been_completed=True - ).exists() - ): - # criteria met - sodium_valproate = 1 - else: - # not eligible for this measure - sodium_valproate = 2 - - # 9. comprehensive_care_planning_agreement - # % of children and young people with epilepsy after 12 months where there is evidence of a comprehensive care plan that is agreed between the person, their family and/or carers and primary and secondary care providers, and the care plan has been updated where necessary - # Calculation Method - # Numerator = Number of children and young people diagnosed with epilepsy at first year AND( with an individualised epilepsy document or copy clinic letter that includes care planning information )AND evidence of agreement AND care plan is up to date including elements where appropriate as below - # Denominator = Number of children and young people diagnosed with epilepsy at first year - - comprehensive_care_planning_agreement = None - if hasattr(registration_instance, 'management'): - # denominator is all children with epilepsy - no denominator - - # eligible for this measure - comprehensive_care_planning_agreement = 0 - if ( - registration_instance.management.individualised_care_plan_in_place - ): - # criteria met - comprehensive_care_planning_agreement = 1 - - # a. patient_held_individualised_epilepsy_document - # % of children and young people with epilepsy after 12 months that had an individualised epilepsy document with individualised epilepsy document or a copy clinic letter that includes care planning information - # Calculation Method - # Numerator = Number of children and young people diagnosed with epilepsy at first year AND( with individualised epilepsy document or copy clinic letter that includes care planning information ) - # Denominator = Number of children and young people diagnosed with epilepsy at first year - - patient_held_individualised_epilepsy_document = 0 - if hasattr(registration_instance, 'management'): - # denominator is all children with epilepsy - no denominator - if( - registration_instance.management.individualised_care_plan_in_place and - registration_instance.management.individualised_care_plan_has_parent_carer_child_agreement and - registration_instance.management.has_individualised_care_plan_been_updated_in_the_last_year - ): - # criteria met - patient_held_individualised_epilepsy_document = 1 - - # b patient_carer_parent_agreement_to_the_care_planning - # % of children and young people with epilepsy after 12 months where there was evidence of agreement between the person, their family and/or carers as appropriate - # Numerator = Number of children and young people diagnosed with epilepsy at first year AND with evidence of agreement - # Denominator = Number of children and young people diagnosed with epilepsy at first year - patient_carer_parent_agreement_to_the_care_planning = 0 - if hasattr(registration_instance, 'management'): - if ( - registration_instance.management.individualised_care_plan_has_parent_carer_child_agreement - ): - patient_carer_parent_agreement_to_the_care_planning = 1 - - # c. care_planning_has_been_updated_when_necessary - # Calculation Method - # Numerator = Number of children and young people diagnosed with epilepsy at first year AND with care plan which is updated where necessary - # Denominator = Number of children and young people diagnosed with epilepsy at first year - - care_planning_has_been_updated_when_necessary = 0 - if hasattr(registration_instance, 'management'): - # denominator is all children with epilepsy - no denominator - if ( - registration_instance.management.has_individualised_care_plan_been_updated_in_the_last_year - ): - # criteria met - care_planning_has_been_updated_when_necessary = 1 - - # 9b. comprehensive_care_planning_content - # Percentage of children diagnosed with epilepsy with documented evidence of communication regarding core elements of care planning. - # Calculation Method - # Numerator = Number of children and young people diagnosed with epilepsy at first year AND evidence of written prolonged seizures plan if prescribed rescue medication AND evidence of discussion regarding water safety AND first aid AND participation and risk AND service contact details AND SUDEP - # Denominator = Number of children and young people diagnosed with epilepsy at first year - - comprehensive_care_planning_content = 0 - if hasattr(registration_instance, 'management'): - # denominator is all children with epilepsy - no denominator - if( - registration_instance.management.has_rescue_medication_been_prescribed and - registration_instance.management.individualised_care_plan_parental_prolonged_seizure_care and - registration_instance.management.individualised_care_plan_include_first_aid and - registration_instance.management.individualised_care_plan_addresses_water_safety and - registration_instance.management.individualised_care_plan_includes_service_contact_details and - registration_instance.management.individualised_care_plan_includes_general_participation_risk and - registration_instance.management.individualised_care_plan_addresses_sudep - ): - # criteria met - comprehensive_care_planning_content = 1 - - # a. parental_prolonged_seizures_care_plan - # Calculation Method - # Numerator = Number of children and young people diagnosed with epilepsy at first year AND prescribed rescue medication AND evidence of a written prolonged seizures plan - # Denominator = Number of children and young people diagnosed with epilepsy at first year AND prescribed rescue medication - - parental_prolonged_seizures_care_plan = None - if hasattr(registration_instance, 'management'): - # denominator - if (registration_instance.management.has_rescue_medication_been_prescribed): - # eligible for this measure - parental_prolonged_seizures_care_plan = 0 - if( - registration_instance.management.has_rescue_medication_been_prescribed and - registration_instance.management.individualised_care_plan_parental_prolonged_seizure_care - ): - # criteria met - parental_prolonged_seizures_care_plan = 1 - else: - # not eligible for this measure - parental_prolonged_seizures_care_plan = 2 - - # b. water_safety - # Calculation Method - # Numerator = Number of children and young people diagnosed with epilepsy at first year AND with evidence of discussion regarding water safety - # Denominator = Number of children and young people diagnosed with epilepsy at first year - water_safety = 0 - if hasattr(registration_instance, 'management'): - # denominator is all children with epilepsy - no denominator - if( - registration_instance.management.individualised_care_plan_addresses_water_safety - ): - water_safety = 1 - - # c. first_aid - # Calculation Method - # Numerator = Number of children and young people diagnosed with epilepsy at first year AND with evidence of discussion regarding first aid - # Denominator = Number of children and young people diagnosed with epilepsy at first year - - first_aid = 0 - if hasattr(registration_instance, 'management'): - # denominator is all children with epilepsy - no denominator - if( - registration_instance.management.individualised_care_plan_include_first_aid - ): - first_aid = 1 - - # d. general_participation_and_risk - # Calculation Method - # Numerator = Number of children and young people diagnosed with epilepsy at first year AND with evidence of discussion regarding general participation and risk - # Denominator = Number of children and young people diagnosed with epilepsy at first year - - general_participation_and_risk = 0 - if hasattr(registration_instance, 'management'): - # denominator is all children with epilepsy - no denominator - if ( - registration_instance.management.individualised_care_plan_includes_general_participation_risk - ): - general_participation_and_risk = 1 - - # e. service_contact_details - # Calculation Method - # Numerator = Number of children and young people diagnosed with epilepsy at first year AND with evidence of discussion of been given service contact details - # Denominator = Number of children and young people diagnosed with epilepsy at first year - - service_contact_details = 0 - if hasattr(registration_instance, 'management'): - # denominator is all children with epilepsy - no denominator - if( - registration_instance.management.individualised_care_plan_includes_service_contact_details - ): - service_contact_details = 1 - - # f. sudep - # Calculation Method - # Numerator = Number of children diagnosed with epilepsy AND had evidence of discussions regarding SUDEP AND evidence of a written prolonged seizures plan at first year - # Denominator = Number of children diagnosed with epilepsy at first year - - sudep = 0 - if hasattr(registration_instance, 'management'): - # denominator is all children with epilepsy - no denominator - if ( - registration_instance.management.individualised_care_plan_parental_prolonged_seizure_care and - registration_instance.management.individualised_care_plan_addresses_sudep - ): - sudep = 1 - - # 10. school_individual_healthcare_plan - # Percentage of children and young people with epilepsy aged 5 years and above with evidence of a school individual healthcare plan by 1 year after first paediatric assessment. - # Calculation Method - # Numerator = Number of children and young people aged 5 years and above diagnosed with epilepsy at first year AND with evidence of EHCP - # Denominator =Number of children and young people aged 5 years and above diagnosed with epilepsy at first year - - school_individual_healthcare_plan = None - if hasattr(registration_instance, 'management'): - - # denominator - if (age_at_first_paediatric_assessment >= 5): - # eligible for this measure - school_individual_healthcare_plan = 0 - if ( - age_at_first_paediatric_assessment >= 5 - ) and ( - registration_instance.management.individualised_care_plan_includes_ehcp - ): - school_individual_healthcare_plan = 1 - else: - # not eligible for this measure - school_individual_healthcare_plan = 2 + return None - """ - Store the KPIs in AuditProgress - """ + if hasattr(registration_instance, "assessment"): + paediatrician_with_expertise_in_epilepsies = score_kpi_1(registration_instance) + + if hasattr(registration_instance, "assessment"): + epilepsy_specialist_nurse = score_kpi_2(registration_instance) + + if has_all_attributes( + registration_instance, ["management", "assessment", "multiaxialdiagnosis"] + ): + tertiary_input = score_kpi_3( + registration_instance, age_at_first_paediatric_assessment + ) + + if hasattr(registration_instance, "assessment"): + epilepsy_surgery_referral = score_kpi_3b(registration_instance) + + if has_all_attributes(registration_instance, ["epilepsycontext", "investigations"]): + ecg = score_kpi_4(registration_instance) + + if has_all_attributes( + registration_instance, ["multiaxialdiagnosis", "investigations"] + ): + mri = score_kpi_5(registration_instance, age_at_first_paediatric_assessment) + + if hasattr(registration_instance, "multiaxialdiagnosis"): + assessment_of_mental_health_issues = score_kpi_6( + registration_instance, age_at_first_paediatric_assessment + ) + + if has_all_attributes(registration_instance, ["multiaxialdiagnosis", "management"]): + mental_health_support = score_kpi_7(registration_instance) + + if hasattr(registration_instance, "management"): + sodium_valproate = score_kpi_8( + registration_instance, age_at_first_paediatric_assessment + ) + + if hasattr(registration_instance, "management"): + comprehensive_care_planning_agreement = score_kpi_9A(registration_instance) + + if hasattr(registration_instance, "management"): + patient_held_individualised_epilepsy_document = score_kpi_9Ai( + registration_instance + ) + + if hasattr(registration_instance, "management"): + patient_carer_parent_agreement_to_the_care_planning = score_kpi_9Aii( + registration_instance + ) + + if hasattr(registration_instance, "management"): + care_planning_has_been_updated_when_necessary = score_kpi_9Aiii( + registration_instance + ) + + if hasattr(registration_instance, "management"): + comprehensive_care_planning_content = score_kpi_9B(registration_instance) + + if hasattr(registration_instance, "management"): + parental_prolonged_seizures_care_plan = score_kpi_9Bi(registration_instance) + + if hasattr(registration_instance, "management"): + water_safety = score_kpi_9Bii(registration_instance) + + if hasattr(registration_instance, "management"): + first_aid = score_kpi_9Biii(registration_instance) + if hasattr(registration_instance, "management"): + general_participation_and_risk = score_kpi_9Biv(registration_instance) + + if hasattr(registration_instance, "management"): + service_contact_details = score_kpi_9Bv(registration_instance) + + if hasattr(registration_instance, "management"): + sudep = score_kpi_9Bvi(registration_instance) + + if hasattr(registration_instance, "management"): + school_individual_healthcare_plan = score_kpi_10( + registration_instance, age_at_first_paediatric_assessment + ) + + # Store the KPIs in AuditProgress kpis = { - 'paediatrician_with_expertise_in_epilepsies': paediatrician_with_expertise_in_epilepsies, - 'epilepsy_specialist_nurse': epilepsy_specialist_nurse, - 'tertiary_input': tertiary_input, - 'epilepsy_surgery_referral': epilepsy_surgery_referral, - 'ecg': ecg, - 'mri': mri, - 'assessment_of_mental_health_issues': assessment_of_mental_health_issues, - 'mental_health_support': mental_health_support, - 'sodium_valproate': sodium_valproate, - 'comprehensive_care_planning_agreement': comprehensive_care_planning_agreement, - 'patient_held_individualised_epilepsy_document': patient_held_individualised_epilepsy_document, - 'patient_carer_parent_agreement_to_the_care_planning': patient_carer_parent_agreement_to_the_care_planning, - 'care_planning_has_been_updated_when_necessary': care_planning_has_been_updated_when_necessary, - 'comprehensive_care_planning_content': comprehensive_care_planning_content, - 'parental_prolonged_seizures_care_plan': parental_prolonged_seizures_care_plan, - 'water_safety': water_safety, - 'first_aid': first_aid, - 'general_participation_and_risk': general_participation_and_risk, - 'service_contact_details': service_contact_details, - 'sudep': sudep, - 'school_individual_healthcare_plan': school_individual_healthcare_plan, + "paediatrician_with_expertise_in_epilepsies": paediatrician_with_expertise_in_epilepsies, + "epilepsy_specialist_nurse": epilepsy_specialist_nurse, + "tertiary_input": tertiary_input, + "epilepsy_surgery_referral": epilepsy_surgery_referral, + "ecg": ecg, + "mri": mri, + "assessment_of_mental_health_issues": assessment_of_mental_health_issues, + "mental_health_support": mental_health_support, + "sodium_valproate": sodium_valproate, + "comprehensive_care_planning_agreement": comprehensive_care_planning_agreement, + "patient_held_individualised_epilepsy_document": patient_held_individualised_epilepsy_document, + "patient_carer_parent_agreement_to_the_care_planning": patient_carer_parent_agreement_to_the_care_planning, + "care_planning_has_been_updated_when_necessary": care_planning_has_been_updated_when_necessary, + "comprehensive_care_planning_content": comprehensive_care_planning_content, + "parental_prolonged_seizures_care_plan": parental_prolonged_seizures_care_plan, + "water_safety": water_safety, + "first_aid": first_aid, + "general_participation_and_risk": general_participation_and_risk, + "service_contact_details": service_contact_details, + "sudep": sudep, + "school_individual_healthcare_plan": school_individual_healthcare_plan, } - KPI.objects.filter( - pk=registration_instance.kpi.pk).update(**kpis) + KPI.objects.filter(pk=registration_instance.kpi.pk).update(**kpis) def annotate_kpis(filtered_organisations, kpi_name="all"): @@ -549,8 +200,67 @@ def annotate_kpis(filtered_organisations, kpi_name="all"): Accepts flag for kpi_name if only individual kpi_name requested """ if kpi_name == "all": - return filtered_organisations.annotate(paediatrician_with_expertise_in_epilepsies_sum=Sum('kpi__paediatrician_with_expertise_in_epilepsies')).annotate(epilepsy_specialist_nurse_sum=Sum('kpi__epilepsy_specialist_nurse')).annotate(tertiary_input_sum=Sum('kpi__tertiary_input')).annotate(epilepsy_surgery_referral_sum=Sum('kpi__epilepsy_surgery_referral')).annotate(ecg_sum=Sum('kpi__ecg')).annotate(mri_sum=Sum('kpi__mri')).annotate(assessment_of_mental_health_issues_sum=Sum('kpi__assessment_of_mental_health_issues')).annotate(mental_health_support_sum=Sum('kpi__mental_health_support')).annotate(comprehensive_care_planning_agreement_sum=Sum('kpi__comprehensive_care_planning_agreement')).annotate(patient_held_individualised_epilepsy_document_sum=Sum('kpi__patient_held_individualised_epilepsy_document')).annotate( - care_planning_has_been_updated_when_necessary_sum=Sum('kpi__care_planning_has_been_updated_when_necessary')).annotate(comprehensive_care_planning_content_sum=Sum('kpi__comprehensive_care_planning_content')).annotate(parental_prolonged_seizures_care_plan_sum=Sum('kpi__parental_prolonged_seizures_care_plan')).annotate(water_safety_sum=Sum('kpi__water_safety')).annotate(first_aid_sum=Sum('kpi__first_aid')).annotate(general_participation_and_risk_sum=Sum('kpi__general_participation_and_risk')).annotate(service_contact_details_sum=Sum('kpi__service_contact_details')).annotate(sudep_sum=Sum('kpi__sudep')).annotate(school_individual_healthcare_plan_sum=Sum('kpi__school_individual_healthcare_plan')) + return ( + filtered_organisations.annotate( + paediatrician_with_expertise_in_epilepsies_sum=Sum( + "kpi__paediatrician_with_expertise_in_epilepsies" + ) + ) + .annotate( + epilepsy_specialist_nurse_sum=Sum("kpi__epilepsy_specialist_nurse") + ) + .annotate(tertiary_input_sum=Sum("kpi__tertiary_input")) + .annotate( + epilepsy_surgery_referral_sum=Sum("kpi__epilepsy_surgery_referral") + ) + .annotate(ecg_sum=Sum("kpi__ecg")) + .annotate(mri_sum=Sum("kpi__mri")) + .annotate( + assessment_of_mental_health_issues_sum=Sum( + "kpi__assessment_of_mental_health_issues" + ) + ) + .annotate(mental_health_support_sum=Sum("kpi__mental_health_support")) + .annotate( + comprehensive_care_planning_agreement_sum=Sum( + "kpi__comprehensive_care_planning_agreement" + ) + ) + .annotate( + patient_held_individualised_epilepsy_document_sum=Sum( + "kpi__patient_held_individualised_epilepsy_document" + ) + ) + .annotate( + care_planning_has_been_updated_when_necessary_sum=Sum( + "kpi__care_planning_has_been_updated_when_necessary" + ) + ) + .annotate( + comprehensive_care_planning_content_sum=Sum( + "kpi__comprehensive_care_planning_content" + ) + ) + .annotate( + parental_prolonged_seizures_care_plan_sum=Sum( + "kpi__parental_prolonged_seizures_care_plan" + ) + ) + .annotate(water_safety_sum=Sum("kpi__water_safety")) + .annotate(first_aid_sum=Sum("kpi__first_aid")) + .annotate( + general_participation_and_risk_sum=Sum( + "kpi__general_participation_and_risk" + ) + ) + .annotate(service_contact_details_sum=Sum("kpi__service_contact_details")) + .annotate(sudep_sum=Sum("kpi__sudep")) + .annotate( + school_individual_healthcare_plan_sum=Sum( + "kpi__school_individual_healthcare_plan" + ) + ) + ) else: ans = filtered_organisations.annotate(kpi_sum=Sum(f"kpi__{kpi_name}")) return ans diff --git a/epilepsy12/common_view_functions/group_for_group.py b/epilepsy12/common_view_functions/group_for_group.py index 56802dce..977bf184 100644 --- a/epilepsy12/common_view_functions/group_for_group.py +++ b/epilepsy12/common_view_functions/group_for_group.py @@ -4,24 +4,20 @@ # rcpch from epilepsy12.constants.user_types import ( - ROLES, - TITLES, - AUDIT_CENTRE_LEAD_CLINICIAN, - TRUST_AUDIT_TEAM_FULL_ACCESS, - AUDIT_CENTRE_CLINICIAN, - TRUST_AUDIT_TEAM_EDIT_ACCESS, + # groups AUDIT_CENTRE_ADMINISTRATOR, + AUDIT_CENTRE_CLINICIAN, + AUDIT_CENTRE_LEAD_CLINICIAN, + RCPCH_AUDIT_TEAM, + RCPCH_AUDIT_PATIENT_FAMILY, + # permissions + TRUST_AUDIT_TEAM_VIEW_ONLY, TRUST_AUDIT_TEAM_EDIT_ACCESS, - RCPCH_AUDIT_LEAD, + TRUST_AUDIT_TEAM_FULL_ACCESS, EPILEPSY12_AUDIT_TEAM_FULL_ACCESS, - RCPCH_AUDIT_ANALYST, - EPILEPSY12_AUDIT_TEAM_EDIT_ACCESS, - RCPCH_AUDIT_ADMINISTRATOR, - EPILEPSY12_AUDIT_TEAM_VIEW_ONLY, - RCPCH_AUDIT_PATIENT_FAMILY, PATIENT_ACCESS, - TRUST_AUDIT_TEAM_VIEW_ONLY, - AUDIT_CENTRE_MANAGER, + # preferences in the view + VIEW_PREFERENCES, ) @@ -38,14 +34,10 @@ def group_for_role(role_key): group = Group.objects.get(name=TRUST_AUDIT_TEAM_EDIT_ACCESS) elif role_key == AUDIT_CENTRE_ADMINISTRATOR: group = Group.objects.get(name=TRUST_AUDIT_TEAM_VIEW_ONLY) - elif role_key == AUDIT_CENTRE_MANAGER: - group = Group.objects.get(name=TRUST_AUDIT_TEAM_VIEW_ONLY) - elif role_key == RCPCH_AUDIT_LEAD: + + elif role_key == RCPCH_AUDIT_TEAM: group = Group.objects.get(name=EPILEPSY12_AUDIT_TEAM_FULL_ACCESS) - elif role_key == RCPCH_AUDIT_ANALYST: - group = Group.objects.get(name=EPILEPSY12_AUDIT_TEAM_EDIT_ACCESS) - elif role_key == RCPCH_AUDIT_ADMINISTRATOR: - group = Group.objects.get(name=EPILEPSY12_AUDIT_TEAM_VIEW_ONLY) + elif role_key == RCPCH_AUDIT_PATIENT_FAMILY: group = Group.objects.get(name=PATIENT_ACCESS) else: diff --git a/epilepsy12/common_view_functions/recalculate_form_generate_response.py b/epilepsy12/common_view_functions/recalculate_form_generate_response.py index 0309aa57..c55e3c68 100644 --- a/epilepsy12/common_view_functions/recalculate_form_generate_response.py +++ b/epilepsy12/common_view_functions/recalculate_form_generate_response.py @@ -1,5 +1,7 @@ from dateutil import relativedelta from datetime import date + +# 3rd Party Imports from django_htmx.http import trigger_client_event from django.shortcuts import render from psycopg2 import DatabaseError @@ -10,7 +12,21 @@ from epilepsy12.models_folder.antiepilepsy_medicine import AntiEpilepsyMedicine from epilepsy12.models_folder.epilepsy12_site import Site +# E12 imports from .calculate_kpis import calculate_kpis +from epilepsy12.constants import ( + Registration_minimum_scorable_fields, + EpilepsyContext_minimum_scorable_fields, + FirstPaediatricAssessment_minimum_scorable_fields, + MultiaxialDiagnosis_minimum_scorable_fields, + Episode_minimum_scorable_fields, + Syndrome_minimum_scorable_fields, + Comorbidity_minimum_scorable_fields, + Assessment_minimum_scorable_fields, + Investigations_minimum_scorable_fields, + Management_minimum_scorable_fields, + AntiEpilepsyMedicine_minimum_scorable_fields, +) def recalculate_form_generate_response( @@ -30,7 +46,7 @@ def recalculate_form_generate_response( context.update({"error_message": error_message}) # calculate totals on form - test_fields_update_audit_progress(model_instance) + update_audit_progress(model_instance) response = render(request=request, template_name=template, context=context) @@ -41,10 +57,7 @@ def recalculate_form_generate_response( return response -# test all fields - - -def test_fields_update_audit_progress(model_instance): +def update_audit_progress(model_instance): """ Calculates all completed fields and compares expected fields Stores these values in AuditProgress @@ -57,10 +70,10 @@ def test_fields_update_audit_progress(model_instance): ) all_completed_fields = completed_fields(model_instance) - all_expected_fields = total_fields_expected(model_instance) - all_completed_fields += number_of_completed_fields_in_related_models(model_instance) + all_expected_fields = total_fields_expected(model_instance) + update_fields = { f"{verbose_name_underscored}_total_expected_fields": all_expected_fields, f"{verbose_name_underscored}_total_completed_fields": all_completed_fields, @@ -83,6 +96,7 @@ def test_fields_update_audit_progress(model_instance): raise Exception(error) +# --8<-- [start:completed_fields] def completed_fields(model_instance): """ Test for all completed fields @@ -135,9 +149,12 @@ def completed_fields(model_instance): return counter +# --8<-- [end:completed_fields] + + def total_fields_expected(model_instance): """ - returns as expected fields for a given model instance, based on user selections + Returns as expected fields for a given model instance, based on user selections. """ model_class_name = model_instance.__class__.__name__ @@ -234,6 +251,7 @@ def total_fields_expected(model_instance): medicines = AntiEpilepsyMedicine.objects.filter( management=model_instance, is_rescue_medicine=False ).all() + if medicines.count() > 0: for medicine in medicines: # essential fields are: @@ -303,6 +321,7 @@ def total_fields_expected(model_instance): return cumulative_score +# TODO: should replace with dataclass constant def avoid_fields(model_instance): """ When looping through fields and counting them as complete/incomplete, these fields depending on the model @@ -311,79 +330,55 @@ def avoid_fields(model_instance): # verbose_name_underscored = model_instance._meta.verbose_name.lower().replace(' ', '_') model_class_name = model_instance.__class__.__name__ + META_VARIABLES = [ + "id", + "updated_at", + "updated_by", + "created_at", + "created_by", + ] + if model_class_name in [ "FirstPaediatricAssessment", "EpilepsyContext", "Assessment", "Investigations", ]: - return [ - "id", - "registration", - "updated_at", - "updated_by", - "created_at", - "created_by", - ] + return META_VARIABLES + ["registration"] + elif model_class_name == "MultiaxialDiagnosis": - return [ - "id", + return META_VARIABLES + [ "registration", "multiaxial_diagnosis", "episode", "syndrome", "comorbidity", - "created_by", - "created_at", - "updated_by", - "updated_at", ] + elif model_class_name == "Management": - return [ - "id", - "registration", - "antiepilepsymedicine", - "created_by", - "created_at", - "updated_by", - "updated_at", - ] + return META_VARIABLES + ["registration", "antiepilepsymedicine"] + elif model_class_name in ["Syndrome", "Comorbidity", "ComorbidityEntity"]: - return [ - "id", - "multiaxial_diagnosis", - "created_by", - "created_at", - "updated_by", - "updated_at", - ] + return META_VARIABLES + ["multiaxial_diagnosis"] + elif model_class_name == "Episode": - return [ - "id", + return META_VARIABLES + [ "multiaxial_diagnosis", "description_keywords", - "created_by", - "created_at", - "updated_by", - "updated_at", "expected_score", "calculated_score", ] + elif model_class_name == "AntiEpilepsyMedicine": - return [ - "id", + return META_VARIABLES + [ "management", "is_rescue_medicine", - "created_by", - "created_at", - "updated_by", - "updated_at", "antiepilepsy_medicine_stop_date", "is_a_pregnancy_prevention_programme_needed", ] + elif model_class_name == "Registration": - return [ - "id", + return META_VARIABLES + [ "management", "assessment", "investigations", @@ -397,12 +392,9 @@ def avoid_fields(model_instance): "cohort", "case", "audit_progress", - "created_by", - "created_at", - "updated_by", - "updated_at", "kpi", ] + elif model_class_name == "MedicineEntity": return [ "id", @@ -419,6 +411,7 @@ def avoid_fields(model_instance): "is_rescue", "history", ] + else: raise ValueError( f"Form scoring error: {model_class_name} not found to return fields to avoid in form calculation." @@ -430,107 +423,51 @@ def scoreable_fields_for_model_class_name(model_class_name): Returns the minimum number of scoreable fields based on the model instance at the time """ - if model_class_name == "EpilepsyContext": - # Essential fields - return len( - [ - "previous_febrile_seizure", - "previous_acute_symptomatic_seizure", - "is_there_a_family_history_of_epilepsy", - "previous_neonatal_seizures", - "diagnosis_of_epilepsy_withdrawn", - "were_any_of_the_epileptic_seizures_convulsive", - "experienced_prolonged_generalized_convulsive_seizures", - "experienced_prolonged_focal_seizures", - ] - ) - elif model_class_name == "FirstPaediatricAssessment": - return len( - [ - "first_paediatric_assessment_in_acute_or_nonacute_setting", - "has_number_of_episodes_since_the_first_been_documented", - "general_examination_performed", - "neurological_examination_performed", - "developmental_learning_or_schooling_problems", - "behavioural_or_emotional_problems", - ] - ) - elif model_class_name == "MultiaxialDiagnosis": + if model_class_name == EpilepsyContext_minimum_scorable_fields.model_name: + return len(EpilepsyContext_minimum_scorable_fields.all_fields) + + elif ( + model_class_name == FirstPaediatricAssessment_minimum_scorable_fields.model_name + ): + return len(FirstPaediatricAssessment_minimum_scorable_fields.all_fields) + + elif model_class_name == MultiaxialDiagnosis_minimum_scorable_fields.model_name: # minimum fields in multiaxial_diagnosis include: # at least one episode that is epileptic fully completed - return len( - [ - "syndrome_present", - "epilepsy_cause_known", - "relevant_impairments_behavioural_educational", - "autistic_spectrum_disorder", - "global_developmental_delay_or_learning_difficulties", - "mental_health_screen", - "mental_health_issue_identified", - ] - ) - elif model_class_name == "Episode": + return len(MultiaxialDiagnosis_minimum_scorable_fields.all_fields) + + elif model_class_name == Episode_minimum_scorable_fields.model_name: # returns minimum number of fields that could be scored for an epileptic episode - return len( - [ - "seizure_onset_date", - "seizure_onset_date_confidence", - "episode_definition", - "has_description_of_the_episode_or_episodes_been_gathered", - "epilepsy_or_nonepilepsy_status", - ] - ) - elif model_class_name == "Syndrome": - return len(["syndrome_diagnosis_date", "syndrome__syndrome_name"]) - elif model_class_name == "Comorbidity": - return len( - ["comorbidity_diagnosis_date", "comorbidity__comorbidityentity__conceptId"] - ) - elif model_class_name == "Assessment": - return len( - [ - "childrens_epilepsy_surgical_service_referral_criteria_met", - "consultant_paediatrician_referral_made", - "paediatric_neurologist_referral_made", - "childrens_epilepsy_surgical_service_referral_made", - "epilepsy_specialist_nurse_referral_made", - ] - ) - elif model_class_name == "Investigations": - return len( - [ - "eeg_indicated", - "twelve_lead_ecg_status", - "ct_head_scan_status", - "mri_indicated", - ] - ) - elif model_class_name == "Management": - return len( - [ - "has_an_aed_been_given", - "has_rescue_medication_been_prescribed", - "individualised_care_plan_in_place", - "has_been_referred_for_mental_health_support", - "has_support_for_mental_health_support", - ] - ) - elif model_class_name == "AntiEpilepsyMedicine": - return len( - [ - "medicine_name", - "antiepilepsy_medicine_start_date", - "antiepilepsy_medicine_risk_discussed", - ] - ) - elif model_class_name == "Registration": - return len(["registration_date", "eligibility_criteria_met"]) + return len(Episode_minimum_scorable_fields.all_fields) + + elif model_class_name == Syndrome_minimum_scorable_fields.model_name: + return len(Syndrome_minimum_scorable_fields.all_fields) + + elif model_class_name == Comorbidity_minimum_scorable_fields.model_name: + return len(Comorbidity_minimum_scorable_fields.all_fields) + + elif model_class_name == Assessment_minimum_scorable_fields.model_name: + return len(Assessment_minimum_scorable_fields.all_fields) + + elif model_class_name == Investigations_minimum_scorable_fields.model_name: + return len(Investigations_minimum_scorable_fields.all_fields) + + elif model_class_name == Management_minimum_scorable_fields.model_name: + return len(Management_minimum_scorable_fields.all_fields) + + elif model_class_name == AntiEpilepsyMedicine_minimum_scorable_fields.model_name: + return len(AntiEpilepsyMedicine_minimum_scorable_fields.all_fields) + + elif model_class_name == Registration_minimum_scorable_fields.model_name: + return len(Registration_minimum_scorable_fields.all_fields) + else: raise ValueError( f"Form scoring error: {model_class_name} does not exist to calculate minimum number of scoreable fields." ) +# TODO: need to come back and write more tests with multiple cases for this as fn does multiple things. So far, have 1 test case: a fully completed Focal Onset seizure `test_count_episode_fields` def count_episode_fields(all_episodes): """ loops through each episode associated with a multiaxial diagnosis and add up expected number of fields @@ -651,6 +588,7 @@ def number_of_completed_fields_in_related_models(model_instance): comorbidities = Comorbidity.objects.filter( multiaxial_diagnosis=model_instance ).all() + if episodes.count() > 0: for episode in episodes: calculated_score = completed_fields(episode) @@ -666,10 +604,12 @@ def number_of_completed_fields_in_related_models(model_instance): cumulative_score += completed_fields(comorbidity) elif model_instance.__class__.__name__ == "Assessment": # also need to count associated records in Site + sites = Site.objects.filter( case=model_instance.registration.case, site_is_actively_involved_in_epilepsy_care=True, ).all() + if sites: for site in sites: if site.site_is_childrens_epilepsy_surgery_centre: diff --git a/epilepsy12/common_view_functions/report_queries.py b/epilepsy12/common_view_functions/report_queries.py index c5ca980d..2a6f9b81 100644 --- a/epilepsy12/common_view_functions/report_queries.py +++ b/epilepsy12/common_view_functions/report_queries.py @@ -3,16 +3,17 @@ # django imports from django.contrib.gis.db.models import Q +from django.apps import apps # E12 imports -from ..models import ( - Case, - Organisation, - NHSRegionEntity, - OPENUKNetworkEntity, - IntegratedCareBoardEntity, - ONSCountryEntity, -) +# from ..models import ( +# Case, +# Organisation, +# NHSRegionEntity, +# OPENUKNetworkEntity, +# IntegratedCareBoardEntity, +# ONSCountryEntity, +# ) def all_registered_cases_for_cohort_and_abstraction_level( @@ -38,6 +39,7 @@ def all_registered_cases_for_cohort_and_abstraction_level( 2. the first paediatric assessment falls within the the dates for that cohort 3. a lead E12 site has been allocated """ + Case = apps.get_model("epilepsy12", "Case") if case_complete: all_cases_for_cohort = Case.objects.filter( @@ -134,9 +136,7 @@ def get_all_countries(): Returns a list of all Countries """ - # return Organisation.objects.order_by('CountryONSCode', - # 'Country').values_list('CountryONSCode', - # 'Country').distinct() + ONSCountryEntity = apps.get_model("epilepsy12", "ONSCountryEntity") return ONSCountryEntity.objects.order_by("Country_ONS_Name") @@ -145,9 +145,7 @@ def get_all_nhs_regions(): Returns a list of all NHS Regions [('Y56', 'London'), ('Y58', 'South West'), ('Y59', 'South East'), ('Y60', 'Midlands (Y60)'), ('Y61', 'East of England'), ('Y62', 'North West'), ('Y63', 'North East and Yorkshire'), (None, None)] """ - # return Organisation.objects.order_by('NHSEnglandRegionCode', - # 'NHSEnglandRegion').values_list('NHSEnglandRegionCode', - # 'NHSEnglandRegion').distinct() + NHSRegionEntity = apps.get_model("epilepsy12", "NHSRegionEntity") return NHSRegionEntity.objects.filter(year=2019).order_by( "NHS_Region", "NHS_Region_Code" ) @@ -158,9 +156,7 @@ def get_all_open_uk_regions(): Returns a list of all OPEN UK Networks [('BRPNF', 'Birmingham Regional Paediatric Neurology Forum'), ('CEWT', "Children's Epilepsy Workstream in Trent"), ('EPEN', 'Eastern Paediatric Epilepsy Network'), ('EPIC', "Mersey and North Wales network 'Epilepsy In Childhood' interest group"), ('NTPEN', 'North Thames Paediatric Epilepsy Network'), ('NWEIG', "North West Children and Young People's Epilepsy Interest Group"), ('ORENG', 'Oxford region epilepsy interest group'), ('PENNEC', 'Paediatric Epilepsy Network for the North East and Cumbria'), ('SETPEG', 'South East Thames Paediatric Epilepsy Group'), ('SETPEG', 'SSouth East Thames Paediatric Epilepsy Group'), ('SWEP', 'South Wales Epilepsy Forum'), ('SWIPE', 'South West Interest Group Paediatric Epilepsy'), ('SWTPEG', 'South West Thames Paediatric Epilepsy Group'), ('TEN', 'Trent Epilepsy Network'), ('WPNN', 'Wessex Paediatric Neurosciences Network'), ('YPEN', 'Yorkshire Paediatric Neurology Network'), (None, None)] """ - # return Organisation.objects.order_by('OPENUKNetworkCode', - # 'OPENUKNetworkName').values_list('OPENUKNetworkCode', - # 'OPENUKNetworkName').distinct() + OPENUKNetworkEntity = apps.get_model("epilepsy12", "OPENUKNetworkEntity") return OPENUKNetworkEntity.objects.order_by("OPEN_UK_Network_Name", "country") @@ -168,9 +164,9 @@ def get_all_icbs(): """ Returns a list of all Integrated Care Boards """ - # return Organisation.objects.order_by('ICBODSCode', - # 'ICBName').values_list('ICBODSCode', - # 'ICBName').distinct() + IntegratedCareBoardEntity = apps.get_model( + "epilepsy12", "IntegratedCareBoardEntity" + ) return IntegratedCareBoardEntity.objects.order_by("ICB_Name") @@ -178,6 +174,7 @@ def get_all_trusts(): """ Returns a list of all Trusts """ + Organisation = apps.get_model("epilepsy12", "Organisation") return ( Organisation.objects.order_by( "ParentOrganisation_OrganisationName", "ParentOrganisation_ODSCode" @@ -193,6 +190,7 @@ def get_all_organisations(): """ Returns a list of all Organisations """ + Organisation = apps.get_model("epilepsy12", "Organisation") return ( Organisation.objects.order_by("OrganisationName", "ODSCode") .values_list("OrganisationName", "ODSCode") diff --git a/epilepsy12/common_view_functions/sanction_user_access.py b/epilepsy12/common_view_functions/sanction_user_access.py index b77b64fb..b4e732d2 100644 --- a/epilepsy12/common_view_functions/sanction_user_access.py +++ b/epilepsy12/common_view_functions/sanction_user_access.py @@ -1,5 +1,5 @@ from django.core.exceptions import PermissionDenied -from ..models import Organisation +from django.apps import apps def return_selected_organisation(user): @@ -9,12 +9,15 @@ def return_selected_organisation(user): the first in the list is returned. Otherwise, an error is raised Accepts a user object. """ + + Organisation = apps.get_model("epilepsy12", "Organisation") + if user.organisation_employer is not None: # current user is affiliated with an existing organisation - set viewable trust to this return Organisation.objects.get(OrganisationName=user.organisation_employer) else: # current user is NOT affiliated with an existing organisation - if user.is_staff or user.is_superuser: + if user.is_rcpch_staff or user.is_superuser or user.is_superuser: # current user is a member of the RCPCH audit team and also not affiliated with a organisation # therefore set selected organisation to first of organisation on the list return Organisation.objects.order_by("OrganisationName").first() @@ -29,7 +32,7 @@ def sanction_user(user): return True else: # current user is NOT affiliated with an existing organisation - if user.is_staff or user.is_superuser: + if user.is_rcpch_staff or user.is_superuser or user.is_superuser: # current user is a member of the RCPCH audit team and also not affiliated with a organisation # therefore set selected organisation to first of organisation on the list return True diff --git a/epilepsy12/common_view_functions/tiles_for_region.py b/epilepsy12/common_view_functions/tiles_for_region.py new file mode 100644 index 00000000..f9639a27 --- /dev/null +++ b/epilepsy12/common_view_functions/tiles_for_region.py @@ -0,0 +1,43 @@ +from typing import Literal +import json + +# django +from django.core.serializers import serialize +from django.apps import apps + +# third party +# + + +def return_tile_for_region( + abstraction_level: Literal["icb", "nhs_region", "lhb", "country"] +): + """ + Returns geojson data for a given region. + """ + IntegratedCareBoardBoundaries = apps.get_model( + "epilepsy12", "IntegratedCareBoardBoundaries" + ) + NHSEnglandRegionBoundaries = apps.get_model( + "epilepsy12", "NHSEnglandRegionBoundaries" + ) + CountryBoundaries = apps.get_model("epilepsy12", "CountryBoundaries") + LocalHealthBoardBoundaries = apps.get_model( + "epilepsy12", "LocalHealthBoardBoundaries" + ) + + model = IntegratedCareBoardBoundaries + + if abstraction_level == "nhs_region": + model = NHSEnglandRegionBoundaries + elif abstraction_level == "country": + model = CountryBoundaries + elif abstraction_level == "lhb": + model = LocalHealthBoardBoundaries + + unedited_tile = serialize("geojson", model.objects.all()) + edited_tile = json.loads(unedited_tile) + edited_tile.pop("crs", None) + tile = json.dumps(edited_tile) + + return tile diff --git a/epilepsy12/common_view_functions/validate_form_update_model.py b/epilepsy12/common_view_functions/validate_form_update_model.py index 858c0686..2551e07a 100644 --- a/epilepsy12/common_view_functions/validate_form_update_model.py +++ b/epilepsy12/common_view_functions/validate_form_update_model.py @@ -1,10 +1,18 @@ -from datetime import datetime, date +# python imports +from datetime import datetime from dateutil.relativedelta import relativedelta + +# django imports from django.utils import timezone -from ..models import * -from ..general_functions import current_cohort_start_date, first_tuesday_in_january +from django.apps import apps + +# third party imports from psycopg2 import DatabaseError +# RCPCH imports +from ..general_functions import current_cohort_start_date, first_tuesday_in_january +from ..validators import epilepsy12_date_validator + def validate_and_update_model( request, @@ -26,7 +34,7 @@ def validate_and_update_model( page_element: string one of 'date_field', 'toggle_button', 'multiple_choice_single_toggle_button', 'multiple_choic_multiple_toggle_button', 'select', 'snomed_select', 'organisation_select' comparison_date_field_name: string corresponding to field name for date in model is_earliest_date: boolean - first_paediatric_assessment_date: date - used to validate + earliest_allowable_date: date - used to validate It replaces the decorator @update_model as decorators can only redirect the request, they cannot pass parameters to the function they wrap. This means that errors raised in updating the model @@ -34,6 +42,13 @@ def validate_and_update_model( It is important that this function is called early on in the view function and that an updated instance of the model AFTER UPDATE is put in the context that is passed back to the template. """ + + # initialize models + SyndromeEntity = apps.get_model("epilepsy12", "SyndromeEntity") + EpilepsyCauseEntity = apps.get_model("epilepsy12", "EpilepsyCauseEntity") + MedicineEntity = apps.get_model("epilepsy12", "MedicineEntity") + Registration = apps.get_model("epilepsy12", "Registration") + if page_element == "toggle_button": # toggle button # the trigger_name of the element here corresponds to whether true or false has been selected @@ -53,11 +68,6 @@ def validate_and_update_model( # multiple_choice_multiple_toggle_button field_value = request.htmx.trigger_name - elif page_element == "date_field": - field_value = datetime.strptime( - request.POST.get(request.htmx.trigger_name), "%Y-%m-%d" - ).date() - elif page_element == "select" or page_element == "snomed_select": if request.htmx.trigger_name == "syndrome_name": syndrome_entity = SyndromeEntity.objects.get( @@ -89,57 +99,28 @@ def validate_and_update_model( # The earlier of the two dates cannot be in the future and cannot be later than the second if supplied. # The later of the two dates CAN be in the future but cannot be earlier than the first if supplied. # If there is no comparison date (eg registration_date) the only stipulation is that it not be in the future. - date_valid = None - date_error = "" - - if earliest_allowable_date: - if field_value < earliest_allowable_date: - # dates cannot be before the earliest allowable date (usually the first paediatric assessment or the cohort start date) - date_error = f"The date you chose ({field_value}) cannot not be before {earliest_allowable_date}." - date_valid = False - - if comparison_date_field_name and date_valid is None: - instance = model.objects.get(pk=model_id) - comparison_date = getattr(instance, comparison_date_field_name) - if is_earliest_date: - if comparison_date: - date_valid = ( - field_value <= comparison_date and field_value <= date.today() - ) - if field_value > comparison_date: - date_error = f"The date you chose ({field_value}) cannot be after {comparison_date}." - if field_value > date.today(): - date_error = f"You cannot choose a date in the future." - else: - date_valid = field_value <= date.today() - if not date_valid: - date_error = f"The date you chose ({field_value}) cannot be in the future." - - else: - if comparison_date: - date_valid = ( - field_value >= comparison_date and field_value <= date.today() - ) - if field_value < comparison_date: - date_error = f"The date you chose ({field_value}) cannot be before {comparison_date}." - if field_value > date.today(): - date_error = ( - f"The date you chose ({field_value}) is in the future." - ) - print(f"{field_value} and {comparison_date}") - else: - # no other date supplied yet - date_valid = True - - elif field_value > date.today(): - # dates cannot be in the future unless they are the second of 2 dates - date_error = f"The date you chose ({field_value}) cannot be in the future." - date_valid = False - - if date_valid: - pass + # This validation step happens in validators.py + field_value = datetime.strptime( + request.POST.get(request.htmx.trigger_name), "%Y-%m-%d" + ).date() + + if is_earliest_date: + first_date = field_value + second_date = None + if comparison_date_field_name: + instance = model.objects.get(pk=model_id) + second_date = getattr(instance, comparison_date_field_name) else: - raise ValueError(date_error) + second_date = field_value + first_date = None + if comparison_date_field_name: + instance = model.objects.get(pk=model_id) + first_date = getattr(instance, comparison_date_field_name) + epilepsy12_date_validator( + first_date=first_date, + second_date=second_date, + earliest_allowable_date=earliest_allowable_date, + ) # update the model @@ -149,8 +130,10 @@ def validate_and_update_model( if field_name == "registration_date": # registration_date cannot be before date of birth registration = Registration.objects.get(pk=model_id) + if relativedelta(field_value, registration.case.date_of_birth).years >= 24: + errors = f"To be included in Epilepsy12, {registration.case} cannot be over 24y at first paediatric assessment." + raise ValueError(errors) if field_value < registration.case.date_of_birth: - date_valid = False errors = f"The date you chose ({field_value.strftime('%d %B %Y')}) cannot not be before {registration.case}'s date of birth." raise ValueError(errors) @@ -159,11 +142,9 @@ def validate_and_update_model( current_cohort_start_date().year + 2 ) + relativedelta(days=7) if field_value < current_cohort_start_date(): - date_valid = False errors = f'The date you entered cannot be before the current cohort start date ({current_cohort_start_date().strftime("%d %B %Y")})' raise ValueError(errors) elif field_value > current_cohort_end_date: - date_valid = False errors = f'The date you entered cannot be after the current cohort end date ({current_cohort_end_date.strftime("%d %B %Y")})' raise ValueError(errors) diff --git a/epilepsy12/constants/__init__.py b/epilepsy12/constants/__init__.py index 9725448c..cc39a641 100644 --- a/epilepsy12/constants/__init__.py +++ b/epilepsy12/constants/__init__.py @@ -19,7 +19,7 @@ from .welsh_organisations import * from .irish_organisations import * from .scottish_organisations import * -from .postcodes import UNKNOWN_POSTCODES, UNKNOWN_POSTCODES_NO_SPACES +from .postcodes import UNKNOWN_POSTCODES_NO_SPACES from .rcpch_organisations import * from .severity import * from .welsh_regions import * @@ -27,3 +27,7 @@ from .country_codes import * from .nhs_england_regions import * from .uk_ons_region_codes import * +from .colors import * +from .abstraction_levels import * +from .valid_nhs_nums import * +from .form_calculation_constants import * diff --git a/epilepsy12/constants/abstraction_levels.py b/epilepsy12/constants/abstraction_levels.py new file mode 100644 index 00000000..cd464cc4 --- /dev/null +++ b/epilepsy12/constants/abstraction_levels.py @@ -0,0 +1,9 @@ +ABSTRACTION_LEVELS = ( + ("organisation", "Organisation"), + ("trust", "Trust/Local Health Board"), + ("icb", "Integrated Care Board"), + ("open_uk", "OPEN UK region"), + ("nhs_region", "NHS England Region"), + ("country", "Country"), + ("national", "National"), +) diff --git a/epilepsy12/constants/colors.py b/epilepsy12/constants/colors.py new file mode 100644 index 00000000..4c3e7169 --- /dev/null +++ b/epilepsy12/constants/colors.py @@ -0,0 +1,54 @@ +RCPCH_LIGHTEST_GREY = '#F3F3F3' +RCPCH_DARK_BLUE = '#0D0D58' +RCPCH_STRONG_BLUE = '#3366CC' +RCPCH_STRONG_BLUE_LIGHT_TINT1 = '#668CD9' +RCPCH_STRONG_BLUE_LIGHT_TINT2 = '#99B3E6' +RCPCH_STRONG_BLUE_LIGHT_TINT3 = '#CCD9F2' +RCPCH_STRONG_BLUE_DARK_TINT = '#405A97' +RCPCH_LIGHT_BLUE = '#11A7F2' +RCPCH_LIGHT_BLUE_TINT1 = '#4DBDF5' +RCPCH_LIGHT_BLUE_TINT2 = '#88D3F9' +RCPCH_LIGHT_BLUE_TINT3 = '#CFE9FC' +RCPCH_LIGHT_BLUE_DARK_TINT = '#0082BC' +RCPCH_PINK = '#E00087' +RCPCH_PINK_LIGHT_TINT1 = '#E840A5' +RCPCH_PINK_LIGHT_TINT2 = '#EF80C3' +RCPCH_PINK_LIGHT_TINT3 = '#F7BFE1' +RCPCH_PINK_DARK_TINT = '#AB1368' +RCPCH_WHITE = '#FFFFFF' +RCPCH_LIGHT_GREY = '#D9D9D9' +RCPCH_MID_GREY = '#B3B3B3' +RCPCH_DARK_GREY = '#808080' +RCPCH_CHARCOAL = '#4D4D4D' +RCPCH_CHARCOAL_DARK = '#191919' +RCPCH_BLACK = '#000000' +RCPCH_RED = '#E60700' +RCPCH_RED_LIGHT_TINT1 = '#EC4540' +RCPCH_RED_LIGHT_TINT2 = '#F38380' +RCPCH_RED_LIGHT_TINT3 = '#F9C1BF' +RCPCH_RED_DARK_TINT = '#B11D23' +RCPCH_ORANGE = '#FF8000' +RCPCH_ORANGE_LIGHT_TINT1 = '#FFA040' +RCPCH_ORANGE_LIGHT_TINT2 = '#FFC080' +RCPCH_ORANGE_LIGHT_TINT3 = '#FFDFBF' +RCPCH_ORANGE_DARK_TINT = '#BF6914' +RCPCH_YELLOW = '#FFD200' +RCPCH_YELLOW_LIGHT_TINT1 = '#FFDD40' +RCPCH_YELLOW_LIGHT_TINT2 = '#FFE980' +RCPCH_YELLOW_LIGHT_TINT3 = '#FFF4BF' +RCPCH_YELLOW_DARK_TINT = '#C5A000' +RCPCH_STRONG_GREEN = '#66CC33' +RCPCH_STRONG_GREEN_LIGHT_TINT1 = '#8CD966' +RCPCH_STRONG_GREEN_LIGHT_TINT2 = '#B3E699' +RCPCH_STRONG_GREEN_LIGHT_TINT3 = '#D9F2CC' +RCPCH_STRONG_GREEN_DARK_TINT = '#53861B' +RCPCH_AQUA_GREEN = '#00BDAA' +RCPCH_AQUA_GREEN_LIGHT_TINT1 = '#40ECBF' +RCPCH_AQUA_GREEN_LIGHT_TINT2 = '#80DED4' +RCPCH_AQUA_GREEN_LIGHT_TINT3 = '#BFEEEA' +RCPCH_AQUA_GREEN_DARK_TINT = '#2E888D' +RCPCH_PURPLE = '#7159AA' +RCPCH_PURPLE_LIGHT_TINT1 = '#AE4CBF' +RCPCH_PURPLE_LIGHT_TINT2 = '#C987D4' +RCPCH_PURPLE_LIGHT_TINT3 = '#E4C3EA' +RCPCH_PURPLE_DARK_TINT = '#66296C' \ No newline at end of file diff --git a/epilepsy12/constants/common.py b/epilepsy12/constants/common.py index de178471..ae44b129 100644 --- a/epilepsy12/constants/common.py +++ b/epilepsy12/constants/common.py @@ -1,14 +1,17 @@ +from dataclasses import dataclass, fields +from typing import Optional + # Common selectors SECTION_STATUS_CHOICES = ( - (-1, "Not Set"), - (0, "Not saved"), - (5, "Disabled"), - (10, "Complete"), - (20, "Incomplete"), - (30, "Errors"), - (60, "TransferredIn"), - (70, "TransferredOut"), + (-1, "Not Set"), + (0, "Not saved"), + (5, "Disabled"), + (10, "Complete"), + (20, "Incomplete"), + (30, "Errors"), + (60, "TransferredIn"), + (70, "TransferredOut"), ) # ASSESSMENT=( @@ -25,46 +28,60 @@ # (10, "10th year of care") # ) -OPT_OUT = ( - ("Y","Yes"), - ("N", "No") -) +OPT_OUT = (("Y", "Yes"), ("N", "No")) -OPT_OUT_UNCERTAIN = ( - ("Y","Yes"), - ("N", "No"), - ("U","Uncertain") -) +OPT_OUT_UNCERTAIN = (("Y", "Yes"), ("N", "No"), ("U", "Uncertain")) -CHECKED_STATUS = ( - (1, "Checked"), - (2,"Unchecked") -) +CHECKED_STATUS = ((1, "Checked"), (2, "Unchecked")) -SEX_TYPE=( +SEX_TYPE = ( # This definition has been updated based on NHS standards # (cf https://www.datadictionary.nhs.uk/attributes/person_gender_code.html) (0, "Not Known"), (1, "Male"), (2, "Female"), - (9, "Not Specified") + (9, "Not Specified"), ) -CHRONICITY=( - (1, "Acute"), - (2, "Non-acute"), - (3, "Don't know") +CHRONICITY = ((1, "Acute"), (2, "Non-acute"), (3, "Don't know")) + +DISORDER_SEVERITY = ( + ("Mil", "Mild"), + ("Mod", "Moderate"), + ("Sev", "Severe"), + ("Pro", "Profound"), ) -DISORDER_SEVERITY=( - ("Mil", "Mild"), - ("Mod", "Moderate"), - ("Sev", "Severe"), - ("Pro", "Profound") +DATE_ACCURACY = ( + ("Apx", "Approximate date"), + ("Exc", "Exact date"), + ("NK", "Not known"), ) -DATE_ACCURACY=( - ("Apx", "Approximate date"), - ("Exc", "Exact date"), - ("NK", "Not known") -) \ No newline at end of file +@dataclass +class DEPRIVATION_QUINTILES_DATACLASS: + first: int= 1 + second: int=2 + third: int=3 + fourth: int=4 + fifth: int=5 + not_known: Optional[int] = None + + @property + def deprivation_quintile_names(self): + + return [field.name for field in fields(self)] + + @property + def deprivation_quintiles(self): + + return [getattr(self, field.name) for field in fields(self)] + + def deprivation_quintiles_int_display_map(self, deprivation_quintile): + + if deprivation_quintile is None: + return 6 + + return deprivation_quintile + +DEPRIVATION_QUINTILES = DEPRIVATION_QUINTILES_DATACLASS() \ No newline at end of file diff --git a/epilepsy12/constants/form_calculation_constants.py b/epilepsy12/constants/form_calculation_constants.py new file mode 100644 index 00000000..c1d575eb --- /dev/null +++ b/epilepsy12/constants/form_calculation_constants.py @@ -0,0 +1,132 @@ +""" +Constants file to be referenced from the `recalculate_form_generate_response` function and its helper functions. +""" +from dataclasses import dataclass + + +@dataclass +class MinimumScorableFieldsForModel: + """ + Used for `scoreable_fields_for_model_class_name` + """ + model_name: str + all_fields: list + + +Registration_minimum_scorable_fields = MinimumScorableFieldsForModel( + 'Registration', + [ + "registration_date", + "eligibility_criteria_met", + ] +) + +EpilepsyContext_minimum_scorable_fields = MinimumScorableFieldsForModel( + 'EpilepsyContext', + [ + "previous_febrile_seizure", + "previous_acute_symptomatic_seizure", + "is_there_a_family_history_of_epilepsy", + "previous_neonatal_seizures", + "diagnosis_of_epilepsy_withdrawn", + "were_any_of_the_epileptic_seizures_convulsive", + "experienced_prolonged_generalized_convulsive_seizures", + "experienced_prolonged_focal_seizures", + ] +) + +FirstPaediatricAssessment_minimum_scorable_fields = MinimumScorableFieldsForModel( + 'FirstPaediatricAssessment', + [ + "first_paediatric_assessment_in_acute_or_nonacute_setting", + "has_number_of_episodes_since_the_first_been_documented", + "general_examination_performed", + "neurological_examination_performed", + "developmental_learning_or_schooling_problems", + "behavioural_or_emotional_problems", + ] +) + +# minimum fields in multiaxial_diagnosis include: +# at least one episode that is epileptic fully completed +MultiaxialDiagnosis_minimum_scorable_fields = MinimumScorableFieldsForModel( + 'MultiaxialDiagnosis', + [ + "syndrome_present", + "epilepsy_cause_known", + "relevant_impairments_behavioural_educational", + "autistic_spectrum_disorder", + "global_developmental_delay_or_learning_difficulties", + "mental_health_screen", + "mental_health_issue_identified", + ] +) + +# returns minimum number of fields that could be scored for an epileptic episode +Episode_minimum_scorable_fields = MinimumScorableFieldsForModel( + 'Episode', + [ + "seizure_onset_date", + "seizure_onset_date_confidence", + "episode_definition", + "has_description_of_the_episode_or_episodes_been_gathered", + "epilepsy_or_nonepilepsy_status", + ] +) + +Syndrome_minimum_scorable_fields = MinimumScorableFieldsForModel( + 'Syndrome', + [ + "syndrome_diagnosis_date", + "syndrome__syndrome_name", + ] +) + +Comorbidity_minimum_scorable_fields = MinimumScorableFieldsForModel( + 'Comorbidity', + [ + "comorbidity_diagnosis_date", + "comorbidity__comorbidityentity__conceptId", + ] +) + +Assessment_minimum_scorable_fields = MinimumScorableFieldsForModel( + 'Assessment', + [ + "childrens_epilepsy_surgical_service_referral_criteria_met", + "consultant_paediatrician_referral_made", + "paediatric_neurologist_referral_made", + "childrens_epilepsy_surgical_service_referral_made", + "epilepsy_specialist_nurse_referral_made", + ] +) + +Investigations_minimum_scorable_fields = MinimumScorableFieldsForModel( + 'Investigations', + [ + "eeg_indicated", + "twelve_lead_ecg_status", + "ct_head_scan_status", + "mri_indicated", + ] +) + +Management_minimum_scorable_fields = MinimumScorableFieldsForModel( + 'Management', + [ + "has_an_aed_been_given", + "has_rescue_medication_been_prescribed", + "individualised_care_plan_in_place", + "has_been_referred_for_mental_health_support", + "has_support_for_mental_health_support", + ] +) + +AntiEpilepsyMedicine_minimum_scorable_fields = MinimumScorableFieldsForModel( + 'AntiEpilepsyMedicine', + [ + "medicine_name", + "antiepilepsy_medicine_start_date", + "antiepilepsy_medicine_risk_discussed", + ] +) diff --git a/epilepsy12/constants/kpi.py b/epilepsy12/constants/kpi.py index 8f85dba8..68146c11 100644 --- a/epilepsy12/constants/kpi.py +++ b/epilepsy12/constants/kpi.py @@ -32,3 +32,9 @@ 'School individual healthcare plan'), ) ) +KPI_SCORE = { + "NOT_SCORED": None, + "FAIL": 0, + "PASS": 1, + "INELIGIBLE": 2, +} \ No newline at end of file diff --git a/epilepsy12/constants/postcodes.py b/epilepsy12/constants/postcodes.py index 12745924..83db87de 100644 --- a/epilepsy12/constants/postcodes.py +++ b/epilepsy12/constants/postcodes.py @@ -1,552 +1,6 @@ -UNKNOWN_POSTCODES = ['ZZ99 3CZ', 'ZZ99 3GZ', 'ZZ99 3WZ', 'ZZ99 3VZ'] -UNKNOWN_POSTCODES_NO_SPACES = ['ZZ993CZ', 'ZZ993GZ', 'ZZ993WZ', 'ZZ993VZ'] +""" +Constants for 'unknown' postcodes +These are Office for National Statistics (ONS) codes for where a postcode is not known +""" - -# DUMMY_POSTCODES = [ -# { -# "postcode": "NR7 0FD", -# "quality": 1, -# "eastings": 628184, -# "northings": 309688, -# "country": "England", -# "nhs_ha": "East of England", -# "longitude": 1.37089, -# "latitude": 52.636928, -# "european_electoral_region": "Eastern", -# "primary_care_trust": "Norfolk", -# "region": "East of England", -# "lsoa": "Broadland 016C", -# "msoa": "Broadland 016", -# "incode": "0FD", -# "outcode": "NR7", -# "parliamentary_constituency": "Norwich North", -# "admin_district": "Broadland", -# "parish": "Thorpe St. Andrew", -# "admin_county": "Norfolk", -# "admin_ward": "Thorpe St Andrew South East", -# "ced": "Thorpe St. Andrew", -# "ccg": "NHS Norfolk and Waveney", -# "nuts": "Norwich and East Norfolk", -# "codes": { -# "admin_district": "E07000144", -# "admin_county": "E10000020", -# "admin_ward": "E05005782", -# "parish": "E04006256", -# "parliamentary_constituency": "E14000863", -# "ccg": "E38000239", -# "ccg_id": "26A", -# "ced": "E58001026", -# "nuts": "TLH15", -# "lsoa": "E01026576", -# "msoa": "E02005535", -# "lau2": "E07000144" -# } -# }, -# { -# "postcode": "PR2 8NR", -# "quality": 1, -# "eastings": 353510, -# "northings": 431459, -# "country": "England", -# "nhs_ha": "North West", -# "longitude": -2.706941, -# "latitude": 53.777376, -# "european_electoral_region": "North West", -# "primary_care_trust": "Central Lancashire", -# "region": "North West", -# "lsoa": "Preston 006D", -# "msoa": "Preston 006", -# "incode": "8NR", -# "outcode": "PR2", -# "parliamentary_constituency": "Wyre and Preston North", -# "admin_district": "Preston", -# "parish": "Preston, unparished area", -# "admin_county": "Lancashire", -# "admin_ward": "Garrison", -# "ced": "Preston Central East", -# "ccg": "NHS Greater Preston", -# "nuts": "Mid Lancashire", -# "codes": { -# "admin_district": "E07000123", -# "admin_county": "E10000017", -# "admin_ward": "E05012199", -# "parish": "E43000225", -# "parliamentary_constituency": "E14001057", -# "ccg": "E38000227", -# "ccg_id": "01E", -# "ced": "E58000804", -# "nuts": "TLD45", -# "lsoa": "E01025243", -# "msoa": "E02005258", -# "lau2": "E07000123" -# } -# }, -# { -# "postcode": "DE55 1RA", -# "quality": 1, -# "eastings": 442102, -# "northings": 354021, -# "country": "England", -# "nhs_ha": "East Midlands", -# "longitude": -1.372925, -# "latitude": 53.081759, -# "european_electoral_region": "East Midlands", -# "primary_care_trust": "Derbyshire County", -# "region": "East Midlands", -# "lsoa": "Amber Valley 003E", -# "msoa": "Amber Valley 003", -# "incode": "1RA", -# "outcode": "DE55", -# "parliamentary_constituency": "Amber Valley", -# "admin_district": "Amber Valley", -# "parish": "Somercotes", -# "admin_county": "Derbyshire", -# "admin_ward": "Somercotes", -# "ced": "Alfreton and Somercotes", -# "ccg": "NHS Derby and Derbyshire", -# "nuts": "South and West Derbyshire", -# "codes": { -# "admin_district": "E07000032", -# "admin_county": "E10000007", -# "admin_ward": "E05003299", -# "parish": "E04002692", -# "parliamentary_constituency": "E14000533", -# "ccg": "E38000229", -# "ccg_id": "15M", -# "ced": "E58000193", -# "nuts": "TLF13", -# "lsoa": "E01019471", -# "msoa": "E02004031", -# "lau2": "E07000032" -# } -# }, -# { -# "postcode": "DD4 9JG", -# "quality": 1, -# "eastings": 342765, -# "northings": 733294, -# "country": "Scotland", -# "nhs_ha": "Tayside", -# "longitude": -2.931043, -# "latitude": 56.488247, -# "european_electoral_region": "Scotland", -# "primary_care_trust": "Dundee Community Health Partnership", -# "region": null, -# "lsoa": "Whitfield - 04", -# "msoa": "Whitfield", -# "incode": "9JG", -# "outcode": "DD4", -# "parliamentary_constituency": "Dundee East", -# "admin_district": "Dundee City", -# "parish": null, -# "admin_county": null, -# "admin_ward": "North East", -# "ced": null, -# "ccg": "Dundee Community Health Partnership", -# "nuts": "Angus and Dundee City", -# "codes": { -# "admin_district": "S12000042", -# "admin_county": "S99999999", -# "admin_ward": "S13002830", -# "parish": "S99999999", -# "parliamentary_constituency": "S14000015", -# "ccg": "S03000039", -# "ccg_id": "039", -# "ced": "S99999999", -# "nuts": "TLM71", -# "lsoa": "S01007785", -# "msoa": "S02001462", -# "lau2": "S30000049" -# } -# }, -# { -# "postcode": "TW7 4PS", -# "quality": 1, -# "eastings": 515157, -# "northings": 177199, -# "country": "England", -# "nhs_ha": "London", -# "longitude": -0.343022, -# "latitude": 51.482033, -# "european_electoral_region": "London", -# "primary_care_trust": "Hounslow", -# "region": "London", -# "lsoa": "Hounslow 009D", -# "msoa": "Hounslow 009", -# "incode": "4PS", -# "outcode": "TW7", -# "parliamentary_constituency": "Brentford and Isleworth", -# "admin_district": "Hounslow", -# "parish": "Hounslow, unparished area", -# "admin_county": null, -# "admin_ward": "Osterley and Spring Grove", -# "ced": null, -# "ccg": "NHS North West London", -# "nuts": "Hounslow and Richmond upon Thames", -# "codes": { -# "admin_district": "E09000018", -# "admin_county": "E99999999", -# "admin_ward": "E05000363", -# "parish": "E43000208", -# "parliamentary_constituency": "E14000593", -# "ccg": "E38000256", -# "ccg_id": "W2U3Z", -# "ced": "E99999999", -# "nuts": "TLI75", -# "lsoa": "E01002680", -# "msoa": "E02000534", -# "lau2": "E09000018" -# } -# }, -# { -# "postcode": "SR1 1DP", -# "quality": 1, -# "eastings": 439835, -# "northings": 557088, -# "country": "England", -# "nhs_ha": "North East", -# "longitude": -1.380266, -# "latitude": 54.906935, -# "european_electoral_region": "North East", -# "primary_care_trust": "Sunderland Teaching", -# "region": "North East", -# "lsoa": "Sunderland 013B", -# "msoa": "Sunderland 013", -# "incode": "1DP", -# "outcode": "SR1", -# "parliamentary_constituency": "Sunderland Central", -# "admin_district": "Sunderland", -# "parish": "Sunderland, unparished area", -# "admin_county": null, -# "admin_ward": "Hendon", -# "ced": null, -# "ccg": "NHS Sunderland", -# "nuts": "Sunderland", -# "codes": { -# "admin_district": "E08000024", -# "admin_county": "E99999999", -# "admin_ward": "E05001158", -# "parish": "E43000178", -# "parliamentary_constituency": "E14000982", -# "ccg": "E38000176", -# "ccg_id": "00P", -# "ced": "E99999999", -# "nuts": "TLC23", -# "lsoa": "E01008703", -# "msoa": "E02001803", -# "lau2": "E08000024" -# } -# }, -# { -# "postcode": "AB24 2SU", -# "quality": 1, -# "eastings": 393406, -# "northings": 808454, -# "country": "Scotland", -# "nhs_ha": "Grampian", -# "longitude": -2.110675, -# "latitude": 57.166871, -# "european_electoral_region": "Scotland", -# "primary_care_trust": "Aberdeen City Community Health Partnership", -# "region": null, -# "lsoa": "Tillydrone - 03", -# "msoa": "Tillydrone", -# "incode": "2SU", -# "outcode": "AB24", -# "parliamentary_constituency": "Aberdeen North", -# "admin_district": "Aberdeen City", -# "parish": null, -# "admin_county": null, -# "admin_ward": "Tillydrone/Seaton/Old Aberdeen", -# "ced": null, -# "ccg": "Aberdeen City Community Health Partnership", -# "nuts": "Aberdeen City and Aberdeenshire", -# "codes": { -# "admin_district": "S12000033", -# "admin_county": "S99999999", -# "admin_ward": "S13002840", -# "parish": "S99999999", -# "parliamentary_constituency": "S14000001", -# "ccg": "S03000012", -# "ccg_id": "012", -# "ced": "S99999999", -# "nuts": "TLM50", -# "lsoa": "S01006677", -# "msoa": "S02001266", -# "lau2": "S30000026" -# } -# }, -# { -# "postcode": "BD23 5QS", -# "quality": 1, -# "eastings": 397838, -# "northings": 464970, -# "country": "England", -# "nhs_ha": "Yorkshire and the Humber", -# "longitude": -2.034539, -# "latitude": 54.080645, -# "european_electoral_region": "Yorkshire and The Humber", -# "primary_care_trust": "North Yorkshire and York", -# "region": "Yorkshire and The Humber", -# "lsoa": "Craven 002C", -# "msoa": "Craven 002", -# "incode": "5QS", -# "outcode": "BD23", -# "parliamentary_constituency": "Skipton and Ripon", -# "admin_district": "Craven", -# "parish": "Threshfield", -# "admin_county": "North Yorkshire", -# "admin_ward": "Upper Wharfedale", -# "ced": "Mid Craven", -# "ccg": "NHS Bradford District and Craven", -# "nuts": "North Yorkshire CC", -# "codes": { -# "admin_district": "E07000163", -# "admin_county": "E10000023", -# "admin_ward": "E05006206", -# "parish": "E04007125", -# "parliamentary_constituency": "E14000928", -# "ccg": "E38000232", -# "ccg_id": "36J", -# "ced": "E58001069", -# "nuts": "TLE22", -# "lsoa": "E01027585", -# "msoa": "E02005743", -# "lau2": "E07000163" -# } -# }, -# { -# "postcode": "BA7 7JR", -# "quality": 1, -# "eastings": 363651, -# "northings": 133244, -# "country": "England", -# "nhs_ha": "South West", -# "longitude": -2.520467, -# "latitude": 51.097342, -# "european_electoral_region": "South West", -# "primary_care_trust": "Somerset", -# "region": "South West", -# "lsoa": "South Somerset 002C", -# "msoa": "South Somerset 002", -# "incode": "7JR", -# "outcode": "BA7", -# "parliamentary_constituency": "Somerton and Frome", -# "admin_district": "South Somerset", -# "parish": "Ansford", -# "admin_county": "Somerset", -# "admin_ward": "Cary", -# "ced": "Castle Cary", -# "ccg": "NHS Somerset", -# "nuts": "Somerset CC", -# "codes": { -# "admin_district": "E07000189", -# "admin_county": "E10000027", -# "admin_ward": "E05012519", -# "parish": "E04008659", -# "parliamentary_constituency": "E14000932", -# "ccg": "E38000150", -# "ccg_id": "11X", -# "ced": "E58001294", -# "nuts": "TLK23", -# "lsoa": "E01029168", -# "msoa": "E02006076", -# "lau2": "E07000189" -# } -# }, -# { -# "postcode": "PR2 1TJ", -# "quality": 1, -# "eastings": 349841, -# "northings": 431145, -# "country": "England", -# "nhs_ha": "North West", -# "longitude": -2.762559, -# "latitude": 53.774213, -# "european_electoral_region": "North West", -# "primary_care_trust": "Central Lancashire", -# "region": "North West", -# "lsoa": "Preston 013C", -# "msoa": "Preston 013", -# "incode": "1TJ", -# "outcode": "PR2", -# "parliamentary_constituency": "Preston", -# "admin_district": "Preston", -# "parish": "Preston, unparished area", -# "admin_county": "Lancashire", -# "admin_ward": "Lea & Larches", -# "ced": "Preston South West", -# "ccg": "NHS Greater Preston", -# "nuts": "Mid Lancashire", -# "codes": { -# "admin_district": "E07000123", -# "admin_county": "E10000017", -# "admin_ward": "E05012202", -# "parish": "E43000225", -# "parliamentary_constituency": "E14000885", -# "ccg": "E38000227", -# "ccg_id": "01E", -# "ced": "E58000811", -# "nuts": "TLD45", -# "lsoa": "E01025265", -# "msoa": "E02005265", -# "lau2": "E07000123" -# } -# }, -# { -# "postcode": "NR13 5GF", -# "quality": 1, -# "eastings": 630852, -# "northings": 310828, -# "country": "England", -# "nhs_ha": "East of England", -# "longitude": 1.411064, -# "latitude": 52.646057, -# "european_electoral_region": "Eastern", -# "primary_care_trust": "Norfolk", -# "region": "East of England", -# "lsoa": "Broadland 008A", -# "msoa": "Broadland 008", -# "incode": "5GF", -# "outcode": "NR13", -# "parliamentary_constituency": "Broadland", -# "admin_district": "Broadland", -# "parish": "Great and Little Plumstead", -# "admin_county": "Norfolk", -# "admin_ward": "Plumstead", -# "ced": "Thorpe St. Andrew", -# "ccg": "NHS Norfolk and Waveney", -# "nuts": "Norwich and East Norfolk", -# "codes": { -# "admin_district": "E07000144", -# "admin_county": "E10000020", -# "admin_ward": "E05005774", -# "parish": "E04006221", -# "parliamentary_constituency": "E14000603", -# "ccg": "E38000239", -# "ccg_id": "26A", -# "ced": "E58001026", -# "nuts": "TLH15", -# "lsoa": "E01026545", -# "msoa": "E02005527", -# "lau2": "E07000144" -# } -# }, -# { -# "postcode": "AL6 0SF", -# "quality": 1, -# "eastings": 525241, -# "northings": 217541, -# "country": "England", -# "nhs_ha": "East of England", -# "longitude": -0.183411, -# "latitude": 51.842441, -# "european_electoral_region": "Eastern", -# "primary_care_trust": "Hertfordshire", -# "region": "East of England", -# "lsoa": "Welwyn Hatfield 001A", -# "msoa": "Welwyn Hatfield 001", -# "incode": "0SF", -# "outcode": "AL6", -# "parliamentary_constituency": "Welwyn Hatfield", -# "admin_district": "Welwyn Hatfield", -# "parish": "Welwyn", -# "admin_county": "Hertfordshire", -# "admin_ward": "Welwyn East", -# "ced": "Welwyn", -# "ccg": "NHS East and North Hertfordshire", -# "nuts": "Hertfordshire CC", -# "codes": { -# "admin_district": "E07000241", -# "admin_county": "E10000015", -# "admin_ward": "E05011072", -# "parish": "E04004822", -# "parliamentary_constituency": "E14001027", -# "ccg": "E38000049", -# "ccg_id": "06K", -# "ced": "E58000675", -# "nuts": "TLH23", -# "lsoa": "E01023965", -# "msoa": "E02004980", -# "lau2": "E07000241" -# } -# }, -# { -# "postcode": "SE22 9DJ", -# "quality": 1, -# "eastings": 533870, -# "northings": 174799, -# "country": "England", -# "nhs_ha": "London", -# "longitude": -0.074598, -# "latitude": 51.45635, -# "european_electoral_region": "London", -# "primary_care_trust": "Southwark", -# "region": "London", -# "lsoa": "Southwark 030B", -# "msoa": "Southwark 030", -# "incode": "9DJ", -# "outcode": "SE22", -# "parliamentary_constituency": "Dulwich and West Norwood", -# "admin_district": "Southwark", -# "parish": "Southwark, unparished area", -# "admin_county": null, -# "admin_ward": "Goose Green", -# "ced": null, -# "ccg": "NHS South East London", -# "nuts": "Lewisham and Southwark", -# "codes": { -# "admin_district": "E09000028", -# "admin_county": "E99999999", -# "admin_ward": "E05011103", -# "parish": "E43000218", -# "parliamentary_constituency": "E14000673", -# "ccg": "E38000244", -# "ccg_id": "72Q", -# "ced": "E99999999", -# "nuts": "TLI44", -# "lsoa": "E01003954", -# "msoa": "E02000836", -# "lau2": "E09000028" -# } -# }, -# { -# "postcode": "GL20 7JN", -# "quality": 1, -# "eastings": 394808, -# "northings": 237717, -# "country": "England", -# "nhs_ha": "West Midlands", -# "longitude": -2.077103, -# "latitude": 52.03783, -# "european_electoral_region": "West Midlands", -# "primary_care_trust": "Worcestershire", -# "region": "West Midlands", -# "lsoa": "Wychavon 019E", -# "msoa": "Wychavon 019", -# "incode": "7JN", -# "outcode": "GL20", -# "parliamentary_constituency": "West Worcestershire", -# "admin_district": "Wychavon", -# "parish": "Kemerton", -# "admin_county": "Worcestershire", -# "admin_ward": "South Bredon Hill", -# "ced": "Bredon", -# "ccg": "NHS Herefordshire and Worcestershire", -# "nuts": "Worcestershire CC", -# "codes": { -# "admin_district": "E07000238", -# "admin_county": "E10000034", -# "admin_ward": "E05007924", -# "parish": "E04010408", -# "parliamentary_constituency": "E14001035", -# "ccg": "E38000236", -# "ccg_id": "18C", -# "ced": "E58001676", -# "nuts": "TLG12", -# "lsoa": "E01032412", -# "msoa": "E02006766", -# "lau2": "E07000238" -# } -# }, -# ] +UNKNOWN_POSTCODES_NO_SPACES = ["ZZ993CZ", "ZZ993GZ", "ZZ993WZ", "ZZ993VZ"] diff --git a/epilepsy12/constants/rcpch_organisations.py b/epilepsy12/constants/rcpch_organisations.py index 79b6ba82..4839294e 100644 --- a/epilepsy12/constants/rcpch_organisations.py +++ b/epilepsy12/constants/rcpch_organisations.py @@ -25,7 +25,7 @@ "Phone": "01223 245151", "Email": "", "Website": "https://www.cuh.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40270", @@ -49,7 +49,7 @@ "Phone": "", "Email": "", "Website": "https://www.airedaletrust.nhs.uk/", - "Fax": "01535 655129" + "Fax": "01535 655129", }, { "OrganisationID": "40195", @@ -73,7 +73,7 @@ "Phone": "", "Email": "", "Website": "http://www.alderhey.nhs.uk", - "Fax": "0151 252 5846" + "Fax": "0151 252 5846", }, { "OrganisationID": "41948", @@ -97,7 +97,7 @@ "Phone": "", "Email": "contactus@northumbria.nhs.uk", "Website": "https://www.northumbria.nhs.uk/our-locations/alnwick-infirmary", - "Fax": "" + "Fax": "", }, { "OrganisationID": "43210", @@ -121,7 +121,7 @@ "Phone": "", "Email": "", "Website": "http://www.buckshealthcare.nhs.uk/For%20patients%20and%20visitors/amersham-hospital.htm", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40163", @@ -145,7 +145,7 @@ "Phone": "", "Email": "wuth.patientexperience@nhs.net", "Website": "http://www.wuth.nhs.uk", - "Fax": "0151 604 7148" + "Fax": "0151 604 7148", }, { "OrganisationID": "42027", @@ -169,7 +169,7 @@ "Phone": "", "Email": "asp-tr.patient.advice@nhs.net", "Website": "http://www.ashfordstpeters.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "72361", @@ -193,7 +193,7 @@ "Phone": "", "Email": "", "Website": "https://www.royalfree.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40570", @@ -217,7 +217,7 @@ "Phone": "", "Email": "", "Website": "http://www.barnsleyhospital.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40570", @@ -241,7 +241,7 @@ "Phone": "", "Email": "", "Website": "http://www.barnsleyhospital.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -258,14 +258,14 @@ "City": "BRISTOL", "County": "AVON", "Postcode": "BS5 0AX", - "Latitude": "", - "Longitude": "", + "Latitude": "51.45640792575672", + "Longitude": "-2.563680201763284", "ParentODSCode": "RVN", "ParentName": "AVON AND WILTSHIRE MENTAL HEALTH PARTNERSHIP NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -282,14 +282,14 @@ "City": "BASILDON", "County": "ESSEX", "Postcode": "SS16 5NL", - "Latitude": "", - "Longitude": "", + "Latitude": "51.558185871010345", + "Longitude": "0.45187032374537445", "ParentODSCode": "RAJ", "ParentName": "MID AND SOUTH ESSEX NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41260", @@ -313,7 +313,7 @@ "Phone": "", "Email": "customercare@hhft.nhs.uk", "Website": "http://www.hampshirehospitals.nhs.uk", - "Fax": "01256 313098" + "Fax": "01256 313098", }, { "OrganisationID": "41405", @@ -337,7 +337,7 @@ "Phone": "", "Email": "", "Website": "http://www.dbth.nhs.uk", - "Fax": "01909 502246" + "Fax": "01909 502246", }, { "OrganisationID": "", @@ -354,14 +354,14 @@ "City": "PORTSMOUTH", "County": "HAMPSHIRE", "Postcode": "PO2 0TA", - "Latitude": "", - "Longitude": "", + "Latitude": "50.822634957956915", + "Longitude": "-1.0703087165064853", "ParentODSCode": "R1C", "ParentName": "SOLENT NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -378,14 +378,14 @@ "City": "BEDFORD", "County": "", "Postcode": "MK42 9DJ", - "Latitude": "", - "Longitude": "", + "Latitude": "52.128773757410244", + "Longitude": "-0.471222131766747", "ParentODSCode": "RC9", "ParentName": "BEDFORDSHIRE HOSPITALS NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41507", @@ -409,7 +409,7 @@ "Phone": "", "Email": "", "Website": "http://www.bwc.nhs.uk", - "Fax": "0121 333 9998" + "Fax": "0121 333 9998", }, { "OrganisationID": "43192", @@ -433,7 +433,7 @@ "Phone": "", "Email": "information@cddft.nhs.uk", "Website": "http://www.cddft.nhs.uk/", - "Fax": "01388 454127" + "Fax": "01388 454127", }, { "OrganisationID": "43136", @@ -457,7 +457,7 @@ "Phone": "", "Email": "", "Website": "https://www.bfwh.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -474,14 +474,14 @@ "City": "WALSALL", "County": "WEST MIDLANDS", "Postcode": "WS3 1LZ", - "Latitude": "", - "Longitude": "", + "Latitude": "52.61485987017649", + "Longitude": "-1.986635562256734", "ParentODSCode": "RYK", "ParentName": "DUDLEY INTEGRATED HEALTH AND CARE NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41957", @@ -505,7 +505,7 @@ "Phone": "", "Email": "contactus@northumbria.nhs.uk", "Website": "https://www.northumbria.nhs.uk/blyth", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -522,14 +522,14 @@ "City": "BRADFORD", "County": "WEST YORKSHIRE", "Postcode": "BD9 6RJ", - "Latitude": "", - "Longitude": "", + "Latitude": "53.80683265969457", + "Longitude": "-1.7966404739876927", "ParentODSCode": "RR8", "ParentName": "LEEDS TEACHING HOSPITALS NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -546,14 +546,14 @@ "City": "BOLTON", "County": "LANCASHIRE", "Postcode": "BL2 6NT", - "Latitude": "", - "Longitude": "", + "Latitude": "53.58260174509509", + "Longitude": "-2.3841852348486086", "ParentODSCode": "RMC", "ParentName": "BOLTON NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "39995", @@ -577,7 +577,7 @@ "Phone": "", "Email": "", "Website": "http://www.uhbristol.nhs.uk/your-hospitals/bristol-royal-hospital-for-children.html", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -594,14 +594,14 @@ "City": "ABERYSTWYTH", "County": "DYFED", "Postcode": "SY23 1ER", - "Latitude": "", - "Longitude": "", + "Latitude": "52.41629608119735", + "Longitude": "-4.071755772843108", "ParentODSCode": "7A2", "ParentName": "HYWEL DDA UNIVERSITY LHB", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "7828770", @@ -625,7 +625,7 @@ "Phone": "", "Email": "", "Website": "https://www.mse.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "43211", @@ -649,7 +649,7 @@ "Phone": "", "Email": "", "Website": "http://www.buckshealthcare.nhs.uk", - "Fax": "01280 824966" + "Fax": "01280 824966", }, { "OrganisationID": "42363", @@ -673,7 +673,7 @@ "Phone": "", "Email": "", "Website": "http://www.ekhuft.nhs.uk/buckland", - "Fax": "" + "Fax": "", }, { "OrganisationID": "43223", @@ -697,7 +697,7 @@ "Phone": "", "Email": "contact@elht.nhs.uk", "Website": "http://www.elht.nhs.uk", - "Fax": "01282 474444" + "Fax": "01282 474444", }, { "OrganisationID": "", @@ -714,14 +714,14 @@ "City": "SOUTHAMPTON", "County": "HAMPSHIRE", "Postcode": "SO16 6YD", - "Latitude": "", - "Longitude": "", + "Latitude": "50.93125793154169", + "Longitude": "-1.433717192863688", "ParentODSCode": "RHM", "ParentName": "UNIVERSITY HOSPITAL SOUTHAMPTON NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "78514", @@ -745,7 +745,7 @@ "Phone": "", "Email": "", "Website": "https://www.uhdb.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42865", @@ -769,7 +769,7 @@ "Phone": "", "Email": "", "Website": "http://www.cht.nhs.uk", - "Fax": "01422 380357" + "Fax": "01422 380357", }, { "OrganisationID": "", @@ -786,14 +786,14 @@ "City": "POOLE", "County": "DORSET", "Postcode": "BH15 1SZ", - "Latitude": "", - "Longitude": "", + "Latitude": "50.719050056794515", + "Longitude": "-1.9807258428334855", "ParentODSCode": "RDY", "ParentName": "DORSET HEALTHCARE UNIVERSITY NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "2917652", @@ -817,7 +817,7 @@ "Phone": "", "Email": "lnwh-tr.trust@nhs.net", "Website": "http://www.lnwh.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -834,14 +834,14 @@ "City": "BEDFORD", "County": "BEDFORDSHIRE", "Postcode": "MK42 7EB", - "Latitude": "", - "Longitude": "", + "Latitude": "52.1095632794134", + "Longitude": "-0.511761474096703", "ParentODSCode": "RYV", "ParentName": "CAMBRIDGESHIRE COMMUNITY SERVICES NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41545", @@ -865,7 +865,7 @@ "Phone": "", "Email": "", "Website": "http://www.chelwest.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41887", @@ -889,7 +889,7 @@ "Phone": "", "Email": "", "Website": "http://www.gloshospitals.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -906,14 +906,14 @@ "City": "CHEPSTOW", "County": "GWENT", "Postcode": "NP16 5YX", - "Latitude": "", - "Longitude": "", + "Latitude": "51.63943343225332", + "Longitude": "-2.686661402962116", "ParentODSCode": "7A6", "ParentName": "ANEURIN BEVAN UNIVERSITY LHB", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40583", @@ -937,7 +937,7 @@ "Phone": "", "Email": "crhft.communications@nhs.net", "Website": "http://www.chesterfieldroyal.nhs.uk", - "Fax": "01246 512737" + "Fax": "01246 512737", }, { "OrganisationID": "", @@ -954,14 +954,14 @@ "City": "CHESTER LE STREET", "County": "COUNTY DURHAM", "Postcode": "DH3 3UR", - "Latitude": "", - "Longitude": "", + "Latitude": "54.861359849136804", + "Longitude": "-1.572211745083105", "ParentODSCode": "RXP", "ParentName": "COUNTY DURHAM AND DARLINGTON NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -978,14 +978,14 @@ "City": "LONDON", "County": "", "Postcode": "SW10 9NH", - "Latitude": "", - "Longitude": "", + "Latitude": "51.48464571866343", + "Longitude": "-0.18172355879372384", "ParentODSCode": "RQM", "ParentName": "CHELSEA AND WESTMINSTER HOSPITAL NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1002,14 +1002,14 @@ "City": "BIRMINGHAM", "County": "WEST MIDLANDS", "Postcode": "B9 5ST", - "Latitude": "", - "Longitude": "", + "Latitude": "52.47799116365538", + "Longitude": "-1.8275362317445312", "ParentODSCode": "RYW", "ParentName": "BIRMINGHAM COMMUNITY HEALTHCARE NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1026,14 +1026,14 @@ "City": "CAMBRIDGE", "County": "CAMBRIDGESHIRE", "Postcode": "CB2 0QQ", - "Latitude": "", - "Longitude": "", + "Latitude": "52.17487593919441", + "Longitude": "0.14141622590743344", "ParentODSCode": "RYV", "ParentName": "CAMBRIDGESHIRE COMMUNITY SERVICES NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1050,14 +1050,14 @@ "City": "BRENTWOOD", "County": "ESSEX", "Postcode": "CM15 8DR", - "Latitude": "", - "Longitude": "", + "Latitude": "51.62369359002008", + "Longitude": "0.31635099703690556", "ParentODSCode": "RAT", "ParentName": "NORTH EAST LONDON NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1074,14 +1074,14 @@ "City": "HARLOW", "County": "ESSEX", "Postcode": "CM17 9TG", - "Latitude": "", - "Longitude": "", + "Latitude": "51.767976263497225", + "Longitude": "0.13251425290988306", "ParentODSCode": "R1L", "ParentName": "ESSEX PARTNERSHIP UNIVERSITY NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1098,14 +1098,14 @@ "City": "STANLEY", "County": "COUNTY DURHAM", "Postcode": "DH9 7TG", - "Latitude": "", - "Longitude": "", + "Latitude": "54.857744", + "Longitude": "-1.736403", "ParentODSCode": "RXP", "ParentName": "COUNTY DURHAM AND DARLINGTON NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1122,14 +1122,14 @@ "City": "BURY ST. EDMUNDS", "County": "SUFFOLK", "Postcode": "IP33 3ND", - "Latitude": "", - "Longitude": "", + "Latitude": "52.24063085442167", + "Longitude": "0.7084705271013394", "ParentODSCode": "RGR", "ParentName": "WEST SUFFOLK NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1146,14 +1146,14 @@ "City": "LLANDUDNO", "County": "GWYNEDD", "Postcode": "LL30 1YE", - "Latitude": "", - "Longitude": "", + "Latitude": "52.24065712570418", + "Longitude": "0.7084597970760184", "ParentODSCode": "7A1", "ParentName": "BETSI CADWALADR UNIVERSITY LHB", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1170,14 +1170,14 @@ "City": "BANGOR", "County": "GWYNEDD", "Postcode": "LL57 2EE", - "Latitude": "", - "Longitude": "", + "Latitude": "53.22551496089767", + "Longitude": "-4.134974832357014", "ParentODSCode": "7A1", "ParentName": "BETSI CADWALADR UNIVERSITY LHB", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1194,14 +1194,14 @@ "City": "PLYMOUTH", "County": "DEVON", "Postcode": "PL2 2PQ", - "Latitude": "", - "Longitude": "", + "Latitude": "50.39051691746257", + "Longitude": "-4.163071330781396", "ParentODSCode": "RK9", "ParentName": "UNIVERSITY HOSPITALS PLYMOUTH NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1218,14 +1218,14 @@ "City": "CHICHESTER", "County": "WEST SUSSEX", "Postcode": "PO19 7HH", - "Latitude": "", - "Longitude": "", + "Latitude": "50.84253071425149", + "Longitude": "-0.7601499913684482", "ParentODSCode": "RYR", "ParentName": "UNIVERSITY HOSPITALS SUSSEX NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1242,14 +1242,14 @@ "City": "LONDON", "County": "GREATER LONDON", "Postcode": "E15 4PT", - "Latitude": "", - "Longitude": "", + "Latitude": "51.536834588132855", + "Longitude": "0.006225110524521736", "ParentODSCode": "RP4", "ParentName": "GREAT ORMOND STREET HOSPITAL FOR CHILDREN NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1266,14 +1266,14 @@ "City": "WARWICK", "County": "WARWICKSHIRE", "Postcode": "CV34 5BW", - "Latitude": "", - "Longitude": "", + "Latitude": "52.2899606215984", + "Longitude": "-1.584414018263402", "ParentODSCode": "RJC", "ParentName": "SOUTH WARWICKSHIRE UNIVERSITY NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1290,14 +1290,14 @@ "City": "LONDON", "County": "GREATER LONDON", "Postcode": "E17 3LA", - "Latitude": "", - "Longitude": "", + "Latitude": "51.588111852487415", + "Longitude": "-0.003012560636604331", "ParentODSCode": "RP4", "ParentName": "GREAT ORMOND STREET HOSPITAL FOR CHILDREN NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1314,14 +1314,14 @@ "City": "UXBRIDGE", "County": "MIDDLESEX", "Postcode": "UB8 3NN", - "Latitude": "", - "Longitude": "", + "Latitude": "51.534177763702324", + "Longitude": "-0.4578701302698584", "ParentODSCode": "RV3", "ParentName": "CENTRAL AND NORTH WEST LONDON NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1338,14 +1338,14 @@ "City": "PORT TALBOT", "County": "WEST GLAMORGAN", "Postcode": "SA12 7BY", - "Latitude": "", - "Longitude": "", + "Latitude": "51.59865037448691", + "Longitude": "-3.802852293530534", "ParentODSCode": "7A3", "ParentName": "SWANSEA BAY UNIVERSITY LOCAL HEALTH BOARD", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1362,14 +1362,14 @@ "City": "MILTON KEYNES", "County": "BUCKINGHAMSHIRE", "Postcode": "MK6 5NG", - "Latitude": "", - "Longitude": "", + "Latitude": "52.024944716501295", + "Longitude": "-0.736349618753685", "ParentODSCode": "RV3", "ParentName": "CENTRAL AND NORTH WEST LONDON NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1386,14 +1386,14 @@ "City": "TRURO", "County": "CORNWALL", "Postcode": "TR1 3XQ", - "Latitude": "", - "Longitude": "", + "Latitude": "50.2662166792816", + "Longitude": "-5.095523529924103", "ParentODSCode": "REF", "ParentName": "ROYAL CORNWALL HOSPITALS NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1410,14 +1410,14 @@ "City": "BRECON", "County": "", "Postcode": "LD3 7NS", - "Latitude": "", - "Longitude": "", + "Latitude": "51.94899731632159", + "Longitude": "-3.3848060452712567", "ParentODSCode": "7A7", "ParentName": "POWYS TEACHING LOCAL HEALTH BOARD", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1434,14 +1434,14 @@ "City": "NOTTINGHAM", "County": "NOTTINGHAMSHIRE", "Postcode": "NG5 1PB", - "Latitude": "", - "Longitude": "", + "Latitude": "52.9905804091911", + "Longitude": "-1.1640642028760453", "ParentODSCode": "RX1", "ParentName": "NOTTINGHAM UNIVERSITY HOSPITALS NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "43182", @@ -1465,7 +1465,7 @@ "Phone": "", "Email": "enquiries@lthtr.nhs.uk", "Website": "https://www.lancsteachinghospitals.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "43108", @@ -1489,7 +1489,7 @@ "Phone": "", "Email": "", "Website": "http://www.swbh.nhs.uk", - "Fax": "0121 507 5636" + "Fax": "0121 507 5636", }, { "OrganisationID": "40342", @@ -1513,7 +1513,7 @@ "Phone": "", "Email": "communications@esneft.nhs.uk", "Website": "https://www.esneft.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1530,14 +1530,14 @@ "City": "DERBY", "County": "DERBYSHIRE", "Postcode": "DE24 8NH", - "Latitude": "", - "Longitude": "", + "Latitude": "52.89510370116093", + "Longitude": "-1.4452974605534492", "ParentODSCode": "RTG", "ParentName": "UNIVERSITY HOSPITALS OF DERBY AND BURTON NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1554,14 +1554,14 @@ "City": "LONDON", "County": "", "Postcode": "N15 3TH", - "Latitude": "", - "Longitude": "", + "Latitude": "51.580052959564135", + "Longitude": "-0.08932988262221664", "ParentODSCode": "RKE", "ParentName": "WHITTINGTON HEALTH NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1578,14 +1578,14 @@ "City": "HUNTINGDON", "County": "CAMBRIDGESHIRE", "Postcode": "PE29 7HN", - "Latitude": "", - "Longitude": "", + "Latitude": "52.34562706391435", + "Longitude": "-0.17570404808129073", "ParentODSCode": "RYV", "ParentName": "CAMBRIDGESHIRE COMMUNITY SERVICES NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1602,14 +1602,14 @@ "City": "NORWICH", "County": "NORFOLK", "Postcode": "NR4 7PA", - "Latitude": "", - "Longitude": "", + "Latitude": "52.6167141242066", + "Longitude": "1.2671810673633923", "ParentODSCode": "RY3", "ParentName": "NORFOLK COMMUNITY HEALTH AND CARE NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1626,14 +1626,14 @@ "City": "EASTBOURNE", "County": "", "Postcode": "BN21 2UD", - "Latitude": "", - "Longitude": "", + "Latitude": "50.78645763637544", + "Longitude": "0.26970748986035503", "ParentODSCode": "RXC", "ParentName": "EAST SUSSEX HEALTHCARE NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1650,14 +1650,14 @@ "City": "WIRRAL", "County": "MERSEYSIDE", "Postcode": "CH49 5PE", - "Latitude": "", - "Longitude": "", + "Latitude": "53.37066114086599", + "Longitude": "-3.095204921241858", "ParentODSCode": "RBL", "ParentName": "WIRRAL UNIVERSITY TEACHING HOSPITAL NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1674,14 +1674,14 @@ "City": "STRATFORD-UPON-AVON", "County": "WARWICKSHIRE", "Postcode": "CV37 6NX", - "Latitude": "", - "Longitude": "", + "Latitude": "52.195127472619795", + "Longitude": "-1.7122910053235376", "ParentODSCode": "RJC", "ParentName": "SOUTH WARWICKSHIRE UNIVERSITY NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1698,14 +1698,14 @@ "City": "CREWE", "County": "CHESHIRE", "Postcode": "CW1 4QJ", - "Latitude": "", - "Longitude": "", + "Latitude": "53.119010495755255", + "Longitude": "-2.475808230401211", "ParentODSCode": "RBT", "ParentName": "MID CHESHIRE HOSPITALS NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1722,14 +1722,14 @@ "City": "DERBY", "County": "", "Postcode": "DE22 3NE", - "Latitude": "", - "Longitude": "", + "Latitude": "52.91110179327077", + "Longitude": "-1.512306403428385", "ParentODSCode": "RTG", "ParentName": "UNIVERSITY HOSPITALS OF DERBY AND BURTON NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1746,14 +1746,14 @@ "City": "BARROW-IN-FURNESS", "County": "CUMBRIA", "Postcode": "LA14 4LF", - "Latitude": "", - "Longitude": "", + "Latitude": "54.13673411203507", + "Longitude": "-3.2083506112939224", "ParentODSCode": "RNN", "ParentName": "NORTH CUMBRIA INTEGRATED CARE NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1770,14 +1770,14 @@ "City": "BARNSLEY", "County": "SOUTH YORKSHIRE", "Postcode": "S70 1LP", - "Latitude": "", - "Longitude": "", + "Latitude": "53.54885593728199", + "Longitude": "-1.4800215002356396", "ParentODSCode": "RXG", "ParentName": "SOUTH WEST YORKSHIRE PARTNERSHIP NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1794,14 +1794,14 @@ "City": "WALSALL", "County": "WEST MIDLANDS", "Postcode": "WS2 9PS", - "Latitude": "", - "Longitude": "", + "Latitude": "52.58303487554701", + "Longitude": "-1.9976477770103798", "ParentODSCode": "RBK", "ParentName": "WALSALL HEALTHCARE NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1818,14 +1818,14 @@ "City": "BASILDON", "County": "ESSEX", "Postcode": "SS14 1EH", - "Latitude": "", - "Longitude": "", + "Latitude": "51.61275615547437", + "Longitude": "0.6648505953412928", "ParentODSCode": "RAT", "ParentName": "NORTH EAST LONDON NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1842,14 +1842,14 @@ "City": "GRAYS", "County": "ESSEX", "Postcode": "RM16 2PX", - "Latitude": "", - "Longitude": "", + "Latitude": "51.49616178096384", + "Longitude": "0.33659109093339895", "ParentODSCode": "RAT", "ParentName": "NORTH EAST LONDON NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1866,14 +1866,14 @@ "City": "SOUTHSEA", "County": "HAMPSHIRE", "Postcode": "PO4 8LD", - "Latitude": "", - "Longitude": "", + "Latitude": "50.796657008704145", + "Longitude": "-1.0493687455907676", "ParentODSCode": "R1C", "ParentName": "SOLENT NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1890,14 +1890,14 @@ "City": "BLACKBURN", "County": "LANCASHIRE", "Postcode": "BB2 3HH", - "Latitude": "", - "Longitude": "", + "Latitude": "53.736147580420365", + "Longitude": "-2.461713564745193", "ParentODSCode": "RXR", "ParentName": "EAST LANCASHIRE HOSPITALS NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1914,14 +1914,14 @@ "City": "SOUTHAMPTON", "County": "HAMPSHIRE", "Postcode": "SO16 6HU", - "Latitude": "", - "Longitude": "", + "Latitude": "50.93151657046671", + "Longitude": "-1.4333675030065531", "ParentODSCode": "R1C", "ParentName": "SOLENT NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -1938,14 +1938,14 @@ "City": "SOUTHAMPTON", "County": "HAMPSHIRE", "Postcode": "SO40 7AR", - "Latitude": "", - "Longitude": "", + "Latitude": "50.89113624486605", + "Longitude": "-1.5229741072550669", "ParentODSCode": "R1C", "ParentName": "SOLENT NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "43032", @@ -1969,7 +1969,7 @@ "Phone": "", "Email": "", "Website": "http://www.esht.nhs.uk/conquest", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41035", @@ -1993,7 +1993,7 @@ "Phone": "", "Email": "cochpals@nhs.net", "Website": "http://www.coch.nhs.uk", - "Fax": "01244 365 292" + "Fax": "01244 365 292", }, { "OrganisationID": "", @@ -2010,14 +2010,14 @@ "City": "DROITWICH", "County": "WORCESTERSHIRE", "Postcode": "WR9 8QU", - "Latitude": "", - "Longitude": "", + "Latitude": "52.26794984957385", + "Longitude": "-2.1533879740866677", "ParentODSCode": "R1A", "ParentName": "HEREFORDSHIRE AND WORCESTERSHIRE HEALTH AND CARE NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -2034,14 +2034,14 @@ "City": "CROYDON", "County": "SURREY", "Postcode": "CR0 7YD", - "Latitude": "", - "Longitude": "", + "Latitude": "51.38943236801409", + "Longitude": "-0.05949026304572937", "ParentODSCode": "RJ6", "ParentName": "CROYDON HEALTH SERVICES NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40924", @@ -2065,7 +2065,7 @@ "Phone": "", "Email": "", "Website": "http://www.croydonhealthservices.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "10855536", @@ -2089,7 +2089,7 @@ "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41265", @@ -2113,7 +2113,7 @@ "Phone": "", "Email": "dgn-tr.enquiries@nhs.net", "Website": "http://www.dgt.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "43202", @@ -2137,7 +2137,7 @@ "Phone": "", "Email": "information@cddft.nhs.uk", "Website": "http://www.cddft.nhs.uk", - "Fax": "01325 743200" + "Fax": "01325 743200", }, { "OrganisationID": "41066", @@ -2161,7 +2161,7 @@ "Phone": "", "Email": "", "Website": "http://www.plymouthhospitals.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41019", @@ -2185,7 +2185,7 @@ "Phone": "", "Email": "", "Website": "http://www.nlg.nhs.uk/hospitals/grimsby", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -2202,14 +2202,14 @@ "City": "READING", "County": "", "Postcode": "RG6 6BZ", - "Latitude": "", - "Longitude": "", + "Latitude": "51.442447786143", + "Longitude": "-0.9354541726665103", "ParentODSCode": "RHW", "ParentName": "ROYAL BERKSHIRE NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41406", @@ -2233,7 +2233,7 @@ "Phone": "", "Email": "", "Website": "http://www.dbth.nhs.uk", - "Fax": "01302 320098" + "Fax": "01302 320098", }, { "OrganisationID": "40130", @@ -2257,7 +2257,7 @@ "Phone": "", "Email": "headquarters@dchft.nhs.uk", "Website": "http://www.dchft.nhs.uk", - "Fax": "01305 254155" + "Fax": "01305 254155", }, { "OrganisationID": "", @@ -2274,14 +2274,14 @@ "City": "WESTON-SUPER-MARE", "County": "AVON", "Postcode": "BS23 3NT", - "Latitude": "", - "Longitude": "", + "Latitude": "51.33946376061602", + "Longitude": "-2.9688690485123415", "ParentODSCode": "RVN", "ParentName": "AVON AND WILTSHIRE MENTAL HEALTH PARTNERSHIP NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "2917654", @@ -2305,7 +2305,7 @@ "Phone": "", "Email": "lnwh-tr.trust@nhs.net", "Website": "https://www.lnwh.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "43033", @@ -2329,7 +2329,7 @@ "Phone": "", "Email": "", "Website": "http://www.esht.nhs.uk/eastbournedgh", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -2346,14 +2346,14 @@ "City": "BRISTOL", "County": "AVON", "Postcode": "BS5 6XX", - "Latitude": "", - "Longitude": "", + "Latitude": "51.48515271161083", + "Longitude": "-3.1650109188615785", "ParentODSCode": "RVJ", "ParentName": "NORTH BRISTOL NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -2370,14 +2370,14 @@ "City": "MANCHESTER", "County": "GREATER MANCHESTER", "Postcode": "M30 0TU", - "Latitude": "", - "Longitude": "", + "Latitude": "53.482882456526816", + "Longitude": "-2.33845275607912", "ParentODSCode": "RM3", "ParentName": "NORTHERN CARE ALLIANCE NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -2394,14 +2394,14 @@ "City": "LLANELLI", "County": "DYFED", "Postcode": "SA15 3SE", - "Latitude": "", - "Longitude": "", + "Latitude": "51.684363752680774", + "Longitude": "-4.156748736588136", "ParentODSCode": "7A2", "ParentName": "HYWEL DDA UNIVERSITY LHB", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42351", @@ -2425,7 +2425,7 @@ "Phone": "", "Email": "pals@epsom-sthelier.nhs.uk", "Website": "http://www.epsom-sthelier.nhs.uk", - "Fax": "01372 735 159" + "Fax": "01372 735 159", }, { "OrganisationID": "", @@ -2442,14 +2442,14 @@ "City": "DEESIDE", "County": "CLWYD", "Postcode": "CH5 2EP", - "Latitude": "", - "Longitude": "", + "Latitude": "53.197806016801934", + "Longitude": "-3.013333702231281", "ParentODSCode": "7A1", "ParentName": "BETSI CADWALADR UNIVERSITY LHB", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41862", @@ -2473,7 +2473,7 @@ "Phone": "", "Email": "", "Website": "http://www.newcastle-hospitals.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42129", @@ -2497,7 +2497,7 @@ "Phone": "", "Email": "", "Website": "http://www.southtees.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40395", @@ -2521,7 +2521,7 @@ "Phone": "", "Email": "", "Website": "https://www.fhft.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42186", @@ -2545,7 +2545,7 @@ "Phone": "", "Email": "trusthq@mbht.nhs.uk", "Website": "http://www.uhmb.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -2562,14 +2562,14 @@ "City": "WASHINGTON", "County": "TYNE AND WEAR", "Postcode": "NE38 7NQ", - "Latitude": "", - "Longitude": "", + "Latitude": "54.89882555930676", + "Longitude": "-1.5304436726140653", "ParentODSCode": "R0B", "ParentName": "SOUTH TYNESIDE AND SUNDERLAND NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -2586,14 +2586,14 @@ "City": "NUNEATON", "County": "WARWICKSHIRE", "Postcode": "CV10 7DJ", - "Latitude": "", - "Longitude": "", + "Latitude": "52.51178977454635", + "Longitude": "-1.47510009235745", "ParentODSCode": "RLT", "ParentName": "GEORGE ELIOT HOSPITAL NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -2610,14 +2610,14 @@ "City": "CARMARTHEN", "County": "DYFED", "Postcode": "SA31 2AF", - "Latitude": "", - "Longitude": "", + "Latitude": "51.86750511544308", + "Longitude": "-4.287558062189436", "ParentODSCode": "7A2", "ParentName": "HYWEL DDA UNIVERSITY LHB", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41889", @@ -2641,7 +2641,7 @@ "Phone": "", "Email": "", "Website": "http://www.gloshospitals.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -2658,14 +2658,14 @@ "City": "WOKING", "County": "SURREY", "Postcode": "GU21 3LQ", - "Latitude": "", - "Longitude": "", + "Latitude": "51.31890703272032", + "Longitude": "-0.5911015293992617", "ParentODSCode": "RTK", "ParentName": "ASHFORD AND ST PETER'S HOSPITALS NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "9445753", @@ -2689,7 +2689,7 @@ "Phone": "", "Email": "", "Website": "https://www.uhb.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -2706,14 +2706,14 @@ "City": "GRANTHAM", "County": "LINCOLNSHIRE", "Postcode": "NG31 8DG", - "Latitude": "", - "Longitude": "", + "Latitude": "52.92118212034121", + "Longitude": "-0.6399615735161382", "ParentODSCode": "RWD", "ParentName": "UNITED LINCOLNSHIRE HOSPITALS NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -2730,14 +2730,14 @@ "City": "GRANTHAM", "County": "LINCOLNSHIRE", "Postcode": "NG31 8DG", - "Latitude": "", - "Longitude": "", + "Latitude": "52.92118212034121", + "Longitude": "-0.6399615735161382", "ParentODSCode": "RWD", "ParentName": "UNITED LINCOLNSHIRE HOSPITALS NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41403", @@ -2761,7 +2761,7 @@ "Phone": "", "Email": "", "Website": "http://www.gosh.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -2778,14 +2778,14 @@ "City": "LONDON", "County": "GREATER LONDON", "Postcode": "SE11 4TH", - "Latitude": "", - "Longitude": "", + "Latitude": "51.49240809148386", + "Longitude": "-0.10466680536798287", "ParentODSCode": "RJ1", "ParentName": "GUY'S AND ST THOMAS' NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -2802,14 +2802,14 @@ "City": "RICHMOND", "County": "SURREY", "Postcode": "TW10 7NF", - "Latitude": "", - "Longitude": "", + "Latitude": "51.43785081919746", + "Longitude": "-0.3146123188645736", "ParentODSCode": "RY9", "ParentName": "HOUNSLOW AND RICHMOND COMMUNITY HEALTHCARE NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "43802", @@ -2833,7 +2833,7 @@ "Phone": "", "Email": "", "Website": "https://www.imperial.nhs.uk/our-locations/hammersmith-hospital", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40261", @@ -2857,7 +2857,7 @@ "Phone": "", "Email": "hello@hdft.nhs.uk", "Website": "http://www.hdft.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -2874,14 +2874,14 @@ "City": "HOUNSLOW", "County": "MIDDLESEX", "Postcode": "TW3 3EL", - "Latitude": "", - "Longitude": "", + "Latitude": "51.46830184092232", + "Longitude": "-0.3709422403006183", "ParentODSCode": "RY9", "ParentName": "HOUNSLOW AND RICHMOND COMMUNITY HEALTHCARE NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "9445752", @@ -2905,7 +2905,7 @@ "Phone": "", "Email": "", "Website": "https://www.uhb.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -2922,14 +2922,14 @@ "City": "ASCOT", "County": "", "Postcode": "SL5 7GB", - "Latitude": "", - "Longitude": "", + "Latitude": "51.40828363735065", + "Longitude": "-0.685306078413111", "ParentODSCode": "RDU", "ParentName": "FRIMLEY HEALTH NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41122", @@ -2953,7 +2953,7 @@ "Phone": "", "Email": "pals@wvt.nhs.uk", "Website": "http://www.wyevalley.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42721", @@ -2977,7 +2977,7 @@ "Phone": "", "Email": "", "Website": "http://www.enherts-tr.nhs.uk/our-hospitals/hertford-county/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42721", @@ -3001,7 +3001,7 @@ "Phone": "", "Email": "", "Website": "http://www.enherts-tr.nhs.uk/our-hospitals/hertford-county/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41954", @@ -3025,7 +3025,7 @@ "Phone": "", "Email": "contactus@northumbria.nhs.uk", "Website": "https://www.northumbria.nhs.uk/hexham", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40095", @@ -3049,7 +3049,7 @@ "Phone": "", "Email": "", "Website": "https://www.thh.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "7353080", @@ -3073,7 +3073,7 @@ "Phone": "", "Email": "", "Website": "https://www.nwangliaft.nhs.uk/our-hospitals/hinchingbrooke-hospital/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41569", @@ -3097,7 +3097,7 @@ "Phone": "", "Email": "", "Website": "http://www.homerton.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41986", @@ -3121,7 +3121,7 @@ "Phone": "", "Email": "pals@ouh.nhs.uk", "Website": "http://www.ouh.nhs.uk/hospitals/horton/default.aspx", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41072", @@ -3145,7 +3145,7 @@ "Phone": "", "Email": "info@uhcw.nhs.uk", "Website": "http://www.uhcw.nhs.uk/", - "Fax": "01788 545151" + "Fax": "01788 545151", }, { "OrganisationID": "42660", @@ -3169,7 +3169,7 @@ "Phone": "", "Email": "", "Website": "https://www.hey.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -3186,14 +3186,14 @@ "City": "ILKESTON", "County": "DERBYSHIRE", "Postcode": "DE7 8LN", - "Latitude": "", - "Longitude": "", + "Latitude": "52.988319908650226", + "Longitude": "-1.3202010304096143", "ParentODSCode": "RTG", "ParentName": "UNIVERSITY HOSPITALS OF DERBY AND BURTON NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "68584", @@ -3217,7 +3217,7 @@ "Phone": "", "Email": "communications@esneft.nhs.uk", "Website": "https://www.esneft.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -3234,14 +3234,14 @@ "City": "MANCHESTER", "County": "GREATER MANCHESTER", "Postcode": "M44 5LH", - "Latitude": "", - "Longitude": "", + "Latitude": "53.437687672356965", + "Longitude": "-2.431705058942659", "ParentODSCode": "RM3", "ParentName": "NORTHERN CARE ALLIANCE NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40663", @@ -3265,7 +3265,7 @@ "Phone": "", "Email": "pals@jpaget.nhs.uk", "Website": "http://www.jpaget.nhs.uk/", - "Fax": "01493 453086" + "Fax": "01493 453086", }, { "OrganisationID": "41987", @@ -3289,7 +3289,7 @@ "Phone": "", "Email": "pals@ouh.nhs.uk", "Website": "http://www.ouh.nhs.uk/hospitals/jr/default.aspx", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42445", @@ -3313,7 +3313,7 @@ "Phone": "", "Email": "", "Website": "http://www.ekhuft.nhs.uk/kentandcanterburyhospital", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41341", @@ -3337,7 +3337,7 @@ "Phone": "", "Email": "enquiries@kgh.nhs.uk", "Website": "http://www.kgh.nhs.uk", - "Fax": "01536 493767" + "Fax": "01536 493767", }, { "OrganisationID": "41042", @@ -3361,7 +3361,7 @@ "Phone": "", "Email": "kch-tr.palsdh@nhs.net", "Website": "http://www.kch.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41053", @@ -3385,7 +3385,7 @@ "Phone": "", "Email": "sfh-tr.pet@nhs.net", "Website": "http://www.sfh-tr.nhs.uk", - "Fax": "01623 621770" + "Fax": "01623 621770", }, { "OrganisationID": "40109", @@ -3409,7 +3409,7 @@ "Phone": "", "Email": "khft.pals@nhs.net", "Website": "http://www.kingstonhospital.nhs.uk/", - "Fax": "020 85472182" + "Fax": "020 85472182", }, { "OrganisationID": "", @@ -3426,14 +3426,14 @@ "City": "BRISTOL", "County": "AVON", "Postcode": "BS15 4DA", - "Latitude": "", - "Longitude": "", + "Latitude": "51.463190232745326", + "Longitude": "-2.499945216472451", "ParentODSCode": "RVN", "ParentName": "AVON AND WILTSHIRE MENTAL HEALTH PARTNERSHIP NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41611", @@ -3457,7 +3457,7 @@ "Phone": "", "Email": "", "Website": "http://www.leedsth.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42680", @@ -3481,7 +3481,7 @@ "Phone": "", "Email": "", "Website": "http://www.leicestershospitals.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40199", @@ -3505,7 +3505,7 @@ "Phone": "", "Email": "", "Website": "http://www.mchft.nhs.uk", - "Fax": "01270 587696" + "Fax": "01270 587696", }, { "OrganisationID": "", @@ -3522,14 +3522,14 @@ "City": "LEAMINGTON SPA", "County": "WARWICKSHIRE", "Postcode": "CV32 7SF", - "Latitude": "", - "Longitude": "", + "Latitude": "52.303557274248234", + "Longitude": "-1.519704461685684", "ParentODSCode": "RJC", "ParentName": "SOUTH WARWICKSHIRE UNIVERSITY NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42670", @@ -3553,7 +3553,7 @@ "Phone": "", "Email": "", "Website": "http://www.ulh.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42670", @@ -3577,7 +3577,7 @@ "Phone": "", "Email": "", "Website": "http://www.ulh.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42718", @@ -3601,7 +3601,7 @@ "Phone": "", "Email": "", "Website": "http://www.enherts-tr.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42718", @@ -3625,7 +3625,7 @@ "Phone": "", "Email": "", "Website": "http://www.enherts-tr.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40233", @@ -3649,7 +3649,7 @@ "Phone": "", "Email": "pals@ldh.nhs.uk", "Website": "https://www.ldh.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -3666,14 +3666,14 @@ "City": "LYMINGTON", "County": "HAMPSHIRE", "Postcode": "SO41 8QD", - "Latitude": "", - "Longitude": "", + "Latitude": "50.76950814986551", + "Longitude": "-1.5442285268512799", "ParentODSCode": "RW1", "ParentName": "SOUTHERN HEALTH NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41031", @@ -3697,7 +3697,7 @@ "Phone": "", "Email": "", "Website": "http://www.eastcheshire.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40242", @@ -3721,7 +3721,7 @@ "Phone": "", "Email": "", "Website": "http://www.york.nhs.uk", - "Fax": "01653 604521" + "Fax": "01653 604521", }, { "OrganisationID": "", @@ -3738,14 +3738,14 @@ "City": "MANCHESTER", "County": "", "Postcode": "M1 6LT", - "Latitude": "", - "Longitude": "", + "Latitude": "53.47525438212471", + "Longitude": "-2.2400255028448073", "ParentODSCode": "R0A", "ParentName": "MANCHESTER UNIVERSITY NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40154", @@ -3769,7 +3769,7 @@ "Phone": "", "Email": "contactus@walsallhealthcare.nhs.uk", "Website": "http://www.walsallhealthcare.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40330", @@ -3793,7 +3793,7 @@ "Phone": "", "Email": "", "Website": "http://www.mkuh.nhs.uk", - "Fax": "01908 669348" + "Fax": "01908 669348", }, { "OrganisationID": "", @@ -3810,14 +3810,14 @@ "City": "MILTON KEYNES", "County": "BUCKINGHAMSHIRE", "Postcode": "MK6 5LD", - "Latitude": "", - "Longitude": "", + "Latitude": "52.02494542953305", + "Longitude": "-0.7362993875660501", "ParentODSCode": "RD8", "ParentName": "MILTON KEYNES UNIVERSITY HOSPITAL NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -3834,14 +3834,14 @@ "City": "SWINDON", "County": "", "Postcode": "SN2 2JG", - "Latitude": "", - "Longitude": "", + "Latitude": "51.58372293121609", + "Longitude": "-1.8112139471060151", "ParentODSCode": "RN3", "ParentName": "GREAT WESTERN HOSPITALS NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -3858,14 +3858,14 @@ "City": "SWANSEA", "County": "WEST GLAMORGAN", "Postcode": "SA6 6NL", - "Latitude": "", - "Longitude": "", + "Latitude": "51.68358816236783", + "Longitude": "-3.9342430863145736", "ParentODSCode": "7A3", "ParentName": "SWANSEA BAY UNIVERSITY LOCAL HEALTH BOARD", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -3882,14 +3882,14 @@ "City": "LONDON", "County": "GREATER LONDON", "Postcode": "NW5 2BX", - "Latitude": "", - "Longitude": "", + "Latitude": "51.54647338497589", + "Longitude": "-0.13911479241869015", "ParentODSCode": "RV3", "ParentName": "CENTRAL AND NORTH WEST LONDON NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -3906,14 +3906,14 @@ "City": "PLYMOUTH", "County": "DEVON", "Postcode": "PL4 7QD", - "Latitude": "", - "Longitude": "", + "Latitude": "50.37910560957554", + "Longitude": "-4.112683344124941", "ParentODSCode": "RK9", "ParentName": "UNIVERSITY HOSPITALS PLYMOUTH NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -3930,14 +3930,14 @@ "City": "TAUNTON", "County": "", "Postcode": "TA1 5DA", - "Latitude": "", - "Longitude": "", + "Latitude": "51.01144171713463", + "Longitude": "-3.121313219854067", "ParentODSCode": "RH5", "ParentName": "SOMERSET NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -3954,14 +3954,14 @@ "City": "PORT TALBOT", "County": "WEST GLAMORGAN", "Postcode": "SA12 7BX", - "Latitude": "", - "Longitude": "", + "Latitude": "51.59929960775662", + "Longitude": "-3.7997706458406086", "ParentODSCode": "7A3", "ParentName": "SWANSEA BAY UNIVERSITY LOCAL HEALTH BOARD", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -3978,14 +3978,14 @@ "City": "ABERGAVENNY", "County": "", "Postcode": "NP7 7EG", - "Latitude": "", - "Longitude": "", + "Latitude": "51.82444440250398", + "Longitude": "-3.0326164868528998", "ParentODSCode": "7A6", "ParentName": "ANEURIN BEVAN UNIVERSITY LHB", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41093", @@ -4009,7 +4009,7 @@ "Phone": "", "Email": "rwh-tr.pals@nhs.net", "Website": "http://www.royalwolverhampton.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -4026,14 +4026,14 @@ "City": "WILLENHALL", "County": "WEST MIDLANDS", "Postcode": "WV12 5RZ", - "Latitude": "", - "Longitude": "", + "Latitude": "52.60933623348631", + "Longitude": "-2.039815918790231", "ParentODSCode": "RBK", "ParentName": "WALSALL HEALTHCARE NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "39967", @@ -4057,7 +4057,7 @@ "Phone": "", "Email": "nuhpals@bartshealth.nhs.uk", "Website": "http://www.bartshealth.nhs.uk", - "Fax": "020 7363 8181" + "Fax": "020 7363 8181", }, { "OrganisationID": "43679", @@ -4081,7 +4081,7 @@ "Phone": "", "Email": "dchst.patientexperienceteam@nhs.net", "Website": "http://www.dchs.nhs.uk/home_redesign/our-services/find_services_by_location/newholmehospital/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -4098,14 +4098,14 @@ "City": "CARDIFF", "County": "SOUTH GLAMORGAN", "Postcode": "CF14 4XW", - "Latitude": "", - "Longitude": "", + "Latitude": "51.50821561439174", + "Longitude": "-3.187478640298089", "ParentODSCode": "7A4", "ParentName": "CARDIFF & VALE UNIVERSITY LHB", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41147", @@ -4129,7 +4129,7 @@ "Phone": "", "Email": "communications@nnuh.nhs.uk", "Website": "http://www.nnuh.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -4146,14 +4146,14 @@ "City": "BARNSTAPLE", "County": "DEVON", "Postcode": "EX31 4JB", - "Latitude": "", - "Longitude": "", + "Latitude": "51.09169453822436", + "Longitude": "-4.050650933822214", "ParentODSCode": "RH8", "ParentName": "ROYAL DEVON UNIVERSITY HEALTHCARE NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -4170,14 +4170,14 @@ "City": "MANCHESTER", "County": "GREATER MANCHESTER", "Postcode": "M8 5RB", - "Latitude": "", - "Longitude": "", + "Latitude": "53.51753825484743", + "Longitude": "-2.227934753662899", "ParentODSCode": "R0A", "ParentName": "MANCHESTER UNIVERSITY NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40091", @@ -4201,7 +4201,7 @@ "Phone": "", "Email": "", "Website": "http://www.northmid.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41969", @@ -4225,7 +4225,7 @@ "Phone": "", "Email": "contactus@northumbria.nhs.uk", "Website": "https://www.northumbria.nhs.uk/north-tyneside", - "Fax": "" + "Fax": "", }, { "OrganisationID": "2917659", @@ -4249,7 +4249,7 @@ "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "2131250", @@ -4273,7 +4273,7 @@ "Phone": "", "Email": "contactus@northumbria.nhs.uk", "Website": "https://www.northumbria.nhs.uk/emergency/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "2917651", @@ -4297,7 +4297,7 @@ "Phone": "", "Email": "lnwh-tr.trust@nhs.net", "Website": "http://www.lnwh.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "43655", @@ -4321,7 +4321,7 @@ "Phone": "", "Email": "", "Website": "http://www.norfolkcommunityhealthandcare.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -4338,14 +4338,14 @@ "City": "LEAMINGTON SPA", "County": "", "Postcode": "CV32 6RW", - "Latitude": "", - "Longitude": "", + "Latitude": "52.31102428092342", + "Longitude": "-1.539027380726561", "ParentODSCode": "RJC", "ParentName": "SOUTH WARWICKSHIRE UNIVERSITY NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42484", @@ -4369,7 +4369,7 @@ "Phone": "", "Email": "soh-tr.info@nhs.net", "Website": "http://www.southportandormskirk.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -4386,14 +4386,14 @@ "City": "BRISTOL", "County": "AVON", "Postcode": "BS14 0BB", - "Latitude": "", - "Longitude": "", + "Latitude": "51.411698943237695", + "Longitude": "-2.5924267932069522", "ParentODSCode": "RVN", "ParentName": "AVON AND WILTSHIRE MENTAL HEALTH PARTNERSHIP NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -4410,14 +4410,14 @@ "City": "LONDON", "County": "GREATER LONDON", "Postcode": "N19 5NF", - "Latitude": "", - "Longitude": "", + "Latitude": "51.566442088321025", + "Longitude": "-0.1388682606380006", "ParentODSCode": "RKE", "ParentName": "WHITTINGTON HEALTH NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -4434,14 +4434,14 @@ "City": "BRISTOL", "County": "AVON", "Postcode": "BS34 5PE", - "Latitude": "", - "Longitude": "", + "Latitude": "51.53153185971164", + "Longitude": "-2.5764474476942887", "ParentODSCode": "RVJ", "ParentName": "NORTH BRISTOL NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -4458,14 +4458,14 @@ "City": "COVENTRY", "County": "WEST MIDLANDS", "Postcode": "CV1 4FS", - "Latitude": "", - "Longitude": "", + "Latitude": "52.414650840431534", + "Longitude": "-1.5059966663377762", "ParentODSCode": "RYG", "ParentName": "COVENTRY AND WARWICKSHIRE PARTNERSHIP NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -4482,14 +4482,14 @@ "City": "SALFORD", "County": "GREATER MANCHESTER", "Postcode": "M6 5FX", - "Latitude": "", - "Longitude": "", + "Latitude": "53.48857472553247", + "Longitude": "-2.284088128527958", "ParentODSCode": "RM3", "ParentName": "NORTHERN CARE ALLIANCE NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40658", @@ -4513,7 +4513,7 @@ "Phone": "", "Email": "", "Website": "https://www.nwangliaft.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42671", @@ -4537,7 +4537,7 @@ "Phone": "", "Email": "", "Website": "http://www.ulh.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42671", @@ -4561,7 +4561,7 @@ "Phone": "", "Email": "", "Website": "http://www.ulh.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "43048", @@ -4585,7 +4585,7 @@ "Phone": "", "Email": "", "Website": "http://www.midyorks.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -4602,14 +4602,14 @@ "City": "POOLE", "County": "", "Postcode": "BH15 2JB", - "Latitude": "", - "Longitude": "", + "Latitude": "50.72147613368994", + "Longitude": "-1.9730246133611542", "ParentODSCode": "R0D", "ParentName": "UNIVERSITY HOSPITALS DORSET NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -4626,14 +4626,14 @@ "City": "MERTHYR TYDFIL", "County": "MID GLAMORGAN", "Postcode": "CF47 9DT", - "Latitude": "", - "Longitude": "", + "Latitude": "51.7642467133928", + "Longitude": "-3.3851091212405837", "ParentODSCode": "7A5", "ParentName": "CWM TAF MORGANNWG UNIVERSITY LOCAL HEALTH BOARD", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -4650,14 +4650,14 @@ "City": "LLANELLI", "County": "DYFED", "Postcode": "SA14 8QF", - "Latitude": "", - "Longitude": "", + "Latitude": "51.69187728463355", + "Longitude": "-4.136129731794417", "ParentODSCode": "7A2", "ParentName": "HYWEL DDA UNIVERSITY LHB", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41556", @@ -4681,7 +4681,7 @@ "Phone": "", "Email": "", "Website": "http://www.pah.nhs.uk/", - "Fax": "01279 429371" + "Fax": "01279 429371", }, { "OrganisationID": "40847", @@ -4705,7 +4705,7 @@ "Phone": "", "Email": "patientsupportservices@uhs.nhs.uk", "Website": "http://www.uhs.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -4722,14 +4722,14 @@ "City": "BRIDGEND", "County": "MID GLAMORGAN", "Postcode": "CF31 1RQ", - "Latitude": "", - "Longitude": "", + "Latitude": "51.517616936821376", + "Longitude": "-3.571760230503241", "ParentODSCode": "7A3", "ParentName": "SWANSEA BAY UNIVERSITY LOCAL HEALTH BOARD", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "1094756", @@ -4753,7 +4753,7 @@ "Phone": "", "Email": "", "Website": "http://pruh.kch.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -4770,14 +4770,14 @@ "City": "NOTTINGHAM", "County": "NOTTINGHAMSHIRE", "Postcode": "NG7 2UH", - "Latitude": "", - "Longitude": "", + "Latitude": "52.94378634553726", + "Longitude": "-1.1841889169193678", "ParentODSCode": "RX1", "ParentName": "NOTTINGHAM UNIVERSITY HOSPITALS NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -4794,14 +4794,14 @@ "City": "NOTTINGHAM", "County": "NOTTINGHAMSHIRE", "Postcode": "NG7 2UH", - "Latitude": "", - "Longitude": "", + "Latitude": "52.94378634553726", + "Longitude": "-1.1841889169193678", "ParentODSCode": "RX1", "ParentName": "NOTTINGHAM UNIVERSITY HOSPITALS NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40868", @@ -4825,7 +4825,7 @@ "Phone": "", "Email": "", "Website": "http://www.porthosp.nhs.uk", - "Fax": "023 9286 6413" + "Fax": "023 9286 6413", }, { "OrganisationID": "43803", @@ -4849,7 +4849,7 @@ "Phone": "", "Email": "", "Website": "https://www.imperial.nhs.uk/our-locations/queen-charlottes-and-chelsea-hospital", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41607", @@ -4873,7 +4873,7 @@ "Phone": "", "Email": "ghnt.pals.service@nhs.net", "Website": "http://www.qegateshead.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "68511", @@ -4897,7 +4897,7 @@ "Phone": "", "Email": "", "Website": "http://www.lewishamandgreenwich.nhs.uk/", - "Fax": "020 8836 4590" + "Fax": "020 8836 4590", }, { "OrganisationID": "42720", @@ -4921,7 +4921,7 @@ "Phone": "", "Email": "", "Website": "http://www.newqeii.info/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42720", @@ -4945,7 +4945,7 @@ "Phone": "", "Email": "", "Website": "http://www.newqeii.info/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42366", @@ -4969,7 +4969,7 @@ "Phone": "", "Email": "", "Website": "http://www.ekhuft.nhs.uk/qeqm", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42349", @@ -4993,7 +4993,7 @@ "Phone": "", "Email": "est-tr.pals@nhs.net", "Website": "http://www.epsom-sthelier.nhs.uk", - "Fax": "020 8641 4546" + "Fax": "020 8641 4546", }, { "OrganisationID": "", @@ -5010,14 +5010,14 @@ "City": "SIDCUP", "County": "KENT", "Postcode": "DA14 6LT", - "Latitude": "", - "Longitude": "", + "Latitude": "51.41924154086068", + "Longitude": "0.1034220330275305", "ParentODSCode": "RN7", "ParentName": "DARTFORD AND GRAVESHAM NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -5034,14 +5034,14 @@ "City": "EAST GRINSTEAD", "County": "WEST SUSSEX", "Postcode": "RH19 3DZ", - "Latitude": "", - "Longitude": "", + "Latitude": "51.135209751134006", + "Longitude": "-0.0011303249790203587", "ParentODSCode": "RWF", "ParentName": "MAIDSTONE AND TUNBRIDGE WELLS NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40568", @@ -5065,7 +5065,7 @@ "Phone": "", "Email": "", "Website": "http://www.bhrhospitals.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -5082,14 +5082,14 @@ "City": "RIPLEY", "County": "DERBYSHIRE", "Postcode": "DE5 3HE", - "Latitude": "", - "Longitude": "", + "Latitude": "53.048059214497236", + "Longitude": "-1.409870024857542", "ParentODSCode": "RTG", "ParentName": "UNIVERSITY HOSPITALS OF DERBY AND BURTON NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -5106,14 +5106,14 @@ "City": "NUNEATON", "County": "WARWICKSHIRE", "Postcode": "CV11 5TY", - "Latitude": "", - "Longitude": "", + "Latitude": "52.518697799442705", + "Longitude": "-1.466747350783283", "ParentODSCode": "RJC", "ParentName": "SOUTH WARWICKSHIRE UNIVERSITY NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -5130,14 +5130,14 @@ "City": "HEREFORD", "County": "HEREFORDSHIRE", "Postcode": "HR2 7RL", - "Latitude": "", - "Longitude": "", + "Latitude": "52.04679652818128", + "Longitude": "-2.720555916976644", "ParentODSCode": "RLQ", "ParentName": "WYE VALLEY NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40573", @@ -5161,7 +5161,7 @@ "Phone": "", "Email": "", "Website": "http://www.therotherhamft.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41724", @@ -5185,7 +5185,7 @@ "Phone": "", "Email": "patient.relations@wwl.nhs.uk", "Website": "http://www.wwl.nhs.uk/hospitals/raei.aspx", - "Fax": "01942 822042" + "Fax": "01942 822042", }, { "OrganisationID": "40884", @@ -5209,7 +5209,7 @@ "Phone": "", "Email": "nhs.choices@royalberkshire.nhs.uk", "Website": "http://www.royalberkshire.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "43224", @@ -5233,7 +5233,7 @@ "Phone": "", "Email": "contact@elht.nhs.uk", "Website": "http://www.elht.nhs.uk", - "Fax": "01254 293512" + "Fax": "01254 293512", }, { "OrganisationID": "41197", @@ -5257,7 +5257,7 @@ "Phone": "", "Email": "", "Website": "http://www.boltonft.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "10955535", @@ -5281,7 +5281,7 @@ "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40472", @@ -5305,7 +5305,7 @@ "Phone": "", "Email": "rcht.patientexperience@nhs.net", "Website": "http://www.royalcornwall.nhs.uk", - "Fax": "01872 252708" + "Fax": "01872 252708", }, { "OrganisationID": "41982", @@ -5329,7 +5329,7 @@ "Phone": "", "Email": "uhdb.contactpalsderby@nhs.net", "Website": "https://www.uhdb.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -5346,14 +5346,14 @@ "City": "EXETER", "County": "DEVON", "Postcode": "EX2 5DW", - "Latitude": "", - "Longitude": "", + "Latitude": "50.71705628305987", + "Longitude": "-3.5057143220612685", "ParentODSCode": "RK9", "ParentName": "UNIVERSITY HOSPITALS PLYMOUTH NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40782", @@ -5377,7 +5377,7 @@ "Phone": "", "Email": "rde-tr.pals@nhs.net", "Website": "http://www.rdehospital.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40073", @@ -5401,7 +5401,7 @@ "Phone": "", "Email": "rf.pals@nhs.net", "Website": "http://www.royalfree.nhs.uk/", - "Fax": "020 7830 2468" + "Fax": "020 7830 2468", }, { "OrganisationID": "", @@ -5418,14 +5418,14 @@ "City": "NEWPORT", "County": "GWENT", "Postcode": "NP20 2UB", - "Latitude": "", - "Longitude": "", + "Latitude": "51.58019390984616", + "Longitude": "-2.9959470071754546", "ParentODSCode": "7A6", "ParentName": "ANEURIN BEVAN UNIVERSITY LHB", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41261", @@ -5449,7 +5449,7 @@ "Phone": "", "Email": "hampshire.hospitals@hhft.nhs.uk", "Website": "http://www.hampshirehospitals.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "8228423", @@ -5473,7 +5473,7 @@ "Phone": "", "Email": "", "Website": "https://mft.nhs.uk/rmch/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -5490,14 +5490,14 @@ "City": "OLDHAM", "County": "LANCASHIRE", "Postcode": "OL1 2JH", - "Latitude": "", - "Longitude": "", + "Latitude": "53.55276952726198", + "Longitude": "-2.12276427085255", "ParentODSCode": "RM3", "ParentName": "NORTHERN CARE ALLIANCE NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "43183", @@ -5521,7 +5521,7 @@ "Phone": "", "Email": "enquiries@lthtr.nhs.uk", "Website": "http://www.lancsteachinghospitals.nhs.uk", - "Fax": "01772 522162" + "Fax": "01772 522162", }, { "OrganisationID": "43348", @@ -5545,7 +5545,7 @@ "Phone": "", "Email": "sath.commsteam@nhs.net", "Website": "http://www.sath.nhs.uk/", - "Fax": "01743 261006" + "Fax": "01743 261006", }, { "OrganisationID": "", @@ -5562,14 +5562,14 @@ "City": "SOUTHAMPTON", "County": "HAMPSHIRE", "Postcode": "SO14 0YG", - "Latitude": "", - "Longitude": "", + "Latitude": "50.91243896406982", + "Longitude": "-1.395580557527431", "ParentODSCode": "RHM", "ParentName": "UNIVERSITY HOSPITAL SOUTHAMPTON NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41013", @@ -5593,7 +5593,7 @@ "Phone": "", "Email": "", "Website": "https://www.uhnm.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "39970", @@ -5617,7 +5617,7 @@ "Phone": "", "Email": "", "Website": "http://www.royalsurrey.nhs.uk", - "Fax": "01483 537747" + "Fax": "01483 537747", }, { "OrganisationID": "", @@ -5634,14 +5634,14 @@ "City": "BATH", "County": "AVON", "Postcode": "BA1 3NG", - "Latitude": "", - "Longitude": "", + "Latitude": "51.39191421412843", + "Longitude": "-2.3901410048590637", "ParentODSCode": "RNZ", "ParentName": "SALISBURY NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -5658,14 +5658,14 @@ "City": "FOLKESTONE", "County": "KENT", "Postcode": "CT19 5BN", - "Latitude": "", - "Longitude": "", + "Latitude": "51.08610340124624", + "Longitude": "1.1728709210454833", "ParentODSCode": "RVV", "ParentName": "EAST KENT HOSPITALS UNIVERSITY NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -5682,14 +5682,14 @@ "City": "WALSALL", "County": "WEST MIDLANDS", "Postcode": "WS4 1HB", - "Latitude": "", - "Longitude": "", + "Latitude": "52.608415215290684", + "Longitude": "-1.9586749294735848", "ParentODSCode": "RBK", "ParentName": "WALSALL HEALTHCARE NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41268", @@ -5713,7 +5713,7 @@ "Phone": "", "Email": "communications@dgh.nhs.uk", "Website": "https://www.dgft.nhs.uk/", - "Fax": "01384 244051" + "Fax": "01384 244051", }, { "OrganisationID": "", @@ -5730,14 +5730,14 @@ "City": "SHEFFIELD", "County": "SOUTH YORKSHIRE", "Postcode": "S10 5DD", - "Latitude": "", - "Longitude": "", + "Latitude": "53.37768457660176", + "Longitude": "-1.5127835592201522", "ParentODSCode": "RCU", "ParentName": "SHEFFIELD CHILDREN'S NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41161", @@ -5761,7 +5761,7 @@ "Phone": "", "Email": "", "Website": "https://www.northerncarealliance.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41354", @@ -5785,7 +5785,7 @@ "Phone": "", "Email": "sft.pals@nhs.net", "Website": "http://www.salisbury.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "9242464", @@ -5809,7 +5809,7 @@ "Phone": "", "Email": "", "Website": "https://www.uhdb.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -5826,14 +5826,14 @@ "City": "SALFORD", "County": "GREATER MANCHESTER", "Postcode": "M5 4DG", - "Latitude": "", - "Longitude": "", + "Latitude": "53.47949508450214", + "Longitude": "-2.2794789420216426", "ParentODSCode": "RM3", "ParentName": "NORTHERN CARE ALLIANCE NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "43107", @@ -5857,7 +5857,7 @@ "Phone": "", "Email": "", "Website": "http://www.swbh.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "864773", @@ -5881,7 +5881,7 @@ "Phone": "", "Email": "", "Website": "http://www.york.nhs.uk", - "Fax": "01723 342 581" + "Fax": "01723 342 581", }, { "OrganisationID": "41021", @@ -5905,7 +5905,7 @@ "Phone": "", "Email": "", "Website": "http://www.nlg.nhs.uk/hospitals/scunthorpe", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40282", @@ -5929,7 +5929,7 @@ "Phone": "", "Email": "sheffield.childrenshospital@sch.nhs.uk", "Website": "http://www.sheffieldchildrens.nhs.uk/", - "Fax": "0114 272 3418" + "Fax": "0114 272 3418", }, { "OrganisationID": "", @@ -5946,14 +5946,14 @@ "City": "NOTTINGHAM", "County": "NOTTINGHAMSHIRE", "Postcode": "NG5 4AD", - "Latitude": "", - "Longitude": "", + "Latitude": "52.98385661535865", + "Longitude": "-1.1409483690398214", "ParentODSCode": "RK5", "ParentName": "SHERWOOD FOREST HOSPITALS NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -5970,14 +5970,14 @@ "City": "LONDON", "County": "GREATER LONDON", "Postcode": "N10 3HU", - "Latitude": "", - "Longitude": "", + "Latitude": "51.5871549291953", + "Longitude": "-0.1487583672793104", "ParentODSCode": "RKE", "ParentName": "WHITTINGTON HEALTH NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -5994,14 +5994,14 @@ "City": "SWANSEA", "County": "WEST GLAMORGAN", "Postcode": "SA2 8QA", - "Latitude": "", - "Longitude": "", + "Latitude": "51.60947883960302", + "Longitude": "-3.9845510304974274", "ParentODSCode": "7A3", "ParentName": "SWANSEA BAY UNIVERSITY LOCAL HEALTH BOARD", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -6018,14 +6018,14 @@ "City": "SKEGNESS", "County": "LINCOLNSHIRE", "Postcode": "PE25 2BS", - "Latitude": "", - "Longitude": "", + "Latitude": "53.14513129808872", + "Longitude": "0.33316904076485293", "ParentODSCode": "RWD", "ParentName": "UNITED LINCOLNSHIRE HOSPITALS NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "9445754", @@ -6049,7 +6049,7 @@ "Phone": "", "Email": "", "Website": "https://www.uhb.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40002", @@ -6073,7 +6073,7 @@ "Phone": "", "Email": "", "Website": "http://www.uhbristol.nhs.uk/patients-and-visitors/your-hospitals/south-bristol-community-hospital/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -6090,14 +6090,14 @@ "City": "BRISTOL", "County": "AVON", "Postcode": "BS16 7FL", - "Latitude": "", - "Longitude": "", + "Latitude": "51.531696956239784", + "Longitude": "-2.5728256133103904", "ParentODSCode": "RVJ", "ParentName": "NORTH BRISTOL NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40843", @@ -6121,7 +6121,7 @@ "Phone": "", "Email": "patientsupportservices@uhs.nhs.uk", "Website": "http://www.uhs.nhs.uk", - "Fax": "023 8120 4715" + "Fax": "023 8120 4715", }, { "OrganisationID": "40067", @@ -6145,7 +6145,7 @@ "Phone": "", "Email": "", "Website": "https://www.mse.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -6162,14 +6162,14 @@ "City": "BUSHEY", "County": "", "Postcode": "WD23 1RD", - "Latitude": "", - "Longitude": "", + "Latitude": "51.637390004846814", + "Longitude": "-0.3305057868647216", "ParentODSCode": "RWG", "ParentName": "WEST HERTFORDSHIRE TEACHING HOSPITALS NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -6186,14 +6186,14 @@ "City": "CARLISLE", "County": "CUMBRIA", "Postcode": "CA2 7HE", - "Latitude": "", - "Longitude": "", + "Latitude": "54.88822587966728", + "Longitude": "-2.972025091691566", "ParentODSCode": "RNN", "ParentName": "NORTH CUMBRIA INTEGRATED CARE NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -6210,14 +6210,14 @@ "City": "NOTTINGHAM", "County": "NOTTINGHAMSHIRE", "Postcode": "NG3 3GG", - "Latitude": "", - "Longitude": "", + "Latitude": "52.96281834731625", + "Longitude": "-1.1361160766496041", "ParentODSCode": "RX1", "ParentName": "NOTTINGHAM UNIVERSITY HOSPITALS NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40949", @@ -6241,7 +6241,7 @@ "Phone": "", "Email": "communications@stgeorges.nhs.uk", "Website": "http://www.stgeorges.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40169", @@ -6265,7 +6265,7 @@ "Phone": "", "Email": "", "Website": "http://www.sthk.nhs.uk", - "Fax": "01744 646301" + "Fax": "01744 646301", }, { "OrganisationID": "", @@ -6282,14 +6282,14 @@ "City": "IPSWICH", "County": "", "Postcode": "IP3 8LX", - "Latitude": "", - "Longitude": "", + "Latitude": "52.05222911984776", + "Longitude": "1.196965668228381", "ParentODSCode": "RDE", "ParentName": "EAST SUFFOLK AND NORTH ESSEX NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42347", @@ -6313,7 +6313,7 @@ "Phone": "", "Email": "pals@epsom-sthelier.nhs.uk", "Website": "http://www.epsom-sthelier.nhs.uk", - "Fax": "020 8641 4546" + "Fax": "020 8641 4546", }, { "OrganisationID": "41615", @@ -6337,7 +6337,7 @@ "Phone": "", "Email": "", "Website": "http://www.leedsth.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -6354,14 +6354,14 @@ "City": "WALSALL", "County": "WEST MIDLANDS", "Postcode": "WS9 9LP", - "Latitude": "", - "Longitude": "", + "Latitude": "52.625493402530005", + "Longitude": "-1.9347685780981938", "ParentODSCode": "RBK", "ParentName": "WALSALL HEALTHCARE NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41381", @@ -6385,7 +6385,7 @@ "Phone": "", "Email": "", "Website": "http://www.nhft.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "60338", @@ -6409,7 +6409,7 @@ "Phone": "", "Email": "iownt.pals@nhs.net", "Website": "https://www.iow.nhs.uk/our-services/mental-health-services/isle-talk.htm", - "Fax": "" + "Fax": "", }, { "OrganisationID": "43800", @@ -6433,7 +6433,7 @@ "Phone": "", "Email": "", "Website": "https://www.imperial.nhs.uk/our-locations/st-marys-hospital", - "Fax": "" + "Fax": "", }, { "OrganisationID": "39991", @@ -6457,7 +6457,7 @@ "Phone": "", "Email": "", "Website": "http://www.uhbristol.nhs.uk/your-hospitals/st-michaels-hospital.html", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42026", @@ -6481,7 +6481,7 @@ "Phone": "", "Email": "asp-tr.patient.advice@nhs.net", "Website": "http://www.ashfordstpeters.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "43869", @@ -6505,7 +6505,7 @@ "Phone": "", "Email": "", "Website": "https://www.uhsussex.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -6522,14 +6522,14 @@ "City": "STOCKPORT", "County": "CHESHIRE", "Postcode": "SK3 8DN", - "Latitude": "", - "Longitude": "", + "Latitude": "53.401726817055895", + "Longitude": "-2.160042086204846", "ParentODSCode": "RWJ", "ParentName": "STOCKPORT NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40919", @@ -6553,7 +6553,7 @@ "Phone": "", "Email": "pals@gstt.nhs.uk", "Website": "https://www.guysandstthomas.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -6570,14 +6570,14 @@ "City": "NEWPORT", "County": "GWENT", "Postcode": "NP20 4SZ", - "Latitude": "", - "Longitude": "", + "Latitude": "51.58256284027904", + "Longitude": "-3.002913490019792", "ParentODSCode": "7A6", "ParentName": "ANEURIN BEVAN UNIVERSITY LHB", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -6594,14 +6594,14 @@ "City": "STAINES", "County": "MIDDLESEX", "Postcode": "TW18 1XD", - "Latitude": "", - "Longitude": "", + "Latitude": "51.429085727109076", + "Longitude": "-0.5006946625144656", "ParentODSCode": "RTK", "ParentName": "ASHFORD AND ST PETER'S HOSPITALS NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42725", @@ -6625,7 +6625,7 @@ "Phone": "", "Email": "pcs@stockport.nhs.uk", "Website": "http://www.stockport.nhs.uk", - "Fax": "0161 419 4679" + "Fax": "0161 419 4679", }, { "OrganisationID": "43208", @@ -6649,7 +6649,7 @@ "Phone": "", "Email": "", "Website": "http://www.buckshealthcare.nhs.uk/For%20patients%20and%20visitors/stoke-mandeville-hospital.htm", - "Fax": "01296 316640" + "Fax": "01296 316640", }, { "OrganisationID": "", @@ -6666,14 +6666,14 @@ "City": "NOTTINGHAM", "County": "NOTTINGHAMSHIRE", "Postcode": "NG8 6LN", - "Latitude": "", - "Longitude": "", + "Latitude": "52.97347694794381", + "Longitude": "-1.2266910978761432", "ParentODSCode": "RX1", "ParentName": "NOTTINGHAM UNIVERSITY HOSPITALS NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "10512886", @@ -6697,7 +6697,7 @@ "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -6714,14 +6714,14 @@ "City": "SWINDON", "County": "WILTSHIRE", "Postcode": "SN1 1ED", - "Latitude": "", - "Longitude": "", + "Latitude": "51.563553146767845", + "Longitude": "-1.7808641421440663", "ParentODSCode": "RN3", "ParentName": "GREAT WESTERN HOSPITALS NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -6738,14 +6738,14 @@ "City": "MANCHESTER", "County": "GREATER MANCHESTER", "Postcode": "M27 6BP", - "Latitude": "", - "Longitude": "", + "Latitude": "53.51252327906953", + "Longitude": "-2.3414097174362394", "ParentODSCode": "RM3", "ParentName": "NORTHERN CARE ALLIANCE NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41222", @@ -6769,7 +6769,7 @@ "Phone": "", "Email": "", "Website": "http://www.tamesidehospital.nhs.uk", - "Fax": "0161 331 6026" + "Fax": "0161 331 6026", }, { "OrganisationID": "", @@ -6786,14 +6786,14 @@ "City": "SOUTHAMPTON", "County": "HAMPSHIRE", "Postcode": "SO16 4XE", - "Latitude": "", - "Longitude": "", + "Latitude": "50.92486696906429", + "Longitude": "-1.4478521063594827", "ParentODSCode": "R1C", "ParentName": "SOLENT NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -6810,14 +6810,14 @@ "City": "CWMBRAN", "County": "", "Postcode": "NP44 8YN", - "Latitude": "", - "Longitude": "", + "Latitude": "51.64835102816606", + "Longitude": "-2.9959835016593503", "ParentODSCode": "7A6", "ParentName": "ANEURIN BEVAN UNIVERSITY LHB", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41247", @@ -6841,7 +6841,7 @@ "Phone": "", "Email": "", "Website": "https://www.gwh.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42131", @@ -6865,7 +6865,7 @@ "Phone": "", "Email": "", "Website": "https://www.southtees.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42686", @@ -6889,7 +6889,7 @@ "Phone": "", "Email": "mtw-tr.palsoffice@nhs.net", "Website": "http://www.mtw.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -6906,14 +6906,14 @@ "City": "PORT TALBOT", "County": "WEST GLAMORGAN", "Postcode": "SA13 2BN", - "Latitude": "", - "Longitude": "", + "Latitude": "51.582985138982984", + "Longitude": "-3.768102780225508", "ParentODSCode": "7A3", "ParentName": "SWANSEA BAY UNIVERSITY LOCAL HEALTH BOARD", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -6930,14 +6930,14 @@ "City": "RUGBY", "County": "WARWICKSHIRE", "Postcode": "CV21 3SR", - "Latitude": "", - "Longitude": "", + "Latitude": "52.370419434583766", + "Longitude": "-1.252717489422648", "ParentODSCode": "RJC", "ParentName": "SOUTH WARWICKSHIRE UNIVERSITY NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "43349", @@ -6961,7 +6961,7 @@ "Phone": "", "Email": "sath.commsteam@nhs.net", "Website": "http://www.sath.nhs.uk", - "Fax": "01952 243405" + "Fax": "01952 243405", }, { "OrganisationID": "40289", @@ -6985,7 +6985,7 @@ "Phone": "", "Email": "", "Website": "http://www.qehkl.nhs.uk", - "Fax": "01553 613 700" + "Fax": "01553 613 700", }, { "OrganisationID": "", @@ -7002,14 +7002,14 @@ "City": "PONTYCLUN", "County": "MID GLAMORGAN", "Postcode": "CF72 8XR", - "Latitude": "", - "Longitude": "", + "Latitude": "51.54786257990656", + "Longitude": "-3.3914877354776594", "ParentODSCode": "7A5", "ParentName": "CWM TAF MORGANNWG UNIVERSITY LOCAL HEALTH BOARD", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -7026,14 +7026,14 @@ "City": "PONTYCLUN", "County": "MID GLAMORGAN", "Postcode": "CF72 8XR", - "Latitude": "", - "Longitude": "", + "Latitude": "51.54786932440031", + "Longitude": "-3.391734482077124", "ParentODSCode": "7A5", "ParentName": "CWM TAF MORGANNWG UNIVERSITY LOCAL HEALTH BOARD", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "39955", @@ -7057,7 +7057,7 @@ "Phone": "", "Email": "rlhpals@bartshealth.nhs.uk", "Website": "http://www.bartshealth.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41863", @@ -7081,7 +7081,7 @@ "Phone": "", "Email": "", "Website": "http://www.newcastle-hospitals.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42710", @@ -7105,7 +7105,7 @@ "Phone": "", "Email": "mtw-tr.palsoffice@nhs.net", "Website": "http://www.mtw.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41078", @@ -7129,7 +7129,7 @@ "Phone": "", "Email": "", "Website": "https://www.whittington.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40006", @@ -7153,7 +7153,7 @@ "Phone": "", "Email": "chiefexecutive.sdhct@nhs.net", "Website": "http://www.torbayandsouthdevon.nhs.uk/", - "Fax": "01803 616334" + "Fax": "01803 616334", }, { "OrganisationID": "", @@ -7170,14 +7170,14 @@ "City": "MANCHESTER", "County": "GREATER MANCHESTER", "Postcode": "M41 5SL", - "Latitude": "", - "Longitude": "", + "Latitude": "53.453839217199565", + "Longitude": "-2.3690510967929255", "ParentODSCode": "RM3", "ParentName": "NORTHERN CARE ALLIANCE NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -7194,14 +7194,14 @@ "City": "MANCHESTER", "County": "", "Postcode": "M32 0TH", - "Latitude": "", - "Longitude": "", + "Latitude": "53.45898933439508", + "Longitude": "-2.2877698025722513", "ParentODSCode": "R0A", "ParentName": "MANCHESTER UNIVERSITY NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41804", @@ -7225,7 +7225,7 @@ "Phone": "", "Email": "", "Website": "https://www.uclh.nhs.uk/our-services/our-hospitals/university-college-hospital", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41070", @@ -7249,7 +7249,7 @@ "Phone": "", "Email": "info@uhcw.nhs.uk", "Website": "http://www.uhcw.nhs.uk", - "Fax": "024 76966056 " + "Fax": "024 76966056 ", }, { "OrganisationID": "40922", @@ -7273,7 +7273,7 @@ "Phone": "", "Email": "", "Website": "http://www.lewishamandgreenwich.nhs.uk", - "Fax": "020 8333 3333" + "Fax": "020 8333 3333", }, { "OrganisationID": "", @@ -7290,14 +7290,14 @@ "City": "PENARTH", "County": "SOUTH GLAMORGAN", "Postcode": "CF64 2XX", - "Latitude": "", - "Longitude": "", + "Latitude": "51.448919571524335", + "Longitude": "-3.2018747900282367", "ParentODSCode": "7A4", "ParentName": "CARDIFF & VALE UNIVERSITY LHB", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42476", @@ -7321,7 +7321,7 @@ "Phone": "", "Email": "communications@nth.nhs.uk", "Website": "http://www.nth.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "43198", @@ -7345,7 +7345,7 @@ "Phone": "", "Email": "information@cddft.nhs.uk", "Website": "http://www.cddft.nhs.uk/", - "Fax": "0191 333 2685" + "Fax": "0191 333 2685", }, { "OrganisationID": "42477", @@ -7369,7 +7369,7 @@ "Phone": "", "Email": "communications@nth.nhs.uk", "Website": "http://www.nth.nhs.uk", - "Fax": "01642 624089" + "Fax": "01642 624089", }, { "OrganisationID": "64420", @@ -7393,7 +7393,7 @@ "Phone": "", "Email": "", "Website": "https://www.berkshirehealthcare.nhs.uk/our-sites/slough/upton-hospital/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40162", @@ -7417,7 +7417,7 @@ "Phone": "", "Email": "wuth.patientexperience@nhs.net", "Website": "http://www.wuth.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -7434,14 +7434,14 @@ "City": "MANCHESTER", "County": "GREATER MANCHESTER", "Postcode": "M28 3EZ", - "Latitude": "", - "Longitude": "", + "Latitude": "53.52506063219724", + "Longitude": "-2.397698469004914", "ParentODSCode": "RM3", "ParentName": "NORTHERN CARE ALLIANCE NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "41960", @@ -7465,7 +7465,7 @@ "Phone": "", "Email": "contactus@northumbria.nhs.uk", "Website": "https://www.northumbria.nhs.uk/wansbeck", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42841", @@ -7489,7 +7489,7 @@ "Phone": "", "Email": "whh.enquiries@nhs.net", "Website": "http://www.whh.nhs.uk", - "Fax": "01925 662424" + "Fax": "01925 662424", }, { "OrganisationID": "40975", @@ -7513,7 +7513,7 @@ "Phone": "", "Email": "", "Website": "http://www.swft.nhs.uk", - "Fax": "01926 482603" + "Fax": "01926 482603", }, { "OrganisationID": "42713", @@ -7537,7 +7537,7 @@ "Phone": "", "Email": "westherts.pals@nhs.net", "Website": "https://www.westhertshospitals.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "10855530", @@ -7561,7 +7561,7 @@ "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "4622618", @@ -7585,7 +7585,7 @@ "Phone": "", "Email": "", "Website": "http://www.chelwest.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40715", @@ -7609,7 +7609,7 @@ "Phone": "", "Email": "", "Website": "http://www.wsh.nhs.uk", - "Fax": "01284 701993" + "Fax": "01284 701993", }, { "OrganisationID": "59909", @@ -7633,7 +7633,7 @@ "Phone": "", "Email": "", "Website": "https://www.southernhealth.nhs.uk/locations/western-community-hospital/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -7650,14 +7650,14 @@ "City": "WESTON-SUPER-MARE", "County": "AVON", "Postcode": "BS23 4TQ", - "Latitude": "", - "Longitude": "", + "Latitude": "51.32239856900821", + "Longitude": "-2.972724858256704", "ParentODSCode": "RVJ", "ParentName": "NORTH BRISTOL NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "3028092", @@ -7681,7 +7681,7 @@ "Phone": "", "Email": "", "Website": "https://www.fhft.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "39964", @@ -7705,7 +7705,7 @@ "Phone": "", "Email": "wxpals.bartshealth@nhs.net", "Website": "http://www.bartshealth.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "40168", @@ -7729,7 +7729,7 @@ "Phone": "", "Email": "", "Website": "http://www.sthk.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42362", @@ -7753,7 +7753,7 @@ "Phone": "", "Email": "", "Website": "http://www.ekhuft.nhs.uk/williamharvey", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -7770,14 +7770,14 @@ "City": "MANCHESTER", "County": "GREATER MANCHESTER", "Postcode": "M20 2LR", - "Latitude": "", - "Longitude": "", + "Latitude": "53.42582006024377", + "Longitude": "-2.2442890475733615", "ParentODSCode": "RM3", "ParentName": "NORTHERN CARE ALLIANCE NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -7794,14 +7794,14 @@ "City": "HAVERFORDWEST", "County": "DYFED", "Postcode": "SA61 2PZ", - "Latitude": "", - "Longitude": "", + "Latitude": "51.81279926221023", + "Longitude": "-4.964727234183426", "ParentODSCode": "7A2", "ParentName": "HYWEL DDA UNIVERSITY LHB", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "42755", @@ -7825,7 +7825,7 @@ "Phone": "", "Email": "", "Website": "http://www.worcsacute.nhs.uk/our-hospitals/worcestershire-royal-hospital/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -7842,14 +7842,14 @@ "City": "WORKINGTON", "County": "CUMBRIA", "Postcode": "CA14 2RW", - "Latitude": "", - "Longitude": "", + "Latitude": "54.642892871753546", + "Longitude": "-3.5507700187869427", "ParentODSCode": "RNN", "ParentName": "NORTH CUMBRIA INTEGRATED CARE NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "43870", @@ -7873,7 +7873,7 @@ "Phone": "", "Email": "", "Website": "https://www.uhsussex.nhs.uk/", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -7890,14 +7890,14 @@ "City": "LEEDS", "County": "WEST YORKSHIRE", "Postcode": "LS12 5SG", - "Latitude": "", - "Longitude": "", + "Latitude": "53.78266025523274", + "Longitude": "-1.6002387438514194", "ParentODSCode": "RY6", "ParentName": "LEEDS COMMUNITY HEALTHCARE NHS TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -7914,14 +7914,14 @@ "City": "WREXHAM", "County": "CLWYD", "Postcode": "LL13 7TD", - "Latitude": "", - "Longitude": "", + "Latitude": "53.047169693895974", + "Longitude": "-3.008734102325263", "ParentODSCode": "7A1", "ParentName": "BETSI CADWALADR UNIVERSITY LHB", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "43209", @@ -7945,7 +7945,7 @@ "Phone": "", "Email": "", "Website": "http://www.buckshealthcare.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -7962,14 +7962,14 @@ "City": "MANCHESTER", "County": "GREATER MANCHESTER", "Postcode": "M23 9LT", - "Latitude": "", - "Longitude": "", + "Latitude": "53.38911906358711", + "Longitude": "-2.2919031193396626", "ParentODSCode": "RM3", "ParentName": "NORTHERN CARE ALLIANCE NHS FOUNDATION TRUST", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "39985", @@ -7993,7 +7993,7 @@ "Phone": "", "Email": "communications@ydh.nhs.uk", "Website": "http://www.yeovilhospital.nhs.uk/", - "Fax": "01935 426850" + "Fax": "01935 426850", }, { "OrganisationID": "40238", @@ -8017,7 +8017,7 @@ "Phone": "", "Email": "", "Website": "http://www.york.nhs.uk", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -8034,14 +8034,14 @@ "City": "RHYL", "County": "CLWYD", "Postcode": "LL18 5UJ", - "Latitude": "", - "Longitude": "", + "Latitude": "53.271949025221794", + "Longitude": "-3.496429715048846", "ParentODSCode": "7A1", "ParentName": "BETSI CADWALADR UNIVERSITY LHB", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -8058,14 +8058,14 @@ "City": "BANGOR", "County": "GWYNEDD", "Postcode": "LL57 2PW", - "Latitude": "", - "Longitude": "", + "Latitude": "53.20922110492283", + "Longitude": "-4.159410640189676", "ParentODSCode": "7A1", "ParentName": "BETSI CADWALADR UNIVERSITY LHB", "Phone": "", "Email": "", "Website": "", - "Fax": "" + "Fax": "", }, { "OrganisationID": "", @@ -8082,10 +8082,10 @@ "City": "HENGOED", "County": "MID GLAMORGAN", "Postcode": "CF82 7EP", - "Latitude": "", - "Longitude": "", + "Latitude": "51.63419223712743", + "Longitude": "-3.233903238440721", "ParentODSCode": "7A6", "ParentName": "ANEURIN BEVAN UNIVERSITY LHB", - "Website": "" - } + "Website": "", + }, ] diff --git a/epilepsy12/constants/severity.py b/epilepsy12/constants/severity.py index d51ef07a..0f000974 100644 --- a/epilepsy12/constants/severity.py +++ b/epilepsy12/constants/severity.py @@ -1,8 +1,8 @@ -MILD = (1, "Mild") -MODERATE = (2, "Moderate") -SEVERE = (3, "Severe") -PROFOUND = (4, "Profound") -UNCERTAIN = (5, "Uncertain") +MILD = ("mild", "Mild") +MODERATE = ("moderate", "Moderate") +SEVERE = ("severe", "Severe") +PROFOUND = ("profound", "Profound") +UNCERTAIN = ("uncertain", "Uncertain") SEVERITY = ( MILD, diff --git a/epilepsy12/constants/user_types.py b/epilepsy12/constants/user_types.py index 723ad97c..8acfc77b 100644 --- a/epilepsy12/constants/user_types.py +++ b/epilepsy12/constants/user_types.py @@ -14,20 +14,14 @@ AUDIT_CENTRE_LEAD_CLINICIAN = 1 AUDIT_CENTRE_CLINICIAN = 2 AUDIT_CENTRE_ADMINISTRATOR = 3 -AUDIT_CENTRE_MANAGER = 8 -RCPCH_AUDIT_LEAD = 4 -RCPCH_AUDIT_ANALYST = 5 -RCPCH_AUDIT_ADMINISTRATOR = 6 +RCPCH_AUDIT_TEAM = 4 RCPCH_AUDIT_PATIENT_FAMILY = 7 ROLES = ( (AUDIT_CENTRE_LEAD_CLINICIAN, "Audit Centre Lead Clinician"), (AUDIT_CENTRE_CLINICIAN, "Audit Centre Clinician"), (AUDIT_CENTRE_ADMINISTRATOR, "Audit Centre Administrator"), - (AUDIT_CENTRE_MANAGER, "Audit Centre Manager"), - (RCPCH_AUDIT_LEAD, "RCPCH Audit Lead"), - (RCPCH_AUDIT_ANALYST, "RCPCH Audit Analyst"), - (RCPCH_AUDIT_ADMINISTRATOR, "RCPCH Audit Administrator"), + (RCPCH_AUDIT_TEAM, "RCPCH Audit Team"), (RCPCH_AUDIT_PATIENT_FAMILY, "RCPCH Audit Children and Family"), ) @@ -35,14 +29,9 @@ (AUDIT_CENTRE_LEAD_CLINICIAN, "Audit Centre Lead Clinician"), (AUDIT_CENTRE_CLINICIAN, "Audit Centre Clinician"), (AUDIT_CENTRE_ADMINISTRATOR, "Audit Centre Administrator"), - (AUDIT_CENTRE_MANAGER, "Audit Centre Manager"), ) -RCPCH_AUDIT_TEAM_ROLES = ( - (RCPCH_AUDIT_LEAD, "RCPCH Audit Lead"), - (RCPCH_AUDIT_ANALYST, "RCPCH Audit Analyst"), - (RCPCH_AUDIT_ADMINISTRATOR, "RCPCH Audit Administrator"), -) +RCPCH_AUDIT_TEAM_ROLES = ((RCPCH_AUDIT_TEAM, "RCPCH Audit Team"),) MR = 1 MRS = 2 @@ -54,13 +43,14 @@ """ Groups +These map to the roles +Role Group +Audit Centre Lead Clinician trust_audit_team_view_only +Audit Centre Clinician trust_audit_team_edit_access +Audit Centre Administrator trust_audit_team_full_access +RCPCH Audit Team epilepsy12_audit_team_full_access +RCPCH Audit Children and Family patient_access """ -# logged in user can view all national data but not logs -EPILEPSY12_AUDIT_TEAM_VIEW_ONLY = "epilepsy12_audit_team_view_only" - -# logged in user can edit but not delete national data. Cannot view or edit logs or permissions. -EPILEPSY12_AUDIT_TEAM_EDIT_ACCESS = "epilepsy12_audit_team_edit_access" - # logged in user access all areas: can create/update/delete any audit data, logs, epilepsy key words and organisation trusts, groups and permissions EPILEPSY12_AUDIT_TEAM_FULL_ACCESS = "epilepsy12_audit_team_full_access" @@ -77,8 +67,6 @@ PATIENT_ACCESS = "patient_access" GROUPS = ( - EPILEPSY12_AUDIT_TEAM_VIEW_ONLY, - EPILEPSY12_AUDIT_TEAM_EDIT_ACCESS, EPILEPSY12_AUDIT_TEAM_FULL_ACCESS, TRUST_AUDIT_TEAM_VIEW_ONLY, TRUST_AUDIT_TEAM_EDIT_ACCESS, @@ -86,6 +74,10 @@ PATIENT_ACCESS, ) +""" +Custom permissions +""" + # Case CAN_LOCK_CHILD_CASE_DATA_FROM_EDITING = ( "can_lock_child_case_data_from_editing", @@ -105,15 +97,12 @@ "can_approve_eligibility", "Can approve eligibility for Epilepsy12.", ) -CAN_REMOVE_APPROVAL_OF_ELIGIBILITY = ( - "can_remove_approval_of_eligibility", - "Can remove approval of eligibiltiy for Epilepsy12.", -) CAN_REGISTER_CHILD_IN_EPILEPSY12 = ( "can_register_child_in_epilepsy12", "Can register child in Epilepsy12. (A cohort number is automatically allocaeted)", ) +# TODO #512 unregistering a child in Epilepsy12 is currently not implemented CAN_UNREGISTER_CHILD_IN_EPILEPSY12 = ( "can_unregister_child_in_epilepsy12", "Can unregister a child in Epilepsy. Their record and previously entered data is untouched.", @@ -149,7 +138,6 @@ CAN_UNLOCK_CHILD_CASE_DATA_FROM_EDITING, CAN_OPT_OUT_CHILD_FROM_INCLUSION_IN_AUDIT, CAN_APPROVE_ELIGIBILITY, - CAN_REMOVE_APPROVAL_OF_ELIGIBILITY, CAN_REGISTER_CHILD_IN_EPILEPSY12, CAN_UNREGISTER_CHILD_IN_EPILEPSY12, CAN_EDIT_EPILEPSY12_LEAD_CENTRE, diff --git a/epilepsy12/constants/valid_nhs_nums.py b/epilepsy12/constants/valid_nhs_nums.py new file mode 100644 index 00000000..b66dc77a --- /dev/null +++ b/epilepsy12/constants/valid_nhs_nums.py @@ -0,0 +1,533 @@ +VALID_NHS_NUMS = [ + "025 845 8593", + "033 786 5248", + "122 646 4505", + "141 430 6806", + "202 939 5676", + "246 601 2510", + "249 686 7298", + "266 614 1802", + "281 392 9905", + "312 221 6868", + "384 994 1833", + "395 454 2625", + "400 0000 004", + "400 0000 012", + "400 0000 020", + "400 0000 039", + "400 0000 047", + "400 0000 055", + "400 0000 063", + "400 0000 071", + "400 0000 098", + "400 0000 101", + "400 0000 128", + "400 0000 136", + "400 0000 144", + "400 0000 152", + "400 0000 160", + "400 0000 179", + "400 0000 187", + "400 0000 195", + "400 0000 209", + "400 0000 217", + "400 0000 225", + "400 0000 233", + "400 0000 241", + "400 0000 268", + "400 0000 276", + "400 0000 284", + "400 0000 292", + "400 0000 306", + "400 0000 314", + "400 0000 322", + "400 0000 330", + "400 0000 349", + "400 0000 357", + "400 0000 365", + "400 0000 373", + "400 0000 381", + "400 0000 403", + "400 0000 411", + "400 0000 438", + "400 0000 446", + "400 0000 454", + "400 0000 462", + "400 0000 470", + "400 0000 489", + "400 0000 497", + "400 0000 500", + "400 0000 519", + "400 0000 527", + "400 0000 535", + "400 0000 543", + "400 0000 551", + "400 0000 578", + "400 0000 586", + "400 0000 594", + "400 0000 608", + "400 0000 616", + "400 0000 624", + "400 0000 632", + "400 0000 640", + "400 0000 659", + "400 0000 667", + "400 0000 675", + "400 0000 683", + "400 0000 691", + "400 0000 705", + "400 0000 713", + "400 0000 721", + "400 0000 748", + "400 0000 756", + "400 0000 764", + "400 0000 772", + "400 0000 780", + "400 0000 799", + "400 0000 802", + "400 0000 810", + "400 0000 829", + "400 0000 837", + "400 0000 845", + "400 0000 853", + "400 0000 861", + "400 0000 888", + "400 0000 896", + "400 0000 918", + "400 0000 926", + "400 0000 934", + "400 0000 942", + "400 0000 950", + "400 0000 969", + "400 0000 977", + "400 0000 985", + "400 0000 993", + "400 0001 000", + "400 0001 019", + "400 0001 027", + "400 0001 035", + "400 0001 043", + "400 0001 051", + "400 0001 078", + "400 0001 086", + "400 0001 094", + "400 0001 108", + "400 0001 116", + "400 0001 124", + "400 0001 132", + "400 0001 140", + "400 0001 159", + "400 0001 167", + "400 0001 175", + "400 0001 183", + "400 0001 191", + "400 0001 205", + "400 0001 213", + "400 0001 221", + "400 0001 248", + "400 0001 256", + "400 0001 264", + "400 0001 272", + "400 0001 280", + "400 0001 299", + "400 0001 302", + "400 0001 310", + "400 0001 329", + "400 0001 337", + "400 0001 345", + "400 0001 353", + "400 0001 361", + "400 0001 388", + "400 0001 396", + "400 0001 418", + "400 0001 426", + "400 0001 434", + "400 0001 442", + "400 0001 450", + "400 0001 469", + "400 0001 477", + "400 0001 485", + "400 0001 493", + "400 0001 507", + "400 0001 515", + "400 0001 523", + "400 0001 531", + "400 0001 558", + "400 0001 566", + "400 0001 574", + "400 0001 582", + "400 0001 590", + "400 0001 604", + "400 0001 612", + "400 0001 620", + "400 0001 639", + "400 0001 647", + "400 0001 655", + "400 0001 663", + "400 0001 671", + "400 0001 698", + "400 0001 701", + "400 0001 728", + "400 0001 736", + "400 0001 744", + "400 0001 752", + "400 0001 760", + "400 0001 779", + "400 0001 787", + "400 0001 795", + "400 0001 809", + "400 0001 817", + "400 0001 825", + "400 0001 833", + "400 0001 841", + "400 0001 868", + "400 0001 876", + "400 0001 884", + "400 0001 892", + "400 0001 906", + "400 0001 914", + "400 0001 922", + "400 0001 930", + "400 0001 949", + "400 0001 957", + "400 0001 965", + "400 0001 973", + "400 0001 981", + "400 0002 007", + "400 0002 015", + "400 0002 023", + "400 0002 031", + "400 0002 058", + "400 0002 066", + "400 0002 074", + "400 0002 082", + "400 0002 090", + "400 0002 104", + "400 0002 112", + "400 0002 120", + "400 0002 139", + "400 0002 147", + "400 0002 155", + "400 0002 163", + "400 0002 171", + "400 0002 198", + "400 0002 201", + "400 0002 228", + "400 0002 236", + "400 0002 244", + "400 0002 252", + "400 0002 260", + "400 0002 279", + "400 0002 287", + "400 0002 295", + "400 0002 309", + "400 0002 317", + "400 0002 325", + "400 0002 333", + "400 0002 341", + "400 0002 368", + "400 0002 376", + "400 0002 384", + "400 0002 392", + "400 0002 406", + "400 0002 414", + "400 0002 422", + "400 0002 430", + "400 0002 449", + "400 0002 457", + "400 0002 465", + "400 0002 473", + "400 0002 481", + "400 0002 503", + "400 0002 511", + "400 0002 538", + "400 0002 546", + "400 0002 554", + "400 0002 562", + "400 0002 570", + "400 0002 589", + "400 0002 597", + "400 0002 600", + "400 0002 619", + "400 0002 627", + "400 0002 635", + "400 0002 643", + "400 0002 651", + "400 0002 678", + "400 0002 686", + "400 0002 694", + "400 0002 708", + "400 0002 716", + "400 0002 724", + "400 0002 732", + "400 0002 740", + "400 0002 759", + "400 0002 767", + "400 0002 775", + "400 0002 783", + "400 0002 791", + "400 0002 805", + "400 0002 813", + "400 0002 821", + "400 0002 848", + "400 0002 856", + "400 0002 864", + "400 0002 872", + "400 0002 880", + "400 0002 899", + "400 0002 902", + "400 0002 910", + "400 0002 929", + "400 0002 937", + "400 0002 945", + "400 0002 953", + "400 0002 961", + "400 0002 988", + "400 0002 996", + "400 0003 003", + "400 0003 011", + "400 0003 038", + "400 0003 046", + "400 0003 054", + "400 0003 062", + "400 0003 070", + "400 0003 089", + "400 0003 097", + "400 0003 100", + "400 0003 119", + "400 0003 127", + "400 0003 135", + "400 0003 143", + "400 0003 151", + "400 0003 178", + "400 0003 186", + "400 0003 194", + "400 0003 208", + "400 0003 216", + "400 0003 224", + "400 0003 232", + "400 0003 240", + "400 0003 259", + "400 0003 267", + "400 0003 275", + "400 0003 283", + "400 0003 291", + "400 0003 305", + "400 0003 313", + "400 0003 321", + "400 0003 348", + "400 0003 356", + "400 0003 364", + "400 0003 372", + "400 0003 380", + "400 0003 399", + "400 0003 402", + "400 0003 410", + "400 0003 429", + "400 0003 437", + "400 0003 445", + "400 0003 453", + "400 0003 461", + "400 0003 488", + "400 0003 496", + "400 0003 518", + "400 0003 526", + "400 0003 534", + "400 0003 542", + "400 0003 550", + "400 0003 569", + "400 0003 577", + "400 0003 585", + "400 0003 593", + "400 0003 607", + "400 0003 615", + "400 0003 623", + "400 0003 631", + "400 0003 658", + "400 0003 666", + "400 0003 674", + "400 0003 682", + "400 0003 690", + "400 0003 704", + "400 0003 712", + "400 0003 720", + "400 0003 739", + "400 0003 747", + "400 0003 755", + "400 0003 763", + "400 0003 771", + "400 0003 798", + "400 0003 801", + "400 0003 828", + "400 0003 836", + "400 0003 844", + "400 0003 852", + "400 0003 860", + "400 0003 879", + "400 0003 887", + "400 0003 895", + "400 0003 909", + "400 0003 917", + "400 0003 925", + "400 0003 933", + "400 0003 941", + "400 0003 968", + "400 0003 976", + "400 0003 984", + "400 0003 992", + "400 0004 018", + "400 0004 026", + "400 0004 034", + "400 0004 042", + "400 0004 050", + "400 0004 069", + "400 0004 077", + "400 0004 085", + "400 0004 093", + "400 0004 107", + "400 0004 115", + "400 0004 123", + "400 0004 131", + "400 0004 158", + "400 0004 166", + "400 0004 174", + "400 0004 182", + "400 0004 190", + "400 0004 204", + "400 0004 212", + "400 0004 220", + "400 0004 239", + "400 0004 247", + "400 0004 255", + "400 0004 263", + "400 0004 271", + "400 0004 298", + "400 0004 301", + "400 0004 328", + "400 0004 336", + "400 0004 344", + "400 0004 352", + "400 0004 360", + "400 0004 379", + "400 0004 387", + "400 0004 395", + "400 0004 409", + "400 0004 417", + "400 0004 425", + "400 0004 433", + "400 0004 441", + "400 0004 468", + "400 0004 476", + "400 0004 484", + "400 0004 492", + "400 0004 506", + "400 0004 514", + "400 0004 522", + "400 0004 530", + "400 0004 549", + "400 0004 557", + "400 0004 565", + "400 0004 573", + "400 0004 581", + "400 0004 603", + "400 0004 611", + "400 0004 638", + "400 0004 646", + "400 0004 654", + "400 0004 662", + "400 0004 670", + "400 0004 689", + "400 0004 697", + "400 0004 700", + "400 0004 719", + "400 0004 727", + "400 0004 735", + "400 0004 743", + "400 0004 751", + "400 0004 778", + "400 0004 786", + "400 0004 794", + "400 0004 808", + "400 0004 816", + "400 0004 824", + "400 0004 832", + "400 0004 840", + "400 0004 859", + "400 0004 867", + "400 0004 875", + "400 0004 883", + "400 0004 891", + "400 0004 905", + "400 0004 913", + "400 0004 921", + "400 0004 948", + "400 0004 956", + "400 0004 964", + "400 0004 972", + "400 0004 980", + "400 0004 999", + "400 0005 006", + "400 0005 014", + "400 0005 022", + "400 0005 030", + "400 0005 049", + "400 0005 057", + "400 0005 065", + "400 0005 073", + "400 0005 081", + "400 0005 103", + "400 0005 111", + "400 0005 138", + "400 0005 146", + "400 0005 154", + "400 0005 162", + "400 0005 170", + "400 0005 189", + "400 0005 197", + "400 0005 200", + "400 0005 219", + "400 0005 227", + "400 0005 235", + "400 0005 243", + "400 0005 251", + "400 0005 278", + "400 0005 286", + "400 0005 294", + "400 0005 308", + "400 0005 316", + "400 0005 324", + "400 0005 332", + "400 0005 340", + "400 0005 359", + "400 0005 367", + "400 0005 375", + "400 0005 383", + "400 0005 391", + "400 0005 405", + "400 0005 413", + "400 0005 421", + "400 0005 448", + "400 0005 456", + "400 0005 464", + "400 0005 472", + "400 0005 480", + "400 0005 499", + "408 489 3633", + "414 120 3971", + "434 151 9743", + "466 950 4908", + "564 046 9218", + "609 517 5399", + "633 669 7231", + "654 489 9194", + "700 302 5485", + "702 173 6118", + "702 943 6245", + "790 447 4190", + "893 139 0955", + "919 515 8006", + "963 988 1260", + "965 865 0562", + "969 003 9563", + "979 742 1953", +] diff --git a/epilepsy12/decorator.py b/epilepsy12/decorator.py index 0351214c..34194c0e 100644 --- a/epilepsy12/decorator.py +++ b/epilepsy12/decorator.py @@ -69,7 +69,7 @@ def decorated(request, **view_parameters): else: if ( request.user.is_rcpch_audit_team_member - or request.user.is_staff + or request.user.is_rcpch_staff or request.user.is_superuser ): # user is an editor member of the RCPCH audit team @@ -198,6 +198,7 @@ def user_may_view_this_organisation(): def decorator(view): def wrapper(request, *args, **kwargs): user = request.user + if kwargs.get("organisation_id") is not None: organisation_requested = Organisation.objects.get( pk=kwargs.get("organisation_id") @@ -205,7 +206,7 @@ def wrapper(request, *args, **kwargs): if (user.is_active and user.email_confirmed) or user.is_superuser: if ( user.is_rcpch_audit_team_member - or user.is_staff + or user.is_rcpch_staff or user.is_superuser ): # RCPCH staff or E12 RCPCH staff can see all children across the UK @@ -316,7 +317,8 @@ def wrapper(request, *args, **kwargs): if ( organisation.exists() or user.is_rcpch_audit_team_member - or user.is_staff + or user.is_rcpch_staff + or user.is_superuser ): return view(request, *args, **kwargs) else: diff --git a/epilepsy12/forms_folder/case_form.py b/epilepsy12/forms_folder/case_form.py index 9e3c6664..35bcf766 100644 --- a/epilepsy12/forms_folder/case_form.py +++ b/epilepsy12/forms_folder/case_form.py @@ -8,44 +8,36 @@ class CaseForm(forms.ModelForm): - unknown_postcode = forms.CharField( - required=False - ) + unknown_postcode = forms.CharField(required=False) first_name = forms.CharField( help_text="Enter the first name.", widget=forms.TextInput( - attrs={ - "class": "form-control", - "placeholder": "First name" - } + attrs={"class": "form-control", "placeholder": "First name"} ), - required=True + required=True, ) surname = forms.CharField( help_text="Enter the surname.", widget=forms.TextInput( - attrs={ - "class": "form-control", - "placeholder": "Surname" - } + attrs={"class": "form-control", "placeholder": "Surname"} ), - required=True + required=True, ) date_of_birth = forms.DateField( widget=forms.TextInput( attrs={ "class": "form-control", "placeholder": "Date of Birth", - "type": "date" + "type": "date", } ), - required=True + required=True, ) sex = forms.ChoiceField( choices=SEX_TYPE, - widget=forms.Select(attrs={'class': 'ui rcpch dropdown'}), - required=True + widget=forms.Select(attrs={"class": "ui rcpch dropdown"}), + required=True, ) nhs_number = forms.CharField( help_text="Enter the NHS Number. This is 10 digits long.", @@ -54,10 +46,10 @@ class CaseForm(forms.ModelForm): "class": "form-control", "placeholder": "NHS Number", "type": "text", - "data-mask": "000 000 0000" + "data-mask": "000 000 0000", } ), - required=True + required=True, ) postcode = forms.CharField( help_text="Enter the postcode.", @@ -68,50 +60,50 @@ class CaseForm(forms.ModelForm): "type": "text", } ), - required=True + required=True, ) ethnicity = forms.ChoiceField( choices=ETHNICITIES, - widget=forms.Select( - attrs={ - 'class': 'ui rcpch dropdown' - } - ), - required=True - ) - locked_at = forms.DateTimeField( - help_text="Time record locked.", - required=False - ) - locked_by = forms.CharField( - help_text="User who locked the record", - required=False + widget=forms.Select(attrs={"class": "ui rcpch dropdown"}), + required=True, ) + locked_at = forms.DateTimeField(help_text="Time record locked.", required=False) + locked_by = forms.CharField(help_text="User who locked the record", required=False) def __init__(self, *args, **kwargs) -> None: super(CaseForm, self).__init__(*args, **kwargs) - self.fields['ethnicity'].widget.attrs.update({ - 'class': 'ui rcpch dropdown' - }) + self.fields["ethnicity"].widget.attrs.update({"class": "ui rcpch dropdown"}) self.existing_nhs_number = self.instance.nhs_number class Meta: model = Case fields = [ - 'first_name', 'surname', 'date_of_birth', 'sex', 'nhs_number', 'postcode', 'ethnicity', 'unknown_postcode' + "first_name", + "surname", + "date_of_birth", + "sex", + "nhs_number", + "postcode", + "ethnicity", + "unknown_postcode", ] def clean_postcode(self): # remove spaces - postcode = str(self.cleaned_data['postcode']).replace(' ', '') - if is_valid_postcode(postcode=postcode): + postcode = str(self.cleaned_data["postcode"]).replace(" ", "") + try: + validated_postcode = is_valid_postcode(postcode=postcode) + except ValueError as error: + raise ValidationError(f"Could not validate postcode: {error}") + + if validated_postcode: return postcode else: - raise ValidationError('Invalid postcode') + raise ValidationError("Invalid postcode") def clean_date_of_birth(self): - date_of_birth = self.cleaned_data['date_of_birth'] + date_of_birth = self.cleaned_data["date_of_birth"] today = date.today() if date_of_birth > today: raise ValidationError("Date of birth cannot be in the future.") @@ -120,8 +112,8 @@ def clean_date_of_birth(self): def clean_nhs_number(self): # remove spaces - nhs_number = self.cleaned_data['nhs_number'] - formatted_number = int(str(nhs_number).replace(' ', '')) + nhs_number = self.cleaned_data["nhs_number"] + formatted_number = int(str(nhs_number).replace(" ", "")) # ensure NHS number is unique in the database if self.existing_nhs_number is not None: @@ -129,15 +121,15 @@ def clean_nhs_number(self): if formatted_number != int(self.existing_nhs_number): # the new number does not match the one stored if Case.objects.filter(nhs_number=formatted_number).exists(): - raise ValidationError('NHS Number already taken!') + raise ValidationError("NHS Number already taken!") else: # this is a new form - check this number is unique in the database if Case.objects.filter(nhs_number=formatted_number).exists(): - raise ValidationError('NHS Number already taken!') + raise ValidationError("NHS Number already taken!") # check NHS number is valid validity = validate_nhs_number(formatted_number) - if validity['valid']: + if validity["valid"]: return formatted_number else: - raise ValidationError(validity['message']) + raise ValidationError(validity["message"]) diff --git a/epilepsy12/forms_folder/epilepsy12_user_form.py b/epilepsy12/forms_folder/epilepsy12_user_form.py index dc4239df..5bc55131 100644 --- a/epilepsy12/forms_folder/epilepsy12_user_form.py +++ b/epilepsy12/forms_folder/epilepsy12_user_form.py @@ -69,6 +69,14 @@ def __init__(self, *args, **kwargs): widget=forms.CheckboxInput(attrs={"class": "ui toggle checkbox"}), required=True ) + is_rcpch_staff = forms.BooleanField( + widget=forms.CheckboxInput(attrs={"class": "ui toggle checkbox"}), required=True + ) + + is_child_or_carer = forms.BooleanField( + widget=forms.CheckboxInput(attrs={"class": "ui toggle checkbox"}), required=True + ) + class Meta: model = Epilepsy12User fields = ( @@ -79,6 +87,7 @@ class Meta: "surname", "is_rcpch_audit_team_member", "is_staff", + "is_rcpch_staff", "is_active", "email_confirmed", ) @@ -98,6 +107,7 @@ class Meta: "surname", "is_rcpch_audit_team_member", "is_staff", + "is_rcpch_staff", "is_active", "email_confirmed", ) @@ -183,6 +193,18 @@ def __init__(self, rcpch_organisation, *args, **kwargs): required=False, ) + is_rcpch_staff = forms.BooleanField( + widget=forms.CheckboxInput(attrs={"class": "ui toggle checkbox"}), + initial=False, + required=False, + ) + + is_child_or_carer = forms.BooleanField( + widget=forms.CheckboxInput(attrs={"class": "ui toggle checkbox"}), + initial=False, + required=False, + ) + is_staff = forms.BooleanField( widget=forms.CheckboxInput(attrs={"class": "ui toggle checkbox"}), initial=False, @@ -205,6 +227,7 @@ class Meta: "first_name", "surname", "is_staff", + "is_rcpch_staff", "is_rcpch_audit_team_member", "is_superuser", "email_confirmed", @@ -247,6 +270,15 @@ def clean_is_superuser(self): self.cleaned_data["view_preference"] = 0 return data + def clean_is_rcpch_staff(self): + """ + if is_rcpch_staff is positive, set view_preference to organisation_view + """ + data = self.cleaned_data["is_rcpch_staff"] + if data: + self.cleaned_data["view_preference"] = 0 + return data + def clean_organisation_employer(self): data = self.cleaned_data["organisation_employer"] return data @@ -256,7 +288,8 @@ def clean(self): if self.rcpch_organisation == "rcpch-staff": # RCPCH staff are not affiliated with any organisation - cleaned_data["is_staff"] = True + cleaned_data["is_staff"] = False + cleaned_data["is_rcpch_staff"] = True cleaned_data["organisation_employer"] = None cleaned_data["is_rcpch_audit_team_member"] = True cleaned_data["view_preference"] = 0 diff --git a/epilepsy12/general_functions/__init__.py b/epilepsy12/general_functions/__init__.py index a6ac4b5d..b4ca5d53 100644 --- a/epilepsy12/general_functions/__init__.py +++ b/epilepsy12/general_functions/__init__.py @@ -7,7 +7,7 @@ from .random_postcodes import * from .fuzzy_matching import * from .fetch_snomed import * -from .time_elapsed import calculate_time_elapsed +from .time_elapsed import stringify_time_elapsed from .key_from_value import * from .value_from_key import * from .date_functions import * @@ -18,3 +18,4 @@ from .ods_search import * from .calculate_kpi_average import * from .item_from_choice import * +from .has_all_attributes import * \ No newline at end of file diff --git a/epilepsy12/general_functions/date_functions.py b/epilepsy12/general_functions/date_functions.py index 8209737b..2165e0b8 100644 --- a/epilepsy12/general_functions/date_functions.py +++ b/epilepsy12/general_functions/date_functions.py @@ -1,21 +1,20 @@ from datetime import date, timedelta +from dateutil.relativedelta import relativedelta, TU +def nth_tuesday_of_year(year:int, n:int)->date: + """Returns the nth Tuesday for a given year. -def next_weekday(d, weekday): - # Thanks to Phihag for this snippet - # https://stackoverflow.com/questions/6558535/find-the-date-for-the-first-monday-after-a-given-date - # 0=Monday - days_ahead = weekday - d.weekday() - if days_ahead <= 0: # Target day already happened this week - days_ahead += 7 - return d + timedelta(days_ahead) + Args: + year (int): year in which to find nth Tuesday of Jan. + n (int): which Tuesday of Jan to return e.g. if year=2022 n=2 returns 2nd Tues of Jan; n=5 returns 1st Tues of Feb. + Returns: + date: nth Tuesday as date. + """ + jan_first = date(year, 1, 1) + return jan_first + relativedelta(weekday=TU(n)) def first_tuesday_in_january(year): - jan_first = date(year, 1, 1) - if jan_first.weekday() == 1: - # Jan first of year supplied is a Tuesday - first_tuesday = jan_first - else: - first_tuesday = next_weekday(jan_first, 1) - return first_tuesday + """Fn which makes it simpler to get first Tues of Jan for a given year, if used already in codebase. + """ + return nth_tuesday_of_year(year, n=1) diff --git a/epilepsy12/general_functions/deprecated_trie.py b/epilepsy12/general_functions/deprecated_trie.py deleted file mode 100644 index b3617e25..00000000 --- a/epilepsy12/general_functions/deprecated_trie.py +++ /dev/null @@ -1,99 +0,0 @@ -from typing import Tuple -import requests - - -def disease_type_search(condition: str): - url = f'http://localhost:8082/v1/snomed/search?s={condition}\&constraint=<64572001' - response = requests.get(url=url) - serialised = response.json() - return serialised - - -class TrieNode: - """A node in the trie structure""" - - def __init__(self, char): - # the character stored in this node - self.char = char - - # whether this can be the end of a word - self.is_end = False - - # a counter indicating how many times a word is inserted - # (if this node's is_end is True) - self.counter = 0 - - # a dictionary of child nodes - # keys are characters, values are nodes - self.children = {} - - -class Trie(object): - """The trie object""" - - def __init__(self): - """ - The trie has at least the root node. - The root node does not store any character - """ - self.root = TrieNode("") - - def insert(self, word): - """Insert a word into the trie""" - node = self.root - - # Loop through each character in the word - # Check if there is no child containing the character, create a new child for the current node - for char in word: - if char in node.children: - node = node.children[char] - else: - # If a character is not found, - # create a new node in the trie - new_node = TrieNode(char) - node.children[char] = new_node - node = new_node - - # Mark the end of a word - node.is_end = True - - # Increment the counter to indicate that we see this word once more - node.counter += 1 - - def dfs(self, node, prefix): - """Depth-first traversal of the trie - - Args: - - node: the node to start with - - prefix: the current prefix, for tracing a - word while traversing the trie - """ - if node.is_end: - self.output.append((prefix + node.char, node.counter)) - - for child in node.children.values(): - self.dfs(child, prefix + node.char) - - def query(self, x): - """Given an input (a prefix), retrieve all words stored in - the trie with that prefix, sort the words by the number of - times they have been inserted - """ - # Use a variable within the class to keep all possible outputs - # As there can be more than one word with such prefix - self.output = [] - node = self.root - - # Check if the prefix is in the trie - for char in x: - if char in node.children: - node = node.children[char] - else: - # cannot found the prefix, return empty list - return [] - - # Traverse the trie to get all candidates - self.dfs(node, x[:-1]) - - # Sort the results in reverse order and return - return sorted(self.output, key=lambda x: x[1], reverse=True) diff --git a/epilepsy12/general_functions/fetch_snomed.py b/epilepsy12/general_functions/fetch_snomed.py index f3a2af14..402ff511 100644 --- a/epilepsy12/general_functions/fetch_snomed.py +++ b/epilepsy12/general_functions/fetch_snomed.py @@ -49,7 +49,7 @@ def fetch_snomed(sctid, syntax): def snomed_search(search_term): - search_url = f'{settings.RCPCH_HERMES_SERVER_URL}/search?s={search_term}\&constraint=<64572001' + search_url = f'{settings.RCPCH_HERMES_SERVER_URL}/search?s={search_term}&constraint=<64572001' response = requests.get(search_url) @@ -118,7 +118,7 @@ def search_ecl(search, ecl): def search_all_epilepsy(search): - search_url = f'{settings.RCPCH_HERMES_SERVER_URL}/search?s={search}\&constraint=<<84757009&offset=0&limit=1000' + search_url = f'{settings.RCPCH_HERMES_SERVER_URL}/search?s={search}&constraint=<<84757009&offset=0&limit=1000' response = requests.get(search_url) @@ -132,7 +132,7 @@ def search_all_epilepsy(search): def search_all_hereditary_epilepsy(search): - search_url = f'{settings.RCPCH_HERMES_SERVER_URL}/search?s={search}\&constraint=(<< 84757009 AND << 363235000 )&offset=0&limit=1000' + search_url = f'{settings.RCPCH_HERMES_SERVER_URL}/search?s={search}&constraint=(<< 84757009 AND << 363235000 )&offset=0&limit=1000' response = requests.get(search_url) @@ -152,7 +152,7 @@ def snomed_search_congenital_neurology(search_term): # 363235000 |Hereditary disorder of nervous system (disorder)| + # 39367000 |Inflammatory disease of the central nervous system (disorder)| - search_url = f'{settings.RCPCH_HERMES_SERVER_URL}/search?s={search_term}\&constraint=<84757009' + search_url = f'{settings.RCPCH_HERMES_SERVER_URL}/search?s={search_term}&constraint=<84757009' response = requests.get(search_url) @@ -166,7 +166,7 @@ def snomed_search_congenital_neurology(search_term): def snomed_medicine_search(search_term): - search_url = f'{settings.RCPCH_HERMES_SERVER_URL}/search?s={search_term}\&constraint=<373873005' + search_url = f'{settings.RCPCH_HERMES_SERVER_URL}/search?s={search_term}&constraint=<373873005' response = requests.get(search_url) diff --git a/epilepsy12/general_functions/has_all_attributes.py b/epilepsy12/general_functions/has_all_attributes.py new file mode 100644 index 00000000..fbff01e8 --- /dev/null +++ b/epilepsy12/general_functions/has_all_attributes.py @@ -0,0 +1,19 @@ +"""Fn which returns True if object has all specified attributes. +""" + + +def has_all_attributes(obj: object, attributes: list[str]) -> bool: + """ + Check if the given object has all the specified attributes. + + Args: + obj (object): The object to check for attributes. + attributes (list[str]): A list of attribute names to check. + + Returns: + bool: True if the object has all the specified attributes, False otherwise. + """ + for attr in attributes: + if not hasattr(obj, attr): + return False + return True diff --git a/epilepsy12/general_functions/index_multiple_deprivation.py b/epilepsy12/general_functions/index_multiple_deprivation.py index 56021c8e..b793f72b 100644 --- a/epilepsy12/general_functions/index_multiple_deprivation.py +++ b/epilepsy12/general_functions/index_multiple_deprivation.py @@ -1,31 +1,37 @@ +""" +Calculates the index of multiple deprivation for a given postcode +""" + +# Standard imports +import logging import requests -import os + +# Third party imports from django.conf import settings -""" -Steps to calculate IMD +# RCPCH imports -1. identify LSOA from postcode - cand do this from https://api.postcodes.io/postcodes/ -2. Use the LSOA to get the IMD - cand do this from -""" +# Logging setup +logger = logging.getLogger(__name__) def imd_for_postcode(user_postcode: str) -> int: """ - This is makes an API call to the RCPCH Census Platform with postcode and quantile_type + Makes an API call to the RCPCH Census Platform with postcode and quantile_type Postcode - can have spaces or not - this is processed by the API Quantile - this is an integer representing what quantiles are requested (eg quintile, decile etc) """ - url = f"{settings.RCPCH_CENSUS_PLATFORM_URL}/index_of_multiple_deprivation_quantile?postcode={user_postcode}&quantile=5" - response = requests.get( - url=url, headers={'Subscription-Key': f'{settings.RCPCH_CENSUS_PLATFORM_TOKEN}'}) + url=f"{settings.RCPCH_CENSUS_PLATFORM_URL}/index_of_multiple_deprivation_quantile?postcode={user_postcode}&quantile=5", + headers={"Subscription-Key": f"{settings.RCPCH_CENSUS_PLATFORM_TOKEN}"}, + timeout=10, # times out after 10 seconds + ) if response.status_code != 200: - print(f"Could not get deprivation score. {response.status_code}") + logger.error( + "Could not get deprivation score. Response status %s", response.status_code + ) return None - result = response.json()['result'] - - return result['data_quantile'] + return response.json()["result"]["data_quantile"] diff --git a/epilepsy12/general_functions/nhs_number.py b/epilepsy12/general_functions/nhs_number.py index fbc3eeec..a8a04d6c 100644 --- a/epilepsy12/general_functions/nhs_number.py +++ b/epilepsy12/general_functions/nhs_number.py @@ -1,3 +1,6 @@ +from random import randint + + def validate_nhs_number(number_to_validate): """ The NHS number must: @@ -8,13 +11,35 @@ def validate_nhs_number(number_to_validate): """ # convert to string and strip any spaces - cleaned_number_as_string = str(number_to_validate).replace(' ', '') + cleaned_number_as_string = str(number_to_validate).replace(" ", "") - # check if the number is not exactly 10 digits + # guard clause - return invalid if the number is not exactly 10 digits if len(cleaned_number_as_string) != 10: return { - 'valid': False, - 'message': 'The NHS Number must be exactly 10 digits long.' + "valid": False, + "message": "The NHS Number must be exactly 10 digits long.", + } + + # turn nhs number into list of ints + nhs_nums = [int(digit) for digit in cleaned_number_as_string] + + # sum the first 9 digits using the weighted multiplication rule + first_nine_digits_weighted_sum = 0 + for i, digit in enumerate(nhs_nums[:-1]): + first_nine_digits_weighted_sum += digit * (10-i) + + # calculate the check_sum_val using mod 11 rule -> result ranges betwee 0-11 + check_sum_val = 11 - (first_nine_digits_weighted_sum % 11) + + # "If the result is 11 then a check digit of 0 is used." + if check_sum_val == 11: + check_sum_val = 0 + + # nhs number valid only if check_sum_val == last digit. NOTE: they also specify an additional check to see whether check_sum_val == 10, which is invalid. However, directly checking whether check_sum_val == last digit inherently confirms check_sum_val is NOT 10. + if check_sum_val == nhs_nums[-1]: + return { + 'valid': True, + 'message': 'Valid NHS number' } else: # remove final digit @@ -23,30 +48,90 @@ def validate_nhs_number(number_to_validate): for i in range(1, 10): # loop through the digits and apply multiplier which counts backwards from 11 # then sum all the products - multiplier = 11-i + multiplier = 11 - i # selects next digit in the number - digit = int(cleaned_number_as_string[i-1]) + digit = int(cleaned_number_as_string[i - 1]) modulus_eleven += digit * multiplier # divide the product by 11 and take the remainder remainder = modulus_eleven % 11 # subtract remaind from 11 to get final checksum - final_check_digit = 11-remainder + final_check_digit = 11 - remainder # if final_check_digit is 11, return 0. If 10, invalid if final_check_digit == 11: final_check_digit = 0 elif final_check_digit == 10: return { - 'valid': False, - 'message': f'{number_to_validate} is an invalid NHS Number.' + "valid": False, + "message": f"{number_to_validate} is an invalid NHS Number.", } if final_check_digit == checksum: - return { - 'valid': True, - 'message': 'Valid NHS number' - } + return {"valid": True, "message": "Valid NHS number"} else: return { - 'valid': False, - 'message': f'{number_to_validate} is an invalid NHS Number.' + "valid": False, + "message": f"{number_to_validate} is an invalid NHS Number.", } + + +def generate_nine_digits(): + """ + Generates a random 9 digit number and the remainder using the modulus 11 method + """ + modulus_eleven = 0 + digit_string = "" + for i in range(1, 10): + # loop through the digits and apply multiplier which counts backwards from 11 + # then sum all the products + multiplier = 11 - i + # selects next digit in the number + digit = randint(1, 9) # int(f'{nine_digit_number}'[i - 1]) + modulus_eleven += digit * multiplier + # divide the product by 11 and take the remainder + digit_string += f"{digit}" + remainder = modulus_eleven % 11 + return int(digit_string), remainder + + +def calculate_checksum(remainder): + """ + Calculates the checksum from the remainder + """ + # subtract remaind from 11 to get final checksum + final_check_digit = 11 - remainder + # if final_check_digit is 11, return 0. If 10, invalid + if final_check_digit == 11: + final_check_digit = 0 + elif final_check_digit == 10: + return None + + return final_check_digit + + +def generate_nhs_number(): + """ + Generates a valid NHS number + """ + final_check_digit = None + + while final_check_digit is None: + # create a base 9 digit number, whose first digit cannot be < 1 + nine_digits, remainder = generate_nine_digits() + # generate a checksum for that number - if None returned, number invalid, repeat + final_check_digit = calculate_checksum(remainder) + + return int(f"{nine_digits}{final_check_digit}") + + +def generate_nhs_numbers(requested_number: int) -> list: + """ + Returns a list of unique valid NHS numbers + param: requested_number defines requested list length + """ + number_set = set() + + while len(number_set) < requested_number: + random_valid_nhs_number = generate_nhs_number() + number_set.add(random_valid_nhs_number) + + return list(number_set) diff --git a/epilepsy12/general_functions/postcode.py b/epilepsy12/general_functions/postcode.py index 428e1936..43eb0655 100644 --- a/epilepsy12/general_functions/postcode.py +++ b/epilepsy12/general_functions/postcode.py @@ -1,24 +1,22 @@ import requests from django.conf import settings -from ..constants import UNKNOWN_POSTCODES +from ..constants import UNKNOWN_POSTCODES_NO_SPACES def is_valid_postcode(postcode): """ - Test if valid postcode using api.postcodes.io + Test if valid postcode using api.postcodes.io Allow any no fixed abode etc standard codes """ # convert to upper case and remove spaces - formatted = postcode.upper().replace(' ', '') + formatted = postcode.upper().replace(" ", "") # look for unknown postcodes - unknown = [code for code in UNKNOWN_POSTCODES if code.replace( - ' ', '') == formatted] - if len(unknown) > 0: + if formatted in UNKNOWN_POSTCODES_NO_SPACES: return True # check against API - url = f"{settings.POSTCODES_IO_API_URL}/{postcode}/validate" + url = f"{settings.POSTCODES_IO_API_URL}/postcodes/{postcode}/validate" response = requests.get(url=url) if response.status_code == 404: print("Postcode validation failure. Could not validate postcode.") @@ -29,12 +27,24 @@ def is_valid_postcode(postcode): def ons_region_for_postcode(postcode): # convert to upper case and remove spaces - formatted = postcode.upper().replace(' ', '') + formatted = postcode.upper().replace(" ", "") # check against API - url = f"{settings.POSTCODES_IO_API_URL}/{formatted}" + url = f"{settings.POSTCODES_IO_API_URL}/postcodes/{formatted}" response = requests.get(url=url) if response.status_code == 404: print("Postcode failure.") return False else: - return response.json()["result"]['region'] + return response.json()["result"]["region"] + + +def return_random_postcode(): + # get random postcode from API + url = f"{settings.POSTCODES_IO_API_URL}/random/postcodes" + + response = requests.get(url=url) + if response.status_code == 404: + print("Postcode generation failure. Could not get random postcode.") + return None + else: + return response.json()["result"]["postcode"] diff --git a/epilepsy12/general_functions/time_elapsed.py b/epilepsy12/general_functions/time_elapsed.py index 7f82030c..6fa7cc0e 100644 --- a/epilepsy12/general_functions/time_elapsed.py +++ b/epilepsy12/general_functions/time_elapsed.py @@ -2,58 +2,46 @@ import math -def calculate_time_elapsed(start_date, end_date): +def handle_interval(interval, singular_name): + if interval == 1: + return f"1 {singular_name}" + elif interval > 1: + return f"{interval} {singular_name}s" + else: + return "" + + +def stringify_time_elapsed(start_date, end_date): + """ + Calculated field. Returns time elapsed between two dates as a string. """ - Calculated field. Returns time elapsed between date EEG requested and performed as a string. - """ if end_date and start_date: - calculated_age = relativedelta( - end_date, start_date) - months = calculated_age.months - years = calculated_age.years - weeks = calculated_age.weeks - days = calculated_age.days - final = '' - if years == 1: - final += f'{calculated_age.years} year' - if (months/12) - years == 1: - final += f'{months} month' - elif (months/12)-years > 1: - final += f'{math.floor((months*12)-years)} months' - else: - return final + elapsed = relativedelta(end_date, start_date) + # Initialise empty string + string_delta = "" - elif years > 1: - final += f'{calculated_age.years} years' - if (months/12) - years == 1: - final += f', {months} month' - elif (months/12)-years > 1: - final += f', {math.floor((months*12)-years)} months' - else: - return final + # >= 1 year return "y years, m months" + if elapsed.years >= 1: + string_delta += handle_interval(elapsed.years, "year") + if elapsed.months > 0: + string_delta += f', {handle_interval(elapsed.months, "month")}' + return string_delta + + # 0 years, 0 - 12 months else: - # under a year of age - if months == 1: - final += f'{months} month' - elif months > 0: - final += f'{months} months, ' - if weeks >= (months*4): - if (weeks-(months*4)) == 1: - final += '1 week' - else: - final += f'{math.floor(weeks-(months*4))} weeks' + # >1 month return "m months" + if elapsed.months > 0: + string_delta += handle_interval(elapsed.months, "month") + # <1 month return "d days" else: - if weeks > 0: - if weeks == 1: - final += f'{math.floor(weeks)} week' - else: - final += f'{math.floor(weeks)} weeks' + if elapsed.weeks > 0: + string_delta += handle_interval(elapsed.weeks, "week") else: - if days > 0: - if days == 1: - final += f'{math.floor(days)} day' - if days > 1: - final += f'{math.floor(days)} days' + if elapsed.days > 0: + string_delta += handle_interval(elapsed.days, "day") else: - final += 'Performed today' - return final + string_delta += "Same day" + return string_delta + + else: + raise ValueError("Both start and end dates must be provided") diff --git a/epilepsy12/management/commands/create_e12_records.py b/epilepsy12/management/commands/create_e12_records.py index bee61c00..d59cedd0 100644 --- a/epilepsy12/management/commands/create_e12_records.py +++ b/epilepsy12/management/commands/create_e12_records.py @@ -1,5 +1,5 @@ # python dependencies -from random import randint, getrandbits +from random import randint, getrandbits, choice from dateutil.relativedelta import relativedelta from datetime import date @@ -49,8 +49,8 @@ NON_EPILEPSY_SLEEP_RELATED_SYMPTOMS, NON_EPILEPTIC_SYNCOPES, NON_EPILEPSY_PAROXYSMS, - ANTIEPILEPSY_MEDICINES, - BENZODIAZEPINE_TYPES, + SEVERITY, + CHRONICITY, ) from ...general_functions import ( random_date, @@ -59,10 +59,10 @@ fetch_ecl, fetch_paediatric_neurodisability_outpatient_diagnosis_simple_reference_set, ) -from ...common_view_functions import test_fields_update_audit_progress, calculate_kpis +from ...common_view_functions import update_audit_progress, calculate_kpis -def create_registrations(): +def create_registrations(verbose=True): """ run through all newly created cases and create registrations """ @@ -126,61 +126,70 @@ def create_registrations(): case=case, audit_progress=audit_progress, kpi=kpi ) else: - print(f"{case} is registered already. Skipping") + if verbose: + print(f"{case} is registered already. Skipping") return case.registration -def create_epilepsy12_record(registration_instance): +def create_epilepsy12_record(registration_instance, verbose=True): """ Creates a full randomised record for a given registration instance. """ # score registration_instance - test_fields_update_audit_progress(model_instance=registration_instance) + update_audit_progress(model_instance=registration_instance) # create random first paediatric assessment first_paediatric_assessment = create_first_paediatric_assessment( - registration_instance=registration_instance + registration_instance=registration_instance, verbose=verbose ) - test_fields_update_audit_progress(model_instance=first_paediatric_assessment) + update_audit_progress(model_instance=first_paediatric_assessment) # create random EpilepsyContext epilepsy_context = create_epilepsy_context( - registration_instance=registration_instance + registration_instance=registration_instance, verbose=verbose ) - test_fields_update_audit_progress(model_instance=epilepsy_context) + update_audit_progress(model_instance=epilepsy_context) # create random Multiaxial Diagnosis multiaxial_diagnosis = create_multiaxial_diagnosis( - registration_instance=registration_instance + registration_instance=registration_instance, verbose=verbose ) - test_fields_update_audit_progress(model_instance=multiaxial_diagnosis) + update_audit_progress(model_instance=multiaxial_diagnosis) # create random Assessment - assessment = create_assessment(registration_instance=registration_instance) - test_fields_update_audit_progress(model_instance=assessment) + assessment = create_assessment( + registration_instance=registration_instance, verbose=verbose + ) + update_audit_progress(model_instance=assessment) # create random Investigations - assessment = create_investigations(registration_instance=registration_instance) - test_fields_update_audit_progress(model_instance=assessment) + assessment = create_investigations( + registration_instance=registration_instance, verbose=verbose + ) + update_audit_progress(model_instance=assessment) # create random Management - management = create_management(registration_instance=registration_instance) - test_fields_update_audit_progress(model_instance=management) + management = create_management( + registration_instance=registration_instance, verbose=verbose + ) + update_audit_progress(model_instance=management) # calculate all the kpis calculate_kpis(registration_instance=registration_instance) -def create_first_paediatric_assessment(registration_instance): +def create_first_paediatric_assessment(registration_instance, verbose=True): """ Complete the first paediatric assessment aspect of the audit """ if not hasattr(registration_instance, "firstpaediatricassessment"): return FirstPaediatricAssessment.objects.create( - first_paediatric_assessment_in_acute_or_nonacute_setting=bool( + first_paediatric_assessment_in_acute_or_nonacute_setting=choice(CHRONICITY)[ + 0 + ], + has_number_of_episodes_since_the_first_been_documented=bool( getrandbits(1) ), - has_number_of_episodes_since_the_first_been_documented=bool(getrandbits(1)), general_examination_performed=bool(getrandbits(1)), neurological_examination_performed=bool(getrandbits(1)), developmental_learning_or_schooling_problems=bool(getrandbits(1)), @@ -188,36 +197,40 @@ def create_first_paediatric_assessment(registration_instance): registration=registration_instance, ) else: - print( - f"First Paediatric assessment exists for {registration_instance.case}. Skipping..." - ) + if verbose: + print( + f"First Paediatric assessment exists for {registration_instance.case}. Skipping..." + ) return registration_instance.firstpaediatricassessment -def create_epilepsy_context(registration_instance): +def create_epilepsy_context(registration_instance, verbose=True): """ Complete the epilepsy_context aspect of the audit """ if not hasattr(registration_instance, "epilepsycontext"): return EpilepsyContext.objects.create( - previous_febrile_seizure=OPT_OUT_UNCERTAIN[randint(0, 2)][0], - previous_acute_symptomatic_seizure=OPT_OUT_UNCERTAIN[randint(0, 2)][0], - is_there_a_family_history_of_epilepsy=OPT_OUT_UNCERTAIN[randint(0, 2)][0], - previous_neonatal_seizures=OPT_OUT_UNCERTAIN[randint(0, 2)][0], + previous_febrile_seizure=choice(OPT_OUT_UNCERTAIN)[0], + previous_acute_symptomatic_seizure=choice(OPT_OUT_UNCERTAIN)[0], + is_there_a_family_history_of_epilepsy=choice(OPT_OUT_UNCERTAIN)[0], + previous_neonatal_seizures=choice(OPT_OUT_UNCERTAIN)[0], diagnosis_of_epilepsy_withdrawn=bool(getrandbits(1)), were_any_of_the_epileptic_seizures_convulsive=bool(getrandbits(1)), - experienced_prolonged_generalized_convulsive_seizures=OPT_OUT_UNCERTAIN[ - randint(0, 2) - ][0], - experienced_prolonged_focal_seizures=OPT_OUT_UNCERTAIN[randint(0, 2)][0], + experienced_prolonged_generalized_convulsive_seizures=choice( + OPT_OUT_UNCERTAIN + )[0], + experienced_prolonged_focal_seizures=choice(OPT_OUT_UNCERTAIN)[0], registration=registration_instance, ) else: - print(f"Epilepsy context exists for {registration_instance.case}. Skipping...") + if verbose: + print( + f"Epilepsy context exists for {registration_instance.case}. Skipping..." + ) return registration_instance.epilepsycontext -def create_multiaxial_diagnosis(registration_instance): +def create_multiaxial_diagnosis(registration_instance, verbose=True): """ Complete the multiaxial diagnosis aspect of the audit, including episodes, syndromes, causes and comorbidities @@ -229,206 +242,184 @@ def create_multiaxial_diagnosis(registration_instance): relevant_impairments_behavioural_educational=bool(getrandbits(1)), mental_health_screen=bool(getrandbits(1)), mental_health_issue_identified=bool(getrandbits(1)), + global_developmental_delay_or_learning_difficulties=bool(getrandbits(1)), + autistic_spectrum_disorder=bool(getrandbits(1)), registration=registration_instance, ) + else: + multiaxial_diagnosis = registration_instance.multiaxialdiagnosis - if multiaxial_diagnosis.syndrome_present: - # create a related syndrome - syndrome_entity = SyndromeEntity.objects.filter( - syndrome_name=SYNDROMES[randint(0, len(SYNDROMES) - 1)][1] - ).get() - Syndrome.objects.create( - syndrome_diagnosis_date=random_date( - start=registration_instance.registration_date, end=date.today() - ), - syndrome=syndrome_entity, - multiaxial_diagnosis=multiaxial_diagnosis, + if multiaxial_diagnosis.global_developmental_delay_or_learning_difficulties: + rand_severity = SEVERITY[randint(0, len(SEVERITY) - 1)][0] + multiaxial_diagnosis.global_developmental_delay_or_learning_difficulties_severity = ( + rand_severity + ) + + if multiaxial_diagnosis.syndrome_present: + # create a related syndrome + syndrome_entity = SyndromeEntity.objects.filter( + syndrome_name=choice(SYNDROMES)[1] + ).get() + Syndrome.objects.create( + syndrome_diagnosis_date=random_date( + start=registration_instance.registration_date, end=date.today() + ), + syndrome=syndrome_entity, + multiaxial_diagnosis=multiaxial_diagnosis, + ) + + if multiaxial_diagnosis.epilepsy_cause_known: + ecl = "<< 363235000" + epilepsy_causes = fetch_ecl(ecl) + random_cause = EpilepsyCauseEntity.objects.filter( + conceptId=epilepsy_causes[randint(0, len(epilepsy_causes) - 1)]["conceptId"] + ).first() + + multiaxial_diagnosis.epilepsy_cause = random_cause + choices = [] + for item in range(0, randint(1, 3)): + chosen_cause = choice(EPILEPSY_CAUSES) + choices.append(chosen_cause[0]) + multiaxial_diagnosis.epilepsy_cause_categories = choices + + if multiaxial_diagnosis.mental_health_issue_identified: + multiaxial_diagnosis.mental_health_issue = choice(NEUROPSYCHIATRIC)[0] + + if multiaxial_diagnosis.relevant_impairments_behavioural_educational: + # add upto 5 comorbidities + for count_item in range(1, randint(2, 5)): + comorbidity_choices = ( + fetch_paediatric_neurodisability_outpatient_diagnosis_simple_reference_set() ) + random_comorbidities = choice(comorbidity_choices) - if multiaxial_diagnosis.epilepsy_cause_known: - ecl = "<< 363235000" - epilepsy_causes = fetch_ecl(ecl) - random_cause = EpilepsyCauseEntity.objects.filter( - conceptId=epilepsy_causes[randint(0, len(epilepsy_causes) - 1)][ - "conceptId" - ] + random_comorbidity = ComorbidityEntity.objects.filter( + conceptId=random_comorbidities["conceptId"] ).first() - multiaxial_diagnosis.epilepsy_cause = random_cause - total_cause_choices = len(EPILEPSY_CAUSES) - random_number_of_choices = randint(1, total_cause_choices) - choices = [] - for choice in range(0, random_number_of_choices): - choices.append(EPILEPSY_CAUSES[randint(0, len(EPILEPSY_CAUSES) - 1)][0]) - multiaxial_diagnosis.epilepsy_cause_categories = choices - - if multiaxial_diagnosis.mental_health_issue_identified: - multiaxial_diagnosis.mental_health_issue = NEUROPSYCHIATRIC[ - randint(0, len(NEUROPSYCHIATRIC) - 1) - ][0] - - if multiaxial_diagnosis.relevant_impairments_behavioural_educational: - # add upto 5 comorbidities - for count_item in range(1, randint(1, 5)): - comorbidity_choices = ( - fetch_paediatric_neurodisability_outpatient_diagnosis_simple_reference_set() + try: + Comorbidity.objects.create( + multiaxial_diagnosis=multiaxial_diagnosis, + comorbidity_diagnosis_date=random_date( + start=registration_instance.registration_date, + end=date.today(), + ), + comorbidityentity=random_comorbidity, ) - random_comorbidities = comorbidity_choices[ - randint(0, len(comorbidity_choices) - 1) - ] - random_comorbidity = ComorbidityEntity.objects.filter( - conceptId=random_comorbidities["conceptId"] - ).first() - - try: - Comorbidity.objects.create( - multiaxial_diagnosis=multiaxial_diagnosis, - comorbidity_diagnosis_date=random_date( - start=registration_instance.registration_date, - end=date.today(), - ), - comorbidityentity=random_comorbidity, - ) - except Exception as e: + except Exception as e: + if verbose: print( f"Failed to create Comorbidity with {random_comorbidity}:{e=}" ) - # create a random number of episodes to a maximum of 5 - for count_item in range(1, randint(1, 5)): - current_cohort_end_date = first_tuesday_in_january( - current_cohort_start_date().year + 2 - ) + relativedelta(days=7) - episode = Episode.objects.create( - multiaxial_diagnosis=multiaxial_diagnosis, - seizure_onset_date=random_date( - start=registration_instance.registration_date - - relativedelta(months=6), - end=date.today(), - ), - seizure_onset_date_confidence=DATE_ACCURACY[ - randint(0, len(DATE_ACCURACY) - 1) - ][0], - episode_definition=EPISODE_DEFINITION[ - randint(0, len(EPISODE_DEFINITION) - 1) - ][0], - ) - - if count_item == 1: - # the first episode must be epileptic, subsequent ones are random - episode.epilepsy_or_nonepilepsy_status = "E" - else: - episode.epilepsy_or_nonepilepsy_status = EPILEPSY_DIAGNOSIS_STATUS[ - randint(0, len(EPILEPSY_DIAGNOSIS_STATUS) - 1) - ][0] + # create a random number of episodes to a maximum of 5 + for count_item in range(1, randint(2, 5)): + episode = Episode.objects.create( + multiaxial_diagnosis=multiaxial_diagnosis, + seizure_onset_date=random_date( + start=registration_instance.registration_date - relativedelta(months=6), + end=date.today(), + ), + seizure_onset_date_confidence=choice(DATE_ACCURACY)[0], + episode_definition=choice(EPISODE_DEFINITION)[0], + ) - episode.has_description_of_the_episode_or_episodes_been_gathered = bool( - getrandbits(1) - ) + if count_item == 1: + # the first episode must be epileptic, subsequent ones are random + episode.epilepsy_or_nonepilepsy_status = "E" + else: + episode.epilepsy_or_nonepilepsy_status = choice(EPILEPSY_DIAGNOSIS_STATUS)[ + 0 + ] - if episode.has_description_of_the_episode_or_episodes_been_gathered: - keyword_array = [] - activity_choices = ( - "running", - "sleeping", - "on their way to school", - "watching YouTube", - "gaming", - ) - description_string = f"{registration_instance.case} was " - for random_number in range(randint(1, 5)): - random_keyword = Keyword.objects.order_by("?").first() - keyword_array.append(random_keyword) + episode.has_description_of_the_episode_or_episodes_been_gathered = bool( + getrandbits(1) + ) - description_string += ( - f"{activity_choices[random_number]} when they developed " - ) - for index, semiology_keyword in enumerate(keyword_array): - if index == len(keyword_array) - 1: - description_string += f"and {semiology_keyword}." - else: - description_string += f"{semiology_keyword}, " - - episode.description = description_string - episode.description_keywords = keyword_array - - if episode.epilepsy_or_nonepilepsy_status == "E": - episode.epileptic_seizure_onset_type = EPILEPSY_SEIZURE_TYPE[ - randint(0, len(EPILEPSY_SEIZURE_TYPE) - 1) - ][0] + if episode.has_description_of_the_episode_or_episodes_been_gathered: + keyword_array = [] + activity_choices = ( + "running", + "sleeping", + "on their way to school", + "watching YouTube", + "gaming", + ) + description_string = f"{registration_instance.case} was " + for random_number in range(randint(1, 5)): + random_keyword = Keyword.objects.order_by("?").first() + keyword_array.append(random_keyword) - if episode.epileptic_seizure_onset_type == "FO": - laterality = LATERALITY[randint(0, len(LATERALITY) - 1)] - motor_manifestation = FOCAL_EPILEPSY_MOTOR_MANIFESTATIONS[ - randint(0, len(FOCAL_EPILEPSY_MOTOR_MANIFESTATIONS) - 1) - ] - nonmotor_manifestation = FOCAL_EPILEPSY_NONMOTOR_MANIFESTATIONS[ - randint(0, len(FOCAL_EPILEPSY_NONMOTOR_MANIFESTATIONS) - 1) - ] - eeg_manifestations = FOCAL_EPILEPSY_EEG_MANIFESTATIONS[ - randint(0, len(FOCAL_EPILEPSY_EEG_MANIFESTATIONS) - 1) - ] - setattr(episode, laterality["name"], True) - setattr(episode, motor_manifestation["name"], True) - setattr(episode, nonmotor_manifestation["name"], True) - setattr(episode, eeg_manifestations["name"], True) - - elif episode.epileptic_seizure_onset_type == "GO": - episode.epileptic_generalised_onset = GENERALISED_SEIZURE_TYPE[ - randint(0, len(GENERALISED_SEIZURE_TYPE) - 1) - ][0] - - elif episode.epilepsy_or_nonepilepsy_status == "NE": - episode.nonepileptic_seizure_unknown_onset = NON_EPILEPSY_SEIZURE_ONSET[ - randint(0, len(NON_EPILEPSY_SEIZURE_ONSET) - 1) - ][0] - episode.nonepileptic_seizure_type = NON_EPILEPSY_SEIZURE_TYPE[ - randint(0, len(NON_EPILEPSY_SEIZURE_TYPE) - 1) + description_string += ( + f"{activity_choices[random_number]} when they developed " + ) + for index, semiology_keyword in enumerate(keyword_array): + if index == len(keyword_array) - 1: + description_string += f"and {semiology_keyword}." + else: + description_string += f"{semiology_keyword}, " + + episode.description = description_string + episode.description_keywords = keyword_array + + if episode.epilepsy_or_nonepilepsy_status == "E": + episode.epileptic_seizure_onset_type = choice(EPILEPSY_SEIZURE_TYPE)[0] + + if episode.epileptic_seizure_onset_type == "FO": + laterality = choice(LATERALITY) + motor_manifestation = choice(FOCAL_EPILEPSY_MOTOR_MANIFESTATIONS) + nonmotor_manifestation = choice(FOCAL_EPILEPSY_NONMOTOR_MANIFESTATIONS) + eeg_manifestations = choice(FOCAL_EPILEPSY_EEG_MANIFESTATIONS) + setattr(episode, laterality["name"], True) + setattr(episode, motor_manifestation["name"], True) + setattr(episode, nonmotor_manifestation["name"], True) + setattr(episode, eeg_manifestations["name"], True) + + elif episode.epileptic_seizure_onset_type == "GO": + episode.epileptic_generalised_onset = GENERALISED_SEIZURE_TYPE[ + randint(0, len(GENERALISED_SEIZURE_TYPE) - 1) ][0] - if episode.nonepileptic_seizure_type == "BPP": - episode.nonepileptic_seizure_behavioural = ( - NON_EPILEPSY_BEHAVIOURAL_ARREST_SYMPTOMS[ - len(NON_EPILEPSY_BEHAVIOURAL_ARREST_SYMPTOMS) - 1 - ][0] - ) - elif episode.nonepileptic_seizure_type == "MAD": - episode.nonepileptic_seizure_migraine = MIGRAINES[ - len(MIGRAINES) - 1 - ][0] - elif episode.nonepileptic_seizure_type == "ME": - episode.nonepileptic_seizure_miscellaneous = EPIS_MISC[ - len(EPIS_MISC) - 1 - ][0] - elif episode.nonepileptic_seizure_type == "SRC": - episode.nonepileptic_seizure_sleep = ( - NON_EPILEPSY_SLEEP_RELATED_SYMPTOMS[ - len(NON_EPILEPSY_SLEEP_RELATED_SYMPTOMS) - 1 - ][0] - ) - elif episode.nonepileptic_seizure_type == "SAS": - episode.nonepileptic_seizure_syncope = NON_EPILEPTIC_SYNCOPES[ - len(NON_EPILEPTIC_SYNCOPES) - 1 - ][0] - elif episode.nonepileptic_seizure_type == "PMD": - episode.nonepileptic_seizure_paroxysmal = NON_EPILEPSY_PAROXYSMS[ - len(NON_EPILEPSY_PAROXYSMS) - 1 - ][0] - else: - pass + elif episode.epilepsy_or_nonepilepsy_status == "NE": + episode.nonepileptic_seizure_unknown_onset = choice( + NON_EPILEPSY_SEIZURE_ONSET + )[0] + episode.nonepileptic_seizure_type = choice(NON_EPILEPSY_SEIZURE_TYPE)[0] + + if episode.nonepileptic_seizure_type == "BPP": + episode.nonepileptic_seizure_behavioural = choice( + NON_EPILEPSY_BEHAVIOURAL_ARREST_SYMPTOMS + )[0] + elif episode.nonepileptic_seizure_type == "MAD": + episode.nonepileptic_seizure_migraine = choice(MIGRAINES)[0] + elif episode.nonepileptic_seizure_type == "ME": + episode.nonepileptic_seizure_miscellaneous = choice(EPIS_MISC)[0] + elif episode.nonepileptic_seizure_type == "SRC": + episode.nonepileptic_seizure_sleep = choice( + NON_EPILEPSY_SLEEP_RELATED_SYMPTOMS + )[0] + elif episode.nonepileptic_seizure_type == "SAS": + episode.nonepileptic_seizure_syncope = choice(NON_EPILEPTIC_SYNCOPES)[0] + elif episode.nonepileptic_seizure_type == "PMD": + episode.nonepileptic_seizure_paroxysmal = choice( + NON_EPILEPSY_PAROXYSMS + )[0] + else: + pass - episode.save() + episode.save() multiaxial_diagnosis.save() return multiaxial_diagnosis else: - print( - f"Multiaxial diagnosis exists for {registration_instance.case}. Skipping..." - ) + if verbose: + print( + f"Multiaxial diagnosis exists for {registration_instance.case}. Skipping..." + ) return registration_instance.multiaxialdiagnosis -def create_assessment(registration_instance): +def create_assessment(registration_instance, verbose=True): """ Complete the assessment aspect of the audit, including specialist sites, """ @@ -492,7 +483,7 @@ def create_assessment(registration_instance): case=registration_instance.case, organisation=random_organisation, ).get() - site.site_is_general_paediatric_centre = True + site.site_is_paediatric_neurology_centre = True site.save() else: Site.objects.create( @@ -521,7 +512,7 @@ def create_assessment(registration_instance): case=registration_instance.case, organisation=random_organisation, ).get() - site.site_is_general_paediatric_centre = True + site.site_is_childrens_epilepsy_surgery_centre = True site.save() else: Site.objects.create( @@ -543,11 +534,12 @@ def create_assessment(registration_instance): assessment.save() return assessment else: - print(f"Assessment exists for {registration_instance.case}. Skipping...") + if verbose: + print(f"Assessment exists for {registration_instance.case}. Skipping...") return registration_instance.assessment -def create_investigations(registration_instance): +def create_investigations(registration_instance, verbose=True): """ Complete the investigations aspect of the audit, including medications """ @@ -577,11 +569,12 @@ def create_investigations(registration_instance): investigations.save() return investigations else: - print(f"Investigations exist for {registration_instance.case}. Skipping...") + if verbose: + print(f"Investigations exist for {registration_instance.case}. Skipping...") return registration_instance.investigations -def create_management(registration_instance): +def create_management(registration_instance, verbose=True): """ Complete the management aspect of the audit, including medications """ @@ -594,7 +587,7 @@ def create_management(registration_instance): ) if management.has_an_aed_been_given: - for count_item in range(1, randint(1, 3)): + for count_item in range(0, randint(1, 3)): # add a random number of medicines up to a total of 3 random_medicine = ( MedicineEntity.objects.filter(is_rescue=False).order_by("?").first() @@ -622,14 +615,19 @@ def create_management(registration_instance): antiepilepsy_medicine.is_a_pregnancy_prevention_programme_needed = bool( getrandbits(1) ) - antiepilepsy_medicine.has_a_valproate_annual_risk_acknowledgement_form_been_completed - antiepilepsy_medicine.is_a_pregnancy_prevention_programme_in_place = bool( - getrandbits(1) - ) + if ( + antiepilepsy_medicine.is_a_pregnancy_prevention_programme_needed + ): + antiepilepsy_medicine.has_a_valproate_annual_risk_acknowledgement_form_been_completed = bool( + getrandbits(1) + ) + antiepilepsy_medicine.is_a_pregnancy_prevention_programme_in_place = bool( + getrandbits(1) + ) antiepilepsy_medicine.save() if management.has_rescue_medication_been_prescribed: - for count_item in range(1, randint(1, 3)): + for count_item in range(1, randint(2, 3)): # add a random number of medicines up to a total of 3 random_medicine = ( MedicineEntity.objects.filter(is_rescue=True).order_by("?").first() @@ -644,7 +642,8 @@ def create_management(registration_instance): medicine_entity=random_medicine, ) else: - print(f"Management exists for {registration_instance.case}. Skipping...") + if verbose: + print(f"Management exists for {registration_instance.case}. Skipping...") return registration_instance.management if management.individualised_care_plan_in_place: @@ -672,8 +671,9 @@ def create_management(registration_instance): management.has_individualised_care_plan_been_updated_in_the_last_year = bool( getrandbits(1) ) - management.has_been_referred_for_mental_health_support = bool(getrandbits(1)) - management.has_support_for_mental_health_support = bool(getrandbits(1)) + + management.has_been_referred_for_mental_health_support = bool(getrandbits(1)) + management.has_support_for_mental_health_support = bool(getrandbits(1)) management.save() return management diff --git a/epilepsy12/management/commands/create_groups.py b/epilepsy12/management/commands/create_groups.py index 173975c4..3d167836 100644 --- a/epilepsy12/management/commands/create_groups.py +++ b/epilepsy12/management/commands/create_groups.py @@ -2,454 +2,487 @@ from django.contrib.auth.models import Group, Permission from django.contrib.contenttypes.models import ContentType from ...constants import GROUPS -from epilepsy12.constants.user_types import EPILEPSY12_AUDIT_TEAM_EDIT_ACCESS, EPILEPSY12_AUDIT_TEAM_FULL_ACCESS, EPILEPSY12_AUDIT_TEAM_VIEW_ONLY, PATIENT_ACCESS, TRUST_AUDIT_TEAM_EDIT_ACCESS, TRUST_AUDIT_TEAM_FULL_ACCESS, TRUST_AUDIT_TEAM_VIEW_ONLY, CAN_CONSENT_TO_AUDIT_PARTICIPATION, CAN_APPROVE_ELIGIBILITY, CAN_REMOVE_APPROVAL_OF_ELIGIBILITY, CAN_REGISTER_CHILD_IN_EPILEPSY12, CAN_UNREGISTER_CHILD_IN_EPILEPSY12, CAN_ALLOCATE_EPILEPSY12_LEAD_CENTRE, CAN_TRANSFER_EPILEPSY12_LEAD_CENTRE, CAN_EDIT_EPILEPSY12_LEAD_CENTRE, CAN_DELETE_EPILEPSY12_LEAD_CENTRE, CAN_APPROVE_ELIGIBILITY, CAN_REMOVE_APPROVAL_OF_ELIGIBILITY, CAN_LOCK_CHILD_CASE_DATA_FROM_EDITING, CAN_UNLOCK_CHILD_CASE_DATA_FROM_EDITING, CAN_OPT_OUT_CHILD_FROM_INCLUSION_IN_AUDIT -from epilepsy12.models import AntiEpilepsyMedicine, Assessment, AuditProgress, Comorbidity, EpilepsyContext, Episode, FirstPaediatricAssessment, Investigations, Management, MultiaxialDiagnosis, Syndrome, Registration, Case, Organisation, Keyword, Site, Epilepsy12User - -# globals -caseContentType = ContentType.objects.get_for_model(Case) -registrationContentType = ContentType.objects.get_for_model( - Registration) -first_paediatric_assessmentContentType = ContentType.objects.get_for_model( - FirstPaediatricAssessment) -epilepsy_contextContentType = ContentType.objects.get_for_model( - EpilepsyContext) -multiaxial_diagnosisContentType = ContentType.objects.get_for_model( - MultiaxialDiagnosis) -episodeContentType = ContentType.objects.get_for_model(Episode) -syndromeContentType = ContentType.objects.get_for_model(Syndrome) -comorbidityContentType = ContentType.objects.get_for_model( - Comorbidity) -assessmentContentType = ContentType.objects.get_for_model( - Assessment) -investigationsContentType = ContentType.objects.get_for_model( - Investigations) -managementContentType = ContentType.objects.get_for_model( - Management) -siteContentType = ContentType.objects.get_for_model( - Site) -antiepilepsymedicineContentType = ContentType.objects.get_for_model( - AntiEpilepsyMedicine) -organisationContentType = ContentType.objects.get_for_model( - Organisation) -keywordContentType = ContentType.objects.get_for_model( - Keyword) -auditprogressContentType = ContentType.objects.get_for_model( - AuditProgress) -epilepsy12userContentType = ContentType.objects.get_for_model( - Epilepsy12User) - -VIEW_PERMISSIONS = [ - {'codename': 'view_case', - 'content_type': caseContentType}, - {'codename': 'view_registration', - 'content_type': registrationContentType}, - {'codename': 'view_firstpaediatricassessment', - 'content_type': first_paediatric_assessmentContentType}, - {'codename': 'view_epilepsycontext', - 'content_type': epilepsy_contextContentType}, - {'codename': 'view_multiaxialdiagnosis', - 'content_type': multiaxial_diagnosisContentType}, - {'codename': 'view_episode', - 'content_type': episodeContentType}, - {'codename': 'view_syndrome', - 'content_type': syndromeContentType}, - {'codename': 'view_comorbidity', - 'content_type': comorbidityContentType}, - {'codename': 'view_assessment', - 'content_type': assessmentContentType}, - {'codename': 'view_investigations', - 'content_type': investigationsContentType}, - {'codename': 'view_management', - 'content_type': managementContentType}, - {'codename': 'view_antiepilepsymedicine', - 'content_type': antiepilepsymedicineContentType}, - {'codename': 'view_site', - 'content_type': siteContentType}, - {'codename': 'view_organisation', - 'content_type': organisationContentType}, - {'codename': 'view_keyword', - 'content_type': keywordContentType}, - {'codename': 'view_auditprogress', - 'content_type': auditprogressContentType}, - {'codename': 'view_epilepsy12user', - 'content_type': epilepsy12userContentType}, -] - -EDITOR_PERMISSIONS = [ - {'codename': 'change_case', - 'content_type': caseContentType}, - {'codename': 'add_case', - 'content_type': caseContentType}, - {'codename': 'change_registration', - 'content_type': registrationContentType}, - {'codename': 'add_registration', - 'content_type': registrationContentType}, - {'codename': 'can_register_child_in_epilepsy12', - 'content_type': registrationContentType}, - {'codename': 'can_unregister_child_in_epilepsy12', - 'content_type': registrationContentType}, - {'codename': 'can_approve_eligibility', - 'content_type': registrationContentType}, - {'codename': 'change_firstpaediatricassessment', - 'content_type': first_paediatric_assessmentContentType}, - {'codename': 'add_firstpaediatricassessment', - 'content_type': first_paediatric_assessmentContentType}, - {'codename': 'view_epilepsycontext', - 'content_type': epilepsy_contextContentType}, - {'codename': 'change_epilepsycontext', - 'content_type': epilepsy_contextContentType}, - {'codename': 'add_epilepsycontext', - 'content_type': epilepsy_contextContentType}, - {'codename': 'change_multiaxialdiagnosis', - 'content_type': multiaxial_diagnosisContentType}, - {'codename': 'add_multiaxialdiagnosis', - 'content_type': multiaxial_diagnosisContentType}, - {'codename': 'change_episode', - 'content_type': episodeContentType}, - {'codename': 'add_episode', - 'content_type': episodeContentType}, - {'codename': 'change_syndrome', - 'content_type': syndromeContentType}, - {'codename': 'add_syndrome', - 'content_type': syndromeContentType}, - {'codename': 'change_comorbidity', - 'content_type': comorbidityContentType}, - {'codename': 'add_comorbidity', - 'content_type': comorbidityContentType}, - {'codename': 'change_assessment', - 'content_type': assessmentContentType}, - {'codename': 'add_assessment', - 'content_type': assessmentContentType}, - {'codename': 'change_investigations', - 'content_type': investigationsContentType}, - {'codename': 'add_investigations', - 'content_type': investigationsContentType}, - {'codename': 'change_management', - 'content_type': managementContentType}, - {'codename': 'add_management', - 'content_type': managementContentType}, - {'codename': 'change_antiepilepsymedicine', - 'content_type': antiepilepsymedicineContentType}, - {'codename': 'add_antiepilepsymedicine', - 'content_type': antiepilepsymedicineContentType}, - {'codename': 'change_site', - 'content_type': siteContentType}, - {'codename': 'add_site', - 'content_type': siteContentType}, - {'codename': 'add_epilepsy12user', - 'content_type': epilepsy12userContentType}, - {'codename': 'change_epilepsy12user', - 'content_type': epilepsy12userContentType} -] - -FULL_ACCESS_PERMISSIONS = [ - {'codename': 'delete_case', 'content_type': caseContentType}, - {'codename': 'delete_registration', - 'content_type': registrationContentType}, - {'codename': 'delete_firstpaediatricassessment', - 'content_type': first_paediatric_assessmentContentType}, - {'codename': 'delete_epilepsycontext', - 'content_type': epilepsy_contextContentType}, - {'codename': 'delete_multiaxialdiagnosis', - 'content_type': multiaxial_diagnosisContentType}, - {'codename': 'delete_episode', 'content_type': episodeContentType}, - {'codename': 'delete_syndrome', - 'content_type': syndromeContentType}, - {'codename': 'delete_comorbidity', - 'content_type': comorbidityContentType}, - {'codename': 'delete_assessment', - 'content_type': assessmentContentType}, - {'codename': 'delete_investigations', - 'content_type': investigationsContentType}, - {'codename': 'delete_management', - 'content_type': managementContentType}, - {'codename': 'delete_site', 'content_type': siteContentType}, - {'codename': 'delete_antiepilepsymedicine', - 'content_type': antiepilepsymedicineContentType}, -] - - -EPILEPSY12_AUDIT_TEAM_VIEW_ONLY_PERMISSIONS = [ - -] - -EPILEPSY12_AUDIT_TEAM_EDIT_ACCESS_PERMISSIONS = [ - {'codename': CAN_APPROVE_ELIGIBILITY[0], - 'content_type': registrationContentType}, - {'codename': CAN_REMOVE_APPROVAL_OF_ELIGIBILITY[0], - 'content_type': registrationContentType}, - {'codename': CAN_REGISTER_CHILD_IN_EPILEPSY12[0], - 'content_type': registrationContentType}, - {'codename': CAN_UNREGISTER_CHILD_IN_EPILEPSY12[0], - 'content_type': registrationContentType}, - {'codename': CAN_TRANSFER_EPILEPSY12_LEAD_CENTRE[0], - 'content_type': siteContentType}, - {'codename': CAN_CONSENT_TO_AUDIT_PARTICIPATION[0], - 'content_type': caseContentType}, - {'codename': CAN_LOCK_CHILD_CASE_DATA_FROM_EDITING[0], - 'content_type': caseContentType}, - {'codename': CAN_UNLOCK_CHILD_CASE_DATA_FROM_EDITING[0], - 'content_type': caseContentType}, - {'codename': CAN_OPT_OUT_CHILD_FROM_INCLUSION_IN_AUDIT[0], - 'content_type': caseContentType}, -] - -EPILEPSY12_AUDIT_TEAM_FULL_ACCESS_PERMISSIONS = [ - {'codename': 'change_organisation', - 'content_type': organisationContentType}, - {'codename': 'add_organisation', - 'content_type': organisationContentType}, - {'codename': 'delete_organisation', - 'content_type': organisationContentType}, - {'codename': 'change_keyword', - 'content_type': keywordContentType}, - {'codename': 'add_keyword', - 'content_type': keywordContentType}, - {'codename': 'delete_keyword', - 'content_type': keywordContentType}, - {'codename': 'delete_auditprogress', - 'content_type': auditprogressContentType}, - {'codename': 'change_auditprogress', - 'content_type': auditprogressContentType}, - {'codename': 'add_auditprogress', - 'content_type': auditprogressContentType}, - {'codename': 'delete_epilepsy12user', - 'content_type': epilepsy12userContentType}, - {'codename': CAN_DELETE_EPILEPSY12_LEAD_CENTRE[0], - 'content_type': siteContentType}, - {'codename': CAN_EDIT_EPILEPSY12_LEAD_CENTRE[0], - 'content_type': siteContentType}, - {'codename': CAN_ALLOCATE_EPILEPSY12_LEAD_CENTRE[0], - 'content_type': siteContentType}, -] - - -TRUST_AUDIT_TEAM_VIEW_ONLY_PERMISSIONS = [ - -] - -TRUST_AUDIT_TEAM_EDIT_ACCESS_PERMISSIONS = [ - - {'codename': CAN_APPROVE_ELIGIBILITY[0], - 'content_type': registrationContentType}, - {'codename': CAN_REGISTER_CHILD_IN_EPILEPSY12[0], - 'content_type': registrationContentType}, - {'codename': CAN_TRANSFER_EPILEPSY12_LEAD_CENTRE[0], - 'content_type': siteContentType}, - {'codename': CAN_LOCK_CHILD_CASE_DATA_FROM_EDITING[0], - 'content_type': caseContentType}, - {'codename': CAN_OPT_OUT_CHILD_FROM_INCLUSION_IN_AUDIT[0], - 'content_type': caseContentType}, - -] - -TRUST_AUDIT_TEAM_FULL_ACCESS_PERMISSIONS = [ - {'codename': CAN_APPROVE_ELIGIBILITY[0], - 'content_type': registrationContentType}, - {'codename': CAN_REGISTER_CHILD_IN_EPILEPSY12[0], - 'content_type': registrationContentType}, - {'codename': CAN_LOCK_CHILD_CASE_DATA_FROM_EDITING[0], - 'content_type': caseContentType}, - {'codename': CAN_OPT_OUT_CHILD_FROM_INCLUSION_IN_AUDIT[0], - 'content_type': caseContentType}, -] - -PATIENT_ACCESS_PERMISSIONS = [ - {'codename': CAN_CONSENT_TO_AUDIT_PARTICIPATION[0], - 'content_type': caseContentType}, -] - - -def initialize_permissions(apps, schema_editor): +from epilepsy12.constants.user_types import ( + # group names + EPILEPSY12_AUDIT_TEAM_FULL_ACCESS, + PATIENT_ACCESS, + TRUST_AUDIT_TEAM_EDIT_ACCESS, + TRUST_AUDIT_TEAM_FULL_ACCESS, + TRUST_AUDIT_TEAM_VIEW_ONLY, + # custom permissions + CAN_CONSENT_TO_AUDIT_PARTICIPATION, + CAN_APPROVE_ELIGIBILITY, + CAN_REGISTER_CHILD_IN_EPILEPSY12, + CAN_UNREGISTER_CHILD_IN_EPILEPSY12, + CAN_ALLOCATE_EPILEPSY12_LEAD_CENTRE, + CAN_TRANSFER_EPILEPSY12_LEAD_CENTRE, + CAN_EDIT_EPILEPSY12_LEAD_CENTRE, + CAN_DELETE_EPILEPSY12_LEAD_CENTRE, + CAN_APPROVE_ELIGIBILITY, + CAN_LOCK_CHILD_CASE_DATA_FROM_EDITING, + CAN_UNLOCK_CHILD_CASE_DATA_FROM_EDITING, + CAN_OPT_OUT_CHILD_FROM_INCLUSION_IN_AUDIT, +) +from epilepsy12.models import ( + AntiEpilepsyMedicine, + Assessment, + AuditProgress, + Comorbidity, + EpilepsyContext, + Episode, + FirstPaediatricAssessment, + Investigations, + Management, + MultiaxialDiagnosis, + Syndrome, + Registration, + Case, + Organisation, + Keyword, + Site, + Epilepsy12User, +) + + +def groups_seeder( + run_create_groups=False, add_permissions_to_existing_groups=False, verbose=True +): + caseContentType = ContentType.objects.get_for_model(Case) + registrationContentType = ContentType.objects.get_for_model(Registration) + first_paediatric_assessmentContentType = ContentType.objects.get_for_model( + FirstPaediatricAssessment + ) + epilepsy_contextContentType = ContentType.objects.get_for_model(EpilepsyContext) + multiaxial_diagnosisContentType = ContentType.objects.get_for_model( + MultiaxialDiagnosis + ) + episodeContentType = ContentType.objects.get_for_model(Episode) + syndromeContentType = ContentType.objects.get_for_model(Syndrome) + comorbidityContentType = ContentType.objects.get_for_model(Comorbidity) + assessmentContentType = ContentType.objects.get_for_model(Assessment) + investigationsContentType = ContentType.objects.get_for_model(Investigations) + managementContentType = ContentType.objects.get_for_model(Management) + siteContentType = ContentType.objects.get_for_model(Site) + antiepilepsymedicineContentType = ContentType.objects.get_for_model( + AntiEpilepsyMedicine + ) + organisationContentType = ContentType.objects.get_for_model(Organisation) + keywordContentType = ContentType.objects.get_for_model(Keyword) + auditprogressContentType = ContentType.objects.get_for_model(AuditProgress) + epilepsy12userContentType = ContentType.objects.get_for_model(Epilepsy12User) + """ - This function is run in migrations/0002_create_groups.py as an initial - data migration at project initialization. it sets up some basic model-level - permissions for different groups when the project is initialised. + Note view permissions include viewing users, but not creating, updating or deleting them + View permissions include viewing but NOT updating or deleting case audit records - 6 groups. Loop through and add custom + NOTE Additional constraints are applied in view decorators to prevent users accessing + records of users or children in organisations other than their own """ + VIEW_PERMISSIONS = [ + # epilepsy12 user + {"codename": "view_epilepsy12user", "content_type": epilepsy12userContentType}, + # case + {"codename": "view_case", "content_type": caseContentType}, + # registration + {"codename": "view_registration", "content_type": registrationContentType}, + # first paediatric assessment + { + "codename": "view_firstpaediatricassessment", + "content_type": first_paediatric_assessmentContentType, + }, + # epilepsy context + { + "codename": "view_epilepsycontext", + "content_type": epilepsy_contextContentType, + }, + # multiaxial diagnosis + { + "codename": "view_multiaxialdiagnosis", + "content_type": multiaxial_diagnosisContentType, + }, + # episode + {"codename": "view_episode", "content_type": episodeContentType}, + # syndrome + {"codename": "view_syndrome", "content_type": syndromeContentType}, + # comorbidity + {"codename": "view_comorbidity", "content_type": comorbidityContentType}, + # assessment + {"codename": "view_assessment", "content_type": assessmentContentType}, + # investigations + {"codename": "view_investigations", "content_type": investigationsContentType}, + # management + {"codename": "view_management", "content_type": managementContentType}, + # antiepilepsy medicine + { + "codename": "view_antiepilepsymedicine", + "content_type": antiepilepsymedicineContentType, + }, + # site + {"codename": "view_site", "content_type": siteContentType}, + # organisation + {"codename": "view_organisation", "content_type": organisationContentType}, + # keyword + {"codename": "view_keyword", "content_type": keywordContentType}, + # audit progress + {"codename": "view_auditprogress", "content_type": auditprogressContentType}, + ] - # Permissions have to be created before applying them - for app_config in apps.get_app_configs(apps, schema_editor): - app_config.models_module = True - create_permissions(app_config, verbosity=0) - app_config.models_module = None - - -def add_permissions_to_existing_groups(): - for group in GROUPS: - print(f'...adding permissions to {group}...') - # add permissions to group - newGroup = Group.objects.filter(name=group).get() - - if group == EPILEPSY12_AUDIT_TEAM_VIEW_ONLY: - # custom permissions - add_permissions_to_group( - EPILEPSY12_AUDIT_TEAM_VIEW_ONLY_PERMISSIONS, newGroup) - # basic permissions - add_permissions_to_group(VIEW_PERMISSIONS, newGroup) - - elif group == EPILEPSY12_AUDIT_TEAM_EDIT_ACCESS: - # custom permissions - add_permissions_to_group( - EPILEPSY12_AUDIT_TEAM_EDIT_ACCESS_PERMISSIONS, newGroup) - # basic permissions - add_permissions_to_group(VIEW_PERMISSIONS, newGroup) - add_permissions_to_group(EDITOR_PERMISSIONS, newGroup) - - elif group == EPILEPSY12_AUDIT_TEAM_FULL_ACCESS: - # custom permissions - add_permissions_to_group( - EPILEPSY12_AUDIT_TEAM_FULL_ACCESS_PERMISSIONS, newGroup) - # basic permissions - add_permissions_to_group(VIEW_PERMISSIONS, newGroup) - add_permissions_to_group(EDITOR_PERMISSIONS, newGroup) - add_permissions_to_group(FULL_ACCESS_PERMISSIONS, newGroup) - - elif group == TRUST_AUDIT_TEAM_VIEW_ONLY: - # custom permissions - add_permissions_to_group( - TRUST_AUDIT_TEAM_VIEW_ONLY_PERMISSIONS, newGroup) - # basic permissions - add_permissions_to_group(VIEW_PERMISSIONS, newGroup) - - elif group == TRUST_AUDIT_TEAM_EDIT_ACCESS: - # custom permissions - add_permissions_to_group( - TRUST_AUDIT_TEAM_VIEW_ONLY_PERMISSIONS, newGroup) - add_permissions_to_group( - TRUST_AUDIT_TEAM_EDIT_ACCESS_PERMISSIONS, newGroup) - # basic permissions - add_permissions_to_group(VIEW_PERMISSIONS, newGroup) - add_permissions_to_group(EDITOR_PERMISSIONS, newGroup) - - elif group == TRUST_AUDIT_TEAM_FULL_ACCESS: - # custom permissions - add_permissions_to_group( - TRUST_AUDIT_TEAM_VIEW_ONLY_PERMISSIONS, newGroup) - add_permissions_to_group( - TRUST_AUDIT_TEAM_EDIT_ACCESS_PERMISSIONS, newGroup) - add_permissions_to_group( - TRUST_AUDIT_TEAM_FULL_ACCESS_PERMISSIONS, newGroup) - # basic permissions - add_permissions_to_group(VIEW_PERMISSIONS, newGroup) - add_permissions_to_group(EDITOR_PERMISSIONS, newGroup) - add_permissions_to_group(FULL_ACCESS_PERMISSIONS, newGroup) - - elif group == PATIENT_ACCESS: - # custom permissions - add_permissions_to_group( - PATIENT_ACCESS_PERMISSIONS, newGroup) - # basic permissions - add_permissions_to_group(VIEW_PERMISSIONS, newGroup) - - else: - print("Error: group does not exist!") - - -def create_groups(): - for group in GROUPS: - if not Group.objects.filter(name=group).exists(): - print(f'...creating group: {group}') - try: - newGroup = Group.objects.create(name=group) - except Exception as error: - print(error) - error = True - - print(f'...adding permissions to {group}...') - # add permissions to group + """ + Administrators have additional privileges in relation to case management + Permissions include create, update and view cases, but not delete them + """ + ADMIN_CASE_MANAGEMENT_PERMISSIONS = [ + {"codename": "change_case", "content_type": caseContentType}, + {"codename": "add_case", "content_type": caseContentType}, + ] + """ + Editors inherit all view permissions + Note editor access permissions do not include creating, updating or deleting Epilepsy12Users. + Editor access include deleting patients + Editor access permissions do include creating, updating or delete patient records + + Editors can create, update and delete neurology, general paediatric and surgical sites, but + cannot create, update or delete lead epilepsy12 centre allocation, or transfer + + NOTE Additional constraints are applied in view decorators to prevent users accessing + records of users or children in organisations other than their own + """ + EDITOR_PERMISSIONS = [ + # case + {"codename": "delete_case", "content_type": caseContentType}, + # registration + {"codename": "change_registration", "content_type": registrationContentType}, + {"codename": "add_registration", "content_type": registrationContentType}, + {"codename": "delete_registration", "content_type": registrationContentType}, + # first paediatric assessment + { + "codename": "change_firstpaediatricassessment", + "content_type": first_paediatric_assessmentContentType, + }, + { + "codename": "add_firstpaediatricassessment", + "content_type": first_paediatric_assessmentContentType, + }, + { + "codename": "delete_firstpaediatricassessment", + "content_type": first_paediatric_assessmentContentType, + }, + # epilepsy context + { + "codename": "change_epilepsycontext", + "content_type": epilepsy_contextContentType, + }, + { + "codename": "delete_epilepsycontext", + "content_type": epilepsy_contextContentType, + }, + { + "codename": "add_epilepsycontext", + "content_type": epilepsy_contextContentType, + }, + # multiaxial diagnosis + { + "codename": "change_multiaxialdiagnosis", + "content_type": multiaxial_diagnosisContentType, + }, + { + "codename": "add_multiaxialdiagnosis", + "content_type": multiaxial_diagnosisContentType, + }, + { + "codename": "delete_multiaxialdiagnosis", + "content_type": multiaxial_diagnosisContentType, + }, + # episode + {"codename": "change_episode", "content_type": episodeContentType}, + {"codename": "add_episode", "content_type": episodeContentType}, + {"codename": "delete_episode", "content_type": episodeContentType}, + # syndrome + {"codename": "change_syndrome", "content_type": syndromeContentType}, + {"codename": "add_syndrome", "content_type": syndromeContentType}, + {"codename": "delete_syndrome", "content_type": syndromeContentType}, + # comorbidity + {"codename": "change_comorbidity", "content_type": comorbidityContentType}, + {"codename": "add_comorbidity", "content_type": comorbidityContentType}, + {"codename": "delete_comorbidity", "content_type": comorbidityContentType}, + # assessment + {"codename": "change_assessment", "content_type": assessmentContentType}, + {"codename": "add_assessment", "content_type": assessmentContentType}, + {"codename": "delete_assessment", "content_type": assessmentContentType}, + # investigations + { + "codename": "change_investigations", + "content_type": investigationsContentType, + }, + {"codename": "add_investigations", "content_type": investigationsContentType}, + { + "codename": "delete_investigations", + "content_type": investigationsContentType, + }, + # mnaagement + {"codename": "change_management", "content_type": managementContentType}, + {"codename": "add_management", "content_type": managementContentType}, + {"codename": "delete_management", "content_type": managementContentType}, + # antiepilepsy medicine + { + "codename": "change_antiepilepsymedicine", + "content_type": antiepilepsymedicineContentType, + }, + { + "codename": "add_antiepilepsymedicine", + "content_type": antiepilepsymedicineContentType, + }, + { + "codename": "delete_antiepilepsymedicine", + "content_type": antiepilepsymedicineContentType, + }, + # sites + {"codename": "change_site", "content_type": siteContentType}, + {"codename": "add_site", "content_type": siteContentType}, + {"codename": "delete_site", "content_type": siteContentType}, + # custom + { + "codename": "can_register_child_in_epilepsy12", + "content_type": registrationContentType, + }, + { + "codename": CAN_APPROVE_ELIGIBILITY[0], + "content_type": registrationContentType, + }, + ] - if group == EPILEPSY12_AUDIT_TEAM_VIEW_ONLY: - # custom permissions - add_permissions_to_group( - EPILEPSY12_AUDIT_TEAM_VIEW_ONLY_PERMISSIONS, newGroup) - # basic permissions - add_permissions_to_group(VIEW_PERMISSIONS, newGroup) + """ + Full access inherit all editor permissions + In addition they can + - create, change and delete Epilepsy12Users + - transfer to another the lead Epilepsy12 centre - elif group == EPILEPSY12_AUDIT_TEAM_EDIT_ACCESS: - # custom permissions - add_permissions_to_group( - EPILEPSY12_AUDIT_TEAM_VIEW_ONLY_PERMISSIONS, newGroup) - add_permissions_to_group( - EPILEPSY12_AUDIT_TEAM_EDIT_ACCESS_PERMISSIONS, newGroup) - # basic permissions - add_permissions_to_group(VIEW_PERMISSIONS, newGroup) - add_permissions_to_group(EDITOR_PERMISSIONS, newGroup) + NOTE Additional constraints are applied in view decorators to prevent users accessing + records of users or children in organisations other than their own + """ + FULL_ACCESS_PERMISSIONS = [ + # epilepsy12 user + {"codename": "add_epilepsy12user", "content_type": epilepsy12userContentType}, + { + "codename": "change_epilepsy12user", + "content_type": epilepsy12userContentType, + }, + { + "codename": "delete_epilepsy12user", + "content_type": epilepsy12userContentType, + }, + { + "codename": CAN_TRANSFER_EPILEPSY12_LEAD_CENTRE[0], + "content_type": siteContentType, + }, + ] - elif group == EPILEPSY12_AUDIT_TEAM_FULL_ACCESS: + """ + Epilepsy12 Team inherit all view, edit and full access permissions. In addition they may: + - unregister children from Epilepsy12 + - opt children out of Epilepsy12 + - allocate, update and delete Epilepsy12 lead site status + - create, update and delete the look up lists for Keyword and Organisation + + NOTE RCPCH team are able to access all users and all children nationally. + """ + EPILEPSY12_AUDIT_TEAM_ACCESS_PERMISSIONS = [ + { + "codename": "can_unregister_child_in_epilepsy12", + "content_type": registrationContentType, + }, + { + "codename": CAN_LOCK_CHILD_CASE_DATA_FROM_EDITING[0], + "content_type": caseContentType, + }, + { + "codename": CAN_UNLOCK_CHILD_CASE_DATA_FROM_EDITING[0], + "content_type": caseContentType, + }, + { + "codename": CAN_OPT_OUT_CHILD_FROM_INCLUSION_IN_AUDIT[0], + "content_type": caseContentType, + }, + { + "codename": CAN_DELETE_EPILEPSY12_LEAD_CENTRE[0], + "content_type": siteContentType, + }, + { + "codename": CAN_EDIT_EPILEPSY12_LEAD_CENTRE[0], + "content_type": siteContentType, + }, + { + "codename": CAN_ALLOCATE_EPILEPSY12_LEAD_CENTRE[0], + "content_type": siteContentType, + }, + {"codename": "change_organisation", "content_type": organisationContentType}, + {"codename": "add_organisation", "content_type": organisationContentType}, + {"codename": "delete_organisation", "content_type": organisationContentType}, + {"codename": "change_keyword", "content_type": keywordContentType}, + {"codename": "add_keyword", "content_type": keywordContentType}, + {"codename": "delete_keyword", "content_type": keywordContentType}, + ] + + PATIENT_ACCESS_PERMISSIONS = [ + { + "codename": CAN_CONSENT_TO_AUDIT_PARTICIPATION[0], + "content_type": caseContentType, + }, + ] + + def initialize_permissions(apps, schema_editor): + """ + This function is run in migrations/0002_create_groups.py as an initial + data migration at project initialization. it sets up some basic model-level + permissions for different groups when the project is initialised. + + 6 groups. Loop through and add custom + """ + + # Permissions have to be created before applying them + for app_config in apps.get_app_configs(apps, schema_editor): + app_config.models_module = True + create_permissions(app_config, verbosity=0) + app_config.models_module = None + + if add_permissions_to_existing_groups: + for group in GROUPS: + if verbose: + print(f"...adding permissions to {group}...") + # add permissions to group + newGroup = Group.objects.filter(name=group).get() + + if group == EPILEPSY12_AUDIT_TEAM_FULL_ACCESS: # custom permissions add_permissions_to_group( - EPILEPSY12_AUDIT_TEAM_VIEW_ONLY_PERMISSIONS, newGroup) - add_permissions_to_group( - EPILEPSY12_AUDIT_TEAM_EDIT_ACCESS_PERMISSIONS, newGroup) - add_permissions_to_group( - EPILEPSY12_AUDIT_TEAM_FULL_ACCESS_PERMISSIONS, newGroup) + EPILEPSY12_AUDIT_TEAM_ACCESS_PERMISSIONS, newGroup + ) # basic permissions add_permissions_to_group(VIEW_PERMISSIONS, newGroup) + add_permissions_to_group(ADMIN_CASE_MANAGEMENT_PERMISSIONS, newGroup) add_permissions_to_group(EDITOR_PERMISSIONS, newGroup) add_permissions_to_group(FULL_ACCESS_PERMISSIONS, newGroup) elif group == TRUST_AUDIT_TEAM_VIEW_ONLY: # custom permissions - add_permissions_to_group( - TRUST_AUDIT_TEAM_VIEW_ONLY_PERMISSIONS, newGroup) + # basic permissions add_permissions_to_group(VIEW_PERMISSIONS, newGroup) elif group == TRUST_AUDIT_TEAM_EDIT_ACCESS: # custom permissions - add_permissions_to_group( - TRUST_AUDIT_TEAM_VIEW_ONLY_PERMISSIONS, newGroup) - add_permissions_to_group( - TRUST_AUDIT_TEAM_EDIT_ACCESS_PERMISSIONS, newGroup) + # basic permissions add_permissions_to_group(VIEW_PERMISSIONS, newGroup) + add_permissions_to_group(ADMIN_CASE_MANAGEMENT_PERMISSIONS, newGroup) add_permissions_to_group(EDITOR_PERMISSIONS, newGroup) elif group == TRUST_AUDIT_TEAM_FULL_ACCESS: # custom permissions - add_permissions_to_group( - TRUST_AUDIT_TEAM_VIEW_ONLY_PERMISSIONS, newGroup) - add_permissions_to_group( - TRUST_AUDIT_TEAM_EDIT_ACCESS_PERMISSIONS, newGroup) - add_permissions_to_group( - TRUST_AUDIT_TEAM_FULL_ACCESS_PERMISSIONS, newGroup) + # basic permissions add_permissions_to_group(VIEW_PERMISSIONS, newGroup) + add_permissions_to_group(ADMIN_CASE_MANAGEMENT_PERMISSIONS, newGroup) add_permissions_to_group(EDITOR_PERMISSIONS, newGroup) add_permissions_to_group(FULL_ACCESS_PERMISSIONS, newGroup) elif group == PATIENT_ACCESS: # custom permissions - add_permissions_to_group( - PATIENT_ACCESS_PERMISSIONS, newGroup) + add_permissions_to_group(PATIENT_ACCESS_PERMISSIONS, newGroup) # basic permissions add_permissions_to_group(VIEW_PERMISSIONS, newGroup) else: - print("Error: group does not exist!") - else: - print(f'{group} already exists. Skipping...') - - -def add_permissions_to_group(permissions_list, group_to_add): - for permission in permissions_list: - codename = permission.get('codename') - content_type = permission.get('content_type') - newPermission = Permission.objects.get( - codename=codename, - content_type=content_type - ) - if group_to_add.permissions.filter(codename=codename).exists(): - print(f'{codename} already exists for this group. Skipping...') - else: - print(f'...Adding {codename}') - group_to_add.permissions.add(newPermission) - - -def delete_and_reallocate_permissions(): - for group in GROUPS: - Group.objects.filter(name=group).delete() - print(f'Deleted {group}') - create_groups() + if verbose: + print("Error: group does not exist!") + + def add_permissions_to_group(permissions_list, group_to_add): + for permission in permissions_list: + codename = permission.get("codename") + content_type = permission.get("content_type") + newPermission = Permission.objects.get( + codename=codename, content_type=content_type + ) + if group_to_add.permissions.filter(codename=codename).exists(): + if verbose: + print(f"{codename} already exists for this group. Skipping...") + else: + if verbose: + print(f"...Adding {codename}") + group_to_add.permissions.add(newPermission) + + if run_create_groups: + for group in GROUPS: + if not Group.objects.filter(name=group).exists(): + if verbose: + print(f"...creating group: {group}") + try: + newGroup = Group.objects.create(name=group) + except Exception as error: + if verbose: + print(error) + error = True + + if verbose: + print(f"...adding permissions to {group}...") + # add permissions to group + + if group == EPILEPSY12_AUDIT_TEAM_FULL_ACCESS: + # custom permissions + add_permissions_to_group( + EPILEPSY12_AUDIT_TEAM_ACCESS_PERMISSIONS, newGroup + ) + # basic permissions + add_permissions_to_group(VIEW_PERMISSIONS, newGroup) + add_permissions_to_group( + ADMIN_CASE_MANAGEMENT_PERMISSIONS, newGroup + ) + add_permissions_to_group(EDITOR_PERMISSIONS, newGroup) + add_permissions_to_group(FULL_ACCESS_PERMISSIONS, newGroup) + + elif group == TRUST_AUDIT_TEAM_VIEW_ONLY: + # custom permissions + + # basic permissions + add_permissions_to_group(VIEW_PERMISSIONS, newGroup) + add_permissions_to_group( + ADMIN_CASE_MANAGEMENT_PERMISSIONS, newGroup + ) + + elif group == TRUST_AUDIT_TEAM_EDIT_ACCESS: + # custom permissions + + # basic permissions + add_permissions_to_group(VIEW_PERMISSIONS, newGroup) + add_permissions_to_group( + ADMIN_CASE_MANAGEMENT_PERMISSIONS, newGroup + ) + add_permissions_to_group(EDITOR_PERMISSIONS, newGroup) + + elif group == TRUST_AUDIT_TEAM_FULL_ACCESS: + # custom permissions + + # basic permissions + add_permissions_to_group(VIEW_PERMISSIONS, newGroup) + add_permissions_to_group( + ADMIN_CASE_MANAGEMENT_PERMISSIONS, newGroup + ) + add_permissions_to_group(EDITOR_PERMISSIONS, newGroup) + add_permissions_to_group(FULL_ACCESS_PERMISSIONS, newGroup) + + elif group == PATIENT_ACCESS: + # custom permissions + add_permissions_to_group(PATIENT_ACCESS_PERMISSIONS, newGroup) + # basic permissions + add_permissions_to_group(VIEW_PERMISSIONS, newGroup) + + else: + if verbose: + print("Error: group does not exist!") + + else: + if verbose: + print("Error: group does not exist!") + + if not verbose: + print("groups_seeder(verbose=False), no output, groups seeded.") diff --git a/epilepsy12/management/commands/seed.py b/epilepsy12/management/commands/seed.py index e2422243..db54df7b 100644 --- a/epilepsy12/management/commands/seed.py +++ b/epilepsy12/management/commands/seed.py @@ -4,58 +4,27 @@ from dateutil.relativedelta import relativedelta from random import randint from django.core.management.base import BaseCommand -from ...general_functions import get_current_cohort_data +from ...general_functions import ( + get_current_cohort_data, + generate_nhs_number, + return_random_postcode, +) from ...constants import ( ETHNICITIES, - DUMMY_NAMES, - SYNDROMES, - SNOMED_BENZODIAZEPINE_TYPES, - SNOMED_ANTIEPILEPSY_MEDICINE_TYPES, - OPEN_UK_NETWORKS, - RCPCH_ORGANISATIONS, ) from ...models import ( Organisation, - Keyword, Case, Site, Registration, - SyndromeEntity, - EpilepsyCauseEntity, - ComorbidityEntity, - MedicineEntity, - AntiEpilepsyMedicine, - IntegratedCareBoardEntity, - OPENUKNetworkEntity, - NHSRegionEntity, - ONSRegionEntity, - ONSCountryEntity, -) -from ...constants import ( - ALL_HOSPITALS, - KEYWORDS, - WELSH_HOSPITALS, - INTEGRATED_CARE_BOARDS_LOCAL_AUTHORITIES, - WELSH_REGIONS, - COUNTRY_CODES, - UK_ONS_REGIONS, ) + from ...general_functions import ( - random_postcodes, random_date, - first_tuesday_in_january, - current_cohort_start_date, - fetch_ecl, - fetch_paediatric_neurodisability_outpatient_diagnosis_simple_reference_set, - ons_region_for_postcode, -) -from .create_groups import ( - create_groups, - add_permissions_to_existing_groups, - delete_and_reallocate_permissions, ) +from .create_groups import groups_seeder from .create_e12_records import create_epilepsy12_record, create_registrations @@ -63,12 +32,22 @@ class Command(BaseCommand): help = "seed database with organisation trust data for testing and development." def add_arguments(self, parser): - parser.add_argument("--mode", type=str, help="Mode") + parser.add_argument("-m", "--mode", type=str, help="Mode") + parser.add_argument( + "-c", + "--cases", + nargs="?", + type=int, + help="Indicates the number of children to be created", + default=50, + ) def handle(self, *args, **options): - if options["mode"] == "seed_dummy_cases": + if options["mode"] == "cases": + cases = options["cases"] self.stdout.write("seeding with dummy case data...") - run_dummy_cases_seed() + run_dummy_cases_seed(cases=cases) + elif options["mode"] == "seed_registrations": self.stdout.write( "register cases in audit and complete all fields with random answers..." @@ -76,10 +55,10 @@ def handle(self, *args, **options): run_registrations() elif options["mode"] == "seed_groups_and_permissions": self.stdout.write("setting up groups and permissions...") - create_groups() + groups_seeder(run_create_groups=True) elif options["mode"] == "add_permissions_to_existing_groups": self.stdout.write("adding permissions to groups...") - add_permissions_to_existing_groups() + groups_seeder(add_permissions_to_existing_groups=True) elif options["mode"] == "delete_all_groups_and_recreate": self.stdout.write("deleting all groups/permissions and reallocating...") elif options["mode"] == "add_existing_medicines_as_foreign_keys": @@ -93,53 +72,43 @@ def handle(self, *args, **options): self.stdout.write("done.") -def run_dummy_cases_seed(): +def run_dummy_cases_seed(verbose=True, cases=50): added = 0 - print("\033[33m", "Seeding fictional cases...", "\033[33m") + if verbose: + print("\033[33m", "Seeding fictional cases...", "\033[33m") # there should not be any cases yet, but sometimes seed gets run more than once if Case.objects.all().exists(): - print(f"Cases already exist. Skipping this step...") + if verbose: + print("Cases already exist. Skipping this step...") return - postcode_list = random_postcodes.generate_postcodes(requested_number=100) - - random_organisations = ["RX1LK", "RK5BC"] - - """ - Commented out section creates cases across 10 organisations, the first being Addenbrooke's - # first populate Addenbrooke's for ease of dev testing - for _ in range(1, 11): - random_organisations.append( - Organisation.objects.get(ODSCode='RGT01')) - - # seed the remaining 9 - for j in range(9): - random_organisation = Organisation.objects.order_by("?").first() - for i in range(1, 11): - random_organisations.append(random_organisation) - """ + if cases is None or cases == 0: + cases = 50 # for index in range(len(DUMMY_NAMES) - 1): # commented out line populates all the names, not just first 20 - for index in range(0, 20): # first 20 names + for index in range(0, cases): # first 20 names random_date = date(randint(2005, 2021), randint(1, 12), randint(1, 28)) - nhs_number = randint(1000000000, 9999999999) - first_name = DUMMY_NAMES[index]["firstname"] - surname = DUMMY_NAMES[index]["lastname"] - gender_object = DUMMY_NAMES[index]["gender"] - if gender_object == "m": - sex = 1 + nhs_number = generate_nhs_number() + sex = randint(1, 2) + random_ethnicity = randint(0, len(choice(ETHNICITIES))) + if sex == 2: + first_name = "Dolly" + surname = f"Shepherd-{index}" else: - sex = 2 + first_name = "Agent" + surname = f"Smith-{index}" + date_of_birth = random_date - postcode = postcode_list[index] - ethnicity = choice(ETHNICITIES)[0] + postcode = return_random_postcode() + ethnicity = ETHNICITIES[random_ethnicity][0] # get a random organisation - if index < 11: - # organisation = random_organisations[index] # line used if populating random hospitals - organisation = Organisation.objects.get(ODSCode=random_organisations[0]) + if index < 50: + organisation = Organisation.objects.get(ODSCode="RGT01") # King's Mill else: - organisation = Organisation.objects.get(ODSCode=random_organisations[1]) + organisation = Organisation.objects.order_by( + "?" + ).first() # random organisation case_has_error = False @@ -156,7 +125,8 @@ def run_dummy_cases_seed(): ) new_case.save() except Exception as e: - print(f"Error saving case: {e}") + if verbose: + print(f"Error saving case: {e}") case_has_error = True if not case_has_error: @@ -169,35 +139,44 @@ def run_dummy_cases_seed(): ) new_site.save() except Exception as e: - print(f"Error saving site: {e}") + if verbose: + print(f"Error saving site: {e}") added += 1 - print( - f"{new_case.first_name} {new_case.surname} at {new_site.organisation.OrganisationName} ({new_site.organisation.ParentOrganisation_OrganisationName})..." - ) + if verbose: + print( + f"{new_case.first_name} {new_case.surname} at {new_site.organisation.OrganisationName} ({new_site.organisation.ParentOrganisation_OrganisationName})..." + ) print(f"Saved {added} cases.") -def run_registrations(): +def run_registrations(verbose=True): """ Calling function to register all cases in Epilepsy12 and complete all fields with random answers """ - print("\033[33m", "Registering fictional cases in Epilepsy12...", "\033[33m") + if verbose: + print("\033[33m", "Registering fictional cases in Epilepsy12...", "\033[33m") + + create_registrations(verbose=verbose) - create_registrations() + complete_registrations(verbose=verbose) - complete_registrations() + if not verbose: + print( + "run_registrations(verbose=False), no output, cases registered and completed." + ) -def complete_registrations(): +def complete_registrations(verbose=True): """ Loop through the registrations and score all fields """ - print( - "\033[33m", - "Completing all the Epilepsy12 fields for the fictional cases...", - "\033[33m", - ) + if verbose: + print( + "\033[33m", + "Completing all the Epilepsy12 fields for the fictional cases...", + "\033[33m", + ) current_cohort = get_current_cohort_data() for registration in Registration.objects.all(): registration.registration_date = random_date( @@ -206,7 +185,7 @@ def complete_registrations(): registration.eligibility_criteria_met = True registration.save() - create_epilepsy12_record(registration_instance=registration) + create_epilepsy12_record(registration_instance=registration, verbose=verbose) def image(): diff --git a/epilepsy12/migrations/0001_initial.py b/epilepsy12/migrations/0001_initial.py index aeaa9a4a..674030c9 100644 --- a/epilepsy12/migrations/0001_initial.py +++ b/epilepsy12/migrations/0001_initial.py @@ -1373,7 +1373,7 @@ class Migration(migrations.Migration): ( "Geocode_Coordinates", django.contrib.gis.db.models.fields.PointField( - blank=True, default=None, null=True, srid=4326 + blank=True, default=None, null=True, srid=27700 ), ), ( @@ -2877,7 +2877,7 @@ class Migration(migrations.Migration): ( "Geocode_Coordinates", django.contrib.gis.db.models.fields.PointField( - blank=True, default=None, null=True, srid=4326 + blank=True, default=None, null=True, srid=27700 ), ), ( @@ -5501,7 +5501,7 @@ class Migration(migrations.Migration): default=None, help_text={ "label": "Is a pregnancy prevention programme indicated?", - "reference": "For girls and young women who are presecribed sodium valproate, it is recommended that pregnancy prevention is actively discussed and documented.", + "reference": "For girls and young women who are prescribed sodium valproate, it is recommended that pregnancy prevention is actively discussed and documented.", }, null=True, ), @@ -5513,7 +5513,7 @@ class Migration(migrations.Migration): default=None, help_text={ "label": "Has a Valproate - Annual Risk Acknowledgment Form been completed?", - "reference": "For girls and young women who are presecribed sodium valproate, it is recommended that Has an annual Valproate - Annual Risk Acknowledgment Form is completed.", + "reference": "For girls and young women who are prescribed sodium valproate, it is recommended that Has an annual Valproate - Annual Risk Acknowledgment Form is completed.", }, null=True, ), @@ -5525,7 +5525,7 @@ class Migration(migrations.Migration): default=None, help_text={ "label": "Is the Valproate Pregnancy Prevention Programme in place?", - "reference": "For girls and young women who are presecribed sodium valproate, it is recommended that pregnancy prevention is actively discussed and documented.", + "reference": "For girls and young women who are prescribed sodium valproate, it is recommended that pregnancy prevention is actively discussed and documented.", }, null=True, ), @@ -6739,7 +6739,7 @@ class Migration(migrations.Migration): default=None, help_text={ "label": "Is a pregnancy prevention programme indicated?", - "reference": "For girls and young women who are presecribed sodium valproate, it is recommended that pregnancy prevention is actively discussed and documented.", + "reference": "For girls and young women who are prescribed sodium valproate, it is recommended that pregnancy prevention is actively discussed and documented.", }, null=True, ), @@ -6751,7 +6751,7 @@ class Migration(migrations.Migration): default=None, help_text={ "label": "Has a Valproate - Annual Risk Acknowledgment Form been completed?", - "reference": "For girls and young women who are presecribed sodium valproate, it is recommended that Has an annual Valproate - Annual Risk Acknowledgment Form is completed.", + "reference": "For girls and young women who are prescribed sodium valproate, it is recommended that Has an annual Valproate - Annual Risk Acknowledgment Form is completed.", }, null=True, ), @@ -6763,7 +6763,7 @@ class Migration(migrations.Migration): default=None, help_text={ "label": "Is the Valproate Pregnancy Prevention Programme in place?", - "reference": "For girls and young women who are presecribed sodium valproate, it is recommended that pregnancy prevention is actively discussed and documented.", + "reference": "For girls and young women who are prescribed sodium valproate, it is recommended that pregnancy prevention is actively discussed and documented.", }, null=True, ), diff --git a/epilepsy12/migrations/0003_seed_organisations.py b/epilepsy12/migrations/0003_seed_organisations.py index f55951f5..8d4449a7 100644 --- a/epilepsy12/migrations/0003_seed_organisations.py +++ b/epilepsy12/migrations/0003_seed_organisations.py @@ -9,9 +9,12 @@ from django.utils import timezone # RCPCH imports -from ..constants import (RCPCH_ORGANISATIONS, - OPEN_UK_NETWORKS, - INTEGRATED_CARE_BOARDS_LOCAL_AUTHORITIES, WELSH_REGIONS) +from ..constants import ( + RCPCH_ORGANISATIONS, + OPEN_UK_NETWORKS, + INTEGRATED_CARE_BOARDS_LOCAL_AUTHORITIES, + WELSH_REGIONS, +) from ..general_functions import ons_region_for_postcode @@ -26,30 +29,38 @@ def seed_organisations(apps, schema_editor): Organisation = apps.get_model("epilepsy12", "Organisation") OPENUKNetworkEntity = apps.get_model("epilepsy12", "OPENUKNetworkEntity") IntegratedCareBoardEntity = apps.get_model( - "epilepsy12", "IntegratedCareBoardEntity") + "epilepsy12", "IntegratedCareBoardEntity" + ) NHSRegionEntity = apps.get_model("epilepsy12", "NHSRegionEntity") ONSRegionEntity = apps.get_model("epilepsy12", "ONSRegionEntity") - print('\033[31m', "Adding new organisations...", '\033[31m') + print("\033[31m", "Adding new RCPCH organisations...", "\033[31m") for added, rcpch_organisation in enumerate(RCPCH_ORGANISATIONS): - # get openuk network code from ods code in constants list # this gets openuk network object from table open_uk_code = next( - item for item in OPEN_UK_NETWORKS if item["ods trust code"] == rcpch_organisation["ParentODSCode"]) + item + for item in OPEN_UK_NETWORKS + if item["ods trust code"] == rcpch_organisation["ParentODSCode"] + ) open_uk_network = OPENUKNetworkEntity.objects.get( - OPEN_UK_Network_Code=open_uk_code["OPEN UK Network Code"]) + OPEN_UK_Network_Code=open_uk_code["OPEN UK Network Code"] + ) # For England, assign NHS England Region Code, ONS Region Code and ICB Code to Organisation # For other nations, this is not possible due to different NHS organisational units - if open_uk_code['country'] == 'England': + if open_uk_code["country"] == "England": # get icb from icb list in constants # then get icb object from table icb_code = next( - item for item in INTEGRATED_CARE_BOARDS_LOCAL_AUTHORITIES if item["ODS Trust Code"] == rcpch_organisation["ParentODSCode"]) + item + for item in INTEGRATED_CARE_BOARDS_LOCAL_AUTHORITIES + if item["ODS Trust Code"] == rcpch_organisation["ParentODSCode"] + ) integrated_care_board = IntegratedCareBoardEntity.objects.get( - ODS_ICB_Code=icb_code["ODS ICB Code"]) + ODS_ICB_Code=icb_code["ODS ICB Code"] + ) # get nhs england region from icb list in constants # then get NHSRegion object from table @@ -57,86 +68,88 @@ def seed_organisations(apps, schema_editor): NHS_Region_Code=icb_code["NHS England Region Code"] ) - if rcpch_organisation['ParentODSCode'] == "RXP": + if rcpch_organisation["ParentODSCode"] == "RXP": # postcodes io error - postcodes not found: hacky work around ons_region_name = "North East" - elif rcpch_organisation['ParentODSCode'] == "RN3": + elif rcpch_organisation["ParentODSCode"] == "RN3": ons_region_name = "South West" - elif rcpch_organisation['ParentODSCode'] == "RM3": + elif rcpch_organisation["ParentODSCode"] == "RM3": ons_region_name = "North West" + elif rcpch_organisation["ParentODSCode"] == "R1C": + ons_region_name = "South West" else: ons_region_name = ons_region_for_postcode( - rcpch_organisation['Postcode']) + rcpch_organisation["Postcode"] + ) ons_region = ONSRegionEntity.objects.filter( - Region_ONS_Name=ons_region_name).get() + Region_ONS_Name=ons_region_name + ).get() # For Wales, assign Health Board, ODS Code, and ONS Region to Organisation - elif open_uk_code['country'] == 'Wales': + elif open_uk_code["country"] == "Wales": # health_board = next( - item for item in WELSH_REGIONS if item['ODS_Code'] == rcpch_organisation["ParentODSCode"] + item + for item in WELSH_REGIONS + if item["ODS_Code"] == rcpch_organisation["ParentODSCode"] ) integrated_care_board = None nhs_region = NHSRegionEntity.objects.get( - NHS_Region_Code=health_board["ODS_Code"]) - ons_region = ONSRegionEntity.objects.filter( - Region_ONS_Name="Wales").get() + NHS_Region_Code=health_board["ODS_Code"] + ) + ons_region = ONSRegionEntity.objects.filter(Region_ONS_Name="Wales").get() else: raise Exception( - f"{open_uk_code['ods trust code']} is not allocated to a country.") + f"{open_uk_code['ods trust code']} is not allocated to a country." + ) # Apply longitude and latitude data, if exists - if hasattr(rcpch_organisation, "longitude") and hasattr(rcpch_organisation, 'latitude'): - if len(rcpch_organisation["longitude"]) > 0 and len(rcpch_organisation['latitude']) > 1: - latitude = rcpch_organisation['latitude'] - longitude = rcpch_organisation['longitude'] - new_point = Point( - x=rcpch_organisation["longitude"], y=rcpch_organisation['latitude']) - else: - latitude = None - longitude = None - new_point = None - else: + new_point = None + try: + latitude = float(rcpch_organisation["Latitude"]) + except: latitude = None - longitude = None - new_point = None + try: + longitude = float(rcpch_organisation["Longitude"]) + except: + latitude = None + + if longitude and latitude: + new_point = Point(x=longitude, y=latitude) # Date-stamps the Organisation information (this data was supplied on 19.04.2023) update_date = datetime(year=2023, month=4, day=19) - timezone_aware_update_date = timezone.make_aware( - update_date, timezone.utc) + timezone_aware_update_date = timezone.make_aware(update_date, timezone.utc) # Create Organisation instances try: Organisation.objects.create( - ODSCode=rcpch_organisation['OrganisationCode'], - OrganisationName=rcpch_organisation['OrganisationName'], - Website=rcpch_organisation['Website'], - Address1=rcpch_organisation['Address1'], - Address2=rcpch_organisation['Address2'], - Address3=rcpch_organisation['Address3'], - City=rcpch_organisation['City'], - County=rcpch_organisation['County'], + ODSCode=rcpch_organisation["OrganisationCode"], + OrganisationName=rcpch_organisation["OrganisationName"], + Website=rcpch_organisation["Website"], + Address1=rcpch_organisation["Address1"], + Address2=rcpch_organisation["Address2"], + Address3=rcpch_organisation["Address3"], + City=rcpch_organisation["City"], + County=rcpch_organisation["County"], Latitude=latitude, Longitude=longitude, - Postcode=rcpch_organisation['Postcode'], + Postcode=rcpch_organisation["Postcode"], Geocode_Coordinates=new_point, - ParentOrganisation_ODSCode=rcpch_organisation['ParentODSCode'], - ParentOrganisation_OrganisationName=rcpch_organisation['ParentName'], + ParentOrganisation_ODSCode=rcpch_organisation["ParentODSCode"], + ParentOrganisation_OrganisationName=rcpch_organisation["ParentName"], LastUpdatedDate=timezone_aware_update_date, openuk_network=open_uk_network, integrated_care_board=integrated_care_board, nhs_region=nhs_region, - ons_region=ons_region + ons_region=ons_region, ).save() - print( - f"{added+1}: {rcpch_organisation['OrganisationName']}") + print(f"{added+1}: {rcpch_organisation['OrganisationName']}") except Exception as error: - print( - f"Unable to save {rcpch_organisation['OrganisationName']}: {error}") + print(f"Unable to save {rcpch_organisation['OrganisationName']}: {error}") - print('All organisations added.') + print("All organisations added.") class Migration(migrations.Migration): diff --git a/epilepsy12/migrations/0009_alter_historicalinvestigations_eeg_request_date_and_more.py b/epilepsy12/migrations/0009_alter_historicalinvestigations_eeg_request_date_and_more.py new file mode 100644 index 00000000..e9b8aecd --- /dev/null +++ b/epilepsy12/migrations/0009_alter_historicalinvestigations_eeg_request_date_and_more.py @@ -0,0 +1,173 @@ +# Generated by Django 4.2.1 on 2023-05-07 22:01 + + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("epilepsy12", "0008_seed_medicines"), + ] + + operations = [ + migrations.AlterField( + model_name="historicalinvestigations", + name="eeg_request_date", + field=models.DateField( + blank=True, + default=None, + help_text={ + "label": "Date EEG requested", + "reference": "Date EEG requested. Even if the EEG was not performed, a request date is still required.", + }, + null=True, + ), + ), + migrations.AlterField( + model_name="historicalmanagement", + name="individualised_care_plan_addresses_sudep", + field=models.BooleanField( + blank=True, + default=None, + help_text={ + "label": "Sudden unexpected death in epilepsy (SUDEP)", + "reference": "Does the individualised care plan address sudden unexpected death in epilepsy?", + }, + null=True, + ), + ), + migrations.AlterField( + model_name="investigations", + name="eeg_request_date", + field=models.DateField( + blank=True, + default=None, + help_text={ + "label": "Date EEG requested", + "reference": "Date EEG requested. Even if the EEG was not performed, a request date is still required.", + }, + null=True, + ), + ), + migrations.AlterField( + model_name="kpi", + name="care_planning_has_been_updated_when_necessary", + field=models.IntegerField( + default=None, + help_text={ + "label": "iii. Care planning has been updated when necessary", + "reference": "Percentage of children and young people with epilepsy after 12 months where there is evidence that the care plan has been updated where necessary.", + }, + null=True, + ), + ), + migrations.AlterField( + model_name="kpi", + name="first_aid", + field=models.IntegerField( + default=None, + help_text={ + "label": "iii. First aid", + "reference": "Percentage of children and young people with epilepsy with evidence of discussion regarding first aid.", + }, + null=True, + ), + ), + migrations.AlterField( + model_name="kpi", + name="general_participation_and_risk", + field=models.IntegerField( + default=None, + help_text={ + "label": "iv. General participation and risk", + "reference": "Percentage of children and young people with epilepsy with evidence of discussion regarding general participation and risk.", + }, + null=True, + ), + ), + migrations.AlterField( + model_name="kpi", + name="parental_prolonged_seizures_care_plan", + field=models.IntegerField( + default=None, + help_text={ + "label": "i. Parental prolonged seizures care plan", + "reference": "Percentage of children and young people with epilepsy who have been prescribed rescue medication and have evidence of a written prolonged seizures plan.", + }, + null=True, + ), + ), + migrations.AlterField( + model_name="kpi", + name="patient_carer_parent_agreement_to_the_care_planning", + field=models.IntegerField( + default=None, + help_text={ + "label": "ii. Patient/carer/parent agreement to the care planning", + "reference": "Percentage of children and young people with epilepsy after 12 months where there was evidence of agreement between the person, their family and/or carers as appropriate.", + }, + null=True, + ), + ), + migrations.AlterField( + model_name="kpi", + name="patient_held_individualised_epilepsy_document", + field=models.IntegerField( + default=None, + help_text={ + "label": "i. Patient-held individualised epilepsy document/copy of clinic letter that includes care planning information", + "reference": "Percentage of children and young people with epilepsy after 12 months that had an individualised epilepsy document with individualised epilepsy document or a copy clinic letter that includes care planning information.", + }, + null=True, + ), + ), + migrations.AlterField( + model_name="kpi", + name="service_contact_details", + field=models.IntegerField( + default=None, + help_text={ + "label": "vi. Service contact details", + "reference": "Percentage of children and young people with epilepsy with evidence of being given service contact details.", + }, + null=True, + ), + ), + migrations.AlterField( + model_name="kpi", + name="sudep", + field=models.IntegerField( + default=None, + help_text={ + "label": "v. Sudden unexpected death in epilepsy", + "reference": "Percentage of children and young people with epilepsy with evidence of discussion regarding SUDEP (Sudden unexpected death in epilepsy) and evidence of a prolonged seizures care plan.", + }, + null=True, + ), + ), + migrations.AlterField( + model_name="kpi", + name="water_safety", + field=models.IntegerField( + default=None, + help_text={ + "label": "ii. Water safety", + "reference": "Percentage of children and young people with epilepsy with evidence of discussion regarding water safety.", + }, + null=True, + ), + ), + migrations.AlterField( + model_name="management", + name="individualised_care_plan_addresses_sudep", + field=models.BooleanField( + blank=True, + default=None, + help_text={ + "label": "Sudden unexpected death in epilepsy (SUDEP)", + "reference": "Does the individualised care plan address sudden unexpected death in epilepsy?", + }, + null=True, + ), + ), + ] diff --git a/epilepsy12/migrations/0010_countryboundaries_integratedcareboardboundaries_and_more.py b/epilepsy12/migrations/0010_countryboundaries_integratedcareboardboundaries_and_more.py new file mode 100644 index 00000000..eeac8db7 --- /dev/null +++ b/epilepsy12/migrations/0010_countryboundaries_integratedcareboardboundaries_and_more.py @@ -0,0 +1,115 @@ +# Generated by Django 4.2.1 on 2023-05-07 22:04 + +import django.contrib.gis.db.models.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("epilepsy12", "0009_alter_historicalinvestigations_eeg_request_date_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="CountryBoundaries", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("ctry22cd", models.CharField(max_length=9)), + ("ctry22nm", models.CharField(max_length=16)), + ("ctry22nmw", models.CharField(max_length=17)), + ("bng_e", models.BigIntegerField()), + ("bng_n", models.BigIntegerField()), + ("long", models.FloatField()), + ("lat", models.FloatField()), + ("globalid", models.CharField(max_length=38)), + ( + "geom", + django.contrib.gis.db.models.fields.MultiPolygonField(srid=27700), + ), + ], + ), + migrations.CreateModel( + name="IntegratedCareBoardBoundaries", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("icb23cd", models.CharField(max_length=9)), + ("icb23nm", models.CharField(max_length=77)), + ("bng_e", models.BigIntegerField()), + ("bng_n", models.BigIntegerField()), + ("long", models.FloatField()), + ("lat", models.FloatField()), + ("globalid", models.CharField(max_length=38)), + ( + "geom", + django.contrib.gis.db.models.fields.MultiPolygonField(srid=27700), + ), + ], + ), + migrations.CreateModel( + name="LocalHealthBoardBoundaries", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("lhb22cd", models.CharField(max_length=9)), + ("lhb22nm", models.CharField(max_length=41)), + ("lhb22nmw", models.CharField(max_length=40)), + ("bng_e", models.FloatField()), + ("bng_n", models.FloatField()), + ("long", models.FloatField()), + ("lat", models.FloatField()), + ("globalid", models.CharField(max_length=38)), + ( + "geom", + django.contrib.gis.db.models.fields.MultiPolygonField(srid=27700), + ), + ], + ), + migrations.CreateModel( + name="NHSEnglandRegionBoundaries", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("nhser22cd", models.CharField(max_length=9)), + ("nhser22nm", models.CharField(max_length=24)), + ("bng_e", models.BigIntegerField()), + ("bng_n", models.BigIntegerField()), + ("long", models.FloatField()), + ("lat", models.FloatField()), + ("globalid", models.CharField(max_length=38)), + ( + "geom", + django.contrib.gis.db.models.fields.MultiPolygonField(srid=27700), + ), + ], + ), + ] diff --git a/epilepsy12/migrations/0011_nhs_england_region_boundaries_seed.py b/epilepsy12/migrations/0011_nhs_england_region_boundaries_seed.py new file mode 100644 index 00000000..41cf33a6 --- /dev/null +++ b/epilepsy12/migrations/0011_nhs_england_region_boundaries_seed.py @@ -0,0 +1,49 @@ +from django.db import migrations +from django.apps import apps as django_apps + +import os +from django.contrib.gis.utils import LayerMapping + +# Each key in the nhsenglandregionboundaries_mapping dictionary corresponds to +nhsenglandregionboundaries_mapping = { + "nhser22cd": "NHSER22CD", + "nhser22nm": "NHSER22NM", + "bng_e": "BNG_E", + "bng_n": "BNG_N", + "long": "LONG", + "lat": "LAT", + "globalid": "GlobalID", + "geom": "MULTIPOLYGON", +} + +app_config = django_apps.get_app_config("epilepsy12") +app_path = app_config.path + +NHS_England_Regions_July_2022_EN_BUC_2022 = os.path.join( + app_path, + "shape_files", + "NHS_England_Regions_July_2022_EN_BUC_2022", + "NHSER_JUL_2022_EN_BUC.shp", +) + + +def load(apps, schema_editor, verbose=True): + NHSEnglandRegionBoundaries = apps.get_model( + "epilepsy12", "NHSEnglandRegionBoundaries" + ) + lm = LayerMapping( + NHSEnglandRegionBoundaries, + NHS_England_Regions_July_2022_EN_BUC_2022, + nhsenglandregionboundaries_mapping, + transform=False, + encoding="utf-8", + ) + lm.save(strict=True, verbose=verbose) + + +class Migration(migrations.Migration): + dependencies = [ + ("epilepsy12", "0010_countryboundaries_integratedcareboardboundaries_and_more"), + ] + + operations = [migrations.RunPython(load)] diff --git a/epilepsy12/migrations/0012_remaining_boundaries_seed.py b/epilepsy12/migrations/0012_remaining_boundaries_seed.py new file mode 100644 index 00000000..669ad3de --- /dev/null +++ b/epilepsy12/migrations/0012_remaining_boundaries_seed.py @@ -0,0 +1,109 @@ +# Generated by Django 4.2.1 on 2023-05-08 06:30 + +import os +from django.db import migrations +from django.apps import apps as django_apps +from django.contrib.gis.utils import LayerMapping + +countryboundaries_mapping = { + "ctry22cd": "CTRY22CD", + "ctry22nm": "CTRY22NM", + "ctry22nmw": "CTRY22NMW", + "bng_e": "BNG_E", + "bng_n": "BNG_N", + "long": "LONG", + "lat": "LAT", + "globalid": "GlobalID", + "geom": "MULTIPOLYGON", +} + +integratedcareboardboundaries_mapping = { + "icb23cd": "ICB23CD", + "icb23nm": "ICB23NM", + "bng_e": "BNG_E", + "bng_n": "BNG_N", + "long": "LONG", + "lat": "LAT", + "globalid": "GlobalID", + "geom": "MULTIPOLYGON", +} + +localhealthboardboundaries_mapping = { + "lhb22cd": "LHB22CD", + "lhb22nm": "LHB22NM", + "lhb22nmw": "LHB22NMW", + "bng_e": "BNG_E", + "bng_n": "BNG_N", + "long": "LONG", + "lat": "LAT", + "globalid": "GlobalID", + "geom": "MULTIPOLYGON", +} + +app_config = django_apps.get_app_config("epilepsy12") +app_path = app_config.path + +Countries_December_2022_UK_BUC = os.path.join( + app_path, + "shape_files", + "Countries_December_2022_UK_BUC", + "CTRY_DEC_2022_UK_BUC.shp", +) + +Integrated_Care_Boards_April_2023_EN_BSC = os.path.join( + app_path, + "shape_files", + "Integrated_Care_Boards_April_2023_EN_BSC", + "ICB_APR_2023_EN_BSC.shp", +) + +Local_Health_Boards_April_2022_WA_BUC_2022 = os.path.join( + app_path, + "shape_files", + "Local_Health_Boards_April_2022_WA_BUC_2022", + "LHB_APR_2022_WA_BUC.shp", +) + + +def load(apps, schema_editor, verbose=True): + CountryBoundaries = apps.get_model("epilepsy12", "CountryBoundaries") + lm = LayerMapping( + CountryBoundaries, + Countries_December_2022_UK_BUC, + countryboundaries_mapping, + transform=False, + encoding="utf-8", + ) + lm.save(strict=True, verbose=verbose) + + IntegratedCareBoardBoundaries = apps.get_model( + "epilepsy12", "IntegratedCareBoardBoundaries" + ) + lm = LayerMapping( + IntegratedCareBoardBoundaries, + Integrated_Care_Boards_April_2023_EN_BSC, + integratedcareboardboundaries_mapping, + transform=False, + encoding="utf-8", + ) + lm.save(strict=True, verbose=verbose) + + LocalHealthBoardBoundaries = apps.get_model( + "epilepsy12", "LocalHealthBoardBoundaries" + ) + lm = LayerMapping( + LocalHealthBoardBoundaries, + Local_Health_Boards_April_2022_WA_BUC_2022, + localhealthboardboundaries_mapping, + transform=False, + encoding="utf-8", + ) + lm.save(strict=True, verbose=verbose) + + +class Migration(migrations.Migration): + dependencies = [ + ("epilepsy12", "0011_nhs_england_region_boundaries_seed"), + ] + + operations = [migrations.RunPython(load)] diff --git a/epilepsy12/migrations/0013_alter_historicalorganisation_geocode_coordinates_and_more.py b/epilepsy12/migrations/0013_alter_historicalorganisation_geocode_coordinates_and_more.py new file mode 100644 index 00000000..49b5c648 --- /dev/null +++ b/epilepsy12/migrations/0013_alter_historicalorganisation_geocode_coordinates_and_more.py @@ -0,0 +1,656 @@ +# Generated by Django 4.2.1 on 2023-06-04 22:09 + +import django.contrib.gis.db.models.fields +from django.db import migrations, models +import django.db.models.deletion +import epilepsy12.models_folder.help_text_mixin + + +class Migration(migrations.Migration): + dependencies = [ + ("epilepsy12", "0012_remaining_boundaries_seed"), + ] + + operations = [ + migrations.AlterField( + model_name="historicalorganisation", + name="Geocode_Coordinates", + field=django.contrib.gis.db.models.fields.PointField( + blank=True, default=None, null=True, srid=27700 + ), + ), + migrations.AlterField( + model_name="organisation", + name="Geocode_Coordinates", + field=django.contrib.gis.db.models.fields.PointField( + blank=True, default=None, null=True, srid=27700 + ), + ), + migrations.CreateModel( + name="KPIAggregation", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "paediatrician_with_expertise_in_epilepsies", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "paediatrician_with_expertise_in_epilepsies_average", + models.FloatField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "paediatrician_with_expertise_in_epilepsies_total", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "epilepsy_specialist_nurse", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "epilepsy_specialist_nurse_average", + models.FloatField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "epilepsy_specialist_nurse_total", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "tertiary_input", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "tertiary_input_average", + models.FloatField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "tertiary_input_total", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "epilepsy_surgery_referral", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "epilepsy_surgery_referral_average", + models.FloatField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "epilepsy_surgery_referral_total", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "ecg", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "ecg_average", + models.FloatField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "ecg_total", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "mri", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "mri_average", + models.FloatField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "mri_total", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "assessment_of_mental_health_issues", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "assessment_of_mental_health_issues_average", + models.FloatField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "assessment_of_mental_health_issues_total", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "mental_health_support", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "mental_health_support_average", + models.FloatField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "mental_health_support_total", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "sodium_valproate", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "sodium_valproate_average", + models.FloatField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "sodium_valproate_total", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "comprehensive_care_planning_agreement", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "comprehensive_care_planning_agreement_average", + models.FloatField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "comprehensive_care_planning_agreement_total", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "patient_held_individualised_epilepsy_document", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "patient_held_individualised_epilepsy_document_average", + models.FloatField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "patient_held_individualised_epilepsy_document_total", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "patient_carer_parent_agreement_to_the_care_planning", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "patient_carer_parent_agreement_to_the_care_planning_average", + models.FloatField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "patient_carer_parent_agreement_to_the_care_planning_total", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "care_planning_has_been_updated_when_necessary", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "care_planning_has_been_updated_when_necessary_average", + models.FloatField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "care_planning_has_been_updated_when_necessary_total", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "comprehensive_care_planning_content", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "comprehensive_care_planning_content_average", + models.FloatField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "comprehensive_care_planning_content_total", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "parental_prolonged_seizures_care_plan", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "parental_prolonged_seizures_care_plan_average", + models.FloatField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "parental_prolonged_seizures_care_plan_total", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "water_safety", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "water_safety_average", + models.FloatField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "water_safety_total", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "first_aid", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "first_aid_average", + models.FloatField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "first_aid_total", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "general_participation_and_risk", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "general_participation_and_risk_average", + models.FloatField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "general_participation_and_risk_total", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "service_contact_details", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "service_contact_details_average", + models.FloatField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "service_contact_details_total", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "sudep", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "sudep_average", + models.FloatField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "sudep_total", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "school_individual_healthcare_plan", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "school_individual_healthcare_plan_average", + models.FloatField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "school_individual_healthcare_plan_total", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "total_number_of_cases", + models.IntegerField( + blank=True, + default=None, + help_text={"label": "", "reference": ""}, + null=True, + ), + ), + ( + "abstraction_level", + models.CharField( + choices=[ + ("organisation", "Organisation"), + ("trust", "Trust/Local Health Board"), + ("icb", "Integrated Care Board"), + ("open_uk", "OPEN UK region"), + ("nhs_region", "NHS England Region"), + ("country", "Country"), + ("national", "National"), + ], + max_length=50, + ), + ), + ( + "open_access", + models.BooleanField(blank=True, default=False, null=True), + ), + ( + "organisation", + models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="epilepsy12.organisation", + ), + ), + ], + options={ + "verbose_name": "KPI Aggregation Result ", + "verbose_name_plural": "KPI Aggregation Results ", + }, + bases=( + models.Model, + epilepsy12.models_folder.help_text_mixin.HelpTextMixin, + ), + ), + ] diff --git a/epilepsy12/migrations/0014_remove_epilepsy12user_bio_and_more.py b/epilepsy12/migrations/0014_remove_epilepsy12user_bio_and_more.py new file mode 100644 index 00000000..b6ba9cd7 --- /dev/null +++ b/epilepsy12/migrations/0014_remove_epilepsy12user_bio_and_more.py @@ -0,0 +1,106 @@ +# Generated by Django 4.2.2 on 2023-06-15 21:22 + +import django.contrib.gis.db.models.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "epilepsy12", + "0013_alter_historicalorganisation_geocode_coordinates_and_more", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="epilepsy12user", + name="bio", + ), + migrations.RemoveField( + model_name="epilepsy12user", + name="twitter_handle", + ), + migrations.RemoveField( + model_name="historicalepilepsy12user", + name="bio", + ), + migrations.RemoveField( + model_name="historicalepilepsy12user", + name="twitter_handle", + ), + migrations.AddField( + model_name="epilepsy12user", + name="is_clinician", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="epilepsy12user", + name="is_patient_or_carer", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="historicalepilepsy12user", + name="is_clinician", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="historicalepilepsy12user", + name="is_patient_or_carer", + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name="historicalmultiaxialdiagnosis", + name="global_developmental_delay_or_learning_difficulties_severity", + field=models.CharField( + blank=True, + choices=[ + ("mild", "Mild"), + ("moderate", "Moderate"), + ("severe", "Severe"), + ("profound", "Profound"), + ("uncertain", "Uncertain"), + ], + default=None, + help_text={ + "label": "Add details on the severity of the neurodevelopmental condition.", + "reference": "Add details on the severity of the neurodevelopmental condition.", + }, + null=True, + ), + ), + migrations.AlterField( + model_name="historicalorganisation", + name="Geocode_Coordinates", + field=django.contrib.gis.db.models.fields.PointField( + blank=True, default=None, null=True, srid=27700 + ), + ), + migrations.AlterField( + model_name="multiaxialdiagnosis", + name="global_developmental_delay_or_learning_difficulties_severity", + field=models.CharField( + blank=True, + choices=[ + ("mild", "Mild"), + ("moderate", "Moderate"), + ("severe", "Severe"), + ("profound", "Profound"), + ("uncertain", "Uncertain"), + ], + default=None, + help_text={ + "label": "Add details on the severity of the neurodevelopmental condition.", + "reference": "Add details on the severity of the neurodevelopmental condition.", + }, + null=True, + ), + ), + migrations.AlterField( + model_name="organisation", + name="Geocode_Coordinates", + field=django.contrib.gis.db.models.fields.PointField( + blank=True, default=None, null=True, srid=27700 + ), + ), + ] diff --git a/epilepsy12/migrations/0015_rename_is_clinician_epilepsy12user_is_rcpch_staff_and_more.py b/epilepsy12/migrations/0015_rename_is_clinician_epilepsy12user_is_rcpch_staff_and_more.py new file mode 100644 index 00000000..67aa0e37 --- /dev/null +++ b/epilepsy12/migrations/0015_rename_is_clinician_epilepsy12user_is_rcpch_staff_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.2 on 2023-06-16 13:14 + +import django.contrib.gis.db.models.fields +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("epilepsy12", "0014_remove_epilepsy12user_bio_and_more"), + ] + + operations = [ + migrations.RenameField( + model_name="epilepsy12user", + old_name="is_clinician", + new_name="is_rcpch_staff", + ), + migrations.RenameField( + model_name="historicalepilepsy12user", + old_name="is_clinician", + new_name="is_rcpch_staff", + ), + migrations.AlterField( + model_name="historicalorganisation", + name="Geocode_Coordinates", + field=django.contrib.gis.db.models.fields.PointField( + blank=True, default=None, null=True, srid=27700 + ), + ), + migrations.AlterField( + model_name="organisation", + name="Geocode_Coordinates", + field=django.contrib.gis.db.models.fields.PointField( + blank=True, default=None, null=True, srid=27700 + ), + ), + ] diff --git a/epilepsy12/migrations/0016_alter_registration_options_and_more.py b/epilepsy12/migrations/0016_alter_registration_options_and_more.py new file mode 100644 index 00000000..a6f8c7c9 --- /dev/null +++ b/epilepsy12/migrations/0016_alter_registration_options_and_more.py @@ -0,0 +1,95 @@ +# Generated by Django 4.2.2 on 2023-06-18 08:12 + +import django.contrib.gis.db.models.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "epilepsy12", + "0015_rename_is_clinician_epilepsy12user_is_rcpch_staff_and_more", + ), + ] + + operations = [ + migrations.AlterModelOptions( + name="registration", + options={ + "permissions": [ + ( + "can_approve_eligibility", + "Can approve eligibility for Epilepsy12.", + ), + ( + "can_register_child_in_epilepsy12", + "Can register child in Epilepsy12. (A cohort number is automatically allocaeted)", + ), + ( + "can_unregister_child_in_epilepsy12", + "Can unregister a child in Epilepsy. Their record and previously entered data is untouched.", + ), + ( + "can_consent_to_audit_participation", + "Can consent to participating in Epilepsy12.", + ), + ], + "verbose_name": "Registration", + "verbose_name_plural": "Registrations", + }, + ), + migrations.AddField( + model_name="auditprogress", + name="consent_patient_confirmed", + field=models.BooleanField(default=None, null=True), + ), + migrations.AddField( + model_name="auditprogress", + name="details_patient_confirmed", + field=models.BooleanField(default=None, null=True), + ), + migrations.AlterField( + model_name="epilepsy12user", + name="role", + field=models.PositiveSmallIntegerField( + blank=True, + choices=[ + (1, "Audit Centre Lead Clinician"), + (2, "Audit Centre Clinician"), + (3, "Audit Centre Administrator"), + (4, "RCPCH Audit Team"), + (7, "RCPCH Audit Children and Family"), + ], + null=True, + ), + ), + migrations.AlterField( + model_name="historicalepilepsy12user", + name="role", + field=models.PositiveSmallIntegerField( + blank=True, + choices=[ + (1, "Audit Centre Lead Clinician"), + (2, "Audit Centre Clinician"), + (3, "Audit Centre Administrator"), + (4, "RCPCH Audit Team"), + (7, "RCPCH Audit Children and Family"), + ], + null=True, + ), + ), + migrations.AlterField( + model_name="historicalorganisation", + name="Geocode_Coordinates", + field=django.contrib.gis.db.models.fields.PointField( + blank=True, default=None, null=True, srid=27700 + ), + ), + migrations.AlterField( + model_name="organisation", + name="Geocode_Coordinates", + field=django.contrib.gis.db.models.fields.PointField( + blank=True, default=None, null=True, srid=27700 + ), + ), + ] diff --git a/epilepsy12/migrations/0017_alter_historicalregistration_registration_date_and_more.py b/epilepsy12/migrations/0017_alter_historicalregistration_registration_date_and_more.py new file mode 100644 index 00000000..00c07fc7 --- /dev/null +++ b/epilepsy12/migrations/0017_alter_historicalregistration_registration_date_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.2 on 2023-06-24 09:37 + +from django.db import migrations, models +import epilepsy12.validators + + +class Migration(migrations.Migration): + dependencies = [ + ("epilepsy12", "0016_alter_registration_options_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="historicalregistration", + name="registration_date", + field=models.DateField( + default=None, + help_text={ + "label": "First paediatric assessment", + "reference": "Setting this date is an irreversible step. Confirmation will be requested to complete this step.", + }, + null=True, + validators=[epilepsy12.validators.not_in_the_future_validator], + ), + ), + migrations.AlterField( + model_name="registration", + name="registration_date", + field=models.DateField( + default=None, + help_text={ + "label": "First paediatric assessment", + "reference": "Setting this date is an irreversible step. Confirmation will be requested to complete this step.", + }, + null=True, + validators=[epilepsy12.validators.not_in_the_future_validator], + ), + ), + ] diff --git a/epilepsy12/models_folder/__init__.py b/epilepsy12/models_folder/__init__.py index de7984a9..6575511c 100644 --- a/epilepsy12/models_folder/__init__.py +++ b/epilepsy12/models_folder/__init__.py @@ -1,4 +1,3 @@ - # These models represent the individual fields that clinicians must score # and drive the forms and templates in the audit. # Their division into discrete files is purely for semantic reasons as @@ -37,8 +36,15 @@ from .kpi import KPI from .visitactivity import VisitActivity +# This stores the results of aggregations of the KPI model for different levels of abstraction +# There is one one KPIAggregation instance for each organisation, trust/lhb, icb, nhs_region, open_uk region, country and national +from .kpi_aggregation import KPIAggregation + # These are helper classes to support the functioning of each model. Do not need to be globally available -from .time_and_user_abstract_base_classes import TimeStampAbstractBaseClass, UserStampAbstractBaseClass +from .time_and_user_abstract_base_classes import ( + TimeStampAbstractBaseClass, + UserStampAbstractBaseClass, +) from .help_text_mixin import HelpTextMixin # clinical entities seeded from clinical APIs (eg SNOMED CT) @@ -62,3 +68,9 @@ from .entities.ons_country_entity import ONSCountryEntity from .entities.nhs_region_entity import NHSRegionEntity from .entities.organisation import Organisation + +# maps +from .entities.nhs_england_region_boundaries import NHSEnglandRegionBoundaries +from .entities.integrated_care_board_boundaries import IntegratedCareBoardBoundaries +from .entities.local_health_board_boundaries import LocalHealthBoardBoundaries +from .entities.country_boundaries import CountryBoundaries diff --git a/epilepsy12/models_folder/antiepilepsy_medicine.py b/epilepsy12/models_folder/antiepilepsy_medicine.py index da8c6375..46dd0073 100644 --- a/epilepsy12/models_folder/antiepilepsy_medicine.py +++ b/epilepsy12/models_folder/antiepilepsy_medicine.py @@ -49,7 +49,7 @@ class AntiEpilepsyMedicine(TimeStampAbstractBaseClass, UserStampAbstractBaseClas is_a_pregnancy_prevention_programme_needed = models.BooleanField( help_text={ 'label': "Is a pregnancy prevention programme indicated?", - 'reference': "For girls and young women who are presecribed sodium valproate, it is recommended that pregnancy prevention is actively discussed and documented.", + 'reference': "For girls and young women who are prescribed sodium valproate, it is recommended that pregnancy prevention is actively discussed and documented.", }, default=None, null=True, @@ -58,7 +58,7 @@ class AntiEpilepsyMedicine(TimeStampAbstractBaseClass, UserStampAbstractBaseClas has_a_valproate_annual_risk_acknowledgement_form_been_completed = models.BooleanField( help_text={ 'label': "Has a Valproate - Annual Risk Acknowledgment Form been completed?", - 'reference': "For girls and young women who are presecribed sodium valproate, it is recommended that Has an annual Valproate - Annual Risk Acknowledgment Form is completed.", + 'reference': "For girls and young women who are prescribed sodium valproate, it is recommended that Has an annual Valproate - Annual Risk Acknowledgment Form is completed.", }, default=None, null=True, @@ -68,7 +68,7 @@ class AntiEpilepsyMedicine(TimeStampAbstractBaseClass, UserStampAbstractBaseClas is_a_pregnancy_prevention_programme_in_place = models.BooleanField( help_text={ 'label': "Is the Valproate Pregnancy Prevention Programme in place?", - 'reference': "For girls and young women who are presecribed sodium valproate, it is recommended that pregnancy prevention is actively discussed and documented.", + 'reference': "For girls and young women who are prescribed sodium valproate, it is recommended that pregnancy prevention is actively discussed and documented.", }, default=None, null=True, @@ -118,9 +118,9 @@ class Meta: def length_of_treatment(self): "Returns length of treatment if dates supplied" if (self.antiepilepsy_medicine_start_date and self.antiepilepsy_medicine_stop_date): - if (self.antiepilepsy_medicine_stop_date > self.antiepilepsy_medicine_start_date): + if (self.antiepilepsy_medicine_stop_date <= self.antiepilepsy_medicine_start_date): raise Exception( - "The medication stop date cannot be before the medication start date.") + "The medication stop date cannot be before or the same as the medication start date.") else: difference = (self.antiepilepsy_medicine_stop_date - diff --git a/epilepsy12/models_folder/assessment.py b/epilepsy12/models_folder/assessment.py index d8b17961..cad22007 100644 --- a/epilepsy12/models_folder/assessment.py +++ b/epilepsy12/models_folder/assessment.py @@ -1,8 +1,11 @@ +from datetime import date +from dateutil.relativedelta import relativedelta + from django.contrib.gis.db import models from simple_history.models import HistoricalRecords from .help_text_mixin import HelpTextMixin -from epilepsy12.general_functions import calculate_time_elapsed +from epilepsy12.general_functions import stringify_time_elapsed from .time_and_user_abstract_base_classes import * @@ -16,122 +19,123 @@ class Assessment(TimeStampAbstractBaseClass, UserStampAbstractBaseClass, HelpTex Detail The cohort number is calculated from the initial date of first paediatric assessment """ + childrens_epilepsy_surgical_service_referral_criteria_met = models.BooleanField( help_text={ - 'label': "Are ANY of these criteria present?", - 'reference': "Have ANY of the criteria for referral to a children's epilepsy surgery service been met?", + "label": "Are ANY of these criteria present?", + "reference": "Have ANY of the criteria for referral to a children's epilepsy surgery service been met?", }, blank=True, default=None, - null=True + null=True, ) consultant_paediatrician_referral_made = models.BooleanField( help_text={ - 'label': 'Has a referral been made to a consultant paediatrician with expertise in epilepsies?', - 'reference': 'Has a referral been made to a consultant paediatrician with expertise in epilepsies?', + "label": "Has a referral been made to a consultant paediatrician with expertise in epilepsies?", + "reference": "Has a referral been made to a consultant paediatrician with expertise in epilepsies?", }, blank=True, default=None, - null=True + null=True, ) consultant_paediatrician_referral_date = models.DateField( help_text={ - 'label': "Date of referral to a consultant paediatrician with expertise in epilepsies.", - 'reference': 'Has a referral been made to a consultant paediatrician with expertise in epilepsies?', + "label": "Date of referral to a consultant paediatrician with expertise in epilepsies.", + "reference": "Has a referral been made to a consultant paediatrician with expertise in epilepsies?", }, blank=True, null=True, - default=None + default=None, ) consultant_paediatrician_input_date = models.DateField( help_text={ - 'label': 'Date seen by a consultant paediatrician with expertise in epilepsies.', - 'reference': 'Date seen by a consultant paediatrician with expertise in epilepsies.' + "label": "Date seen by a consultant paediatrician with expertise in epilepsies.", + "reference": "Date seen by a consultant paediatrician with expertise in epilepsies.", }, blank=True, default=None, - null=True + null=True, ) paediatric_neurologist_referral_made = models.BooleanField( help_text={ - 'label': "Has a referral to a consultant paediatric neurologist been made?", - 'reference': "Has a referral to a consultant paediatric neurologist been made?" + "label": "Has a referral to a consultant paediatric neurologist been made?", + "reference": "Has a referral to a consultant paediatric neurologist been made?", }, blank=True, default=None, - null=True + null=True, ) paediatric_neurologist_referral_date = models.DateField( help_text={ - 'label': "Date of referral to a consultant paediatric neurologist.", - 'reference': "Date of referral to a consultant paediatric neurologist.", + "label": "Date of referral to a consultant paediatric neurologist.", + "reference": "Date of referral to a consultant paediatric neurologist.", }, blank=True, default=None, - null=True + null=True, ) paediatric_neurologist_input_date = models.DateField( help_text={ - 'label': "Date seen by consultant paediatric neurologist.", - 'reference': "Date seen by consultant paediatric neurologist.", + "label": "Date seen by consultant paediatric neurologist.", + "reference": "Date seen by consultant paediatric neurologist.", }, blank=True, null=True, - default=None + default=None, ) childrens_epilepsy_surgical_service_referral_made = models.BooleanField( help_text={ - 'label': "Has a referral to a children's epilepsy surgery service been made?", - 'reference': "Has a referral to a children's epilepsy surgery service been made?", + "label": "Has a referral to a children's epilepsy surgery service been made?", + "reference": "Has a referral to a children's epilepsy surgery service been made?", }, blank=True, default=None, - null=True + null=True, ) childrens_epilepsy_surgical_service_referral_date = models.DateField( help_text={ - 'label': "Date of referral to a children's epilepsy surgery service", - 'reference': "Date of referral to a children's epilepsy surgery service", + "label": "Date of referral to a children's epilepsy surgery service", + "reference": "Date of referral to a children's epilepsy surgery service", }, blank=True, default=None, - null=True + null=True, ) childrens_epilepsy_surgical_service_input_date = models.DateField( help_text={ - 'label': "Date seen by children's epilepsy surgery service", - 'reference': "Date seen by children's epilepsy surgery service", + "label": "Date seen by children's epilepsy surgery service", + "reference": "Date seen by children's epilepsy surgery service", }, blank=True, default=None, - null=True + null=True, ) epilepsy_specialist_nurse_referral_made = models.BooleanField( help_text={ - 'label': "Has a referral to an epilepsy nurse specialist been made?", - 'reference': "Has a referral to an epilepsy nurse specialist been made?", + "label": "Has a referral to an epilepsy nurse specialist been made?", + "reference": "Has a referral to an epilepsy nurse specialist been made?", }, blank=True, default=None, - null=True + null=True, ) epilepsy_specialist_nurse_referral_date = models.DateField( help_text={ - 'label': "Date of referral to an epilepsy nurse specialist", - 'reference': "Date of referral to an epilepsy nurse specialist", + "label": "Date of referral to an epilepsy nurse specialist", + "reference": "Date of referral to an epilepsy nurse specialist", }, blank=True, default=None, - null=True + null=True, ) epilepsy_specialist_nurse_input_date = models.DateField( help_text={ - 'label': "Date seen by an epilepsy nurse specialist", - 'reference': "Date seen by an epilepsy nurse specialist", + "label": "Date seen by an epilepsy nurse specialist", + "reference": "Date seen by an epilepsy nurse specialist", }, blank=True, default=None, - null=True + null=True, ) history = HistoricalRecords() @@ -144,42 +148,73 @@ def _history_user(self): def _history_user(self, value): self.updated_by = value - """ - calculated fields - """ + def get_current_date(self): + return date.today() def consultant_paediatrician_wait(self): """ Calculated field. Returns time elapsed between date consultant paediatrician review requested and performed as a string. """ - if self.consultant_paediatrician_referral_date and self.consultant_paediatrician_input_date: - return calculate_time_elapsed(self.consultant_paediatrician_referral_date, self.consultant_paediatrician_input_date) + if ( + self.consultant_paediatrician_referral_date + and self.consultant_paediatrician_input_date + ): + return ( + self.consultant_paediatrician_input_date + - self.consultant_paediatrician_referral_date + ).days + else: + return None def paediatric_neurologist_wait(self): """ Calculated field. Returns time elapsed between date paediatric neurologist review requested and performed as a string. """ - if self.paediatric_neurologist_referral_date and self.paediatric_neurologist_input_date: - return calculate_time_elapsed(self.paediatric_neurologist_referral_date, self.paediatric_neurologist_input_date) + if ( + self.paediatric_neurologist_referral_date + and self.paediatric_neurologist_input_date + ): + return ( + self.paediatric_neurologist_input_date + - self.paediatric_neurologist_referral_date + ).days + else: + return None def childrens_epilepsy_surgery_wait(self): """ Calculated field. Returns time elapsed between date children's epilepsy surgery service review requested and performed as a string. """ - if self.childrens_epilepsy_surgical_service_referral_date and self.childrens_epilepsy_surgical_service_input_date: - return calculate_time_elapsed(self.childrens_epilepsy_surgical_service_referral_date, self.childrens_epilepsy_surgical_service_input_date) + if ( + self.childrens_epilepsy_surgical_service_referral_date + and self.childrens_epilepsy_surgical_service_input_date + ): + return ( + self.childrens_epilepsy_surgical_service_input_date + - self.childrens_epilepsy_surgical_service_referral_date + ).days + else: + return None def epilepsy_nurse_specialist_wait(self): """ Calculated field. Returns time elapsed between date children's epilepsy surgery service review requested and performed as a string. """ - if self.epilepsy_specialist_nurse_referral_date and self.epilepsy_specialist_nurse_input_date: - return calculate_time_elapsed(self.epilepsy_specialist_nurse_referral_date, self.epilepsy_specialist_nurse_input_date) + if ( + self.epilepsy_specialist_nurse_referral_date + and self.epilepsy_specialist_nurse_input_date + ): + return ( + self.epilepsy_specialist_nurse_input_date + - self.epilepsy_specialist_nurse_referral_date + ).days + else: + return None # raise ValueError("Both referral and input dates must be provided") registration = models.OneToOneField( - 'epilepsy12.Registration', + "epilepsy12.Registration", on_delete=models.CASCADE, - verbose_name="related registration" + verbose_name="related registration", ) class Meta: @@ -189,9 +224,7 @@ class Meta: def __str__(self) -> str: return f"Assessment Milestones for {self.registration.case}" - def save( - self, - *args, **kwargs) -> None: + def save(self, *args, **kwargs) -> None: # called on save of a record return super().save(*args, **kwargs) diff --git a/epilepsy12/models_folder/audit_progress.py b/epilepsy12/models_folder/audit_progress.py index 51cfa1a3..c58339ac 100644 --- a/epilepsy12/models_folder/audit_progress.py +++ b/epilepsy12/models_folder/audit_progress.py @@ -8,105 +8,60 @@ class AuditProgress(models.Model, HelpTextMixin): It tracks how many fields are complete """ - registration_complete = models.BooleanField( - default=False, - null=True - ) + registration_complete = models.BooleanField(default=False, null=True) registration_total_expected_fields = models.SmallIntegerField( - "Total Number of fields expected", - default=0, - null=True + "Total Number of fields expected", default=0, null=True ) registration_total_completed_fields = models.SmallIntegerField( - "Total Number of fields completed", - default=0, - null=True - ) - first_paediatric_assessment_complete = models.BooleanField( - default=False, - null=True + "Total Number of fields completed", default=0, null=True ) + first_paediatric_assessment_complete = models.BooleanField(default=False, null=True) first_paediatric_assessment_total_expected_fields = models.SmallIntegerField( - "Total Number of fields expected", - default=0, - null=True + "Total Number of fields expected", default=0, null=True ) first_paediatric_assessment_total_completed_fields = models.SmallIntegerField( - "Total Number of fields completed", - default=0, - null=True - ) - assessment_complete = models.BooleanField( - null=True, - default=False + "Total Number of fields completed", default=0, null=True ) + assessment_complete = models.BooleanField(null=True, default=False) assessment_total_expected_fields = models.SmallIntegerField( - "Total Number of fields expected", - default=0, - null=True + "Total Number of fields expected", default=0, null=True ) assessment_total_completed_fields = models.SmallIntegerField( - "Total Number of fields completed", - default=0, - null=True - ) - epilepsy_context_complete = models.BooleanField( - null=True, - default=False + "Total Number of fields completed", default=0, null=True ) + epilepsy_context_complete = models.BooleanField(null=True, default=False) epilepsy_context_total_expected_fields = models.SmallIntegerField( - "Total Number of fields expected", - default=0, - null=True + "Total Number of fields expected", default=0, null=True ) epilepsy_context_total_completed_fields = models.SmallIntegerField( - "Total Number of fields completed", - default=0, - null=True - ) - multiaxial_diagnosis_complete = models.BooleanField( - null=True, - default=False + "Total Number of fields completed", default=0, null=True ) + multiaxial_diagnosis_complete = models.BooleanField(null=True, default=False) multiaxial_diagnosis_total_expected_fields = models.SmallIntegerField( - "Total Number of fields expected", - default=0, - null=True + "Total Number of fields expected", default=0, null=True ) multiaxial_diagnosis_total_completed_fields = models.SmallIntegerField( - "Total Number of fields completed", - default=0, - null=True - ) - investigations_complete = models.BooleanField( - default=False, - null=True + "Total Number of fields completed", default=0, null=True ) + investigations_complete = models.BooleanField(default=False, null=True) investigations_total_expected_fields = models.SmallIntegerField( - "Total Number of fields expected", - default=0, - null=True + "Total Number of fields expected", default=0, null=True ) investigations_total_completed_fields = models.SmallIntegerField( - "Total Number of fields completed", - default=0, - null=True - ) - management_complete = models.BooleanField( - default=False, - null=True + "Total Number of fields completed", default=0, null=True ) + management_complete = models.BooleanField(default=False, null=True) management_total_expected_fields = models.SmallIntegerField( - "Total Number of fields expected", - default=0, - null=True + "Total Number of fields expected", default=0, null=True ) management_total_completed_fields = models.SmallIntegerField( - "Total Number of fields completed", - default=0, - null=True + "Total Number of fields completed", default=0, null=True ) + consent_patient_confirmed = models.BooleanField(default=None, null=True) + + details_patient_confirmed = models.BooleanField(default=None, null=True) + """ Calculated fields """ @@ -167,17 +122,18 @@ def total_expected_fields(self): @property def audit_complete(self): if ( - self.registration_complete and - self.first_paediatric_assessment_complete and - self.epilepsy_context_complete and - self.assessment_complete and - self.multiaxial_diagnosis_complete and - self.investigations_complete and - self.management_complete): + self.registration_complete + and self.first_paediatric_assessment_complete + and self.epilepsy_context_complete + and self.assessment_complete + and self.multiaxial_diagnosis_complete + and self.investigations_complete + and self.management_complete + ): return True else: return False class Meta: - verbose_name = 'Audit Progress' - verbose_name_plural = 'Audit Progresses' + verbose_name = "Audit Progress" + verbose_name_plural = "Audit Progresses" diff --git a/epilepsy12/models_folder/case.py b/epilepsy12/models_folder/case.py index 977ecd0d..aacd432d 100644 --- a/epilepsy12/models_folder/case.py +++ b/epilepsy12/models_folder/case.py @@ -1,7 +1,6 @@ # python -from dateutil import relativedelta from datetime import date -import math +from dateutil.relativedelta import relativedelta # django from django.contrib.gis.db import models @@ -13,8 +12,16 @@ # epilepsy12 from .help_text_mixin import HelpTextMixin -from ..constants import SEX_TYPE, ETHNICITIES, UNKNOWN_POSTCODES, CAN_LOCK_CHILD_CASE_DATA_FROM_EDITING, CAN_UNLOCK_CHILD_CASE_DATA_FROM_EDITING, CAN_OPT_OUT_CHILD_FROM_INCLUSION_IN_AUDIT, CAN_CONSENT_TO_AUDIT_PARTICIPATION -from ..general_functions import imd_for_postcode +from ..constants import ( + SEX_TYPE, + ETHNICITIES, + UNKNOWN_POSTCODES_NO_SPACES, + CAN_LOCK_CHILD_CASE_DATA_FROM_EDITING, + CAN_UNLOCK_CHILD_CASE_DATA_FROM_EDITING, + CAN_OPT_OUT_CHILD_FROM_INCLUSION_IN_AUDIT, + CAN_CONSENT_TO_AUDIT_PARTICIPATION, +) +from ..general_functions import imd_for_postcode, stringify_time_elapsed from .time_and_user_abstract_base_classes import * @@ -23,7 +30,7 @@ class Case(TimeStampAbstractBaseClass, UserStampAbstractBaseClass, HelpTextMixin This class holds information about each child or young person Each case is unique """ - # _id = models.ObjectIdField() + locked = models.BooleanField( """ This determines if the case is locked from editing @@ -38,19 +45,15 @@ class Case(TimeStampAbstractBaseClass, UserStampAbstractBaseClass, HelpTextMixin "Locked", default=False, blank=True, - null=True - ) - locked_at = models.DateTimeField( - "Date record locked", null=True, - blank=True ) + locked_at = models.DateTimeField("Date record locked", null=True, blank=True) locked_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name="locked by", null=True, - blank=True + blank=True, ) nhs_number = models.CharField( # the NHS number for England and Wales "NHS Number", @@ -61,7 +64,7 @@ class Case(TimeStampAbstractBaseClass, UserStampAbstractBaseClass, HelpTextMixin # validators=[MinLengthValidator( # should be other validation before saving - need to strip out spaces # limit_value=10, # message="The NHS number must be 10 digits long." - # )] + # )] #TODO #489 ) # TODO #13 NHS Number must be hidden - use case_uuid as proxy first_name = CharField( "First name", @@ -90,15 +93,10 @@ class Case(TimeStampAbstractBaseClass, UserStampAbstractBaseClass, HelpTextMixin max_length=8, blank=True, null=True, - # validators=[validate_postcode] + # validators=[validate_postcode] #TODO #490 ) - ethnicity = CharField( - max_length=4, - choices=ETHNICITIES, - blank=True, - null=True - ) + ethnicity = CharField(max_length=4, choices=ETHNICITIES, blank=True, null=True) index_of_multiple_deprivation_quintile = models.PositiveSmallIntegerField( # this is a calculated field - it relies on the availability of the Deprivare server running @@ -106,17 +104,17 @@ class Case(TimeStampAbstractBaseClass, UserStampAbstractBaseClass, HelpTextMixin "index of multiple deprivation calculated from MySociety data.", blank=True, editable=False, - null=True + null=True, ) history = HistoricalRecords() # relationships organisations = models.ManyToManyField( - 'epilepsy12.Organisation', - through='Site', - related_name='cases', - through_fields=('case', 'organisation') + "epilepsy12.Organisation", + through="Site", + related_name="cases", + through_fields=("case", "organisation"), ) @property @@ -127,74 +125,41 @@ def _history_user(self): def _history_user(self, value): self.updated_by = value - @property - def age(self): - today = date.today() - calculated_age = relativedelta.relativedelta( - today, self.date_of_birth) - months = calculated_age.months - years = calculated_age.years - weeks = calculated_age.weeks - days = calculated_age.days - final = '' - if years == 1: - final += f'{calculated_age.years} year' - if (months/12) - years == 1: - final += f'{months} month' - elif (months/12)-years > 1: - final += f'{math.floor((months*12)-years)} months' - else: - return final - - elif years > 1: - final += f'{calculated_age.years} years' - if (months/12) - years == 1: - final += f', {months} month' - elif (months/12)-years > 1: - final += f', {math.floor((months*12)-years)} months' - else: - return final - else: - # under a year of age - if months == 1: - final += f'{months} month' - elif months > 0: - final += f'{months} months, ' - if weeks >= (months*4): - if (weeks-(months*4)) == 1: - final += '1 week' - else: - final += f'{math.floor(weeks-(months*4))} weeks' - else: - if weeks > 0: - if weeks == 1: - final += f'{math.floor(weeks)} week' - else: - final += f'{math.floor(weeks)} weeks' - else: - if days > 0: - if days == 1: - final += f'{math.floor(days)} day' - if days > 1: - final += f'{math.floor(days)} days' - else: - final += 'Happy birthday' - return final - - def save( - self, - *args, **kwargs) -> None: - - # This field requires the deprivare api to be running - # note if one of ['ZZ99 3CZ','ZZ99 3GZ','ZZ99 3WZ','ZZ99 3VZ'], represent not known, not known - England, - # not known - Wales or no fixed abode + def age_days(self, today_date=date.today()): + """ + Returns the age of the patient in years, months and days + This is a calculated field + Date of birth is required + Today's date is optional and defaults to date.today() + """ + # return stringify_time_elapsed(self.date_of_birth, today_date) + return (today_date - self.date_of_birth).days + + def age(self, today_date=date.today()): + """ + Returns the age of the patient in years, months and days + This is a calculated field + Date of birth is required + Today's date is optional and defaults to date.today() + """ + return stringify_time_elapsed(self.date_of_birth, today_date) + + def save(self, *args, **kwargs) -> None: + # calculate the index of multiple deprivation quintile if the postcode is present + # Skips the calculation if the postcode is on the 'unknown' list if self.postcode: - # test if unknown - unknown = [code for code in UNKNOWN_POSTCODES if code.replace( - ' ', '') == str(self.postcode).replace(' ', '')] - if len(unknown) < 1: - self.index_of_multiple_deprivation_quintile = imd_for_postcode( - self.postcode) + if str(self.postcode).replace(" ", "") not in UNKNOWN_POSTCODES_NO_SPACES: + try: + self.index_of_multiple_deprivation_quintile = imd_for_postcode( + self.postcode + ) + except Exception as error: + # Deprivation score not persisted if deprivation score server down + self.index_of_multiple_deprivation_quintile = None + print( + f"Cannot calculate deprivation score for {self.postcode}: {error}" + ) + pass return super().save(*args, **kwargs) def delete(self, *args, **kwargs): @@ -206,15 +171,15 @@ def delete(self, *args, **kwargs): super(Case, self).delete(*args, **kwargs) class Meta: - verbose_name = 'Patient' - verbose_name_plural = 'Patients' + verbose_name = "Patient" + verbose_name_plural = "Patients" # custom permissions for Case class permissions = [ CAN_LOCK_CHILD_CASE_DATA_FROM_EDITING, CAN_UNLOCK_CHILD_CASE_DATA_FROM_EDITING, CAN_OPT_OUT_CHILD_FROM_INCLUSION_IN_AUDIT, - CAN_CONSENT_TO_AUDIT_PARTICIPATION + CAN_CONSENT_TO_AUDIT_PARTICIPATION, ] def __str__(self) -> str: - return f'{self.first_name} {self.surname}' + return f"{self.first_name} {self.surname}" diff --git a/epilepsy12/models_folder/comorbidity.py b/epilepsy12/models_folder/comorbidity.py index 2853a6c7..8b142952 100644 --- a/epilepsy12/models_folder/comorbidity.py +++ b/epilepsy12/models_folder/comorbidity.py @@ -62,4 +62,4 @@ class Meta: verbose_name_plural = "Comorbidities" def __str__(self) -> str: - return self.comorbidity.preferredTerm + return self.comorbidityentity.preferredTerm diff --git a/epilepsy12/models_folder/entities/country_boundaries.py b/epilepsy12/models_folder/entities/country_boundaries.py new file mode 100644 index 00000000..bbaa56d5 --- /dev/null +++ b/epilepsy12/models_folder/entities/country_boundaries.py @@ -0,0 +1,33 @@ +# This is an auto-generated Django model module created by ogrinspect. +from django.contrib.gis.db import models + + +class CountryBoundaries(models.Model): + ctry22cd = models.CharField(max_length=9) + ctry22nm = models.CharField(max_length=16) + ctry22nmw = models.CharField(max_length=17) + bng_e = models.BigIntegerField() + bng_n = models.BigIntegerField() + long = models.FloatField() + lat = models.FloatField() + globalid = models.CharField(max_length=38) + geom = models.MultiPolygonField(srid=27700) + + def __str__(self) -> str: + return self.ctry22nm + + +""" +# Auto-generated `LayerMapping` dictionary for CountryBoundaries model +countryboundaries_mapping = { + 'ctry22cd': 'CTRY22CD', + 'ctry22nm': 'CTRY22NM', + 'ctry22nmw': 'CTRY22NMW', + 'bng_e': 'BNG_E', + 'bng_n': 'BNG_N', + 'long': 'LONG', + 'lat': 'LAT', + 'globalid': 'GlobalID', + 'geom': 'MULTIPOLYGON', +} +""" diff --git a/epilepsy12/models_folder/entities/integrated_care_board_boundaries.py b/epilepsy12/models_folder/entities/integrated_care_board_boundaries.py new file mode 100644 index 00000000..2138e47a --- /dev/null +++ b/epilepsy12/models_folder/entities/integrated_care_board_boundaries.py @@ -0,0 +1,36 @@ +# django +from django.contrib.gis.db import models + +# 3rd party + +# rcpch +# This is an auto-generated Django model module created by ogrinspect. + + +class IntegratedCareBoardBoundaries(models.Model): + icb23cd = models.CharField(max_length=9) + icb23nm = models.CharField(max_length=77) + bng_e = models.BigIntegerField() + bng_n = models.BigIntegerField() + long = models.FloatField() + lat = models.FloatField() + globalid = models.CharField(max_length=38) + geom = models.MultiPolygonField(srid=27700) + + def __str__(self) -> str: + return self.icb23nm + + +""" +# Auto-generated `LayerMapping` dictionary for IntegratedCareBoardBoundaries model +integratedcareboardboundaries_mapping = { + 'icb23cd': 'ICB23CD', + 'icb23nm': 'ICB23NM', + 'bng_e': 'BNG_E', + 'bng_n': 'BNG_N', + 'long': 'LONG', + 'lat': 'LAT', + 'globalid': 'GlobalID', + 'geom': 'MULTIPOLYGON', +} +""" diff --git a/epilepsy12/models_folder/entities/integrated_care_board_entity.py b/epilepsy12/models_folder/entities/integrated_care_board_entity.py index 3375da13..9684b20c 100644 --- a/epilepsy12/models_folder/entities/integrated_care_board_entity.py +++ b/epilepsy12/models_folder/entities/integrated_care_board_entity.py @@ -3,15 +3,14 @@ class IntegratedCareBoardEntity(TimeStampAbstractBaseClass, UserStampAbstractBaseClass): - ODS_ICB_Code = models.CharField() ONS_ICB_Boundary_Code = models.CharField() ICB_Name = models.CharField() class Meta: - verbose_name = 'ICB Name' - verbose_name_plural = 'ICB Names' - ordering = ('ICB_Name',) + verbose_name = "ICB Name" + verbose_name_plural = "ICB Names" + ordering = ("ICB_Name",) def __str__(self) -> str: return self.ICB_Name diff --git a/epilepsy12/models_folder/entities/local_health_board_boundaries.py b/epilepsy12/models_folder/entities/local_health_board_boundaries.py new file mode 100644 index 00000000..aa90b0f0 --- /dev/null +++ b/epilepsy12/models_folder/entities/local_health_board_boundaries.py @@ -0,0 +1,33 @@ +# This is an auto-generated Django model module created by ogrinspect. +from django.contrib.gis.db import models + + +class LocalHealthBoardBoundaries(models.Model): + lhb22cd = models.CharField(max_length=9) + lhb22nm = models.CharField(max_length=41) + lhb22nmw = models.CharField(max_length=40) + bng_e = models.FloatField() + bng_n = models.FloatField() + long = models.FloatField() + lat = models.FloatField() + globalid = models.CharField(max_length=38) + geom = models.MultiPolygonField(srid=27700) + + def __str__(self) -> str: + return self.lhb22nm + + +""" +# Auto-generated `LayerMapping` dictionary for LocalHealthBoardBoundaries model +localhealthboardboundaries_mapping = { + 'lhb22cd': 'LHB22CD', + 'lhb22nm': 'LHB22NM', + 'lhb22nmw': 'LHB22NMW', + 'bng_e': 'BNG_E', + 'bng_n': 'BNG_N', + 'long': 'LONG', + 'lat': 'LAT', + 'globalid': 'GlobalID', + 'geom': 'MULTIPOLYGON', +} +""" diff --git a/epilepsy12/models_folder/entities/nhs_england_region_boundaries.py b/epilepsy12/models_folder/entities/nhs_england_region_boundaries.py new file mode 100644 index 00000000..aa063623 --- /dev/null +++ b/epilepsy12/models_folder/entities/nhs_england_region_boundaries.py @@ -0,0 +1,26 @@ +# This is an auto-generated Django model module created by ogrinspect. +from django.contrib.gis.db import models + + +class NHSEnglandRegionBoundaries(models.Model): + nhser22cd = models.CharField(max_length=9) + nhser22nm = models.CharField(max_length=24) + bng_e = models.BigIntegerField() + bng_n = models.BigIntegerField() + long = models.FloatField() + lat = models.FloatField() + globalid = models.CharField(max_length=38) + geom = models.MultiPolygonField(srid=27700) + + +# Auto-generated `LayerMapping` dictionary for NHSEnglandRegionBoundaries model +# nhsenglandregionboundaries_mapping = { +# "nhser22cd": "NHSER22CD", +# "nhser22nm": "NHSER22NM", +# "bng_e": "BNG_E", +# "bng_n": "BNG_N", +# "long": "LONG", +# "lat": "LAT", +# "globalid": "GlobalID", +# "geom": "MULTIPOLYGON", +# } diff --git a/epilepsy12/models_folder/entities/nhs_region_entity.py b/epilepsy12/models_folder/entities/nhs_region_entity.py index 6649b783..4173de57 100644 --- a/epilepsy12/models_folder/entities/nhs_region_entity.py +++ b/epilepsy12/models_folder/entities/nhs_region_entity.py @@ -8,9 +8,9 @@ class NHSRegionEntity(TimeStampAbstractBaseClass, UserStampAbstractBaseClass): year = models.IntegerField(null=True, blank=True) class Meta: - verbose_name = 'NHS Region Name' - verbose_name_plural = 'NHS Region Names' - ordering = ('NHS_Region',) + verbose_name = "NHS Region Name" + verbose_name_plural = "NHS Region Names" + ordering = ("NHS_Region",) def __str__(self) -> str: return self.NHS_Region diff --git a/epilepsy12/models_folder/entities/ons_region_entity.py b/epilepsy12/models_folder/entities/ons_region_entity.py index 76f2ab1f..160c8487 100644 --- a/epilepsy12/models_folder/entities/ons_region_entity.py +++ b/epilepsy12/models_folder/entities/ons_region_entity.py @@ -7,14 +7,13 @@ class ONSRegionEntity(TimeStampAbstractBaseClass, UserStampAbstractBaseClass): Region_ONS_Name = models.CharField() ons_country = models.ForeignKey( - 'epilepsy12.ONSCountryEntity', - on_delete=models.PROTECT + "epilepsy12.ONSCountryEntity", on_delete=models.PROTECT ) class Meta: - verbose_name = 'ONS Region' - verbose_name_plural = 'ONS Regions' - ordering = ('Region_ONS_Name',) + verbose_name = "ONS Region" + verbose_name_plural = "ONS Regions" + ordering = ("Region_ONS_Name",) def __str__(self) -> str: return self.Region_ONS_Name diff --git a/epilepsy12/models_folder/entities/open_uk_network_entity.py b/epilepsy12/models_folder/entities/open_uk_network_entity.py index 093d8398..479bdaac 100644 --- a/epilepsy12/models_folder/entities/open_uk_network_entity.py +++ b/epilepsy12/models_folder/entities/open_uk_network_entity.py @@ -8,9 +8,9 @@ class OPENUKNetworkEntity(TimeStampAbstractBaseClass, UserStampAbstractBaseClass country = models.CharField() class Meta: - verbose_name = 'OPENUK Network' - verbose_name_plural = 'OPENUK Networks' - ordering = ('OPEN_UK_Network_Name',) + verbose_name = "OPENUK Network" + verbose_name_plural = "OPENUK Networks" + ordering = ("OPEN_UK_Network_Name",) def __str__(self) -> str: return self.OPEN_UK_Network_Name diff --git a/epilepsy12/models_folder/entities/organisation.py b/epilepsy12/models_folder/entities/organisation.py index d3a8f380..63a07e0f 100644 --- a/epilepsy12/models_folder/entities/organisation.py +++ b/epilepsy12/models_folder/entities/organisation.py @@ -1,6 +1,12 @@ # django from django.contrib.gis.db import models -from django.contrib.gis.db.models import PointField, CharField, FloatField, DateTimeField +from django.contrib.gis.db.models import ( + PointField, + CharField, + FloatField, + DateTimeField, +) + # 3rd party from simple_history.models import HistoricalRecords @@ -10,119 +16,48 @@ class Organisation(models.Model): This class details information about organisations. It represents a list of organisations that can be looked up """ - ODSCode = CharField( - max_length=100, - null=True, - blank=True, - default=None - ) - OrganisationName = CharField( - max_length=100, - null=True, - blank=True, - default=None - ) - Website = CharField( - max_length=100, - null=True, - blank=True, - default=None - ) - Address1 = CharField( - max_length=100, - null=True, - blank=True, - default=None - ) - Address2 = CharField( - max_length=100, - null=True, - blank=True, - default=None - ) - Address3 = CharField( - max_length=100, - null=True, - blank=True, - default=None - ) - City = CharField( - max_length=100, - null=True, - blank=True, - default=None - ) - County = CharField( - max_length=100, - null=True, - blank=True, - default=None - ) - Latitude = FloatField( - max_length=100, - null=True, - blank=True, - default=None - ) - Longitude = FloatField( - null=True, - blank=True, - default=None - ) - Postcode = CharField( - max_length=10, - null=True, - blank=True, - default=None - ) - Geocode_Coordinates = PointField( - null=True, - blank=True, - default=None - ) + + ODSCode = CharField(max_length=100, null=True, blank=True, default=None) + OrganisationName = CharField(max_length=100, null=True, blank=True, default=None) + Website = CharField(max_length=100, null=True, blank=True, default=None) + Address1 = CharField(max_length=100, null=True, blank=True, default=None) + Address2 = CharField(max_length=100, null=True, blank=True, default=None) + Address3 = CharField(max_length=100, null=True, blank=True, default=None) + City = CharField(max_length=100, null=True, blank=True, default=None) + County = CharField(max_length=100, null=True, blank=True, default=None) + Latitude = FloatField(max_length=100, null=True, blank=True, default=None) + Longitude = FloatField(null=True, blank=True, default=None) + Postcode = CharField(max_length=10, null=True, blank=True, default=None) + Geocode_Coordinates = PointField(null=True, blank=True, default=None, srid=27700) ParentOrganisation_ODSCode = CharField( - max_length=100, - null=True, - blank=True, - default=None + max_length=100, null=True, blank=True, default=None ) ParentOrganisation_OrganisationName = CharField( - max_length=100, - null=True, - blank=True, - default=None - ) - LastUpdatedDate = DateTimeField( - max_length=100, - null=True, - blank=True, - default=None + max_length=100, null=True, blank=True, default=None ) + LastUpdatedDate = DateTimeField(max_length=100, null=True, blank=True, default=None) history = HistoricalRecords() # relationships nhs_region = models.ForeignKey( - 'epilepsy12.NHSRegionEntity', - on_delete=models.PROTECT + "epilepsy12.NHSRegionEntity", on_delete=models.PROTECT ) integrated_care_board = models.ForeignKey( - 'epilepsy12.IntegratedCareBoardEntity', + "epilepsy12.IntegratedCareBoardEntity", null=True, blank=True, - on_delete=models.PROTECT + on_delete=models.PROTECT, ) openuk_network = models.ForeignKey( - 'epilepsy12.OPENUKNetworkEntity', - on_delete=models.PROTECT + "epilepsy12.OPENUKNetworkEntity", on_delete=models.PROTECT ) ons_region = models.ForeignKey( - 'epilepsy12.ONSRegionEntity', - on_delete=models.PROTECT + "epilepsy12.ONSRegionEntity", on_delete=models.PROTECT ) @property @@ -134,10 +69,10 @@ def _history_user(self, value): self.updated_by = value class Meta: - indexes = [models.Index(fields=['OrganisationName'])] - verbose_name = 'Organisation' - verbose_name_plural = 'Organisations' - ordering = ('OrganisationName',) + indexes = [models.Index(fields=["OrganisationName"])] + verbose_name = "Organisation" + verbose_name_plural = "Organisations" + ordering = ("OrganisationName",) def __str__(self) -> str: return self.OrganisationName diff --git a/epilepsy12/models_folder/epilepsy12user.py b/epilepsy12/models_folder/epilepsy12user.py index cd8c854c..a2c724a4 100644 --- a/epilepsy12/models_folder/epilepsy12user.py +++ b/epilepsy12/models_folder/epilepsy12user.py @@ -11,25 +11,11 @@ from simple_history.models import HistoricalRecords # rcpch +from epilepsy12.common_view_functions.group_for_group import group_for_role from epilepsy12.constants.user_types import ( ROLES, TITLES, - AUDIT_CENTRE_LEAD_CLINICIAN, - TRUST_AUDIT_TEAM_FULL_ACCESS, - AUDIT_CENTRE_CLINICIAN, - TRUST_AUDIT_TEAM_EDIT_ACCESS, - AUDIT_CENTRE_ADMINISTRATOR, - TRUST_AUDIT_TEAM_EDIT_ACCESS, - RCPCH_AUDIT_LEAD, - EPILEPSY12_AUDIT_TEAM_FULL_ACCESS, - RCPCH_AUDIT_ANALYST, - EPILEPSY12_AUDIT_TEAM_EDIT_ACCESS, - RCPCH_AUDIT_ADMINISTRATOR, - EPILEPSY12_AUDIT_TEAM_VIEW_ONLY, - RCPCH_AUDIT_PATIENT_FAMILY, - PATIENT_ACCESS, - TRUST_AUDIT_TEAM_VIEW_ONLY, - AUDIT_CENTRE_MANAGER, + # preferences in the view VIEW_PREFERENCES, ) @@ -53,9 +39,9 @@ def create_user(self, email, password, first_name, role, **extra_fields): raise ValueError(_("You must provide an email address")) if not extra_fields.get("organisation_employer") and not extra_fields.get( - "is_staff" + "is_rcpch_staff" ): - # Non-RCPCH staff (is_staff) are not affiliated with a organisation + # Non-RCPCH staff (is_rcpch_staff) are not affiliated with a organisation raise ValueError( _("You must provide the name of your main organisation trust.") ) @@ -85,30 +71,10 @@ def create_user(self, email, password, first_name, role, **extra_fields): """ Allocate Groups - the groups already have permissions allocated """ - if user.role == AUDIT_CENTRE_LEAD_CLINICIAN: - group = Group.objects.get(name=TRUST_AUDIT_TEAM_FULL_ACCESS) - elif user.role == AUDIT_CENTRE_CLINICIAN: - group = Group.objects.get(name=TRUST_AUDIT_TEAM_EDIT_ACCESS) - elif user.role == AUDIT_CENTRE_ADMINISTRATOR: - group = Group.objects.get(name=TRUST_AUDIT_TEAM_VIEW_ONLY) - elif user.role == AUDIT_CENTRE_MANAGER: - group = Group.objects.get(name=TRUST_AUDIT_TEAM_VIEW_ONLY) - elif user.role == RCPCH_AUDIT_LEAD: - group = Group.objects.get(name=EPILEPSY12_AUDIT_TEAM_FULL_ACCESS) - elif user.role == RCPCH_AUDIT_ANALYST: - group = Group.objects.get(name=EPILEPSY12_AUDIT_TEAM_EDIT_ACCESS) - elif user.role == RCPCH_AUDIT_ADMINISTRATOR: - group = Group.objects.get(name=EPILEPSY12_AUDIT_TEAM_VIEW_ONLY) - elif user.role == RCPCH_AUDIT_PATIENT_FAMILY: - group = Group.objects.get(name=PATIENT_ACCESS) - else: - # no group - group = Group.objects.get(name=TRUST_AUDIT_TEAM_VIEW_ONLY) + group = group_for_role(user.role) user.save() user.groups.add(group) - print(f"creating {user} in {user.groups }") - return user def create_superuser(self, email, password, **extra_fields): @@ -120,6 +86,7 @@ def create_superuser(self, email, password, **extra_fields): extra_fields.setdefault("is_active", True) extra_fields.setdefault("is_staff", True) extra_fields.setdefault("is_rcpch_audit_team_member", True) + extra_fields.setdefault("is_rcpch_staff", True) extra_fields.setdefault("email_confirmed", True) # National level preference extra_fields.setdefault("view_preference", 2) @@ -137,32 +104,8 @@ def create_superuser(self, email, password, **extra_fields): Allocate Roles """ - self.allocate_group_based_on_role(logged_in_user) - - def allocate_group_based_on_role(self, user): - if user.role == AUDIT_CENTRE_LEAD_CLINICIAN: - group = Group.objects.get(name=TRUST_AUDIT_TEAM_FULL_ACCESS) - user.is_staff = True - elif user.role == AUDIT_CENTRE_CLINICIAN: - group = Group.objects.get(name=TRUST_AUDIT_TEAM_EDIT_ACCESS) - user.is_staff = True - elif user.role == AUDIT_CENTRE_ADMINISTRATOR: - group = Group.objects.get(name=TRUST_AUDIT_TEAM_EDIT_ACCESS) - user.is_staff = True - elif user.role == RCPCH_AUDIT_LEAD: - group = Group.objects.get(name=EPILEPSY12_AUDIT_TEAM_FULL_ACCESS) - user.is_staff = True - elif user.role == RCPCH_AUDIT_ANALYST: - group = Group.objects.get(name=EPILEPSY12_AUDIT_TEAM_EDIT_ACCESS) - user.is_staff = True - elif user.role == RCPCH_AUDIT_ADMINISTRATOR: - group = Group.objects.get(name=EPILEPSY12_AUDIT_TEAM_VIEW_ONLY) - elif user.role == RCPCH_AUDIT_PATIENT_FAMILY: - group = Group.objects.get(name=PATIENT_ACCESS) - else: - # no group - group = Group.objects.get(name=TRUST_AUDIT_TEAM_VIEW_ONLY) - user.groups.add(group) + group = group_for_role(logged_in_user.role) + logged_in_user.groups.add(group) class Epilepsy12User(AbstractUser, PermissionsMixin): @@ -188,21 +131,26 @@ class Epilepsy12User(AbstractUser, PermissionsMixin): unique=True, error_messages={"unique": _("This email address is already in use.")}, ) - bio = models.CharField( - help_text=_("Share something about yourself."), - max_length=500, - blank=True, - null=True, - ) is_active = models.BooleanField(default=False) is_staff = models.BooleanField( - # reflects if user is an RCPCH member of staff. This means they are not affiliated with a organisation trust + # reflects if user has access to admin default=False ) is_superuser = models.BooleanField(default=False) is_rcpch_audit_team_member = models.BooleanField( - # reflects is a member of the RCPCH audit team. If is_staff is false, user is also a clinician and therefore must - # be affiliated with a organisation trust + # reflects is a member of the RCPCH audit team. If is_rcpch_audit_team_member is True and + # is_rcpch_staff is False, user is also a clinician/organisation admin and therefore must + # may be affiliated with a organisation trust + default=False + ) + is_rcpch_staff = models.BooleanField( + # reflects if user is an RCPCH employee + # must be affiliated with an organisation + default=False + ) + is_patient_or_carer = models.BooleanField( + # reflects is a patient or carer + # must be affiliated with an organisation default=False ) view_preference = models.SmallIntegerField( @@ -213,8 +161,6 @@ class Epilepsy12User(AbstractUser, PermissionsMixin): ) date_joined = models.DateTimeField(default=timezone.now) role = models.PositiveSmallIntegerField(choices=ROLES, blank=True, null=True) - twitter_handle = models.CharField(max_length=255, null=True, blank=True) - email_confirmed = models.BooleanField(default=False) history = HistoricalRecords() diff --git a/epilepsy12/models_folder/episode.py b/epilepsy12/models_folder/episode.py index 9b458259..70344c3d 100644 --- a/epilepsy12/models_folder/episode.py +++ b/epilepsy12/models_folder/episode.py @@ -256,7 +256,7 @@ class Episode(TimeStampAbstractBaseClass, UserStampAbstractBaseClass, HelpTextMi ) # nonepileptic seizure onset - + # TODO: is this the correct name for field? Should it be 'nonepileptic_seizure_onset' nonepileptic_seizure_unknown_onset = models.CharField( help_text={ 'label': 'How best describes the onset of the nonepileptic episode(s)?', diff --git a/epilepsy12/models_folder/investigations.py b/epilepsy12/models_folder/investigations.py index ebd46b76..5ad48a60 100644 --- a/epilepsy12/models_folder/investigations.py +++ b/epilepsy12/models_folder/investigations.py @@ -1,12 +1,16 @@ -# django -from django.contrib.gis.db import models +# TODO: attribute names inconsistent e.g. eeg_indicatED & eeg_performED_date (past tense) but eeg_request_date (present tense); and mri_indicated but mri_BRAIN_requested_date & mri_BRAIN_reported_date; and perhaps eeg_PERFORMED but mri_BRAIN_REPORTED Should refactor + +# standard imports +from datetime import date +from dateutil.relativedelta import relativedelta # 3rd party +from django.contrib.gis.db import models from simple_history.models import HistoricalRecords # rcpch from .help_text_mixin import HelpTextMixin -from ..general_functions import calculate_time_elapsed +from ..general_functions import stringify_time_elapsed from .time_and_user_abstract_base_classes import * @@ -16,7 +20,7 @@ class Investigations( eeg_indicated = models.BooleanField( help_text={ "label": "Has a first EEG been requested?", - "reference": "All children with Epilepsy should have an EEG", + "reference": "If a diagnosis of epilepsy is suspected, a routine EEG should be carried out to support the diagnosis. CYP undergoing initial investigations for epilepsy should have tests within 4 weeks of being requested.", }, default=None, null=True, @@ -107,18 +111,17 @@ def mri_wait(self): Calculated field. Returns time elapsed between date EEG requested and performed as a string. """ if self.mri_brain_reported_date and self.mri_brain_requested_date: - return calculate_time_elapsed( - self.mri_brain_requested_date, self.mri_brain_reported_date - ) + return (self.mri_brain_reported_date - self.mri_brain_requested_date).days def eeg_wait(self): """ Calculated field. Returns time elapsed between date EEG requested and performed as a string. """ if self.eeg_performed_date and self.eeg_request_date: - return calculate_time_elapsed( - self.eeg_request_date, self.eeg_performed_date - ) + return (self.eeg_performed_date - self.eeg_request_date).days + + def get_current_date(self): + return date.today() # relationships registration = models.OneToOneField( diff --git a/epilepsy12/models_folder/kpi.py b/epilepsy12/models_folder/kpi.py index 08b4e4fc..2feca392 100644 --- a/epilepsy12/models_folder/kpi.py +++ b/epilepsy12/models_folder/kpi.py @@ -413,6 +413,36 @@ class KPI(models.Model, HelpTextMixin): ) parent_trust = models.CharField(max_length=250) + + def get_kpis(self): + """ + Returns dictionary of KPI attributes with related scores. + """ + kpis = { + "paediatrician_with_expertise_in_epilepsies" : self.paediatrician_with_expertise_in_epilepsies, + "epilepsy_specialist_nurse" : self.epilepsy_specialist_nurse, + "tertiary_input" : self.tertiary_input, + "epilepsy_surgery_referral" : self.epilepsy_surgery_referral, + "ecg" : self.ecg, + "mri" : self.mri, + "assessment_of_mental_health_issues" : self.assessment_of_mental_health_issues, + "mental_health_support" : self.mental_health_support, + "sodium_valproate" : self.sodium_valproate, + "comprehensive_care_planning_agreement" : self.comprehensive_care_planning_agreement, + "patient_held_individualised_epilepsy_document" : self.patient_held_individualised_epilepsy_document, + "patient_carer_parent_agreement_to_the_care_planning" : self.patient_carer_parent_agreement_to_the_care_planning, + "care_planning_has_been_updated_when_necessary" : self.care_planning_has_been_updated_when_necessary, + "comprehensive_care_planning_content" : self.comprehensive_care_planning_content, + "parental_prolonged_seizures_care_plan" : self.parental_prolonged_seizures_care_plan, + "water_safety" : self.water_safety, + "first_aid" : self.first_aid, + "general_participation_and_risk" : self.general_participation_and_risk, + "sudep" : self.sudep, + "service_contact_details" : self.service_contact_details, + "school_individual_healthcare_plan" : self.school_individual_healthcare_plan, + } + + return kpis class Meta: verbose_name = _("KPI ") diff --git a/epilepsy12/models_folder/kpi_aggregation.py b/epilepsy12/models_folder/kpi_aggregation.py new file mode 100644 index 00000000..971bcd74 --- /dev/null +++ b/epilepsy12/models_folder/kpi_aggregation.py @@ -0,0 +1,420 @@ +from django.contrib.gis.db import models +from django.utils.translation import gettext_lazy as _ +from .help_text_mixin import HelpTextMixin + +# RCPCH imports +from ..constants import ABSTRACTION_LEVELS + + +class KPIAggregation(models.Model, HelpTextMixin): + """ + KPI summary statistics + Model is updated each time a new case is registered and fully scored + There is an instance of KPI_Aggregation for each organisation each level of abstraction ('organisation', 'trust', 'icb', 'open_uk', 'nhs_region', 'country', 'national') + """ + + paediatrician_with_expertise_in_epilepsies = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + paediatrician_with_expertise_in_epilepsies_average = models.FloatField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + paediatrician_with_expertise_in_epilepsies_total = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + epilepsy_specialist_nurse = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + epilepsy_specialist_nurse_average = models.FloatField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + epilepsy_specialist_nurse_total = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + tertiary_input = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + tertiary_input_average = models.FloatField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + tertiary_input_total = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + epilepsy_surgery_referral = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + epilepsy_surgery_referral_average = models.FloatField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + epilepsy_surgery_referral_total = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + ecg = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + ecg_average = models.FloatField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + ecg_total = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + mri = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + mri_average = models.FloatField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + mri_total = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + assessment_of_mental_health_issues = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + assessment_of_mental_health_issues_average = models.FloatField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + assessment_of_mental_health_issues_total = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + mental_health_support = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + mental_health_support_average = models.FloatField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + mental_health_support_total = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + sodium_valproate = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + sodium_valproate_average = models.FloatField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + sodium_valproate_total = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + comprehensive_care_planning_agreement = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + comprehensive_care_planning_agreement_average = models.FloatField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + comprehensive_care_planning_agreement_total = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + patient_held_individualised_epilepsy_document = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + patient_held_individualised_epilepsy_document_average = models.FloatField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + patient_held_individualised_epilepsy_document_total = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + patient_carer_parent_agreement_to_the_care_planning = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + patient_carer_parent_agreement_to_the_care_planning_average = models.FloatField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + patient_carer_parent_agreement_to_the_care_planning_total = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + care_planning_has_been_updated_when_necessary = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + care_planning_has_been_updated_when_necessary_average = models.FloatField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + care_planning_has_been_updated_when_necessary_total = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + comprehensive_care_planning_content = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + comprehensive_care_planning_content_average = models.FloatField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + comprehensive_care_planning_content_total = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + parental_prolonged_seizures_care_plan = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + parental_prolonged_seizures_care_plan_average = models.FloatField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + parental_prolonged_seizures_care_plan_total = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + water_safety = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + water_safety_average = models.FloatField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + water_safety_total = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + first_aid = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + first_aid_average = models.FloatField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + first_aid_total = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + general_participation_and_risk = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + general_participation_and_risk_average = models.FloatField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + general_participation_and_risk_total = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + service_contact_details = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + service_contact_details_average = models.FloatField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + service_contact_details_total = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + sudep = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + sudep_average = models.FloatField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + sudep_total = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + school_individual_healthcare_plan = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + school_individual_healthcare_plan_average = models.FloatField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + school_individual_healthcare_plan_total = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + total_number_of_cases = models.IntegerField( + help_text={"label": "", "reference": ""}, + default=None, + null=True, + blank=True, + ) + abstraction_level = models.CharField( + choices=ABSTRACTION_LEVELS, + max_length=50, + ) + + open_access = models.BooleanField(default=False, null=True, blank=True) + + organisation = models.ForeignKey( + to="epilepsy12.Organisation", + blank=True, + default=None, + null=True, + on_delete=models.CASCADE, + ) + + class Meta: + verbose_name = _("KPI Aggregation Result ") + verbose_name_plural = _("KPI Aggregation Results ") + + def __str__(self): + return f"KPI aggregation results for child in {self.organisation.OrganisationName}({self.organisation.ParentOrganisation_OrganisationName})" diff --git a/epilepsy12/models_folder/registration.py b/epilepsy12/models_folder/registration.py index 855aa4e1..dd035c73 100644 --- a/epilepsy12/models_folder/registration.py +++ b/epilepsy12/models_folder/registration.py @@ -1,78 +1,90 @@ # python from dateutil.relativedelta import relativedelta from datetime import datetime + # django from django.contrib.gis.db import models + # 3rd party from simple_history.models import HistoricalRecords + # rcpch from .help_text_mixin import HelpTextMixin -from ..constants import CAN_APPROVE_ELIGIBILITY, CAN_REMOVE_APPROVAL_OF_ELIGIBILITY, CAN_REGISTER_CHILD_IN_EPILEPSY12, CAN_UNREGISTER_CHILD_IN_EPILEPSY12, CAN_CONSENT_TO_AUDIT_PARTICIPATION +from ..constants import ( + CAN_APPROVE_ELIGIBILITY, + CAN_REGISTER_CHILD_IN_EPILEPSY12, + CAN_UNREGISTER_CHILD_IN_EPILEPSY12, + CAN_CONSENT_TO_AUDIT_PARTICIPATION, +) from .time_and_user_abstract_base_classes import * -from ..general_functions import first_tuesday_in_january, cohort_number_from_enrolment_date +from ..general_functions import nth_tuesday_of_year, cohort_number_from_enrolment_date +from ..validators import not_in_the_future_validator -class Registration(TimeStampAbstractBaseClass, UserStampAbstractBaseClass, HelpTextMixin): +class Registration( + TimeStampAbstractBaseClass, UserStampAbstractBaseClass, HelpTextMixin +): """ A record is created in the Registration class every time a case is registered for the audit """ registration_date = models.DateField( help_text={ - 'label': "First paediatric assessment", - 'reference': "Setting this date is an irreversible step. Confirmation will be requested to complete this step.", + "label": "First paediatric assessment", + "reference": "Setting this date is an irreversible step. Confirmation will be requested to complete this step.", }, null=True, - default=None + default=None, + validators=[not_in_the_future_validator], ) registration_close_date = models.DateField( help_text={ - 'label': "First paediatric assessment closing date", - 'reference': "Date on which the registration is due to close", + "label": "First paediatric assessment closing date", + "reference": "Date on which the registration is due to close", }, default=None, - null=True + null=True, ) audit_submission_date = models.DateField( help_text={ - 'label': "Epilepsy12 submission date", - 'reference': "Date on which the audit submission is due. It is always on the 2nd Tuesday in January.", + "label": "Epilepsy12 submission date", + "reference": "Date on which the audit submission is due. It is always on the 2nd Tuesday in January.", }, default=None, - null=True + null=True, ) - def audit_submission_date_calculation(self): - if (self.registration_date): - one_year_complete_year = self.registration_date_one_year_on().year - second_tuesday_this_year = first_tuesday_in_january( - datetime.today().date().year) + relativedelta(days=7) - if self.registration_date_one_year_on() <= second_tuesday_this_year: - second_tuesday = second_tuesday_this_year - else: - second_tuesday = first_tuesday_in_january( - one_year_complete_year+1) + relativedelta(days=7) - return second_tuesday - else: - return None + def audit_submission_date_calculation(self) -> datetime.date: + """Returns audit submission date. + + Defined as registration date + 1 year, and then the first occurring 2nd Tuesday of Jan. + + Returns: + datetime.date: audit submission date. + """ + if self.registration_date: + registration_plus_one_year = self.registration_date + relativedelta(years=1) + + second_tuesday_next_year = nth_tuesday_of_year( + registration_plus_one_year.year, n=2 + ) + + second_tuesday_two_years = nth_tuesday_of_year( + registration_plus_one_year.year + 1, n=2 + ) - def registration_date_one_year_on(self): - if (self.registration_date): - return self.registration_date+relativedelta(years=1) + if registration_plus_one_year <= second_tuesday_next_year: + return second_tuesday_next_year + else: + return second_tuesday_two_years else: return None - eligibility_criteria_met = models.BooleanField( - default=None, - null=True - ) + eligibility_criteria_met = models.BooleanField(default=None, null=True) - cohort = models.PositiveSmallIntegerField( - default=None, - null=True - ) + cohort = models.PositiveSmallIntegerField(default=None, null=True) history = HistoricalRecords() @@ -84,55 +96,48 @@ def _history_user(self): def _history_user(self, value): self.updated_by = value + def get_current_date(self): + return datetime.now().date() + @property - def days_remaining_before_submission(self): + def days_remaining_before_submission(self) -> int: + """Returns remaining days between current datetime and submission datetime, minimum value 0.""" if self.audit_submission_date: - today = datetime.now().date() - remaining_time = self.audit_submission_date - today - if remaining_time.days < 0: - return 0 - return remaining_time.days + remaining_dateime = self.audit_submission_date - self.get_current_date() + return remaining_dateime.days if remaining_dateime.days > 0 else 0 # relationships - case = models.OneToOneField( - 'epilepsy12.Case', - on_delete=models.PROTECT, - null=True - ) + case = models.OneToOneField("epilepsy12.Case", on_delete=models.PROTECT, null=True) audit_progress = models.OneToOneField( - 'epilepsy12.AuditProgress', - on_delete=models.CASCADE, - null=True + "epilepsy12.AuditProgress", on_delete=models.CASCADE, null=True ) - kpi = models.OneToOneField( - "epilepsy12.KPI", - on_delete=models.CASCADE, - null=True - ) + kpi = models.OneToOneField("epilepsy12.KPI", on_delete=models.CASCADE, null=True) class Meta: - verbose_name = 'Registration' - verbose_name_plural = 'Registrations' + verbose_name = "Registration" + verbose_name_plural = "Registrations" permissions = [ CAN_APPROVE_ELIGIBILITY, - CAN_REMOVE_APPROVAL_OF_ELIGIBILITY, CAN_REGISTER_CHILD_IN_EPILEPSY12, CAN_UNREGISTER_CHILD_IN_EPILEPSY12, - CAN_CONSENT_TO_AUDIT_PARTICIPATION + CAN_CONSENT_TO_AUDIT_PARTICIPATION, ] def save(self, *args, **kwargs) -> None: if self.registration_date is not None: - self.registration_close_date = self.registration_date_one_year_on() + self.registration_close_date = self.registration_date + relativedelta( + years=1 + ) self.audit_submission_date = self.audit_submission_date_calculation() - self.cohort = cohort_number_from_enrolment_date( - self.registration_date) + self.cohort = cohort_number_from_enrolment_date(self.registration_date) return super().save(*args, **kwargs) def __str__(self) -> str: if self.registration_date: - return f'Epilepsy12 registration for {self.case} on {self.registration_date}' + return ( + f"Epilepsy12 registration for {self.case} on {self.registration_date}" + ) else: - return f'Epilepsy12 registration for {self.case} incomplete.' + return f"Epilepsy12 registration for {self.case} incomplete." diff --git a/epilepsy12/models_folder/syndrome.py b/epilepsy12/models_folder/syndrome.py index 54fb44a3..4d85401e 100644 --- a/epilepsy12/models_folder/syndrome.py +++ b/epilepsy12/models_folder/syndrome.py @@ -56,7 +56,7 @@ def _history_user(self, value): ) def __str__(self) -> str: - return f'{self.syndrome.syndrome_name} on {self.syndrome_diagnosis_date}' + return f'{self.syndrome.syndrome_name} on {self.syndrome_diagnosis_date}' if self.syndrome else f"Empty syndrome" class Meta: verbose_name = 'Syndrome' diff --git a/epilepsy12/serializers.py b/epilepsy12/serializers.py index 8dcc536d..41b7f002 100644 --- a/epilepsy12/serializers.py +++ b/epilepsy12/serializers.py @@ -10,34 +10,65 @@ class Epilepsy12UserSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Epilepsy12User - fields = ['first_name', 'surname', 'title', 'email', 'username', 'bio', 'is_active', 'is_staff', - 'is_superuser', 'is_rcpch_audit_team_member', 'view_preference', 'date_joined', 'role', 'twitter_handle'] + fields = [ + "first_name", + "surname", + "title", + "email", + "username", + "is_active", + "is_staff", + "is_rcpch_staff", + "is_superuser", + "is_rcpch_audit_team_member", + "view_preference", + "date_joined", + "role", + ] class GroupSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Group - fields = ['url', 'name'] + fields = ["url", "name"] class CaseSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Case - fields = ['locked', 'locked_at', 'locked_by', 'nhs_number', 'first_name', 'surname', 'sex', - 'date_of_birth', 'postcode', 'ethnicity', 'index_of_multiple_deprivation_quintile', 'organisations'] + fields = [ + "locked", + "locked_at", + "locked_by", + "nhs_number", + "first_name", + "surname", + "sex", + "date_of_birth", + "postcode", + "ethnicity", + "index_of_multiple_deprivation_quintile", + "organisations", + ] def is_future_date(value): if value > datetime.now().date(): raise serializers.ValidationError( - {'registration_date': 'First paediatric assessment cannot be in the future.'}) + { + "registration_date": "First paediatric assessment cannot be in the future." + } + ) return value def is_true(value): if not value: raise serializers.ValidationError( - {'eligibility_criteria_met': 'Eligibility criteria must have been met to be registered in Epilepsy12.'}) + { + "eligibility_criteria_met": "Eligibility criteria must have been met to be registered in Epilepsy12." + } + ) return value @@ -46,146 +77,317 @@ class RegistrationSerializer(serializers.HyperlinkedModelSerializer): cohort = serializers.IntegerField(read_only=True) registration_date = serializers.DateField(validators=[is_future_date]) eligibility_criteria_met = serializers.BooleanField( - required=True, validators=[is_true]) - child_name = serializers.CharField(source='case', required=False) + required=True, validators=[is_true] + ) + child_name = serializers.CharField(source="case", required=False) class Meta: model = Registration case = serializers.PrimaryKeyRelatedField(queryset=Case.objects.all()) audit_progress = serializers.PrimaryKeyRelatedField( - queryset=AuditProgress.objects.all()) - fields = ['pk', 'case', 'registration_date', - 'registration_close_date', 'cohort', 'eligibility_criteria_met', 'child_name'] + queryset=AuditProgress.objects.all() + ) + fields = [ + "pk", + "case", + "registration_date", + "registration_close_date", + "cohort", + "eligibility_criteria_met", + "child_name", + ] class AuditProgressSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = AuditProgress - fields = '__all__' + fields = "__all__" class EpilepsyContextSerializer(serializers.ModelSerializer): class Meta: model = EpilepsyContext registration = serializers.PrimaryKeyRelatedField( - queryset=Registration.objects.all()) - fields = ['previous_febrile_seizure', 'previous_acute_symptomatic_seizure', 'is_there_a_family_history_of_epilepsy', 'previous_neonatal_seizures', 'diagnosis_of_epilepsy_withdrawn', - 'were_any_of_the_epileptic_seizures_convulsive', 'experienced_prolonged_generalized_convulsive_seizures', 'experienced_prolonged_focal_seizures'] + queryset=Registration.objects.all() + ) + fields = [ + "previous_febrile_seizure", + "previous_acute_symptomatic_seizure", + "is_there_a_family_history_of_epilepsy", + "previous_neonatal_seizures", + "diagnosis_of_epilepsy_withdrawn", + "were_any_of_the_epileptic_seizures_convulsive", + "experienced_prolonged_generalized_convulsive_seizures", + "experienced_prolonged_focal_seizures", + ] class CaseSerializer(serializers.HyperlinkedModelSerializer): - class Meta: model = Case - fields = ['locked', 'locked_at', 'locked_by', 'nhs_number', 'first_name', 'surname', 'sex', - 'date_of_birth', 'postcode', 'ethnicity', 'index_of_multiple_deprivation_quintile'] + fields = [ + "locked", + "locked_at", + "locked_by", + "nhs_number", + "first_name", + "surname", + "sex", + "date_of_birth", + "postcode", + "ethnicity", + "index_of_multiple_deprivation_quintile", + ] class FirstPaediatricAssessmentSerializer(serializers.ModelSerializer): - child_name = serializers.CharField( - source='registration.case', required=False) + child_name = serializers.CharField(source="registration.case", required=False) class Meta: model = FirstPaediatricAssessment registration = serializers.PrimaryKeyRelatedField( - queryset=Registration.objects.all()) + queryset=Registration.objects.all() + ) audit_progress = serializers.PrimaryKeyRelatedField( - queryset=AuditProgress.objects.all()) - fields = ['id', 'first_paediatric_assessment_in_acute_or_nonacute_setting', 'has_number_of_episodes_since_the_first_been_documented', - 'general_examination_performed', 'neurological_examination_performed', 'developmental_learning_or_schooling_problems', 'behavioural_or_emotional_problems', 'child_name'] + queryset=AuditProgress.objects.all() + ) + fields = [ + "id", + "first_paediatric_assessment_in_acute_or_nonacute_setting", + "has_number_of_episodes_since_the_first_been_documented", + "general_examination_performed", + "neurological_examination_performed", + "developmental_learning_or_schooling_problems", + "behavioural_or_emotional_problems", + "child_name", + ] class MultiaxialDiagnosisSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = MultiaxialDiagnosis registration = serializers.PrimaryKeyRelatedField( - queryset=Registration.objects.all()) - fields = ['syndrome_present', 'epilepsy_cause_known', 'epilepsy_cause', 'epilepsy_cause_categories', - 'relevant_impairments_behavioural_educational', 'mental_health_screen', 'mental_health_issue_identified', 'mental_health_issue'] + queryset=Registration.objects.all() + ) + fields = [ + "syndrome_present", + "epilepsy_cause_known", + "epilepsy_cause", + "epilepsy_cause_categories", + "relevant_impairments_behavioural_educational", + "mental_health_screen", + "mental_health_issue_identified", + "mental_health_issue", + ] class EpisodeSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Episode multiaxial_diagnosis = serializers.PrimaryKeyRelatedField( - queryset=MultiaxialDiagnosis.objects.all()) - fields = ['seizure_onset_date', 'seizure_onset_date_confidence', 'episode_definition', 'has_description_of_the_episode_or_episodes_been_gathered', 'description', 'description_keywords', 'epilepsy_or_nonepilepsy_status', 'epileptic_seizure_onset_type', 'nonepileptic_seizure_type', 'epileptic_generalised_onset', 'focal_onset_impaired_awareness', 'focal_onset_automatisms', 'focal_onset_atonic', 'focal_onset_clonic', 'focal_onset_left', 'focal_onset_right', 'focal_onset_epileptic_spasms', 'focal_onset_hyperkinetic', 'focal_onset_myoclonic', 'focal_onset_tonic', 'focal_onset_autonomic', - 'focal_onset_behavioural_arrest', 'focal_onset_cognitive', 'focal_onset_emotional', 'focal_onset_sensory', 'focal_onset_centrotemporal', 'focal_onset_temporal', 'focal_onset_frontal', 'focal_onset_parietal', 'focal_onset_occipital', 'focal_onset_gelastic', 'focal_onset_focal_to_bilateral_tonic_clonic', 'nonepileptic_seizure_unknown_onset', 'nonepileptic_seizure_syncope', 'nonepileptic_seizure_behavioural', 'nonepileptic_seizure_sleep', 'nonepileptic_seizure_paroxysmal', 'nonepileptic_seizure_migraine', 'nonepileptic_seizure_miscellaneous', 'nonepileptic_seizure_other'] + queryset=MultiaxialDiagnosis.objects.all() + ) + fields = [ + "seizure_onset_date", + "seizure_onset_date_confidence", + "episode_definition", + "has_description_of_the_episode_or_episodes_been_gathered", + "description", + "description_keywords", + "epilepsy_or_nonepilepsy_status", + "epileptic_seizure_onset_type", + "nonepileptic_seizure_type", + "epileptic_generalised_onset", + "focal_onset_impaired_awareness", + "focal_onset_automatisms", + "focal_onset_atonic", + "focal_onset_clonic", + "focal_onset_left", + "focal_onset_right", + "focal_onset_epileptic_spasms", + "focal_onset_hyperkinetic", + "focal_onset_myoclonic", + "focal_onset_tonic", + "focal_onset_autonomic", + "focal_onset_behavioural_arrest", + "focal_onset_cognitive", + "focal_onset_emotional", + "focal_onset_sensory", + "focal_onset_centrotemporal", + "focal_onset_temporal", + "focal_onset_frontal", + "focal_onset_parietal", + "focal_onset_occipital", + "focal_onset_gelastic", + "focal_onset_focal_to_bilateral_tonic_clonic", + "nonepileptic_seizure_unknown_onset", + "nonepileptic_seizure_syncope", + "nonepileptic_seizure_behavioural", + "nonepileptic_seizure_sleep", + "nonepileptic_seizure_paroxysmal", + "nonepileptic_seizure_migraine", + "nonepileptic_seizure_miscellaneous", + "nonepileptic_seizure_other", + ] class SyndromeSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Syndrome multiaxial_diagnosis = serializers.PrimaryKeyRelatedField( - queryset=MultiaxialDiagnosis.objects.all()) - fields = ['syndrome_diagnosis_date', 'syndrome'] + queryset=MultiaxialDiagnosis.objects.all() + ) + fields = ["syndrome_diagnosis_date", "syndrome"] class ComorbiditySerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Comorbidity multiaxial_diagnosis = serializers.PrimaryKeyRelatedField( - queryset=MultiaxialDiagnosis.objects.all()) - fields = ['comorbidity_diagnosis_date', 'comorbidityentity'] + queryset=MultiaxialDiagnosis.objects.all() + ) + fields = ["comorbidity_diagnosis_date", "comorbidityentity"] class InvestigationsSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Investigations registration = serializers.PrimaryKeyRelatedField( - queryset=Registration.objects.all()) - fields = ['eeg_indicated', 'eeg_request_date', 'eeg_performed_date', 'twelve_lead_ecg_status', 'ct_head_scan_status', - 'mri_indicated', 'mri_brain_requested_date', 'mri_brain_reported_date', 'mri_wait', 'eeg_wait'] + queryset=Registration.objects.all() + ) + fields = [ + "eeg_indicated", + "eeg_request_date", + "eeg_performed_date", + "twelve_lead_ecg_status", + "ct_head_scan_status", + "mri_indicated", + "mri_brain_requested_date", + "mri_brain_reported_date", + "mri_wait", + "eeg_wait", + ] class AssessmentSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Assessment registration = serializers.PrimaryKeyRelatedField( - queryset=Registration.objects.all()) - fields = ['childrens_epilepsy_surgical_service_referral_criteria_met', 'consultant_paediatrician_referral_made', 'consultant_paediatrician_referral_date', 'consultant_paediatrician_input_date', 'paediatric_neurologist_referral_made', 'paediatric_neurologist_referral_date', 'paediatric_neurologist_input_date', - 'childrens_epilepsy_surgical_service_referral_made', 'childrens_epilepsy_surgical_service_referral_date', 'childrens_epilepsy_surgical_service_input_date', 'epilepsy_specialist_nurse_referral_made', 'epilepsy_specialist_nurse_referral_date', 'epilepsy_specialist_nurse_input_date'] + queryset=Registration.objects.all() + ) + fields = [ + "childrens_epilepsy_surgical_service_referral_criteria_met", + "consultant_paediatrician_referral_made", + "consultant_paediatrician_referral_date", + "consultant_paediatrician_input_date", + "paediatric_neurologist_referral_made", + "paediatric_neurologist_referral_date", + "paediatric_neurologist_input_date", + "childrens_epilepsy_surgical_service_referral_made", + "childrens_epilepsy_surgical_service_referral_date", + "childrens_epilepsy_surgical_service_input_date", + "epilepsy_specialist_nurse_referral_made", + "epilepsy_specialist_nurse_referral_date", + "epilepsy_specialist_nurse_input_date", + ] class ManagementSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Management registration = serializers.PrimaryKeyRelatedField( - queryset=Registration.objects.all()) - fields = ['has_an_aed_been_given', 'has_rescue_medication_been_prescribed', 'individualised_care_plan_in_place', 'individualised_care_plan_date', 'individualised_care_plan_has_parent_carer_child_agreement', 'individualised_care_plan_includes_service_contact_details', 'individualised_care_plan_include_first_aid', 'individualised_care_plan_parental_prolonged_seizure_care', - 'individualised_care_plan_includes_general_participation_risk', 'individualised_care_plan_addresses_water_safety', 'individualised_care_plan_addresses_sudep', 'individualised_care_plan_includes_ehcp', 'has_individualised_care_plan_been_updated_in_the_last_year', 'has_been_referred_for_mental_health_support', 'has_support_for_mental_health_support'] + queryset=Registration.objects.all() + ) + fields = [ + "has_an_aed_been_given", + "has_rescue_medication_been_prescribed", + "individualised_care_plan_in_place", + "individualised_care_plan_date", + "individualised_care_plan_has_parent_carer_child_agreement", + "individualised_care_plan_includes_service_contact_details", + "individualised_care_plan_include_first_aid", + "individualised_care_plan_parental_prolonged_seizure_care", + "individualised_care_plan_includes_general_participation_risk", + "individualised_care_plan_addresses_water_safety", + "individualised_care_plan_addresses_sudep", + "individualised_care_plan_includes_ehcp", + "has_individualised_care_plan_been_updated_in_the_last_year", + "has_been_referred_for_mental_health_support", + "has_support_for_mental_health_support", + ] class AntiEpilepsyMedicineSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = AntiEpilepsyMedicine management = serializers.PrimaryKeyRelatedField( - queryset=Management.objects.all()) - fields = ['medicine_id', 'medicine_name', 'is_rescue_medicine', 'antiepilepsy_medicine_snomed_code', 'antiepilepsy_medicine_snomed_preferred_name', 'antiepilepsy_medicine_start_date', - 'antiepilepsy_medicine_stop_date', 'antiepilepsy_medicine_risk_discussed', 'is_a_pregnancy_prevention_programme_needed', 'is_a_pregnancy_prevention_programme_in_place'] + queryset=Management.objects.all() + ) + fields = [ + "medicine_id", + "medicine_name", + "is_rescue_medicine", + "antiepilepsy_medicine_snomed_code", + "antiepilepsy_medicine_snomed_preferred_name", + "antiepilepsy_medicine_start_date", + "antiepilepsy_medicine_stop_date", + "antiepilepsy_medicine_risk_discussed", + "is_a_pregnancy_prevention_programme_needed", + "is_a_pregnancy_prevention_programme_in_place", + ] class SiteSerializer(serializers.HyperlinkedModelSerializer): - child_name = serializers.CharField(source='case') + child_name = serializers.CharField(source="case") class Meta: model = Site - case = serializers.PrimaryKeyRelatedField( - queryset=Case.objects.all()) + case = serializers.PrimaryKeyRelatedField(queryset=Case.objects.all()) organisation = serializers.PrimaryKeyRelatedField( - queryset=Organisation.objects.all()) - fields = ['site_is_actively_involved_in_epilepsy_care', 'site_is_primary_centre_of_epilepsy_care', - 'site_is_childrens_epilepsy_surgery_centre', 'site_is_paediatric_neurology_centre', 'site_is_general_paediatric_centre', 'case', 'organisation', 'child_name'] + queryset=Organisation.objects.all() + ) + fields = [ + "site_is_actively_involved_in_epilepsy_care", + "site_is_primary_centre_of_epilepsy_care", + "site_is_childrens_epilepsy_surgery_centre", + "site_is_paediatric_neurology_centre", + "site_is_general_paediatric_centre", + "case", + "organisation", + "child_name", + ] class OrganisationSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Organisation - fields = ['OrganisationID', 'OrganisationCode', 'OrganisationType', 'SubType', 'Sector', 'OrganisationStatus', 'IsPimsManaged', 'OrganisationName', - 'Address1', 'Address2', 'Address3', 'City', 'County', 'Postcode', 'Latitude', 'Longitude', 'ParentODSCode', 'ParentName', 'Phone', 'Email', 'Website', 'Fax'] + fields = [ + "OrganisationID", + "OrganisationCode", + "OrganisationType", + "SubType", + "Sector", + "OrganisationStatus", + "IsPimsManaged", + "OrganisationName", + "Address1", + "Address2", + "Address3", + "City", + "County", + "Postcode", + "Latitude", + "Longitude", + "ParentODSCode", + "ParentName", + "Phone", + "Email", + "Website", + "Fax", + ] class KeywordSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Keyword - fields = ['keyword', 'category'] + fields = ["keyword", "category"] diff --git a/epilepsy12/shape_files/Countries_December_2022_GB_BFC_Detailed/CTRY_DEC_2022_GB_BFC.cpg b/epilepsy12/shape_files/Countries_December_2022_GB_BFC_Detailed/CTRY_DEC_2022_GB_BFC.cpg new file mode 100644 index 00000000..3ad133c0 --- /dev/null +++ b/epilepsy12/shape_files/Countries_December_2022_GB_BFC_Detailed/CTRY_DEC_2022_GB_BFC.cpg @@ -0,0 +1 @@ +UTF-8 \ No newline at end of file diff --git a/epilepsy12/shape_files/Countries_December_2022_GB_BFC_Detailed/CTRY_DEC_2022_GB_BFC.dbf b/epilepsy12/shape_files/Countries_December_2022_GB_BFC_Detailed/CTRY_DEC_2022_GB_BFC.dbf new file mode 100644 index 00000000..c15c3f95 Binary files /dev/null and b/epilepsy12/shape_files/Countries_December_2022_GB_BFC_Detailed/CTRY_DEC_2022_GB_BFC.dbf differ diff --git a/epilepsy12/shape_files/Countries_December_2022_GB_BFC_Detailed/CTRY_DEC_2022_GB_BFC.prj b/epilepsy12/shape_files/Countries_December_2022_GB_BFC_Detailed/CTRY_DEC_2022_GB_BFC.prj new file mode 100644 index 00000000..fec0ee28 --- /dev/null +++ b/epilepsy12/shape_files/Countries_December_2022_GB_BFC_Detailed/CTRY_DEC_2022_GB_BFC.prj @@ -0,0 +1 @@ +PROJCS["British_National_Grid",GEOGCS["GCS_OSGB_1936",DATUM["D_OSGB_1936",SPHEROID["Airy_1830",6377563.396,299.3249646]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",400000.0],PARAMETER["False_Northing",-100000.0],PARAMETER["Central_Meridian",-2.0],PARAMETER["Scale_Factor",0.9996012717],PARAMETER["Latitude_Of_Origin",49.0],UNIT["Meter",1.0]] \ No newline at end of file diff --git a/epilepsy12/shape_files/Countries_December_2022_GB_BFC_Detailed/CTRY_DEC_2022_GB_BFC.shp b/epilepsy12/shape_files/Countries_December_2022_GB_BFC_Detailed/CTRY_DEC_2022_GB_BFC.shp new file mode 100644 index 00000000..a40aa81c Binary files /dev/null and b/epilepsy12/shape_files/Countries_December_2022_GB_BFC_Detailed/CTRY_DEC_2022_GB_BFC.shp differ diff --git a/epilepsy12/shape_files/Countries_December_2022_GB_BFC_Detailed/CTRY_DEC_2022_GB_BFC.shp.xml b/epilepsy12/shape_files/Countries_December_2022_GB_BFC_Detailed/CTRY_DEC_2022_GB_BFC.shp.xml new file mode 100644 index 00000000..5f046e7d --- /dev/null +++ b/epilepsy12/shape_files/Countries_December_2022_GB_BFC_Detailed/CTRY_DEC_2022_GB_BFC.shp.xml @@ -0,0 +1,2 @@ + +20230321124222001.0TRUE diff --git a/epilepsy12/shape_files/Countries_December_2022_GB_BFC_Detailed/CTRY_DEC_2022_GB_BFC.shx b/epilepsy12/shape_files/Countries_December_2022_GB_BFC_Detailed/CTRY_DEC_2022_GB_BFC.shx new file mode 100644 index 00000000..20be45a5 Binary files /dev/null and b/epilepsy12/shape_files/Countries_December_2022_GB_BFC_Detailed/CTRY_DEC_2022_GB_BFC.shx differ diff --git a/epilepsy12/shape_files/Countries_December_2022_UK_BUC/CTRY_DEC_2022_UK_BUC.cpg b/epilepsy12/shape_files/Countries_December_2022_UK_BUC/CTRY_DEC_2022_UK_BUC.cpg new file mode 100644 index 00000000..3ad133c0 --- /dev/null +++ b/epilepsy12/shape_files/Countries_December_2022_UK_BUC/CTRY_DEC_2022_UK_BUC.cpg @@ -0,0 +1 @@ +UTF-8 \ No newline at end of file diff --git a/epilepsy12/shape_files/Countries_December_2022_UK_BUC/CTRY_DEC_2022_UK_BUC.dbf b/epilepsy12/shape_files/Countries_December_2022_UK_BUC/CTRY_DEC_2022_UK_BUC.dbf new file mode 100644 index 00000000..fc38b934 Binary files /dev/null and b/epilepsy12/shape_files/Countries_December_2022_UK_BUC/CTRY_DEC_2022_UK_BUC.dbf differ diff --git a/epilepsy12/shape_files/Countries_December_2022_UK_BUC/CTRY_DEC_2022_UK_BUC.prj b/epilepsy12/shape_files/Countries_December_2022_UK_BUC/CTRY_DEC_2022_UK_BUC.prj new file mode 100644 index 00000000..fec0ee28 --- /dev/null +++ b/epilepsy12/shape_files/Countries_December_2022_UK_BUC/CTRY_DEC_2022_UK_BUC.prj @@ -0,0 +1 @@ +PROJCS["British_National_Grid",GEOGCS["GCS_OSGB_1936",DATUM["D_OSGB_1936",SPHEROID["Airy_1830",6377563.396,299.3249646]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",400000.0],PARAMETER["False_Northing",-100000.0],PARAMETER["Central_Meridian",-2.0],PARAMETER["Scale_Factor",0.9996012717],PARAMETER["Latitude_Of_Origin",49.0],UNIT["Meter",1.0]] \ No newline at end of file diff --git a/epilepsy12/shape_files/Countries_December_2022_UK_BUC/CTRY_DEC_2022_UK_BUC.shp b/epilepsy12/shape_files/Countries_December_2022_UK_BUC/CTRY_DEC_2022_UK_BUC.shp new file mode 100644 index 00000000..0379ce3f Binary files /dev/null and b/epilepsy12/shape_files/Countries_December_2022_UK_BUC/CTRY_DEC_2022_UK_BUC.shp differ diff --git a/epilepsy12/shape_files/Countries_December_2022_UK_BUC/CTRY_DEC_2022_UK_BUC.shp.xml b/epilepsy12/shape_files/Countries_December_2022_UK_BUC/CTRY_DEC_2022_UK_BUC.shp.xml new file mode 100644 index 00000000..96044fbb --- /dev/null +++ b/epilepsy12/shape_files/Countries_December_2022_UK_BUC/CTRY_DEC_2022_UK_BUC.shp.xml @@ -0,0 +1,2 @@ + +20230328123733001.0TRUE diff --git a/epilepsy12/shape_files/Countries_December_2022_UK_BUC/CTRY_DEC_2022_UK_BUC.shx b/epilepsy12/shape_files/Countries_December_2022_UK_BUC/CTRY_DEC_2022_UK_BUC.shx new file mode 100644 index 00000000..61ef5a60 Binary files /dev/null and b/epilepsy12/shape_files/Countries_December_2022_UK_BUC/CTRY_DEC_2022_UK_BUC.shx differ diff --git a/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_BSC/ICB_APR_2023_EN_BSC.cpg b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_BSC/ICB_APR_2023_EN_BSC.cpg new file mode 100644 index 00000000..3ad133c0 --- /dev/null +++ b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_BSC/ICB_APR_2023_EN_BSC.cpg @@ -0,0 +1 @@ +UTF-8 \ No newline at end of file diff --git a/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_BSC/ICB_APR_2023_EN_BSC.dbf b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_BSC/ICB_APR_2023_EN_BSC.dbf new file mode 100644 index 00000000..cdbf0bc8 Binary files /dev/null and b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_BSC/ICB_APR_2023_EN_BSC.dbf differ diff --git a/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_BSC/ICB_APR_2023_EN_BSC.prj b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_BSC/ICB_APR_2023_EN_BSC.prj new file mode 100644 index 00000000..fec0ee28 --- /dev/null +++ b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_BSC/ICB_APR_2023_EN_BSC.prj @@ -0,0 +1 @@ +PROJCS["British_National_Grid",GEOGCS["GCS_OSGB_1936",DATUM["D_OSGB_1936",SPHEROID["Airy_1830",6377563.396,299.3249646]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",400000.0],PARAMETER["False_Northing",-100000.0],PARAMETER["Central_Meridian",-2.0],PARAMETER["Scale_Factor",0.9996012717],PARAMETER["Latitude_Of_Origin",49.0],UNIT["Meter",1.0]] \ No newline at end of file diff --git a/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_BSC/ICB_APR_2023_EN_BSC.shp b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_BSC/ICB_APR_2023_EN_BSC.shp new file mode 100644 index 00000000..dd348ab2 Binary files /dev/null and b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_BSC/ICB_APR_2023_EN_BSC.shp differ diff --git a/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_BSC/ICB_APR_2023_EN_BSC.shp.xml b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_BSC/ICB_APR_2023_EN_BSC.shp.xml new file mode 100644 index 00000000..77998fc3 --- /dev/null +++ b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_BSC/ICB_APR_2023_EN_BSC.shp.xml @@ -0,0 +1,2 @@ + +20230404165946001.0TRUE diff --git a/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_BSC/ICB_APR_2023_EN_BSC.shx b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_BSC/ICB_APR_2023_EN_BSC.shx new file mode 100644 index 00000000..551c7888 Binary files /dev/null and b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_BSC/ICB_APR_2023_EN_BSC.shx differ diff --git a/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_Detailed/ICB_APR_2023_EN_BFC.cpg b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_Detailed/ICB_APR_2023_EN_BFC.cpg new file mode 100644 index 00000000..3ad133c0 --- /dev/null +++ b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_Detailed/ICB_APR_2023_EN_BFC.cpg @@ -0,0 +1 @@ +UTF-8 \ No newline at end of file diff --git a/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_Detailed/ICB_APR_2023_EN_BFC.dbf b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_Detailed/ICB_APR_2023_EN_BFC.dbf new file mode 100644 index 00000000..8c5d97f4 Binary files /dev/null and b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_Detailed/ICB_APR_2023_EN_BFC.dbf differ diff --git a/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_Detailed/ICB_APR_2023_EN_BFC.prj b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_Detailed/ICB_APR_2023_EN_BFC.prj new file mode 100644 index 00000000..fec0ee28 --- /dev/null +++ b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_Detailed/ICB_APR_2023_EN_BFC.prj @@ -0,0 +1 @@ +PROJCS["British_National_Grid",GEOGCS["GCS_OSGB_1936",DATUM["D_OSGB_1936",SPHEROID["Airy_1830",6377563.396,299.3249646]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",400000.0],PARAMETER["False_Northing",-100000.0],PARAMETER["Central_Meridian",-2.0],PARAMETER["Scale_Factor",0.9996012717],PARAMETER["Latitude_Of_Origin",49.0],UNIT["Meter",1.0]] \ No newline at end of file diff --git a/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_Detailed/ICB_APR_2023_EN_BFC.shp b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_Detailed/ICB_APR_2023_EN_BFC.shp new file mode 100644 index 00000000..f3c9ad46 Binary files /dev/null and b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_Detailed/ICB_APR_2023_EN_BFC.shp differ diff --git a/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_Detailed/ICB_APR_2023_EN_BFC.shp.xml b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_Detailed/ICB_APR_2023_EN_BFC.shp.xml new file mode 100644 index 00000000..16c61ff9 --- /dev/null +++ b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_Detailed/ICB_APR_2023_EN_BFC.shp.xml @@ -0,0 +1,2 @@ + +20230331105926001.0TRUE diff --git a/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_Detailed/ICB_APR_2023_EN_BFC.shx b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_Detailed/ICB_APR_2023_EN_BFC.shx new file mode 100644 index 00000000..84e1716c Binary files /dev/null and b/epilepsy12/shape_files/Integrated_Care_Boards_April_2023_EN_Detailed/ICB_APR_2023_EN_BFC.shx differ diff --git a/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BFC_2022_Detailed/LHB_APR_2022_WA_BFC.cpg b/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BFC_2022_Detailed/LHB_APR_2022_WA_BFC.cpg new file mode 100644 index 00000000..3ad133c0 --- /dev/null +++ b/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BFC_2022_Detailed/LHB_APR_2022_WA_BFC.cpg @@ -0,0 +1 @@ +UTF-8 \ No newline at end of file diff --git a/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BFC_2022_Detailed/LHB_APR_2022_WA_BFC.dbf b/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BFC_2022_Detailed/LHB_APR_2022_WA_BFC.dbf new file mode 100644 index 00000000..36b5784d Binary files /dev/null and b/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BFC_2022_Detailed/LHB_APR_2022_WA_BFC.dbf differ diff --git a/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BFC_2022_Detailed/LHB_APR_2022_WA_BFC.prj b/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BFC_2022_Detailed/LHB_APR_2022_WA_BFC.prj new file mode 100644 index 00000000..fec0ee28 --- /dev/null +++ b/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BFC_2022_Detailed/LHB_APR_2022_WA_BFC.prj @@ -0,0 +1 @@ +PROJCS["British_National_Grid",GEOGCS["GCS_OSGB_1936",DATUM["D_OSGB_1936",SPHEROID["Airy_1830",6377563.396,299.3249646]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",400000.0],PARAMETER["False_Northing",-100000.0],PARAMETER["Central_Meridian",-2.0],PARAMETER["Scale_Factor",0.9996012717],PARAMETER["Latitude_Of_Origin",49.0],UNIT["Meter",1.0]] \ No newline at end of file diff --git a/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BFC_2022_Detailed/LHB_APR_2022_WA_BFC.shp b/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BFC_2022_Detailed/LHB_APR_2022_WA_BFC.shp new file mode 100644 index 00000000..7336b6bd Binary files /dev/null and b/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BFC_2022_Detailed/LHB_APR_2022_WA_BFC.shp differ diff --git a/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BFC_2022_Detailed/LHB_APR_2022_WA_BFC.shx b/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BFC_2022_Detailed/LHB_APR_2022_WA_BFC.shx new file mode 100644 index 00000000..64fc8cf6 Binary files /dev/null and b/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BFC_2022_Detailed/LHB_APR_2022_WA_BFC.shx differ diff --git a/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BUC_2022/LHB_APR_2022_WA_BUC.cpg b/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BUC_2022/LHB_APR_2022_WA_BUC.cpg new file mode 100644 index 00000000..3ad133c0 --- /dev/null +++ b/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BUC_2022/LHB_APR_2022_WA_BUC.cpg @@ -0,0 +1 @@ +UTF-8 \ No newline at end of file diff --git a/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BUC_2022/LHB_APR_2022_WA_BUC.dbf b/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BUC_2022/LHB_APR_2022_WA_BUC.dbf new file mode 100644 index 00000000..444b6ab9 Binary files /dev/null and b/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BUC_2022/LHB_APR_2022_WA_BUC.dbf differ diff --git a/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BUC_2022/LHB_APR_2022_WA_BUC.prj b/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BUC_2022/LHB_APR_2022_WA_BUC.prj new file mode 100644 index 00000000..fec0ee28 --- /dev/null +++ b/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BUC_2022/LHB_APR_2022_WA_BUC.prj @@ -0,0 +1 @@ +PROJCS["British_National_Grid",GEOGCS["GCS_OSGB_1936",DATUM["D_OSGB_1936",SPHEROID["Airy_1830",6377563.396,299.3249646]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",400000.0],PARAMETER["False_Northing",-100000.0],PARAMETER["Central_Meridian",-2.0],PARAMETER["Scale_Factor",0.9996012717],PARAMETER["Latitude_Of_Origin",49.0],UNIT["Meter",1.0]] \ No newline at end of file diff --git a/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BUC_2022/LHB_APR_2022_WA_BUC.shp b/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BUC_2022/LHB_APR_2022_WA_BUC.shp new file mode 100644 index 00000000..ba1c9b9a Binary files /dev/null and b/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BUC_2022/LHB_APR_2022_WA_BUC.shp differ diff --git a/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BUC_2022/LHB_APR_2022_WA_BUC.shx b/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BUC_2022/LHB_APR_2022_WA_BUC.shx new file mode 100644 index 00000000..4f105b10 Binary files /dev/null and b/epilepsy12/shape_files/Local_Health_Boards_April_2022_WA_BUC_2022/LHB_APR_2022_WA_BUC.shx differ diff --git a/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.GSS_CODE.atx b/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.GSS_CODE.atx new file mode 100644 index 00000000..986e80bc Binary files /dev/null and b/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.GSS_CODE.atx differ diff --git a/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.NAME.atx b/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.NAME.atx new file mode 100644 index 00000000..37a1b1eb Binary files /dev/null and b/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.NAME.atx differ diff --git a/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.dbf b/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.dbf new file mode 100644 index 00000000..fc90153f Binary files /dev/null and b/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.dbf differ diff --git a/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.prj b/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.prj new file mode 100644 index 00000000..db3d2714 --- /dev/null +++ b/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.prj @@ -0,0 +1 @@ +PROJCS["British_National_Grid",GEOGCS["GCS_OSGB_1936",DATUM["D_OSGB_1936",SPHEROID["Airy_1830",6377563.396,299.3249646]],PRIMEM["Greenwich",0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",400000],PARAMETER["False_Northing",-100000],PARAMETER["Central_Meridian",-2],PARAMETER["Scale_Factor",0.999601272],PARAMETER["Latitude_Of_Origin",49],UNIT["Meter",1]] \ No newline at end of file diff --git a/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.sbn b/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.sbn new file mode 100644 index 00000000..620ff13b Binary files /dev/null and b/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.sbn differ diff --git a/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.sbx b/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.sbx new file mode 100644 index 00000000..0a35b9c3 Binary files /dev/null and b/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.sbx differ diff --git a/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.shp b/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.shp new file mode 100644 index 00000000..6a066eec Binary files /dev/null and b/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.shp differ diff --git a/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.shp.xml b/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.shp.xml new file mode 100644 index 00000000..7c764c70 --- /dev/null +++ b/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.shp.xml @@ -0,0 +1,2 @@ + +20131017122722001.0FGDC CSDGM MetadataTRUEAddIndex Q:\Teams\GIS&I\GIS\Processing\OrdnanceSurvey\BoundaryLine_2013_10\ESRI\London\London_Borough_Excluding_MHW.shp NAME # UNIQUE ASCENDINGAddIndex Q:\Teams\GIS&I\GIS\Processing\OrdnanceSurvey\BoundaryLine_2013_10\ESRI\London\London_Borough_Excluding_MHW.shp GSS_CODE # UNIQUE ASCENDINGAddSpatialIndex Q:\Teams\GIS&I\GIS\Processing\OrdnanceSurvey\BoundaryLine_2013_10\ESRI\London\London_Borough_Excluding_MHW.shp 0 0 0 diff --git a/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.shx b/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.shx new file mode 100644 index 00000000..b4d98c55 Binary files /dev/null and b/epilepsy12/shape_files/London_Boroughs/London_Borough_Excluding_MHW.shx differ diff --git a/epilepsy12/shape_files/NHS_England_Regions_April_2020_FEB_EN_2022/NHS_England_Regions_April_2020_FEB_EN.cpg b/epilepsy12/shape_files/NHS_England_Regions_April_2020_FEB_EN_2022/NHS_England_Regions_April_2020_FEB_EN.cpg new file mode 100644 index 00000000..3ad133c0 --- /dev/null +++ b/epilepsy12/shape_files/NHS_England_Regions_April_2020_FEB_EN_2022/NHS_England_Regions_April_2020_FEB_EN.cpg @@ -0,0 +1 @@ +UTF-8 \ No newline at end of file diff --git a/epilepsy12/shape_files/NHS_England_Regions_April_2020_FEB_EN_2022/NHS_England_Regions_April_2020_FEB_EN.dbf b/epilepsy12/shape_files/NHS_England_Regions_April_2020_FEB_EN_2022/NHS_England_Regions_April_2020_FEB_EN.dbf new file mode 100644 index 00000000..d55ec787 Binary files /dev/null and b/epilepsy12/shape_files/NHS_England_Regions_April_2020_FEB_EN_2022/NHS_England_Regions_April_2020_FEB_EN.dbf differ diff --git a/epilepsy12/shape_files/NHS_England_Regions_April_2020_FEB_EN_2022/NHS_England_Regions_April_2020_FEB_EN.prj b/epilepsy12/shape_files/NHS_England_Regions_April_2020_FEB_EN_2022/NHS_England_Regions_April_2020_FEB_EN.prj new file mode 100644 index 00000000..fec0ee28 --- /dev/null +++ b/epilepsy12/shape_files/NHS_England_Regions_April_2020_FEB_EN_2022/NHS_England_Regions_April_2020_FEB_EN.prj @@ -0,0 +1 @@ +PROJCS["British_National_Grid",GEOGCS["GCS_OSGB_1936",DATUM["D_OSGB_1936",SPHEROID["Airy_1830",6377563.396,299.3249646]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",400000.0],PARAMETER["False_Northing",-100000.0],PARAMETER["Central_Meridian",-2.0],PARAMETER["Scale_Factor",0.9996012717],PARAMETER["Latitude_Of_Origin",49.0],UNIT["Meter",1.0]] \ No newline at end of file diff --git a/epilepsy12/shape_files/NHS_England_Regions_April_2020_FEB_EN_2022/NHS_England_Regions_April_2020_FEB_EN.shp b/epilepsy12/shape_files/NHS_England_Regions_April_2020_FEB_EN_2022/NHS_England_Regions_April_2020_FEB_EN.shp new file mode 100644 index 00000000..b8142b06 Binary files /dev/null and b/epilepsy12/shape_files/NHS_England_Regions_April_2020_FEB_EN_2022/NHS_England_Regions_April_2020_FEB_EN.shp differ diff --git a/epilepsy12/shape_files/NHS_England_Regions_April_2020_FEB_EN_2022/NHS_England_Regions_April_2020_FEB_EN.shp.xml b/epilepsy12/shape_files/NHS_England_Regions_April_2020_FEB_EN_2022/NHS_England_Regions_April_2020_FEB_EN.shp.xml new file mode 100644 index 00000000..1c618d3b --- /dev/null +++ b/epilepsy12/shape_files/NHS_England_Regions_April_2020_FEB_EN_2022/NHS_England_Regions_April_2020_FEB_EN.shp.xml @@ -0,0 +1,2 @@ + +20230322114408001.0TRUE diff --git a/epilepsy12/shape_files/NHS_England_Regions_April_2020_FEB_EN_2022/NHS_England_Regions_April_2020_FEB_EN.shx b/epilepsy12/shape_files/NHS_England_Regions_April_2020_FEB_EN_2022/NHS_England_Regions_April_2020_FEB_EN.shx new file mode 100644 index 00000000..8afb2043 Binary files /dev/null and b/epilepsy12/shape_files/NHS_England_Regions_April_2020_FEB_EN_2022/NHS_England_Regions_April_2020_FEB_EN.shx differ diff --git a/epilepsy12/shape_files/NHS_England_Regions_July_2022_EN_BUC_2022/NHSER_JUL_2022_EN_BUC.cpg b/epilepsy12/shape_files/NHS_England_Regions_July_2022_EN_BUC_2022/NHSER_JUL_2022_EN_BUC.cpg new file mode 100644 index 00000000..3ad133c0 --- /dev/null +++ b/epilepsy12/shape_files/NHS_England_Regions_July_2022_EN_BUC_2022/NHSER_JUL_2022_EN_BUC.cpg @@ -0,0 +1 @@ +UTF-8 \ No newline at end of file diff --git a/epilepsy12/shape_files/NHS_England_Regions_July_2022_EN_BUC_2022/NHSER_JUL_2022_EN_BUC.dbf b/epilepsy12/shape_files/NHS_England_Regions_July_2022_EN_BUC_2022/NHSER_JUL_2022_EN_BUC.dbf new file mode 100644 index 00000000..80a8e7a9 Binary files /dev/null and b/epilepsy12/shape_files/NHS_England_Regions_July_2022_EN_BUC_2022/NHSER_JUL_2022_EN_BUC.dbf differ diff --git a/epilepsy12/shape_files/NHS_England_Regions_July_2022_EN_BUC_2022/NHSER_JUL_2022_EN_BUC.prj b/epilepsy12/shape_files/NHS_England_Regions_July_2022_EN_BUC_2022/NHSER_JUL_2022_EN_BUC.prj new file mode 100644 index 00000000..fec0ee28 --- /dev/null +++ b/epilepsy12/shape_files/NHS_England_Regions_July_2022_EN_BUC_2022/NHSER_JUL_2022_EN_BUC.prj @@ -0,0 +1 @@ +PROJCS["British_National_Grid",GEOGCS["GCS_OSGB_1936",DATUM["D_OSGB_1936",SPHEROID["Airy_1830",6377563.396,299.3249646]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",400000.0],PARAMETER["False_Northing",-100000.0],PARAMETER["Central_Meridian",-2.0],PARAMETER["Scale_Factor",0.9996012717],PARAMETER["Latitude_Of_Origin",49.0],UNIT["Meter",1.0]] \ No newline at end of file diff --git a/epilepsy12/shape_files/NHS_England_Regions_July_2022_EN_BUC_2022/NHSER_JUL_2022_EN_BUC.shp b/epilepsy12/shape_files/NHS_England_Regions_July_2022_EN_BUC_2022/NHSER_JUL_2022_EN_BUC.shp new file mode 100644 index 00000000..fd82e075 Binary files /dev/null and b/epilepsy12/shape_files/NHS_England_Regions_July_2022_EN_BUC_2022/NHSER_JUL_2022_EN_BUC.shp differ diff --git a/epilepsy12/shape_files/NHS_England_Regions_July_2022_EN_BUC_2022/NHSER_JUL_2022_EN_BUC.shx b/epilepsy12/shape_files/NHS_England_Regions_July_2022_EN_BUC_2022/NHSER_JUL_2022_EN_BUC.shx new file mode 100644 index 00000000..cb1e4e6a Binary files /dev/null and b/epilepsy12/shape_files/NHS_England_Regions_July_2022_EN_BUC_2022/NHSER_JUL_2022_EN_BUC.shx differ diff --git a/epilepsy12/tasks.py b/epilepsy12/tasks.py new file mode 100644 index 00000000..d4dd0ba5 --- /dev/null +++ b/epilepsy12/tasks.py @@ -0,0 +1,184 @@ +from typing import Literal + +from .models import Organisation, KPIAggregation +from .common_view_functions import ( + all_registered_cases_for_cohort_and_abstraction_level, + aggregate_all_eligible_kpi_fields, +) +from .general_functions import get_current_cohort_data + + +def aggregate_kpis_for_each_level_of_abstraction_by_organisation_asynchronously( + organisation_id: str, kpi_measure=None, open_access: bool = False +): + """ + Reporting function. + Selects children according to each level of abstraction ('organisation', 'trust', 'icb', 'open_uk', 'nhs_region', 'country', 'national') + The KPIs for each child are then aggregated and results persisted + """ + + # get the latest cohort + cohort_data = get_current_cohort_data() + + # get the organisation instance + organisation = Organisation.objects.get(pk=organisation_id) + + # get all children by level of abstraction + all_scored_completed_cases_in_current_cohort_by_organisation = ( + all_registered_cases_for_cohort_and_abstraction_level( + organisation_instance=organisation, + cohort=cohort_data["cohort"], + case_complete=True, + abstraction_level="organisation", + ) + ) + + all_scored_completed_cases_in_current_cohort_by_trust_or_lhb = ( + all_registered_cases_for_cohort_and_abstraction_level( + organisation_instance=organisation, + cohort=cohort_data["cohort"], + case_complete=True, + abstraction_level="trust", + ) + ) + + all_scored_completed_cases_in_current_cohort_by_icb = ( + all_registered_cases_for_cohort_and_abstraction_level( + organisation_instance=organisation, + cohort=cohort_data["cohort"], + case_complete=True, + abstraction_level="icb", + ) + ) + + all_scored_completed_cases_in_current_cohort_by_nhs_region = ( + all_registered_cases_for_cohort_and_abstraction_level( + organisation_instance=organisation, + cohort=cohort_data["cohort"], + case_complete=True, + abstraction_level="nhs_region", + ) + ) + + all_scored_completed_cases_in_current_cohort_by_open_uk_region = ( + all_registered_cases_for_cohort_and_abstraction_level( + organisation_instance=organisation, + cohort=cohort_data["cohort"], + case_complete=True, + abstraction_level="open_uk", + ) + ) + + all_scored_completed_cases_in_current_cohort_by_country = ( + all_registered_cases_for_cohort_and_abstraction_level( + organisation_instance=organisation, + cohort=cohort_data["cohort"], + case_complete=True, + abstraction_level="country", + ) + ) + + all_scored_completed_cases_in_current_cohort_by_national = ( + all_registered_cases_for_cohort_and_abstraction_level( + organisation_instance=organisation, + cohort=cohort_data["cohort"], + case_complete=True, + abstraction_level="national", + ) + ) + + # aggregate the kpis by level of abstraction and persist the results in a table + organisation_kpis = aggregate_all_eligible_kpi_fields( + filtered_cases=all_scored_completed_cases_in_current_cohort_by_organisation, + kpi_measure=kpi_measure, + ) + trust_kpis = aggregate_all_eligible_kpi_fields( + filtered_cases=all_scored_completed_cases_in_current_cohort_by_trust_or_lhb, + kpi_measure=kpi_measure, + ) + icb_kpis = aggregate_all_eligible_kpi_fields( + filtered_cases=all_scored_completed_cases_in_current_cohort_by_icb, + kpi_measure=kpi_measure, + ) + nhs_kpis = aggregate_all_eligible_kpi_fields( + filtered_cases=all_scored_completed_cases_in_current_cohort_by_nhs_region, + kpi_measure=kpi_measure, + ) + open_uk_kpis = aggregate_all_eligible_kpi_fields( + filtered_cases=all_scored_completed_cases_in_current_cohort_by_open_uk_region, + kpi_measure=kpi_measure, + ) + country_kpis = aggregate_all_eligible_kpi_fields( + filtered_cases=all_scored_completed_cases_in_current_cohort_by_country, + kpi_measure=kpi_measure, + ) + national_kpis = aggregate_all_eligible_kpi_fields( + filtered_cases=all_scored_completed_cases_in_current_cohort_by_national, + kpi_measure=kpi_measure, + ) + + # update each aggregated object with the open_access flag + organisation_kpis.update( + {"abstraction_level": "organisation", "open_access": open_access} + ) + trust_kpis.update({"abstraction_level": "trust", "open_access": open_access}) + icb_kpis.update({"abstraction_level": "icb", "open_access": open_access}) + nhs_kpis.update({"abstraction_level": "nhs_region", "open_access": open_access}) + open_uk_kpis.update({"abstraction_level": "open_uk", "open_access": open_access}) + country_kpis.update({"abstraction_level": "country", "open_access": open_access}) + national_kpis.update({"abstraction_level": "national", "open_access": open_access}) + + # store the results in KPIAggregation model + persist_aggregation_results_for_abstraction_level( + organisation_id=organisation_id, + results=organisation_kpis, + abstraction_level="organisation", + ) + persist_aggregation_results_for_abstraction_level( + organisation_id=organisation_id, results=trust_kpis, abstraction_level="trust" + ) + persist_aggregation_results_for_abstraction_level( + organisation_id=organisation_id, results=icb_kpis, abstraction_level="icb" + ) + persist_aggregation_results_for_abstraction_level( + organisation_id=organisation_id, + results=nhs_kpis, + abstraction_level="nhs_region", + ) + persist_aggregation_results_for_abstraction_level( + organisation_id=organisation_id, + results=open_uk_kpis, + abstraction_level="open_uk", + ) + persist_aggregation_results_for_abstraction_level( + organisation_id=organisation_id, + results=country_kpis, + abstraction_level="country", + ) + persist_aggregation_results_for_abstraction_level( + organisation_id=organisation_id, + results=national_kpis, + abstraction_level="national", + ) + + +def persist_aggregation_results_for_abstraction_level( + organisation_id: str, + results: dict, + abstraction_level: Literal[ + "organisation", "trust", "icb", "nhs_region", "open_uk", "country", "national" + ] = "organisation", +): + """ + Private function to store the aggregation results in KPI_Aggregation results table + """ + organisation = Organisation.objects.get(pk=organisation_id) + if KPIAggregation.objects.filter( + organisation=organisation, abstraction_level=abstraction_level + ).exists(): + KPIAggregation.objects.filter( + organisation=organisation, abstraction_level=abstraction_level + ).update(**results) + else: + results.update({"organisation": organisation}) + KPIAggregation.objects.create(**results) diff --git a/epilepsy12/templatetags/epilepsy12_template_tags.py b/epilepsy12/templatetags/epilepsy12_template_tags.py index 6ee7404c..a30478f7 100644 --- a/epilepsy12/templatetags/epilepsy12_template_tags.py +++ b/epilepsy12/templatetags/epilepsy12_template_tags.py @@ -1,4 +1,5 @@ import re +import math from django import template from django.utils.safestring import mark_safe @@ -128,6 +129,21 @@ def matches_model_field(field_name, model): return False +@register.simple_tag +def wait_days_and_weeks(day_number): + if day_number is None: + return "" + if day_number < 7: + return f"{day_number} days" + else: + weeks = math.floor(day_number / 7) + remaining_days = day_number - (weeks * 7) + if remaining_days > 0: + return f"{weeks} weeks, {remaining_days} days" + else: + return f"{weeks} weeks" + + @register.filter def snomed_concept(concept_id): if concept_id is None: diff --git a/epilepsy12/tests/UserDataClasses.py b/epilepsy12/tests/UserDataClasses.py new file mode 100644 index 00000000..19121752 --- /dev/null +++ b/epilepsy12/tests/UserDataClasses.py @@ -0,0 +1,81 @@ +""" +Set up dataclasses for E12 User Test Fixtures +""" + +# Standard Imports +from dataclasses import dataclass +from epilepsy12.common_view_functions import group_for_role + +# RCPCH Imports +from epilepsy12.constants.user_types import ( + AUDIT_CENTRE_ADMINISTRATOR, + AUDIT_CENTRE_CLINICIAN, + AUDIT_CENTRE_LEAD_CLINICIAN, + RCPCH_AUDIT_TEAM, + TRUST_AUDIT_TEAM_VIEW_ONLY, + TRUST_AUDIT_TEAM_EDIT_ACCESS, + TRUST_AUDIT_TEAM_FULL_ACCESS, + EPILEPSY12_AUDIT_TEAM_FULL_ACCESS, +) + + +@dataclass +class TestUser: + role: int + role_str: str + is_clinical_audit_team: bool = False + is_active: bool = False + is_staff: bool = False + is_rcpch_audit_team_member: bool = False + is_rcpch_staff: bool = False + + @property + def group_name(self): + return group_for_role(self.role) + + +test_user_audit_centre_administrator_data = TestUser( + role=AUDIT_CENTRE_ADMINISTRATOR, + is_active=True, + is_staff=False, + role_str="AUDIT_CENTRE_ADMINISTRATOR", + is_rcpch_audit_team_member=False, + is_rcpch_staff=False, +) + +test_user_audit_centre_clinician_data = TestUser( + role=AUDIT_CENTRE_CLINICIAN, + role_str="AUDIT_CENTRE_CLINICIAN", + is_active=True, + is_staff=False, + is_rcpch_audit_team_member=False, + is_rcpch_staff=False, +) + +test_user_audit_centre_lead_clinician_data = TestUser( + role=AUDIT_CENTRE_LEAD_CLINICIAN, + role_str="AUDIT_CENTRE_LEAD_CLINICIAN", + is_active=True, + is_staff=False, + is_rcpch_audit_team_member=False, + is_rcpch_staff=False, +) + +test_user_clinicial_audit_team_data = TestUser( + role=AUDIT_CENTRE_LEAD_CLINICIAN, + role_str="CLINICAL_AUDIT_TEAM", + is_active=True, + is_staff=False, + is_clinical_audit_team=True, + is_rcpch_audit_team_member=True, + is_rcpch_staff=False, +) + +test_user_rcpch_audit_team_data = TestUser( + role=RCPCH_AUDIT_TEAM, + role_str="RCPCH_AUDIT_TEAM", + is_active=True, + is_staff=False, + is_rcpch_audit_team_member=True, + is_rcpch_staff=True, +) diff --git a/epilepsy12/tests/__init__.py b/epilepsy12/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/epilepsy12/tests/common_view_functions_tests/CreateKPIMetrics.py b/epilepsy12/tests/common_view_functions_tests/CreateKPIMetrics.py new file mode 100644 index 00000000..bb8f84f4 --- /dev/null +++ b/epilepsy12/tests/common_view_functions_tests/CreateKPIMetrics.py @@ -0,0 +1,451 @@ +"""Helper class for creating an answer set for E12CaseFactory constructor. See KPIMetric Class docstrings for details. + +Details on what fields are being set inside factories according to KPI PASS | FAIL | INELIGIBLE: + +- KPI 1 + - PASS: + registration__assessment__pass_paediatrician_with_expertise_in_epilepsies=True + - FAIL: + registration__assessment__fail_paediatrician_with_expertise_in_epilepsies=True + +- KPI 2 + - PASS: + registration__assessment__pass_epilepsy_specialist_nurse=True + - FAIL: + registration__assessment__fail_epilepsy_specialist_nurse=True + +- KPI 3 + 5 ELIGIBILITY FLAG +> NOTE: Due to eligibility reasons, KPI3+5 eligible only when age is 1y (<2y), KPI 6+8+10 only eligible when age is 12y(>=12y). + eligible_kpi_3_5_ineligible_6_8_10=True, + +- KPI 3 & 3b > NOTE: IF either PASS/FAIL flags set here, eligible_kpi_3_5_ineligible_6_8_10 must be used! + - PASS: + registration__assessment__pass_tertiary_input_AND_epilepsy_surgery_referral=True, + - FAIL: + registration__assessment__fail_tertiary_input_AND_epilepsy_surgery_referral=True, + +- KPI 4 + - PASS: + registration__epilepsy_context__pass_ecg=True, + registration__investigations__pass_ecg=True, + - FAIL: + registration__epilepsy_context__fail_ecg=True, + registration__investigations__fail_ecg=True, + - INELIGIBLE: + registration__epilepsy_context__ineligible_ecg=True, + +- KPI 5 > NOTE: IF either PASS/FAIL flags set here, eligible_kpi_3_5_ineligible_6_8_10 must be used! + - PASS: + registration__investigations__pass_mri=True, + - FAIL: + registration__investigations__fail_mri=True, + +- KPI 6 + > NOTE: If either PASS/FAIL flags set here, must use flag eligible_kpi_6_8_10_ineligible_3_5. + - PASS: + pass_assessment_of_mental_health_issues=True, + registration__pass_assessment_of_mental_health_issues=True, + registration__multiaxial_diagnosis__pass_assessment_of_mental_health_issues=True, + - FAIL: + fail_assessment_of_mental_health_issues=True, + registration__fail_assessment_of_mental_health_issues=True, + registration__multiaxial_diagnosis__fail_assessment_of_mental_health_issues=True, + +- KPI 7 + - PASS: + registration__multiaxial_diagnosis__pass_mental_health_support=True, + registration__management__pass_mental_health_support=True, + - FAIL: + registration__multiaxial_diagnosis__fail_mental_health_support=True, + registration__management__fail_mental_health_support=True, + - INELIGIBLE: + registration__multiaxial_diagnosis__ineligible_mental_health_support=True, + +- KPI 8 + > NOTE: If either PASS/FAIL flags set here, must use flag eligible_kpi_6_8_10_ineligible_3_5. + - PASS: + pass_sodium_valproate = True, + registration__management__sodium_valproate = 'pass', + - FAIL: + fail_sodium_valproate = True, + registration__management__sodium_valproate = 'fail', + +- KPI 9 +> NOTE: as these sub-measures are all related, and flags are applied sequentially, we can only set ALL to pass or ALL to fail. + - PASS + registration__management__pass_kpi_9=True, + - FAIL + > NOTE: if fail flag set, parental_prolonged_seizures_care_plan is set to ineligible (due to is_rescue_medicine_prescribed=False) + registration__management__fail_kpi_9=True, + +- KPI 10 +> NOTE: NOTE: If either PASS/FAIL flags set here, must use flag eligible_kpi_6_8_10_ineligible_3_5. + - PASS + pass_school_individual_healthcare_plan = True, + registration__management__pass_school_individual_healthcare_plan=True, + - FAIL + fail_school_individual_healthcare_plan = True, + registration__management__fail_school_individual_healthcare_plan=True, +""" + +from dataclasses import dataclass +from typing import Literal + + +class KPIMetric: + """ + Helper Class to feed in valid KPI PASS/FAIL/INELIGIBLE metrics to the e12_case_factory. + + Returns a dictionary of keyword arguments which can be passed into the E12CaseFactory constructor to produce a completed audit according to provided values. + + Usage: + + 1. Instantiate a KPIMetric object, providing True/False values for BOTH `eligible_kpi_3_5` and `eligible_kpi_6_8_10`. One must be True, the other False. + + NOTE: this is because different age ranges determine eligibility of these 5 KPIs. To simplify the process of generating fake Cases, it was decided to condense the possibility space to just age=1yo (eligible for KPI 3 & 5), and age=12yo (eligible for KPI 6,8,10). + + 2. Use the .generate_metrics() method to return dictionary of keyword args. You must provide a value for each of the following KPIs, depending on whether `eligible_kpi_3_5` + `eligible_kpi_6_8_10` were True/False. + + NOTE: a KeyError is raised if the required KPI kwarg is not provided. + + `eligible_kpi_3_5=True`, kwargs and values to provide: + + - kpi_1 = 'PASS' OR 'FAIL' + - kpi_2 = 'PASS' OR 'FAIL' + - kpi_3 = 'PASS' OR 'FAIL' -> NOTE: eligibility is set in constructor. + - kpi_4 = 'PASS' OR 'FAIL' OR 'INELIGIBLE' + - kpi_5 = 'PASS' OR 'FAIL' -> NOTE: eligibility is set in constructor. + - kpi_7 = 'PASS' OR 'FAIL' OR 'INELIGIBLE' + - kpi_9 = 'PASS' OR 'FAIL' + + `eligible_kpi_6_8_10=True`, kwargs and values to provide: + + - kpi_1 = 'PASS' OR 'FAIL' + - kpi_2 = 'PASS' OR 'FAIL' + - kpi_4 = 'PASS' OR 'FAIL' OR 'INELIGIBLE' + - kpi_6 = 'PASS' OR 'FAIL' -> NOTE: eligibility is set in constructor. + - kpi_7 = 'PASS' OR 'FAIL' OR 'INELIGIBLE' + - kpi_8 = 'PASS' OR 'FAIL' -> NOTE: eligibility is set in constructor. + - kpi_9 = 'PASS' OR 'FAIL' + - kpi_10 = 'PASS' OR 'FAIL' -> NOTE: eligibility is set in constructor. + + Example usage: + + ```python + + metric_3_5_eligible = KPIMetric(eligible_kpi_3_5=True, eligible_kpi_6_8_10=False) + metric_kpi_6_8_10_eligible = KPIMetric(eligible_kpi_3_5=False, eligible_kpi_6_8_10=True) + + answer_set_1 = metric_3_5_eligible.generate_metrics( + kpi_1='PASS', + kpi_2='PASS', + kpi_3='PASS', + kpi_4='INELIGIBLE', + kpi_5='FAIL', + kpi_7='PASS', + kpi_9='PASS', + ) + answer_set_2 = metric_kpi_6_8_10_eligible.generate_metrics( + kpi_1='PASS', + kpi_2='PASS', + kpi_4='INELIGIBLE', + kpi_6='FAIL', + kpi_7='PASS', + kpi_8='PASS', + kpi_9='FAIL', + kpi_10='PASS', + ) + + case_1 = e12_case_factory(**answer_set_1) + case_2 = e12_case_factory(**answer_set_2) + """ + + def __init__(self, eligible_kpi_3_5: bool, eligible_kpi_6_8_10: bool): + if eligible_kpi_3_5 and eligible_kpi_6_8_10: + raise ValueError( + "Only one of the eligibility variables can be True. Currently both are set True." + ) + + self.eligible_kpi_3_5 = eligible_kpi_3_5 + self.eligible_kpi_6_8_10 = eligible_kpi_6_8_10 + + # Each of these Class properties define the FactoryBoy flags which need to be set inside the e12CaseFactory constructor for that KPI to be a PASS | FAIL | INVALID. + + @property + def PASS_KPI_1(self): + return { + "registration__assessment__pass_paediatrician_with_expertise_in_epilepsies": True, + } + + @property + def FAIL_KPI_1(self): + return { + "registration__assessment__fail_paediatrician_with_expertise_in_epilepsies": True + } + + @property + def PASS_KPI_2(self): + return { + "registration__assessment__pass_epilepsy_specialist_nurse": True, + } + + @property + def FAIL_KPI_2(self): + return { + "registration__assessment__fail_epilepsy_specialist_nurse": True, + } + + @property + def PASS_KPI_3(self): + return { + "registration__assessment__pass_tertiary_input_AND_epilepsy_surgery_referral": True, + } + + @property + def FAIL_KPI_3(self): + return { + "registration__assessment__fail_tertiary_input_AND_epilepsy_surgery_referral": True, + } + + @property + def PASS_KPI_4(self): + return { + "registration__epilepsy_context__pass_ecg": True, + "registration__investigations__pass_ecg": True, + } + + @property + def FAIL_KPI_4(self): + return { + "registration__epilepsy_context__fail_ecg": True, + "registration__investigations__fail_ecg": True, + } + + @property + def INELIGIBLE_KPI_4(self): + return { + "registration__epilepsy_context__ineligible_ecg": True, + } + + @property + def PASS_KPI_5(self): + return { + "registration__investigations__pass_mri": True, + } + + @property + def FAIL_KPI_5(self): + return { + "registration__investigations__fail_mri": True, + } + + @property + def PASS_KPI_6(self): + return { + "pass_assessment_of_mental_health_issues": True, + "registration__pass_assessment_of_mental_health_issues": True, + "registration__multiaxial_diagnosis__pass_assessment_of_mental_health_issues": True, + } + + @property + def FAIL_KPI_6(self): + return { + "fail_assessment_of_mental_health_issues": True, + "registration__fail_assessment_of_mental_health_issues": True, + "registration__multiaxial_diagnosis__fail_assessment_of_mental_health_issues": True, + } + + @property + def PASS_KPI_7(self): + return { + "registration__multiaxial_diagnosis__pass_mental_health_support": True, + "registration__management__pass_mental_health_support": True, + } + + @property + def FAIL_KPI_7(self): + return { + "registration__multiaxial_diagnosis__fail_mental_health_support": True, + "registration__management__fail_mental_health_support": True, + } + + @property + def INELIGIBLE_KPI_7(self): + return { + "registration__multiaxial_diagnosis__ineligible_mental_health_support": True, + } + + @property + def PASS_KPI_8(self): + return { + "pass_sodium_valproate": True, + "registration__management__sodium_valproate": "pass", + } + + @property + def FAIL_KPI_8(self): + return { + "fail_sodium_valproate": True, + "registration__management__sodium_valproate": "fail", + } + + @property + def PASS_KPI_9(self): + return { + "registration__management__pass_kpi_9": True, + } + + @property + def FAIL_KPI_9(self): + return { + "registration__management__fail_kpi_9": True, + } + + @property + def PASS_KPI_10(self): + return { + "pass_school_individual_healthcare_plan": True, + "registration__management__pass_school_individual_healthcare_plan": True, + } + + @property + def FAIL_KPI_10(self): + return { + "fail_school_individual_healthcare_plan": True, + "registration__management__fail_school_individual_healthcare_plan": True, + } + + def check_value_allowed(self, kpi: str, value: str, add_ineligible: bool = False): + """Raise value error if provided value not in list of available values.""" + available_values = ["PASS", "FAIL"] + if add_ineligible: + available_values += ["INELIGIBLE"] + if value not in available_values: + raise ValueError( + f"Incorrect value provided for {kpi}: {value}. Available values are {available_values}" + ) + + def generate_metrics(self, **kwargs: Literal["PASS", "FAIL"])->dict: + """ + Generate a dictionary of flags according to KPI metrics. + """ + + e12_case_factory_constructor_args = {} + + # Determine which age-dependent eligible KPI values can be used: + if self.eligible_kpi_3_5: + # add relevant fields age fields to return dict to set eligibility + e12_case_factory_constructor_args.update( + {"eligible_kpi_3_5_ineligible_6_8_10": True} + ) + + # NOT eligible for kpi 6,8,10 + for kpi in kwargs.keys(): + if kpi in ["kpi_6", "kpi_8", "kpi_10"]: + raise ValueError( + f"{self.eligible_kpi_3_5=} and {self.eligible_kpi_6_8_10=}. {kpi} can not be used as automatically set ineligible." + ) + + # KPI 3 FIELDS TO ADD + self.check_value_allowed(kpi="kpi_3", value=kwargs["kpi_3"]) + if kwargs["kpi_3"] == "PASS": + e12_case_factory_constructor_args.update(self.PASS_KPI_3) + else: + e12_case_factory_constructor_args.update(self.FAIL_KPI_3) + + # KPI 5 FIELDS TO ADD + self.check_value_allowed(kpi="kpi_5", value=kwargs["kpi_5"]) + if kwargs["kpi_5"] == "PASS": + e12_case_factory_constructor_args.update(self.PASS_KPI_5) + elif kwargs["kpi_5"] == "FAIL": + e12_case_factory_constructor_args.update(self.FAIL_KPI_5) + + # eligible_kpi_6_8_10=True + else: + # add relevant fields age fields to return dict to set eligibility + e12_case_factory_constructor_args.update( + { + "eligible_kpi_6_8_10_ineligible_3_5": True, + # extra ineligibility kpi 3 + "registration__assessment__ineligible_tertiary_input_AND_epilepsy_surgery_referral": True, + # extra ineligibility kpi5 + "registration__ineligible_mri": True, + "registration__multiaxial_diagnosis__ineligible_mri": True, + "registration__multiaxial_diagnosis__syndrome_entity__ineligible_mri": True, + } + ) + + # NOT eligible for 3,5 + for kpi in kwargs.keys(): + if kpi in ["kpi_3", "kpi_5"]: + raise ValueError( + f"{self.eligible_kpi_3_5=} and {self.eligible_kpi_6_8_10=}. {kpi} can not be used as automatically set ineligible." + ) + + # KPI 6 FIELDS TO ADD + self.check_value_allowed(kpi="kpi_6", value=kwargs["kpi_6"]) + if kwargs["kpi_6"] == "PASS": + e12_case_factory_constructor_args.update(self.PASS_KPI_6) + elif kwargs["kpi_6"] == "FAIL": + e12_case_factory_constructor_args.update(self.FAIL_KPI_6) + + # KPI 8 FIELDS TO ADD + self.check_value_allowed(kpi="kpi_8", value=kwargs["kpi_8"]) + if kwargs["kpi_8"] == "PASS": + e12_case_factory_constructor_args.update(self.PASS_KPI_8) + elif kwargs["kpi_8"] == "FAIL": + e12_case_factory_constructor_args.update(self.FAIL_KPI_8) + + # KPI 10 FIELDS TO ADD + self.check_value_allowed(kpi="kpi_10", value=kwargs["kpi_10"]) + if kwargs["kpi_10"] == "PASS": + e12_case_factory_constructor_args.update(self.PASS_KPI_10) + elif kwargs["kpi_10"] == "FAIL": + e12_case_factory_constructor_args.update(self.FAIL_KPI_10) + + # Go through and add non age-dependent fields + + # KPI 1 FIELDS TO ADD + self.check_value_allowed(kpi="kpi_1", value=kwargs["kpi_1"]) + if kwargs["kpi_1"] == "PASS": + e12_case_factory_constructor_args.update(self.PASS_KPI_1) + else: + e12_case_factory_constructor_args.update(self.FAIL_KPI_1) + + # KPI 2 FIELDS TO ADD + self.check_value_allowed(kpi="kpi_2", value=kwargs["kpi_2"]) + if kwargs["kpi_2"] == "PASS": + e12_case_factory_constructor_args.update(self.PASS_KPI_2) + else: + e12_case_factory_constructor_args.update(self.FAIL_KPI_2) + + # KPI 4 FIELDS TO ADD + self.check_value_allowed( + kpi="kpi_4", value=kwargs["kpi_4"], add_ineligible=True + ) + if kwargs["kpi_4"] == "PASS": + e12_case_factory_constructor_args.update(self.PASS_KPI_4) + elif kwargs["kpi_4"] == "FAIL": + e12_case_factory_constructor_args.update(self.FAIL_KPI_4) + else: + e12_case_factory_constructor_args.update(self.INELIGIBLE_KPI_4) + + # KPI 7 FIELDS TO ADD + self.check_value_allowed( + kpi="kpi_7", value=kwargs["kpi_7"], add_ineligible=True + ) + if kwargs["kpi_7"] == "PASS": + e12_case_factory_constructor_args.update(self.PASS_KPI_7) + elif kwargs["kpi_7"] == "FAIL": + e12_case_factory_constructor_args.update(self.FAIL_KPI_7) + else: + e12_case_factory_constructor_args.update(self.INELIGIBLE_KPI_7) + + # KPI 9 FIELDS TO ADD + self.check_value_allowed(kpi="kpi_9", value=kwargs["kpi_9"]) + if kwargs["kpi_9"] == "PASS": + e12_case_factory_constructor_args.update(self.PASS_KPI_9) + else: + e12_case_factory_constructor_args.update(self.FAIL_KPI_9) + + return e12_case_factory_constructor_args diff --git a/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_1.py b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_1.py new file mode 100644 index 00000000..f85507e7 --- /dev/null +++ b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_1.py @@ -0,0 +1,177 @@ +""" +Measure 1 - paediatrician_with_expertise_in_epilepsies +- [x] Measure 1 passed (registration.kpi.paediatrician_with_expertise_in_epilepsies = 1) are seen within 2 weeks of referral +registration_instance.assessment.epilepsy_specialist_nurse_input_date <= (registration_instance.assessment.epilepsy_specialist_nurse_referral_date + relativedelta(days=+14)) +- [x] Measure 1 failed (registration.assessment.paediatrician_with_expertise_in_epilepsies = 0) if paediatrician seen after two weeks from referral or not referred +- [x] Measure 1 None if incomplete (assessment.consultant_paediatrician_referral_date or assessment.consultant_paediatrician_input_date or assessment.consultant_paediatrician_referral_made is None) + +Test Measure 2 - % of children and young people with epilepsy, with input by a ‘consultant paediatrician with expertise in epilepsies’ within 2 weeks of initial referral + +Number of children and young people [diagnosed with epilepsy] at first year +AND ( + who had [input from a paediatrician with expertise in epilepsy] + OR + a [input from a paediatric neurologist] within 2 weeks of initial referral. (initial referral to mean first paediatric assessment) + ) +""" + +# Standard imports +import pytest +from datetime import date +from dateutil.relativedelta import relativedelta + +# Third party imports + +# RCPCH imports +from epilepsy12.common_view_functions import calculate_kpis +from epilepsy12.models import ( + Registration, + KPI, +) +from epilepsy12.constants import KPI_SCORE + + +@pytest.mark.django_db +def test_measure_1_should_pass_seen_paediatrician( + e12_case_factory, +): + """ + *PASS* + 1) consultant_paediatrician_referral_made = True + consultant_paediatrician_referral_date = registration date + consultant_paediatrician_input_date <= 14 days after referral + """ + + REFERRAL_DATE = date(2023, 1, 1) + INPUT_DATE = REFERRAL_DATE + relativedelta(days=14) + + # creates a case with all audit values filled + case = e12_case_factory( + registration__assessment__consultant_paediatrician_referral_made=True, + registration__assessment__consultant_paediatrician_referral_date=REFERRAL_DATE, + registration__assessment__consultant_paediatrician_input_date=INPUT_DATE, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + # ensure we get the updated database object, not the Python object + kpi_score = KPI.objects.get( + pk=registration.kpi.pk + ).paediatrician_with_expertise_in_epilepsies + + assert ( + kpi_score == KPI_SCORE["PASS"] + ), f"Patient saw a Paediatrician IN {INPUT_DATE - REFERRAL_DATE} after referral, but did not pass measure" + + +@pytest.mark.django_db +def test_measure_1_should_pass_seen_neurologist( + e12_case_factory, +): + """ + *PASS* + 2) paediatric_neurologist_referral_made = True + paediatric_neurologist_referral_date = registration date + paediatric_neurologist_input_date <= 14 days after referral + + """ + + REFERRAL_DATE = date(2023, 1, 1) + INPUT_DATE = REFERRAL_DATE + relativedelta(days=14) + + # creates a case with all audit values filled + case = e12_case_factory( + registration__assessment__paediatric_neurologist_referral_made=True, + registration__assessment__paediatric_neurologist_referral_date=REFERRAL_DATE, + registration__assessment__paediatric_neurologist_input_date=INPUT_DATE, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + # ensure we get the updated database object, not the Python object + kpi_score = KPI.objects.get( + pk=registration.kpi.pk + ).paediatrician_with_expertise_in_epilepsies + + assert ( + kpi_score == KPI_SCORE["PASS"] + ), f"Patient saw a Neurologist IN {INPUT_DATE - REFERRAL_DATE} after referral, but did not pass measure" + + +@pytest.mark.django_db +def test_measure_1_should_fail_not_seen_14_days_after_referral( + e12_case_factory, +): + """ + *FAIL* + 1) consultant_paediatrician_referral_made = True + consultant_paediatrician_referral_date = registration date + consultant_paediatrician_input_date > 14 days after referral + paediatric_neurologist_referral_made = True + paediatric_neurologist_referral_date = registration date + paediatric_neurologist_input_date > 14 days after referral + + """ + + REFERRAL_DATE = date(2023, 1, 1) + INPUT_DATE = REFERRAL_DATE + relativedelta(days=15) + + # creates a case with all audit values filled + case = e12_case_factory( + registration__assessment__consultant_paediatrician_referral_made=True, + registration__assessment__consultant_paediatrician_referral_date=REFERRAL_DATE, + registration__assessment__consultant_paediatrician_input_date=INPUT_DATE, + registration__assessment__paediatric_neurologist_referral_made=True, + registration__assessment__paediatric_neurologist_referral_date=REFERRAL_DATE, + registration__assessment__paediatric_neurologist_input_date=INPUT_DATE, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + # ensure we get the updated database object, not the Python object + kpi_score = KPI.objects.get( + pk=registration.kpi.pk + ).paediatrician_with_expertise_in_epilepsies + + assert ( + kpi_score == KPI_SCORE["FAIL"] + ), f"Patient did not see a Paediatrician/Neurologist within 14 days of referral (seen after {INPUT_DATE - REFERRAL_DATE}), but did not fail measure" + +@pytest.mark.django_db +def test_measure_1_should_fail_no_doctor_involved( + e12_case_factory, +): + """ + *FAIL* + 1) consultant_paediatrician_referral_made = False + paediatric_neurologist_referral_made = False + """ + + # creates a case with all audit values filled + case = e12_case_factory( + registration__assessment__consultant_paediatrician_referral_made=False, + registration__assessment__paediatric_neurologist_referral_made=False, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + # ensure we get the updated database object, not the Python object + kpi_score = KPI.objects.get( + pk=registration.kpi.pk + ).paediatrician_with_expertise_in_epilepsies + + assert ( + kpi_score == KPI_SCORE["FAIL"] + ), f"Patient did not see a Paediatrician/Neurologist, but did not fail measure" diff --git a/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_10.py b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_10.py new file mode 100644 index 00000000..ba7928b8 --- /dev/null +++ b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_10.py @@ -0,0 +1,72 @@ +""" +10 `school_individual_healthcare_plan` - Percentage of children and young people with epilepsy aged 5 years and above with evidence of a school individual healthcare plan by 1 year after first paediatric assessment. + +Number of children and young people aged 5 years and above diagnosed with epilepsy at first year AND with evidence of EHCP + +PASS: +- [x] management.individualised_care_plan_includes_ehcp = True +FAIL: +- [x] management.individualised_care_plan_includes_ehcp = False +INELIGIBLE: +- [x] age_at_first_paediatric_assessment >= 5 +""" +# Standard imports +from datetime import date +from dateutil.relativedelta import relativedelta + +# Third party imports +import pytest + +# RCPCH imports +from epilepsy12.common_view_functions import calculate_kpis +from epilepsy12.constants import KPI_SCORE +from epilepsy12.models import KPI, Registration + + +@pytest.mark.parametrize( + "age,individualised_care_plan_includes_ehcp, expected_score", + [ + (relativedelta(years=5), True, KPI_SCORE["PASS"]), + (relativedelta(years=5), False, KPI_SCORE["FAIL"]), + (relativedelta(years=4, months=11), False, KPI_SCORE["INELIGIBLE"]), + ], +) +@pytest.mark.django_db +def test_measure_10_school_individual_healthcare_plan( + e12_case_factory, + age, + individualised_care_plan_includes_ehcp, + expected_score, +): + REGISTRATION_DATE = date(2023, 1, 1) + DATE_OF_BIRTH = REGISTRATION_DATE - age + + # create case + case = e12_case_factory( + date_of_birth=DATE_OF_BIRTH, + registration__registration_date=REGISTRATION_DATE, + registration__management__individualised_care_plan_includes_ehcp=individualised_care_plan_includes_ehcp, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + # get KPI score + kpi_score = KPI.objects.get( + pk=registration.kpi.pk + ).school_individual_healthcare_plan + + if expected_score == KPI_SCORE["PASS"]: + assertion_message = ( + f"individualised_care_plan_includes_ehcp is True but not passing" + ) + elif expected_score == KPI_SCORE["FAIL"]: + assertion_message = ( + f"individualised_care_plan_includes_ehcp is False but not failing" + ) + else: + assertion_message = f"age 4y11mo (<5yo) but not scoring as ineligible" + + assert kpi_score == expected_score, assertion_message diff --git a/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_2.py b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_2.py new file mode 100644 index 00000000..0a858a27 --- /dev/null +++ b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_2.py @@ -0,0 +1,168 @@ +""" +Measure 2 `epilepsy_specialist_nurse` +- [x] Measure 2 passed (registration.kpi.epilepsy_specialist_nurse = 1) are seen in first year of care +registration_instance.assessment.epilepsy_specialist_nurse_input_date and registration_instance.assessment.epilepsy_specialist_nurse_referral_made are not None +- [x] Measure 2 failed (registration.assessment.paediatrician_with_expertise_in_epilepsies = 0) if epilepsy_specialist_nurse not seen after referral or not referred +- [x] Measure 2 None if incomplete (assessment.epilepsy_specialist_nurse_referral_made or assessment.epilepsy_specialist_nurse_input_date or assessment.epilepsy_specialist_nurse_referral_date is None) + +Test Measure 2 - % of children and young people with epilepsy, with input by epilepsy specialist nurse within the first year of care + +Number of children and young people [diagnosed with epilepsy] +AND +who had [referral AND input to an Epilepsy Specialist Nurse] by first year +""" + +# Standard imports +import pytest +from datetime import date +from dateutil.relativedelta import relativedelta + +# Third party imports + +# RCPCH imports +from epilepsy12.common_view_functions import calculate_kpis +from epilepsy12.models import ( + Registration, + KPI, +) +from epilepsy12.constants import KPI_SCORE + + + +@pytest.mark.parametrize( + "epilepsy_specialist_nurse_referral_made,epilepsy_specialist_nurse_referral_date,epilepsy_specialist_nurse_input_date, expected_score", + [ + (None, None, None, KPI_SCORE['NOT_SCORED']), + (True, None, None, KPI_SCORE['NOT_SCORED']), + (True, date(2023,1,1), None, KPI_SCORE['NOT_SCORED']), + ] +) +@pytest.mark.django_db +def test_measure_2_should_not_score( + e12_case_factory, + epilepsy_specialist_nurse_referral_made, + epilepsy_specialist_nurse_referral_date, + epilepsy_specialist_nurse_input_date, + expected_score +): + """ + *NOT_SCORED* + 1) ANY epilepsy_nurse field is none + """ + case = e12_case_factory( + registration__assessment__epilepsy_specialist_nurse_referral_made=epilepsy_specialist_nurse_referral_made, + registration__assessment__epilepsy_specialist_nurse_referral_date=epilepsy_specialist_nurse_referral_date, + registration__assessment__epilepsy_specialist_nurse_input_date=epilepsy_specialist_nurse_input_date + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + kpi_score = KPI.objects.get(pk=registration.kpi.pk).epilepsy_specialist_nurse + + assert ( + kpi_score == expected_score + ), f"{registration.assessment.epilepsy_specialist_nurse_referral_made = } but measure isn't `not scoring`" + + + +@pytest.mark.django_db +def test_measure_2_should_fail_no_referral( + e12_case_factory, +): + """ + *FAIL* + 1) kpi.epilepsy_specialist_nurse_referral_made = False + """ + case = e12_case_factory( + registration__assessment__epilepsy_specialist_nurse_referral_made=False + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + kpi_score = KPI.objects.get(pk=registration.kpi.pk).epilepsy_specialist_nurse + + assert ( + kpi_score == KPI_SCORE["FAIL"] + ), f"{registration.assessment.epilepsy_specialist_nurse_referral_made = } but measure is not failing" + + + +@pytest.mark.django_db +def test_measure_2_should_fail_not_seen_before_close_date( + e12_case_factory, +): + """ + *FAIL* + 2) kpi.epilepsy_specialist_nurse_referral_made = True + AND ( + kpi.epilepsy_specialist_nurse_referral_date > registration.registration_close_date + OR + kpi.epilepsy_specialist_nurse_input_date > registration.registration_close_date + ) + """ + REGISTRATION_DATE = date(2023, 1, 1) + AFTER_REGISTRATION_CLOSE_REFERRAL_DATE = REGISTRATION_DATE + relativedelta( + years=1, days=5 + ) + AFTER_REGISTRATION_CLOSE_INPUT_DATE = ( + AFTER_REGISTRATION_CLOSE_REFERRAL_DATE + relativedelta(days=5) + ) + + case = e12_case_factory( + registration__assessment__epilepsy_specialist_nurse_referral_made=True, + registration__registration_date=REGISTRATION_DATE, + registration__assessment__epilepsy_specialist_nurse_referral_date=AFTER_REGISTRATION_CLOSE_REFERRAL_DATE, + registration__assessment__epilepsy_specialist_nurse_input_date=AFTER_REGISTRATION_CLOSE_INPUT_DATE, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + kpi_score = KPI.objects.get(pk=registration.kpi.pk).epilepsy_specialist_nurse + + assert ( + kpi_score == KPI_SCORE["FAIL"] + ), f"Seen after close date ({registration.registration_date =}, {registration.registration_close_date =}, {AFTER_REGISTRATION_CLOSE_REFERRAL_DATE = }, {AFTER_REGISTRATION_CLOSE_INPUT_DATE = }) but measure is not failing" + + +@pytest.mark.django_db +def test_measure_2_should_pass_timely_referral( + e12_case_factory, +): + """ + *PASS* + 1) kpi.epilepsy_specialist_nurse_referral_made = True + AND + kpi.epilepsy_specialist_nurse_referral_date <= registration.registration_close_date + AND + kpi.epilepsy_specialist_nurse_input_date <= registration.registration_close_date + """ + REGISTRATION_DATE = date(2023, 1, 1) + PASSING_REFERRAL_DATE = REGISTRATION_DATE + relativedelta(days=5) + PASSING_INPUT_DATE = PASSING_REFERRAL_DATE + relativedelta(days=5) + + case = e12_case_factory( + registration__assessment__epilepsy_specialist_nurse_referral_made=True, + registration__registration_date=REGISTRATION_DATE, + registration__assessment__epilepsy_specialist_nurse_referral_date=PASSING_REFERRAL_DATE, + registration__assessment__epilepsy_specialist_nurse_input_date=PASSING_INPUT_DATE, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + kpi_score = KPI.objects.get(pk=registration.kpi.pk).epilepsy_specialist_nurse + + assert ( + kpi_score == KPI_SCORE["PASS"] + ), f"Seen by epilepsy nurse within {PASSING_INPUT_DATE - PASSING_REFERRAL_DATE} but measure is not passing" diff --git a/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_3.py b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_3.py new file mode 100644 index 00000000..d04c8004 --- /dev/null +++ b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_3.py @@ -0,0 +1,324 @@ +""" +Tests for Measure 3 `tertiary_input. + +Each test depends on whether child has been referred / seen by a neurologist OR epilepsy surgery OR both, so first each test parametrizes each of these cases. + +- [x] Measure 3 passed (registration.kpi.tertiary_input == 1) if age at first paediatric assessment is < 3 and seen by neurologist / epilepsy surgery/both ( where age_at_first_paediatric_assessment = relativedelta(registration_instance.registration_date,registration_instance.case.date_of_birth).years) +- [x] Measure 3 passed (registration.kpi.tertiary_input == 1) if child is on 3 or more AEMS (see lines 115-120 for query) and seen by neurologist / epilepsy surgery/both +- [x] Measure 3 passed (registration.kpi.tertiary_input == 1) if child is under 4 and has myoclonic epilepsy (lines 128-133) and seen by neurologist / epilepsy surgery/both +- [x] Measure 3 passed (registration.kpi.tertiary_input == 1) if child is eligible for epilepsy surgery (registration_instance.assessment.childrens_epilepsy_surgical_service_referral_criteria_met) and seen by neurologist / epilepsy surgery/both +- [x] Measure 3 failed (registration.kpi.tertiary_input == 0) if age at first paediatric assessment is < 3 and not seen by neurologist / epilepsy surgery/both ( where age_at_first_paediatric_assessment = relativedelta(registration_instance.registration_date,registration_instance.case.date_of_birth).years) +- [x] Measure 3 failed (registration.kpi.tertiary_input == 0) if child is on 3 or more AEMS (see lines 115-120 for query) and not seen by neurologist / epilepsy surgery/both +- [x] Measure 3 failed (registration.kpi.tertiary_input == 0) if child is under 4 and has myoclonic epilepsy (lines 128-133) and not seen by neurologist / epilepsy surgery/both +- [x] Measure 3 failed (registration.kpi.tertiary_input == 0) if child is eligible for epilepsy surgery (registration_instance.assessment.childrens_epilepsy_surgical_service_referral_criteria_met) and not seen by neurologist / epilepsy surgery/both +- [x] Measure 3 ineligible (registration.kpi.tertiary_input == 2) if age at first paediatric assessment is > 3 and not not on 3 or more drugs and not eligible for epilepsy surgery and not >4y with myoclonic epilepsy +Measure 3b +- [x] Measure 3b passed (registration.kp.epilepsy_surgery_referral ==1 ) if met criteria for surgery and evidence of referral or being seen (line 224) + +Test Measure 3 - % of children and young people meeting defined criteria for paediatric neurology referral, with input of tertiary care and/or CESS referral within the first year of care + +Numerator: "Number of children ([less than 3 years old at first assessment] AND [diagnosed with epilepsy] OR (number of children and young people diagnosed with epilepsy who had [3 or more maintenance AEDS] at first year) OR (Number of children less than 4 years old at first assessment with epilepsy AND myoclonic seizures) OR (number of children and young people diagnosed with epilepsy who met [CESS criteria] ) AND had [evidence of referral or involvement of a paediatric neurologist] OR [evidence of referral or involvement of CESS]" + +^above in English: + PASS IF ANY OF: + 1. (age <= 3yo at first assessment) AND (seen by neurologist / epilepsy surgery/both) + 2. ((age < 4yo) AND (myoclonic epilepsy)) AND (seen by neurologist / epilepsy surgery/both) + 3. (on >= 3 AEMS) AND (seen by neurologist / epilepsy surgery/both) + 4. (eligible for epilepsy surgery) AND (seen by neurologist / epilepsy surgery/both) + OR MORE SIMPLY: + If *criteria met* AND *referred/seen by neurologist / epilepsy surgery/both* +""" + +# Standard imports +import pytest +from datetime import date +from dateutil.relativedelta import relativedelta + +# Third party imports + +# RCPCH imports +from epilepsy12.common_view_functions import calculate_kpis +from epilepsy12.models import ( + Registration, + KPI, + AntiEpilepsyMedicine, + MedicineEntity, + Episode, +) +from epilepsy12.constants import ( + KPI_SCORE, + GENERALISED_SEIZURE_TYPE, +) + +# sets up paramtrization constant for running tests against seen neurologist/surgery/both/neither + +CASE_PARAM_NAMES = "PAEDIATRIC_NEUROLOGIST_REFERRAL_MADE, CHILDRENS_EPILEPSY_SURGICAL_SERVICE_REFERRAL_MADE, expected_kpi_score" + +CASE_PARAM_VALUES = [ + (True, True, KPI_SCORE["PASS"]), + (True, False, KPI_SCORE["PASS"]), + (False, True, KPI_SCORE["PASS"]), + (False, False, KPI_SCORE["FAIL"]), +] + + +@pytest.mark.parametrize( + CASE_PARAM_NAMES, + CASE_PARAM_VALUES, +) +@pytest.mark.django_db +def test_measure_3_age_3yo( + e12_case_factory, + PAEDIATRIC_NEUROLOGIST_REFERRAL_MADE, + CHILDRENS_EPILEPSY_SURGICAL_SERVICE_REFERRAL_MADE, + expected_kpi_score, +): + """ + *PASS* + 1) age at First Paediatric Assessment (FPA) is <= 3 && seen by BOTH neurologist / CESS + 1) age at First Paediatric Assessment (FPA) is <= 3 && seen by ONLY neurologist + 2) age at First Paediatric Assessment (FPA) is <= 3 && seen by ONLY epilepsy surgery + *FAIL* + 1) age at First Paediatric Assessment (FPA) is <= 3 && NOT seen by (neurologist OR epilepsy surgery) + """ + + # a child who is exactly 3 at registration_date (=FPA) + DATE_OF_BIRTH = date(2021, 1, 1) + REGISTRATION_DATE = DATE_OF_BIRTH + relativedelta(years=3) + + case = e12_case_factory( + date_of_birth=DATE_OF_BIRTH, + registration__registration_date=REGISTRATION_DATE, + registration__assessment__paediatric_neurologist_referral_made=PAEDIATRIC_NEUROLOGIST_REFERRAL_MADE, + registration__assessment__childrens_epilepsy_surgical_service_referral_made=CHILDRENS_EPILEPSY_SURGICAL_SERVICE_REFERRAL_MADE, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + kpi_score = KPI.objects.get(pk=registration.kpi.pk).tertiary_input + + assert kpi_score == expected_kpi_score, ( + f"Age at FPA is 3yo and seen by neurologist / surgery / both but did not pass measure" + if expected_kpi_score == KPI_SCORE["PASS"] + else f"Age at FPA is 3yo but not seen by neurologist / surgery and did not fail measure" + ) + + +@pytest.mark.parametrize( + CASE_PARAM_NAMES, + CASE_PARAM_VALUES, +) +@pytest.mark.django_db +def test_measure_3_3AEMs_seen( + e12_case_factory, + PAEDIATRIC_NEUROLOGIST_REFERRAL_MADE, + CHILDRENS_EPILEPSY_SURGICAL_SERVICE_REFERRAL_MADE, + expected_kpi_score, +): + """ + *PASS* + 1) child is on 3 or more AEMS && seen by BOTH neurologist / CESS + 1) child is on 3 or more AEMS && seen by ONLY neurologist + 2) child is on 3 or more AEMS && seen by ONLY epilepsy surgery + *FAIL* + 1) child is on 3 or more AEMS && NOT seen by (neurologist OR epilepsy surgery) + """ + + REGISTRATION_DATE = date( + 2023, 1, 1 + ) # explicit setting to ensure aems started before registration close date + + case = e12_case_factory( + registration__registration_date=REGISTRATION_DATE, + registration__assessment__paediatric_neurologist_referral_made=PAEDIATRIC_NEUROLOGIST_REFERRAL_MADE, + registration__assessment__childrens_epilepsy_surgical_service_referral_made=CHILDRENS_EPILEPSY_SURGICAL_SERVICE_REFERRAL_MADE, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + # create total of 3 AEMs related to this registration instance (already has 1 by default so only add 2) + aems_to_add = MedicineEntity.objects.filter( + medicine_name__in=["Zonisamide", "Vigabatrin"] + ) + for aem_to_add in aems_to_add: + new_aem = AntiEpilepsyMedicine.objects.create( + management=registration.management, + medicine_entity=aem_to_add, + is_rescue_medicine=False, + antiepilepsy_medicine_start_date=REGISTRATION_DATE + relativedelta(days=5), + ) + new_aem.save() + aems_count = AntiEpilepsyMedicine.objects.filter( + management=registration.management, + is_rescue_medicine=False, + antiepilepsy_medicine_start_date__lt=registration.registration_close_date, + ).count() + + calculate_kpis(registration_instance=registration) + + kpi_score = KPI.objects.get(pk=registration.kpi.pk).tertiary_input + + assert kpi_score == expected_kpi_score, ( + f"On >= 3 AEMS (n={aems_count}) and seen by neurologist / epilepsy surgery/both but did not pass measure" + if expected_kpi_score == KPI_SCORE["PASS"] + else f"On >= 3 AEMS (n={aems_count}) and not seen by neurologist / surgery and did not fail measure" + ) + + +@pytest.mark.parametrize( + CASE_PARAM_NAMES, + CASE_PARAM_VALUES, +) +@pytest.mark.django_db +def test_measure_3_lt_4yo_myoclonic_seen( + e12_case_factory, + e12_episode_factory, + PAEDIATRIC_NEUROLOGIST_REFERRAL_MADE, + CHILDRENS_EPILEPSY_SURGICAL_SERVICE_REFERRAL_MADE, + expected_kpi_score, +): + """ + *PASS* + 1) child is under 4 and has myoclonic epilepsy && seen by BOTH neurologist / CESS + 1) child is under 4 and has myoclonic epilepsy && seen by ONLY neurologist + 2) child is under 4 and has myoclonic epilepsy && seen by ONLY epilepsy surgery + *FAIL* + 1) child is under 4 and has myoclonic epilepsy && NOT seen by (neurologist OR epilepsy surgery) + """ + + # SET UP CONSTANTS + DATE_OF_BIRTH = date(2021, 1, 1) + REGISTRATION_DATE = DATE_OF_BIRTH + relativedelta( + years=3, months=11 + ) # a child who is 3y11m at registration_date (=FPA) + MYOCLONIC = GENERALISED_SEIZURE_TYPE[5][0] + + case = e12_case_factory( + date_of_birth=DATE_OF_BIRTH, + registration__registration_date=REGISTRATION_DATE, + registration__assessment__paediatric_neurologist_referral_made=PAEDIATRIC_NEUROLOGIST_REFERRAL_MADE, + registration__assessment__childrens_epilepsy_surgical_service_referral_made=CHILDRENS_EPILEPSY_SURGICAL_SERVICE_REFERRAL_MADE, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + # Assign a myoclonic episode + e12_episode_factory.create( + multiaxial_diagnosis=registration.multiaxialdiagnosis, + epileptic_seizure_onset_type_generalised=True, + epileptic_generalised_onset=MYOCLONIC, + ) + + # count myoclonic episodes attached to confirm + episodes = Episode.objects.filter( + multiaxial_diagnosis=registration.multiaxialdiagnosis, + epilepsy_or_nonepilepsy_status="E", + epileptic_generalised_onset=MYOCLONIC, + ) + + calculate_kpis(registration_instance=registration) + + kpi_score = KPI.objects.get(pk=registration.kpi.pk).tertiary_input + + assert kpi_score == expected_kpi_score, ( + f"Has myoclonic episode (n = {episodes.count()}) and seen by neurologist / epilepsy surgery/both but did not pass measure" + if expected_kpi_score == KPI_SCORE["PASS"] + else f"Has myoclonic episode (n = {episodes.count()}) and not seen by neurologist / surgery and did not fail measure" + ) + + +@pytest.mark.parametrize( + CASE_PARAM_NAMES, + CASE_PARAM_VALUES, +) +@pytest.mark.django_db +def test_measure_3b_meets_CESS_seen( + e12_case_factory, + PAEDIATRIC_NEUROLOGIST_REFERRAL_MADE, + CHILDRENS_EPILEPSY_SURGICAL_SERVICE_REFERRAL_MADE, + expected_kpi_score, +): + """ + *PASS* + 1) child is eligible for epilepsy surgery (assessment.childrens_epilepsy_surgical_service_referral_criteria_met) && seen by BOTH neurologist / CESS + 1) child is eligible for epilepsy surgery (assessment.childrens_epilepsy_surgical_service_referral_criteria_met) && seen by ONLY neurologist + 2) child is eligible for epilepsy surgery (assessment.childrens_epilepsy_surgical_service_referral_criteria_met) && seen by ONLY epilepsy surgery + *FAIL* + 1) child is eligible for epilepsy surgery (assessment.childrens_epilepsy_surgical_service_referral_criteria_met) && NOT seen by (neurologist OR epilepsy surgery) + """ + + case = e12_case_factory( + registration__assessment__childrens_epilepsy_surgical_service_referral_criteria_met = True, + registration__assessment__paediatric_neurologist_referral_made = PAEDIATRIC_NEUROLOGIST_REFERRAL_MADE, + registration__assessment__childrens_epilepsy_surgical_service_referral_made = CHILDRENS_EPILEPSY_SURGICAL_SERVICE_REFERRAL_MADE + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + kpi_score = KPI.objects.get(pk=registration.kpi.pk).epilepsy_surgery_referral + + assert kpi_score == expected_kpi_score, ( + f"Met CESS criteria and seen by neurologist / epilepsy surgery/both but did not pass measure" + if expected_kpi_score == KPI_SCORE["PASS"] + else f"Met CESS criteria and not seen by neurologist / surgery and did not fail measure" + ) + + +@pytest.mark.django_db +def test_measure_3_ineligible( + e12_case_factory, + e12_episode_factory, +): + """ + *INELIGIBLE* + 1) age at first paediatric assessment is > 3y + and + not on 3 or more drugs + and + not >4y with myoclonic epilepsy + and + not eligible for epilepsy surgery + """ + + # a child who is exactly 3y1mo at registration_date (=FPA) + DATE_OF_BIRTH = date(2021, 1, 1) + REGISTRATION_DATE = DATE_OF_BIRTH + relativedelta( + years=4, + ) + + # default N(AEMs) = 1, override not required + + # <4y without myoclonic epilepsy + OTHER = GENERALISED_SEIZURE_TYPE[-1][0] + + case = e12_case_factory( + date_of_birth=DATE_OF_BIRTH, + registration__registration_date=REGISTRATION_DATE, + registration__assessment__childrens_epilepsy_surgical_service_referral_criteria_met=False, # not eligible epilepsy surgery criteria + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + # Assign a NON-MYOCLONIC episode (OTHER) + e12_episode_factory.create( + multiaxial_diagnosis=registration.multiaxialdiagnosis, + epileptic_seizure_onset_type_generalised=True, + epileptic_generalised_onset=OTHER, + ) + + calculate_kpis(registration_instance=registration) + + kpi_score = KPI.objects.get(pk=registration.kpi.pk).tertiary_input + + assert ( + kpi_score == KPI_SCORE["INELIGIBLE"] + ), f"Child does not meet any criteria but is not scoring as ineligible" diff --git a/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_4.py b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_4.py new file mode 100644 index 00000000..d19fc95a --- /dev/null +++ b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_4.py @@ -0,0 +1,108 @@ +""" +Measure 4 `ecg` + +% of children and young people with convulsive seizures and epilepsy, with an ECG at first year + +- [x] Measure 4 passed (registration.kpi.ecg == 1) if ECG performed and seizure convulsive (registration_instance.epilepsycontext.were_any_of_the_epileptic_seizures_convulsive and registration_instance.investigations.twelve_lead_ecg_status) +- [x] Measure 4 failed (registration.kpi.ecg == 0) if ECG not performed and seizure convulsive (not registration_instance.epilepsycontext.were_any_of_the_epileptic_seizures_convulsive and registration_instance.investigations.twelve_lead_ecg_status) +- [x] Measure 4 ineligible (registration.kpi.ecg == 2) if seizure not convulsive (not registration_instance.epilepsycontext.were_any_of_the_epileptic_seizures_convulsive) +""" + +# Standard imports +import pytest + +# Third party imports + +# RCPCH imports +from epilepsy12.common_view_functions import calculate_kpis +from epilepsy12.models import ( + Registration, + KPI, +) +from epilepsy12.constants import ( + KPI_SCORE, +) + + +@pytest.mark.parametrize( + "CONVULSIVE_SEIZURE,ECG_STATUS,EXPECTED_SCORE", + [ + (True, True, KPI_SCORE["PASS"]), + (True, False, KPI_SCORE["FAIL"]), + (False, False, KPI_SCORE["INELIGIBLE"]), + ], +) +@pytest.mark.django_db +def test_measure_4_ecg_for_convulsive_seizure( + e12_case_factory, + CONVULSIVE_SEIZURE, + ECG_STATUS, + EXPECTED_SCORE, +): + """ + *PASS* + 1) seizure convulsive + ECG performed + *FAIL* + 1) seizure convulsive + no ECG + *INELIGIBLE* + 1) seizure not convulsive + """ + + case = e12_case_factory( + registration__epilepsy_context__were_any_of_the_epileptic_seizures_convulsive=CONVULSIVE_SEIZURE, + registration__investigations__twelve_lead_ecg_status=ECG_STATUS, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + kpi_score = KPI.objects.get(pk=registration.kpi.pk).ecg + + # INELIGIBLE CASE + if EXPECTED_SCORE == KPI_SCORE["INELIGIBLE"]: + assert ( + kpi_score == EXPECTED_SCORE + ), f"Seizure not convulsive, (no ECG) but not being scored as ineligible" + + # PASS / FAIL CASES + assert ( + kpi_score == EXPECTED_SCORE + ), f"Seizure convulsive, {'' if KPI_SCORE['PASS'] else 'no'} ECG performed, but not {'pass' if KPI_SCORE['PASS'] else 'fail'}ing measure" + + +@pytest.mark.parametrize( + "CONVULSIVE_SEIZURE,ECG_STATUS,EXPECTED_SCORE", + [ + (None, True, KPI_SCORE["NOT_SCORED"]), + (True, None, KPI_SCORE["NOT_SCORED"]), + ], +) +@pytest.mark.django_db +def test_measure_4_not_scored( + e12_case_factory, CONVULSIVE_SEIZURE, ECG_STATUS, EXPECTED_SCORE +): + """ + *NOT SCORED* + 1) seizure convulsive is None + 2) twelve_lead_ecg_status is None + """ + + case = e12_case_factory( + registration__epilepsy_context__were_any_of_the_epileptic_seizures_convulsive=CONVULSIVE_SEIZURE, + registration__investigations__twelve_lead_ecg_status=ECG_STATUS, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + kpi_score = KPI.objects.get(pk=registration.kpi.pk).ecg + + # define assertion message + reason = "Convulsive seizure" if not registration.epilepsycontext.were_any_of_the_epileptic_seizures_convulsive else "ECG field" + + # NOT SCORED + assert kpi_score == EXPECTED_SCORE, f"{reason} field is None but not being scored as 'NOT_SCORED'" diff --git a/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_5.py b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_5.py new file mode 100644 index 00000000..53f240c7 --- /dev/null +++ b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_5.py @@ -0,0 +1,242 @@ +""" +Measure 5 `mri` + +Number of children and young people diagnosed with epilepsy at first year AND who are NOT JME or JAE or CAE or CECTS/Rolandic OR number of children aged under 2 years at first assessment with a diagnosis of epilepsy at first year AND who had an MRI within 6 weeks of request + += + +COUNT(kids) +AND + NOT (JME or JAE or CAE or CECTS/Rolandic) + OR + age@FPA <2y +AND + had MRI within 6 weeks of request + +- [ x] Measure 5 passed (registration.kpi.mri == 1) if MRI done in 6 weeks and are NOT JME or JAE or CAE or CECTS/Rolandic or under 2 y (lines 270-324) +- [ x] Measure 5 failed (registration.kpi.mri == 0) if MRI not done in 6 weeks and are NOT JME or JAE or CAE or CECTS/Rolandic or under 2 y (lines 270-324) +- [ x] Measure 5 ineligible (registration.kpi.mri == 0) if JME or JAE or CAE or CECTS/Rolandic +""" + +# Standard imports +import pytest +from datetime import date +from dateutil.relativedelta import relativedelta + +# Third party imports +from django.contrib.gis.db.models import Q + +# RCPCH imports +from epilepsy12.common_view_functions import calculate_kpis +from epilepsy12.models import ( + Registration, + KPI, + Syndrome, + SyndromeEntity, + MultiaxialDiagnosis, +) +from epilepsy12.constants import ( + KPI_SCORE, + SYNDROMES, +) + + +@pytest.mark.parametrize( + "DATE_OF_BIRTH,REGISTRATION_DATE,TIMELY_MRI,EXPECTED_SCORE", + [ + ( + # <2yo, MRI done within 6 weeks of refer + date(2022, 1, 1), + date(2023, 12, 31), + True, + KPI_SCORE["PASS"], + ), + ( # 2yo, MRI NOT done within 6 weeks of refer + date(2022, 1, 1), + date(2023, 12, 31), + False, + KPI_SCORE["FAIL"], + ) + ], +) +@pytest.mark.django_db +def test_measure_5_mri_under2yo( + e12_case_factory, DATE_OF_BIRTH, REGISTRATION_DATE, TIMELY_MRI, EXPECTED_SCORE +): + """ + *PASS* + 1) MRI done in 6 weeks post-referral and are under 2y@FPA + *FAIL* + 1) MRI NOT done in 6 weeks post-referral and are under 2y@FPA + """ + MRI_REQUESTED_DATE = REGISTRATION_DATE + relativedelta(days=1) + + MRI_REPORTED_DATE = MRI_REQUESTED_DATE + relativedelta(weeks=6) + if not TIMELY_MRI: + MRI_REPORTED_DATE = MRI_REQUESTED_DATE + relativedelta(weeks=6, days=1) + + case = e12_case_factory( + date_of_birth=DATE_OF_BIRTH, + registration__registration_date=REGISTRATION_DATE, + registration__investigations__mri_brain_requested_date=MRI_REQUESTED_DATE, + registration__investigations__mri_brain_reported_date=MRI_REPORTED_DATE, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + kpi_score = KPI.objects.get(pk=registration.kpi.pk).mri + + if EXPECTED_SCORE == KPI_SCORE["PASS"]: + assert ( + kpi_score == EXPECTED_SCORE + ), f"MRI booked <= 6 weeks but not passing measure" + elif EXPECTED_SCORE == KPI_SCORE["FAIL"]: + assert ( + kpi_score == EXPECTED_SCORE + ), f"MRI booked > 6 weeks but not failing measure" + + +@pytest.mark.parametrize( + "TIMELY_MRI,EXPECTED_SCORE", + [ + ( + # MRI done within 6 weeks of refer + True, + KPI_SCORE["PASS"], + ), + ( # MRI NOT done within 6 weeks of refer + False, + KPI_SCORE["FAIL"], + ), + ], +) +@pytest.mark.django_db +def test_measure_5_mri_syndromes_pass_fail( + e12_case_factory, TIMELY_MRI, EXPECTED_SCORE +): + """ + *PASS* + 1) MRI done in 6 weeks post-referral and are NOT (JME or JAE or CAE or CECTS/Rolandic) + *FAIL* + 1) MRI NOT done in 6 weeks post-referral and are NOT (JME or JAE or CAE or CECTS/Rolandic) + """ + REGISTRATION_DATE = date(2023, 1, 1) + + MRI_REQUESTED_DATE = REGISTRATION_DATE + relativedelta(days=1) + + MRI_REPORTED_DATE = MRI_REQUESTED_DATE + relativedelta(weeks=6) + if not TIMELY_MRI: + MRI_REPORTED_DATE += relativedelta(days=1) + + case = e12_case_factory( + registration__registration_date=REGISTRATION_DATE, + registration__investigations__mri_brain_requested_date=MRI_REQUESTED_DATE, + registration__investigations__mri_brain_reported_date=MRI_REPORTED_DATE, + ) + + # set syndrome present to False + multiaxial_diagnosis = MultiaxialDiagnosis.objects.get( + registration=case.registration + ) + multiaxial_diagnosis.syndrome_present = False + multiaxial_diagnosis.save() + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + # get attached syndromes + relevant_syndromes = Syndrome.objects.filter( + Q(multiaxial_diagnosis=registration.multiaxialdiagnosis) + & + # ELECTROCLINICAL SYNDROMES: BECTS/JME/JAE/CAE currently not included + Q( + syndrome__syndrome_name__in=[ + "Self-limited epilepsy with centrotemporal spikes", + "Juvenile myoclonic epilepsy", + "Juvenile absence epilepsy", + "Childhood absence epilepsy", + ] + ) + ) + # remove syndromes to ensure just testing pass/fail + if relevant_syndromes.exists(): + relevant_syndromes.delete() + + calculate_kpis(registration_instance=registration) + + kpi_score = KPI.objects.get(pk=registration.kpi.pk).mri + + if EXPECTED_SCORE == KPI_SCORE["PASS"]: + assert ( + kpi_score == EXPECTED_SCORE + ), f"None of JME or JAE or CAE or CECTS/Rolandi present, MRI booked <= 6 weeks but not passing measure" + + elif EXPECTED_SCORE == KPI_SCORE["FAIL"]: + assert ( + kpi_score == EXPECTED_SCORE + ), f"None of JME or JAE or CAE or CECTS/Rolandi present, MRI booked > 6 weeks but not failing measure" + + +@pytest.mark.parametrize( + "SYNDROME_TYPE_PRESENT", + [ + (SYNDROMES[18][1]), # Juvenile myoclonic epilepsy + (SYNDROMES[17][1]), # Juvenile absence epilepsy + (SYNDROMES[16][1]), # Childhood absence epilepsy + (SYNDROMES[3][1]), # Self-limited epilepsy with centrotemporal spikes + ], +) +@pytest.mark.django_db +def test_measure_5_mri_syndromes_ineligible( + e12_case_factory, e12_syndrome_factory, SYNDROME_TYPE_PRESENT +): + """ + *INELIGIBLE* + 1) ONE OF: + JME or JAE or CAE or CECTS/Rolandic + AND + age_at_first_paediatric_assessment >= 2 (testing using age at fpa = 2y exactly) + """ + + REGISTRATION_DATE = date(2023, 1, 1) + DATE_OF_BIRTH = REGISTRATION_DATE - relativedelta(years=2) + + case = e12_case_factory( + date_of_birth=DATE_OF_BIRTH, + registration__registration_date=REGISTRATION_DATE, + ) + + # set syndrome present to True + multiaxial_diagnosis = MultiaxialDiagnosis.objects.get( + registration=case.registration + ) + multiaxial_diagnosis.syndrome_present = True + multiaxial_diagnosis.save() + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + # ensure starting with no default syndromes + Syndrome.objects.filter(multiaxial_diagnosis=registration.multiaxialdiagnosis).delete() + + # save the ineligible syndrome type + new_syndrome = e12_syndrome_factory( + multiaxial_diagnosis=registration.multiaxialdiagnosis, + syndrome=SyndromeEntity.objects.get(syndrome_name=SYNDROME_TYPE_PRESENT), + ) + new_syndrome.save() + + calculate_kpis(registration_instance=registration) + + kpi_score = KPI.objects.get(pk=registration.kpi.pk).mri + + # ensure we get syndromes from db + SYNDROME_TYPE_PRESENT = Syndrome.objects.get( + multiaxial_diagnosis=registration.multiaxialdiagnosis + ) + assert ( + kpi_score == KPI_SCORE["INELIGIBLE"] + ), f"{SYNDROME_TYPE_PRESENT} present, should be ineligible" diff --git a/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_6.py b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_6.py new file mode 100644 index 00000000..1691fa9b --- /dev/null +++ b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_6.py @@ -0,0 +1,82 @@ +""" +Measure 6 `assessment_of_mental_health_issues` - Number of children and young people over 5 years diagnosed with epilepsy AND who had documented evidence of enquiry or screening for their mental health + +- [x] Measure 6 passed (registration.kpi.assessment_of_mental_health_issues == 1) if (age_at_first_paediatric_assessment >= 5) and (registration_instance.multiaxialdiagnosis.mental_health_screen) +- [x] Measure 6 failed (registration.kpi.assessment_of_mental_health_issues == 0) if (age_at_first_paediatric_assessment >= 5) and not (registration_instance.multiaxialdiagnosis.mental_health_screen) +- [x] Measure 6 ineligible (registration.kpi.assessment_of_mental_health_issues == 2) if (age_at_first_paediatric_assessment < 5) +""" + +from datetime import date + +# Standard imports +import pytest +from dateutil.relativedelta import relativedelta + +# Third party imports + +# RCPCH imports +from epilepsy12.common_view_functions import calculate_kpis +from epilepsy12.constants import KPI_SCORE +from epilepsy12.models import KPI, Registration, MultiaxialDiagnosis + + +@pytest.mark.parametrize( + "age_fpa,mental_health_screen_done, expected_score", + [ + (relativedelta(years=5), True, KPI_SCORE["PASS"]), + (relativedelta(years=5), False, KPI_SCORE["FAIL"]), + (relativedelta(years=4, months=11), True, KPI_SCORE["INELIGIBLE"]), + ], +) +@pytest.mark.django_db +def test_measure_6_screen_mental_health( + e12_case_factory, age_fpa, mental_health_screen_done, expected_score +): + """ + *PASS* + 1) (age_at_first_paediatric_assessment >= 5) and (registration_instance.multiaxialdiagnosis.mental_health_screen) + *FAIL* + 1) (age_at_first_paediatric_assessment >= 5) and NOT (registration_instance.multiaxialdiagnosis.mental_health_screen) + *INELIGIBLE* + 1) (age_at_first_paediatric_assessment < 5) + """ + + DATE_OF_BIRTH = date(2018, 1, 1) + REGISTRATION_DATE = DATE_OF_BIRTH + age_fpa + + # create case + case = e12_case_factory( + date_of_birth=DATE_OF_BIRTH, + registration__registration_date=REGISTRATION_DATE, + ) + + multiaxial_diagnosis = MultiaxialDiagnosis.objects.get( + registration=case.registration + ) + multiaxial_diagnosis.mental_health_screen = mental_health_screen_done + multiaxial_diagnosis.save() + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + kpi_score = KPI.objects.get( + pk=registration.kpi.pk + ).assessment_of_mental_health_issues + + # get age for AssertionError message + age = relativedelta(case.registration.registration_date, case.date_of_birth).years + + if expected_score == KPI_SCORE["PASS"]: + assertion_message = ( + f"Age >= 5yo ({age}yo) with mental health screen but not passing measure" + ) + elif expected_score == KPI_SCORE["FAIL"]: + assertion_message = ( + f"Age >= 5yo ({age}yo) with NO mental health screen but not failing measure" + ) + else: + assertion_message = f"Age < 5yo ({age}yo) should be ineligible for measure" + + assert kpi_score == expected_score, assertion_message diff --git a/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_7.py b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_7.py new file mode 100644 index 00000000..36cffde5 --- /dev/null +++ b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_7.py @@ -0,0 +1,94 @@ +""" +Measure 7 `mental_health_support` - Number of children and young people diagnosed with epilepsy AND had a mental health issue identified AND had evidence of mental health support received + +- [x] Measure 7 passed (registration.kpi.mental_health_support == 1) if registration_instance.multiaxialdiagnosis.mental_health_issue_identified and registration_instance.management.has_support_for_mental_health_support +- [x] Measure 7 failed (registration.kpi.mental_health_support == 0) if not registration_instance.multiaxialdiagnosis.mental_health_issue_identified and registration_instance.management.has_support_for_mental_health_support +- [x] Measure 7 ineligible (registration.kpi.mental_health_support == 2) if not registration_instance.multiaxialdiagnosis.mental_health_issue_identified +""" + +# Standard imports +from datetime import date +import pytest +from dateutil.relativedelta import relativedelta + +# Third party imports + +# RCPCH imports +from epilepsy12.common_view_functions import calculate_kpis +from epilepsy12.constants import KPI_SCORE +from epilepsy12.models import ( + KPI, + Registration, + Management, + MultiaxialDiagnosis, +) + + +@pytest.mark.parametrize( + "mental_health_issue_identified,has_support_for_mental_health_support, expected_score", + [ + (True, True, KPI_SCORE["PASS"]), + (True, False, KPI_SCORE["FAIL"]), + (False, None, KPI_SCORE["INELIGIBLE"]), + ], +) +@pytest.mark.django_db +def test_measure_7_mental_health_support( + e12_case_factory, + mental_health_issue_identified, + has_support_for_mental_health_support, + expected_score, +): + """ + *PASS* + 1) mental_health_issue_identified and has_support_for_mental_health_support + *FAIL* + 1) mental_health_issue_identified and NOT has_support_for_mental_health_support + *INELIGIBLE* + 1) NOT mental_health_issue_identified + + """ + + # ensure child is old enough to be scored on mental health + DATE_OF_BIRTH = date(2018, 1, 1) + REGISTRATION_DATE = DATE_OF_BIRTH + relativedelta(years=5) + + # create case + case = e12_case_factory( + date_of_birth=DATE_OF_BIRTH, + registration__registration_date=REGISTRATION_DATE, + ) + + # update multiaxial diagnosis to corresponding mental health issue identified + multiaxial_diagnosis = MultiaxialDiagnosis.objects.get( + registration=case.registration + ) + multiaxial_diagnosis.mental_health_issue_identified = mental_health_issue_identified + multiaxial_diagnosis.save() + + # update management to corresponding mental health support + management = Management.objects.get(registration=case.registration) + management.has_support_for_mental_health_support = ( + has_support_for_mental_health_support + ) + management.save() + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + kpi_score = KPI.objects.get(pk=registration.kpi.pk).mental_health_support + + if expected_score == KPI_SCORE["PASS"]: + assertion_message = ( + f"Mental health issue identified and given support but not passing measure" + ) + elif expected_score == KPI_SCORE["FAIL"]: + assertion_message = f"Mental health issue identified but NOT given support but not failing measure" + else: + assertion_message = ( + f"No mental health issue identified but not scoring ineligible" + ) + + assert kpi_score == expected_score, assertion_message diff --git a/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_8.py b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_8.py new file mode 100644 index 00000000..be8aa28b --- /dev/null +++ b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_8.py @@ -0,0 +1,225 @@ +""" +Measure 8 `sodium_valproate` - Percentage of all females 12 years and above currently on valproate treatment with annual risk acknowledgement form completed + +Number of females >= 12yo diagnosed with epilepsy at first year + AND on valproate + AND annual risk acknowledgement forms completed + AND pregnancy prevention programme in place + +- [x] Measure 8 passed (registration.kpi.sodium_valproate == 1) if (age_at_first_paediatric_assessment >= 12 and sex == 2 and medicine is valproate) and is_a_pregnancy_prevention_programme_needed==True and has_a_valproate_annual_risk_acknowledgement_form_been_completed==True +- [x] Measure 8 failed (registration.kpi.sodium_valproate == 0) if (age_at_first_paediatric_assessment >= 12 and sex == 2 and medicine is valproate) and is_a_pregnancy_prevention_programme_needed is False or None +- [x] Measure 8 failed (registration.kpi.sodium_valproate == 0) if (age_at_first_paediatric_assessment >= 12 and sex == 2 and medicine is valproate) and has_a_valproate_annual_risk_acknowledgement_form_been_completed is False or None + +- [ x] Measure 8 ineligible (registration.kpi.sodium_valproate == 2) if age_at_first_paediatric_assessment < 12 +- [ x] Measure 8 ineligible (registration.kpi.sodium_valproate == 2) if registration_instance.case.sex == 1 +- [ x] Measure 8 ineligible (registration.kpi.sodium_valproate == 2) if registration_instance.management.has_an_aed_been_given == False +- [ x] Measure 8 ineligible (registration.kpi.sodium_valproate == 2) if AEM is not valproate or AEM is None +""" + +# Standard imports +from datetime import date +from dateutil.relativedelta import relativedelta + +# Third party imports +import pytest + +# RCPCH imports +from epilepsy12.common_view_functions import calculate_kpis +from epilepsy12.constants import KPI_SCORE, SEX_TYPE +from epilepsy12.models import KPI, AntiEpilepsyMedicine, Registration, MedicineEntity + + +@pytest.mark.parametrize( + "is_a_pregnancy_prevention_programme_needed, has_a_valproate_annual_risk_acknowledgement_form_been_completed,expected_score", + [ + (True, True, KPI_SCORE["PASS"]), + (False, None, KPI_SCORE["FAIL"]), + (None, False, KPI_SCORE["FAIL"]), + (None, None, KPI_SCORE["FAIL"]), + ], +) +@pytest.mark.django_db +def test_measure_8_sodium_valproate_risk_eligible( + e12_case_factory, + is_a_pregnancy_prevention_programme_needed, + has_a_valproate_annual_risk_acknowledgement_form_been_completed, + expected_score, +): + """ + *PASS* + 1) (age_at_first_paediatric_assessment >= 12 && sex == 2 && medicine is valproate) + && is_a_pregnancy_prevention_programme_needed==True + && has_a_valproate_annual_risk_acknowledgement_form_been_completed==True + *FAIL* + 1) (age_at_first_paediatric_assessment >= 12 && sex == 2 && medicine is valproate) + && is_a_pregnancy_prevention_programme_needed is False OR None + 2) (age_at_first_paediatric_assessment >= 12 && sex == 2 && medicine is valproate) + && has_a_valproate_annual_risk_acknowledgement_form_been_completed is False OR None + + """ + + # Explicitly set age to exactly 12yo and sex female (=2) + REGISTRATION_DATE = date(2023, 1, 1) + DATE_OF_BIRTH = REGISTRATION_DATE - relativedelta(years=12) + SEX = SEX_TYPE[2][0] + + # create case + case = e12_case_factory( + sex=SEX, + date_of_birth=DATE_OF_BIRTH, + registration__registration_date=REGISTRATION_DATE, + registration__management__has_an_aed_been_given = True + ) + + # get management + management = case.registration.management + + # clean current AEMs if any are set + AntiEpilepsyMedicine.objects.filter(management=management).delete() + + # create and save a valproate AEM entry with paramtrized constants + AntiEpilepsyMedicine.objects.create( + management=management, + is_rescue_medicine=False, + medicine_entity=MedicineEntity.objects.get(medicine_name="Sodium valproate"), + antiepilepsy_medicine_risk_discussed=True, + is_a_pregnancy_prevention_programme_needed=is_a_pregnancy_prevention_programme_needed, + has_a_valproate_annual_risk_acknowledgement_form_been_completed=has_a_valproate_annual_risk_acknowledgement_form_been_completed, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + # get KPI score + kpi_score = KPI.objects.get(pk=registration.kpi.pk).sodium_valproate + + if expected_score == KPI_SCORE["PASS"]: + assertion_message = f">=12yo on valproate with valproate pregnancy prevention in place & completed annual risk acknowledgement, but not passing measure" + elif expected_score == KPI_SCORE["FAIL"]: + assertion_message = f">=12yo on valproate with \n{is_a_pregnancy_prevention_programme_needed=}\n{has_a_valproate_annual_risk_acknowledgement_form_been_completed=},\nbut not failing measure" + + assert kpi_score == expected_score, assertion_message + + +@pytest.mark.parametrize( + "age, sex, aed_given, not_valproate, expected_score", + [ + ( + relativedelta(years=11, months=11), + SEX_TYPE[2][0], + True, + False, + KPI_SCORE["INELIGIBLE"], + ), # 11y11mo, F, aed given, valproate + ( + relativedelta(years=12), + SEX_TYPE[1][0], + True, + False, + KPI_SCORE["INELIGIBLE"], + ), # 12yo, M, aed given, valproate + ( + relativedelta(years=12), + SEX_TYPE[2][0], + False, + True, + KPI_SCORE["INELIGIBLE"], + ), # 12yo, F, aed NOT given, NO valproate + ( + relativedelta(years=12), + SEX_TYPE[2][0], + True, + True, + KPI_SCORE["INELIGIBLE"], + ), # 12yo, F, aed given, NOT valproate + ], +) +@pytest.mark.django_db +def test_measure_8_sodium_valproate_risk_ineligible( + e12_case_factory, age, sex, aed_given, not_valproate, expected_score +): + """ + *INELIGIBLE* + 1) age_at_first_paediatric_assessment < 12 + 2) registration_instance.case.sex == 1 + 3) registration_instance.management.has_an_aed_been_given == False + 4) AEM is not valproate or AEM is None + + """ + + # Explicitly set paramtrized age and sex + REGISTRATION_DATE = date(2023, 1, 1) + DATE_OF_BIRTH = REGISTRATION_DATE - age + SEX = sex + + # create case + case = e12_case_factory( + sex=SEX, + date_of_birth=DATE_OF_BIRTH, + registration__registration_date=REGISTRATION_DATE, + ) + + # get management + management = case.registration.management + + # get current AEMs + aem = AntiEpilepsyMedicine.objects.filter(management=management) + + # clean current aems + aem.delete() + + if aed_given: + + # AED given, update management + management.has_an_aed_been_given = True + management.save() + + # create and save an AEM entry which ISN'T valproate + if not_valproate: + AntiEpilepsyMedicine.objects.create( + management=management, + is_rescue_medicine=False, + medicine_entity=MedicineEntity.objects.get(medicine_name="Lorazepam"), + ) + + # create and save a valproate AEM entry. Only case is <12yoF or 12yoM + else: + AntiEpilepsyMedicine.objects.create( + management=management, + is_rescue_medicine=False, + medicine_entity=MedicineEntity.objects.get( + medicine_name="Sodium valproate" + ), + antiepilepsy_medicine_risk_discussed=True, + ) + + else: + # no AED given, update management + management.has_an_aed_been_given = False + management.save() + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + # get KPI score + kpi_score = KPI.objects.get(pk=registration.kpi.pk).sodium_valproate + + # set assertion error reason + if aed_given and not_valproate: + assertion_message = f"Not on valproate yet not being scored as ineligible" + else: + if age == relativedelta(years=11, months=11): + reason = "age is 11y11m (<12y)" + elif sex == SEX_TYPE[1][0]: + reason = "male" + elif not aed_given: + reason = "no AED given" + assertion_message = ( + f"On valproate but {reason} yet not being scored as ineligible" + ) + + assert kpi_score == expected_score, assertion_message diff --git a/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_9A.py b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_9A.py new file mode 100644 index 00000000..6ac0a8f8 --- /dev/null +++ b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_9A.py @@ -0,0 +1,71 @@ +""" +9A `comprehensive_care_planning_agreement` - Percentage of children and young people with epilepsy after 12 months where there is evidence of a comprehensive care plan that is agreed between the person, their family and/or carers and primary and secondary care providers, and the care plan has been updated where necessary. + +- [x] Measure 9A passed (registration.kpi.comprehensive_care_planning_agreement == 1) if individualised_care_plan_in_place and care_planning_has_been_updated_when_necessary +- [x] Measure 9A failed (registration.kpi.comprehensive_care_planning_agreement == 0) if individualised_care_plan_in_place == False +- [x] Measure 9A failed (registration.kpi.comprehensive_care_planning_agreement == 0) if care_planning_has_been_updated_when_necessary == False + + +""" + +# Standard imports + +# Third party imports +import pytest + +# RCPCH imports +from epilepsy12.common_view_functions import calculate_kpis +from epilepsy12.constants import KPI_SCORE +from epilepsy12.models import KPI, Registration + + +@pytest.mark.parametrize( + "individualised_care_plan_in_place, has_individualised_care_plan_been_updated_in_the_last_year,expected_score", + [ + (True, True, KPI_SCORE["PASS"]), + (False, True, KPI_SCORE["FAIL"]), + (True, False, KPI_SCORE["FAIL"]), + ], +) +@pytest.mark.django_db +def test_measure_9A_comprehensive_care_plan( + e12_case_factory, + individualised_care_plan_in_place, + has_individualised_care_plan_been_updated_in_the_last_year, + expected_score, +): + """ + *PASS* + 1) individualised_care_plan_in_place and has_individualised_care_plan_been_updated_in_the_last_year + *FAIL* + 1) individualised_care_plan_in_place == False + 2) has_individualised_care_plan_been_updated_in_the_last_year == False + """ + + # create case + case = e12_case_factory( + registration__management__individualised_care_plan_in_place=individualised_care_plan_in_place, + registration__management__has_individualised_care_plan_been_updated_in_the_last_year=has_individualised_care_plan_been_updated_in_the_last_year, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + # get KPI score + kpi_score = KPI.objects.get( + pk=registration.kpi.pk + ).comprehensive_care_planning_agreement + + if expected_score == KPI_SCORE["PASS"]: + assertion_message = f"Care plan in place, updated in last year, but not passing" + elif expected_score == KPI_SCORE["FAIL"]: + if not individualised_care_plan_in_place: + assertion_message = ( + f"No individualised_care_plan_in_place but not failing measure" + ) + else: + assertion_message = f"has_individualised_care_plan_been_updated_in_the_last_year=False but not failing measure" + + assert kpi_score == expected_score, assertion_message diff --git a/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_9Ai.py b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_9Ai.py new file mode 100644 index 00000000..ab23ae85 --- /dev/null +++ b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_9Ai.py @@ -0,0 +1,83 @@ +""" +9i `patient_held_individualised_epilepsy_document` - Percentage of children and young people with epilepsy after 12 months that had an individualised epilepsy document with individualised epilepsy document or a copy clinic letter that includes care planning information. + +All must be true to pass: + individualised_care_plan_in_place + individualised_care_plan_has_parent_carer_child_agreement + has_individualised_care_plan_been_updated_in_the_last_year + +- [x] Measure 9i passed (registration.kpi.comprehensive_care_planning_agreement == 1) if all true +- [x] Measure 9i failed (registration.kpi.comprehensive_care_planning_agreement == 1) if individualised_care_plan_in_place == False and others not None +- [x] Measure 9i failed (registration.kpi.comprehensive_care_planning_agreement == 1) if individualised_care_plan_has_parent_carer_child_agreement == False and others not None +- [x] Measure 9i failed (registration.kpi.comprehensive_care_planning_agreement == 1) if has_individualised_care_plan_been_updated_in_the_last_year == False and others not None +""" +# Standard imports + +# Third party imports +import pytest + +# RCPCH imports +from epilepsy12.common_view_functions import calculate_kpis +from epilepsy12.constants import KPI_SCORE +from epilepsy12.models import KPI, Registration + + +@pytest.mark.parametrize( + "individualised_care_plan_in_place, individualised_care_plan_has_parent_carer_child_agreement,has_individualised_care_plan_been_updated_in_the_last_year, expected_score", + [ + (True, True, True, KPI_SCORE["PASS"]), + (False, True, True, KPI_SCORE["FAIL"]), + (True, False, True, KPI_SCORE["FAIL"]), + (True, True, False, KPI_SCORE["FAIL"]), + ], +) +@pytest.mark.django_db +def test_measure_9Ai_epilepsy_document( + e12_case_factory, + individualised_care_plan_in_place, + individualised_care_plan_has_parent_carer_child_agreement, + has_individualised_care_plan_been_updated_in_the_last_year, + expected_score, +): + """ + *PASS* + 1) all true: + individualised_care_plan_in_place + individualised_care_plan_has_parent_carer_child_agreement + has_individualised_care_plan_been_updated_in_the_last_year + *FAIL* + 1) individualised_care_plan_in_place == False + 2) individualised_care_plan_has_parent_carer_child_agreement == False + 3) has_individualised_care_plan_been_updated_in_the_last_year == False + """ + + # create case + case = e12_case_factory( + registration__management__individualised_care_plan_in_place=individualised_care_plan_in_place, + registration__management__individualised_care_plan_has_parent_carer_child_agreement=individualised_care_plan_has_parent_carer_child_agreement, + registration__management__has_individualised_care_plan_been_updated_in_the_last_year=has_individualised_care_plan_been_updated_in_the_last_year, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + # get KPI score + kpi_score = KPI.objects.get( + pk=registration.kpi.pk + ).patient_held_individualised_epilepsy_document + + if expected_score == KPI_SCORE["PASS"]: + assertion_message = f"Care plan in place, with carer-child-agreement, updated in last year, but not passing" + elif expected_score == KPI_SCORE["FAIL"]: + if not individualised_care_plan_in_place: + assertion_message = ( + f"No individualised_care_plan_in_place but not failing measure" + ) + elif not has_individualised_care_plan_been_updated_in_the_last_year: + assertion_message = f"has_individualised_care_plan_been_updated_in_the_last_year=False but not failing measure" + else: + assertion_message = f"individualised_care_plan_has_parent_carer_child_agreement=False but not failing measure" + + assert kpi_score == expected_score, assertion_message diff --git a/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_9Aii.py b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_9Aii.py new file mode 100644 index 00000000..a72b6c22 --- /dev/null +++ b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_9Aii.py @@ -0,0 +1,64 @@ +""" +9ii `patient_carer_parent_agreement_to_the_care_planning` - Percentage of children and young people with epilepsy after 12 months where there was evidence of agreement between the person, their family and/or carers as appropriate. + +pass criteria: +Number of children and young people diagnosed with epilepsy at first year + AND +with evidence of agreement + +- [x] Measure 9ii passed (registration.kpi.patient_carer_parent_agreement_to_the_care_planning == 1) if registration_instance.management.individualised_care_plan_has_parent_carer_child_agreement == True +- [x] Measure 9ii failed (registration.kpi.patient_carer_parent_agreement_to_the_care_planning == 1) if registration_instance.management.individualised_care_plan_has_parent_carer_child_agreement == False +""" +# Standard imports + +# Third party imports +import pytest + +# RCPCH imports +from epilepsy12.common_view_functions import calculate_kpis +from epilepsy12.constants import KPI_SCORE +from epilepsy12.models import KPI, Registration + + +@pytest.mark.parametrize( + "individualised_care_plan_has_parent_carer_child_agreement, expected_score", + [ + (True, KPI_SCORE["PASS"]), + (False, KPI_SCORE["FAIL"]), + ], +) +@pytest.mark.django_db +def test_measure_9Aii_carer_patient_plan( + e12_case_factory, + individualised_care_plan_has_parent_carer_child_agreement, + expected_score, +): + """ + *PASS* + 1) registration_instance.management.individualised_care_plan_has_parent_carer_child_agreement == True + *FAIL* + 1) registration_instance.management.individualised_care_plan_has_parent_carer_child_agreement == False + """ + + # create case + case = e12_case_factory( + registration__management__individualised_care_plan_has_parent_carer_child_agreement=individualised_care_plan_has_parent_carer_child_agreement, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + # get KPI score + kpi_score = KPI.objects.get( + pk=registration.kpi.pk + ).patient_carer_parent_agreement_to_the_care_planning + + if expected_score == KPI_SCORE["PASS"]: + assertion_message = f"individualised_care_plan_has_parent_carer_child_agreement in place but not passing" + + elif expected_score == KPI_SCORE["FAIL"]: + assertion_message = f"individualised_care_plan_has_parent_carer_child_agreement NOT in place but not failing" + + assert kpi_score == expected_score, assertion_message diff --git a/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_9Aiii.py b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_9Aiii.py new file mode 100644 index 00000000..f1e7ac37 --- /dev/null +++ b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_9Aiii.py @@ -0,0 +1,61 @@ +""" +9iii `care_planning_has_been_updated_when_necessary` - Number of children and young people diagnosed with epilepsy at first year AND with care plan which is updated where necessary + +PASS: +- [x] management.has_individualised_care_plan_been_updated_in_the_last_year = True +FAIL: +- [x] management.has_individualised_care_plan_been_updated_in_the_last_year = False +""" +# Standard imports + +# Third party imports +import pytest + +# RCPCH imports +from epilepsy12.common_view_functions import calculate_kpis +from epilepsy12.constants import KPI_SCORE +from epilepsy12.models import KPI, Registration + + +@pytest.mark.parametrize( + "has_individualised_care_plan_been_updated_in_the_last_year, expected_score", + [ + (True, KPI_SCORE["PASS"]), + (False, KPI_SCORE["FAIL"]), + ], +) +@pytest.mark.django_db +def test_measure_9Aiii_care_plan_update( + e12_case_factory, + has_individualised_care_plan_been_updated_in_the_last_year, + expected_score, +): + """ + *PASS* + 1) registration_instance.management.has_individualised_care_plan_been_updated_in_the_last_year == True + *FAIL* + 1) registration_instance.management.has_individualised_care_plan_been_updated_in_the_last_year == False + """ + + # create case + case = e12_case_factory( + registration__management__has_individualised_care_plan_been_updated_in_the_last_year=has_individualised_care_plan_been_updated_in_the_last_year, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + # get KPI score + kpi_score = KPI.objects.get( + pk=registration.kpi.pk + ).care_planning_has_been_updated_when_necessary + + if expected_score == KPI_SCORE["PASS"]: + assertion_message = f"has_individualised_care_plan_been_updated_in_the_last_year is True in place but not passing" + + elif expected_score == KPI_SCORE["FAIL"]: + assertion_message = f"has_individualised_care_plan_been_updated_in_the_last_year is False NOT in place but not failing" + + assert kpi_score == expected_score, assertion_message diff --git a/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_9B.py b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_9B.py new file mode 100644 index 00000000..95b5399d --- /dev/null +++ b/epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_9B.py @@ -0,0 +1,423 @@ +""" +PASS: +- ALL TRUE +FAIL: +- Any false + +[x] - 9B `comprehensive_care_planning_content` - Percentage of children diagnosed with epilepsy with documented evidence of communication regarding core elements of care planning. + +Number of children and young people diagnosed with epilepsy at first year + AND evidence of written prolonged seizures plan if prescribed rescue medication + AND evidence of discussion regarding water safety + AND first aid + AND participation + and risk + AND service contact details + AND SUDEP + + +[x] - 9Bi. `parental_prolonged_seizures_care_plan` - Number of children and young people diagnosed with epilepsy at first year AND prescribed rescue medication AND evidence of a written prolonged seizures plan + +Number of children and young people diagnosed with epilepsy at first year + AND has_rescue_medication_been_prescribed + AND individualised_care_plan_parental_prolonged_seizure_care + + +[x] - 9Bii. `water_safety` - Number of children and young people diagnosed with epilepsy at first year AND with evidence of discussion regarding water safety + +Number of children and young people diagnosed with epilepsy at first year + AND individualised_care_plan_addresses_water_safety + + +[x] - 9Biii. `first_aid` - Number of children and young people diagnosed with epilepsy at first year AND with evidence of discussion regarding first aid + +Number of children and young people diagnosed with epilepsy at first year + AND individualised_care_plan_include_first_aid + + +[x] - 9Biv. `general_participation_and_risk` - Number of children and young people diagnosed with epilepsy at first year AND with evidence of discussion regarding general participation and risk + +Number of children and young people diagnosed with epilepsy at first year + AND individualised_care_plan_includes_general_participation_risk + + +[x] - 9Bv. `service_contact_details` - Number of children and young people diagnosed with epilepsy at first year AND with evidence of discussion of been given service contact details + +Number of children and young people diagnosed with epilepsy at first year + AND individualised_care_plan_includes_service_contact_details + + +[x] - 9Bvi. `sudep` - Number of children diagnosed with epilepsy AND had evidence of discussions regarding SUDEP AND evidence of a written prolonged seizures plan at first year + +Number of children and young people diagnosed with epilepsy at first year + AND individualised_care_plan_parental_prolonged_seizure_care + AND individualised_care_plan_addresses_sudep + + +""" +# Standard imports + +# Third party imports +import pytest + +# RCPCH imports +from epilepsy12.common_view_functions import calculate_kpis +from epilepsy12.constants import KPI_SCORE +from epilepsy12.models import KPI, Registration + + +@pytest.mark.parametrize( + "has_rescue_medication_been_prescribed,individualised_care_plan_parental_prolonged_seizure_care,individualised_care_plan_include_first_aid,individualised_care_plan_addresses_water_safety,individualised_care_plan_includes_service_contact_details,individualised_care_plan_includes_general_participation_risk,individualised_care_plan_addresses_sudep, expected_score", + [ + (True, True, True, True, True, True, True, KPI_SCORE["PASS"]), + (True, False, True, True, True, True, True, KPI_SCORE["FAIL"]), + (True, True, False, True, True, True, True, KPI_SCORE["FAIL"]), + (True, True, True, False, True, True, True, KPI_SCORE["FAIL"]), + (True, True, True, True, False, True, True, KPI_SCORE["FAIL"]), + (True, True, True, True, True, False, True, KPI_SCORE["FAIL"]), + (True, True, True, True, True, True, False, KPI_SCORE["FAIL"]), + ], +) +@pytest.mark.django_db +def test_measure_9B_comprehensive_care_planning_content( + e12_case_factory, + has_rescue_medication_been_prescribed, + individualised_care_plan_parental_prolonged_seizure_care, + individualised_care_plan_include_first_aid, + individualised_care_plan_addresses_water_safety, + individualised_care_plan_includes_service_contact_details, + individualised_care_plan_includes_general_participation_risk, + individualised_care_plan_addresses_sudep, + expected_score, +): + """ + *PASS* + 1) ALL True + *FAIL* + 1) ANY False + """ + + # create case + case = e12_case_factory( + registration__management__has_rescue_medication_been_prescribed=has_rescue_medication_been_prescribed, + registration__management__individualised_care_plan_parental_prolonged_seizure_care=individualised_care_plan_parental_prolonged_seizure_care, + registration__management__individualised_care_plan_include_first_aid=individualised_care_plan_include_first_aid, + registration__management__individualised_care_plan_addresses_water_safety=individualised_care_plan_addresses_water_safety, + registration__management__individualised_care_plan_includes_service_contact_details=individualised_care_plan_includes_service_contact_details, + registration__management__individualised_care_plan_includes_general_participation_risk=individualised_care_plan_includes_general_participation_risk, + registration__management__individualised_care_plan_addresses_sudep=individualised_care_plan_addresses_sudep, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + # get KPI score + kpi_score = KPI.objects.get( + pk=registration.kpi.pk + ).comprehensive_care_planning_content + + # get KPI incorrectly not failing + if expected_score == KPI_SCORE["PASS"]: + assertion_message = f"has_individualised_care_plan_been_updated_in_the_last_year is True in place but not passing" + + elif expected_score == KPI_SCORE["FAIL"]: + assessed_measures = [ + "has_rescue_medication_been_prescribed", + "individualised_care_plan_parental_prolonged_seizure_care", + "individualised_care_plan_include_first_aid", + "individualised_care_plan_addresses_water_safety", + "individualised_care_plan_includes_service_contact_details", + "individualised_care_plan_includes_general_participation_risk", + "individualised_care_plan_addresses_sudep", + ] + for key, val in vars(registration.management).items(): + if (key in assessed_measures) and not val: + assertion_message = f"{key} is False but not failing" + break + + assert kpi_score == expected_score, assertion_message + + + + +@pytest.mark.parametrize( + f"has_rescue_medication_been_prescribed, individualised_care_plan_parental_prolonged_seizure_care, expected_score", + [ + (True, True, KPI_SCORE["PASS"]), + (True, False, KPI_SCORE["FAIL"]), + (False, False, KPI_SCORE["INELIGIBLE"]), + ], +) +@pytest.mark.django_db +def test_measure_9Bi_parental_prolonged_seizures_care_plan( + e12_case_factory, + has_rescue_medication_been_prescribed, + individualised_care_plan_parental_prolonged_seizure_care, + expected_score, +): + """ + *PASS* + 1) individualised_care_plan_parental_prolonged_seizure_care =True + *FAIL* + 1) individualised_care_plan_parental_prolonged_seizure_care = False + *INELIGIBLE* + 1) has_rescue_medication_been_prescribed = False + """ + + # create case + case = e12_case_factory( + registration__management__has_rescue_medication_been_prescribed=has_rescue_medication_been_prescribed, + registration__management__individualised_care_plan_parental_prolonged_seizure_care=individualised_care_plan_parental_prolonged_seizure_care, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + # get KPI score + kpi_score = KPI.objects.get( + pk=registration.kpi.pk + ).parental_prolonged_seizures_care_plan + + # get KPI incorrectly not failing + if expected_score == KPI_SCORE["PASS"]: + assertion_message = f"individualised_care_plan_parental_prolonged_seizure_care is True but not passing" + + elif expected_score == KPI_SCORE["FAIL"]: + assertion_message = f"individualised_care_plan_parental_prolonged_seizure_care is False but not failing" + else: + assertion_message = f"not on rescue medicine, but not being scored as ineligible" + + assert kpi_score == expected_score, assertion_message + + +@pytest.mark.parametrize( + f"individualised_care_plan_addresses_water_safety, expected_score", + [ + (True, KPI_SCORE["PASS"]), + (False, KPI_SCORE["FAIL"]), + ], +) +@pytest.mark.django_db +def test_measure_9Bii_water_safety( + e12_case_factory, + individualised_care_plan_addresses_water_safety, + expected_score, +): + """ + *PASS* + 1) individualised_care_plan_addresses_water_safety =True + *FAIL* + 1) individualised_care_plan_addresses_water_safety = False + """ + + # create case + case = e12_case_factory( + registration__management__individualised_care_plan_addresses_water_safety=individualised_care_plan_addresses_water_safety, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + # get KPI score + kpi_score = KPI.objects.get( + pk=registration.kpi.pk + ).water_safety + + # get KPI incorrectly not failing + if expected_score == KPI_SCORE["PASS"]: + assertion_message = f"individualised_care_plan_addresses_water_safety is True but not passing" + + elif expected_score == KPI_SCORE["FAIL"]: + assertion_message = f"individualised_care_plan_addresses_water_safety is False but not failing" + + assert kpi_score == expected_score, assertion_message + + +@pytest.mark.parametrize( + f"individualised_care_plan_include_first_aid, expected_score", + [ + (True, KPI_SCORE["PASS"]), + (False, KPI_SCORE["FAIL"]), + ], +) +@pytest.mark.django_db +def test_measure_9Biii_first_aid( + e12_case_factory, + individualised_care_plan_include_first_aid, + expected_score, +): + """ + *PASS* + 1) individualised_care_plan_include_first_aid =True + *FAIL* + 1) individualised_care_plan_include_first_aid = False + """ + + # create case + case = e12_case_factory( + registration__management__individualised_care_plan_include_first_aid=individualised_care_plan_include_first_aid, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + # get KPI score + kpi_score = KPI.objects.get( + pk=registration.kpi.pk + ).first_aid + + # get KPI incorrectly not failing + if expected_score == KPI_SCORE["PASS"]: + assertion_message = f"individualised_care_plan_include_first_aid is True but not passing" + + elif expected_score == KPI_SCORE["FAIL"]: + assertion_message = f"individualised_care_plan_include_first_aid is False but not failing" + + assert kpi_score == expected_score, assertion_message + + +@pytest.mark.parametrize( + f"individualised_care_plan_includes_general_participation_risk, expected_score", + [ + (True, KPI_SCORE["PASS"]), + (False, KPI_SCORE["FAIL"]), + ], +) +@pytest.mark.django_db +def test_measure_9Biv_general_participation_and_risk( + e12_case_factory, + individualised_care_plan_includes_general_participation_risk, + expected_score, +): + """ + *PASS* + 1) individualised_care_plan_includes_general_participation_risk =True + *FAIL* + 1) individualised_care_plan_includes_general_participation_risk = False + """ + + # create case + case = e12_case_factory( + registration__management__individualised_care_plan_includes_general_participation_risk=individualised_care_plan_includes_general_participation_risk, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + # get KPI score + kpi_score = KPI.objects.get( + pk=registration.kpi.pk + ).general_participation_and_risk + + # get KPI incorrectly not failing + if expected_score == KPI_SCORE["PASS"]: + assertion_message = f"individualised_care_plan_includes_general_participation_risk is True but not passing" + + elif expected_score == KPI_SCORE["FAIL"]: + assertion_message = f"individualised_care_plan_includes_general_participation_risk is False but not failing" + + assert kpi_score == expected_score, assertion_message + + +@pytest.mark.parametrize( + f"individualised_care_plan_includes_service_contact_details, expected_score", + [ + (True, KPI_SCORE["PASS"]), + (False, KPI_SCORE["FAIL"]), + ], +) +@pytest.mark.django_db +def test_measure_9Bv_service_contact_details( + e12_case_factory, + individualised_care_plan_includes_service_contact_details, + expected_score, +): + """ + *PASS* + 1) individualised_care_plan_includes_service_contact_details =True + *FAIL* + 1) individualised_care_plan_includes_service_contact_details = False + """ + + # create case + case = e12_case_factory( + registration__management__individualised_care_plan_includes_service_contact_details=individualised_care_plan_includes_service_contact_details, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + # get KPI score + kpi_score = KPI.objects.get( + pk=registration.kpi.pk + ).service_contact_details + + # get KPI incorrectly not failing + if expected_score == KPI_SCORE["PASS"]: + assertion_message = f"individualised_care_plan_includes_service_contact_details is True but not passing" + + elif expected_score == KPI_SCORE["FAIL"]: + assertion_message = f"individualised_care_plan_includes_service_contact_details is False but not failing" + + assert kpi_score == expected_score, assertion_message + + +@pytest.mark.parametrize( + f"individualised_care_plan_parental_prolonged_seizure_care, individualised_care_plan_addresses_sudep, expected_score", + [ + (True, True, KPI_SCORE["PASS"]), + (True, False, KPI_SCORE["FAIL"]), + (False, True, KPI_SCORE["FAIL"]), + ], +) +@pytest.mark.django_db +def test_measure_9Bvi_sudep( + e12_case_factory, + individualised_care_plan_parental_prolonged_seizure_care, + individualised_care_plan_addresses_sudep, + expected_score, +): + """ + *PASS* + 1) BOTH True + *FAIL* + 1) Either False + """ + + # create case + case = e12_case_factory( + registration__management__individualised_care_plan_parental_prolonged_seizure_care=individualised_care_plan_parental_prolonged_seizure_care, + registration__management__individualised_care_plan_addresses_sudep=individualised_care_plan_addresses_sudep, + ) + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + calculate_kpis(registration_instance=registration) + + # get KPI score + kpi_score = KPI.objects.get( + pk=registration.kpi.pk + ).sudep + + # get KPI incorrectly not failing + if expected_score == KPI_SCORE["PASS"]: + assertion_message = f"BOTH \nindividualised_care_plan_parental_prolonged_seizure_care\nindividualised_care_plan_addresses_sudep are True but not passing" + + elif expected_score == KPI_SCORE["FAIL"]: + reason = 'individualised_care_plan_parental_prolonged_seizure_care' if not individualised_care_plan_parental_prolonged_seizure_care else 'individualised_care_plan_addresses_sudep' + assertion_message = f"{reason} is False but not failing" + + assert kpi_score == expected_score, assertion_message \ No newline at end of file diff --git a/epilepsy12/tests/common_view_functions_tests/test_aggregate_by.py b/epilepsy12/tests/common_view_functions_tests/test_aggregate_by.py new file mode 100644 index 00000000..e934791a --- /dev/null +++ b/epilepsy12/tests/common_view_functions_tests/test_aggregate_by.py @@ -0,0 +1,487 @@ +"""Tests for aggregate_by.py functions. +""" + +# python imports +import pytest +import random + +# 3rd party imports + + +# E12 imports +from epilepsy12.common_view_functions import ( + cases_aggregated_by_sex, + cases_aggregated_by_deprivation_score, + cases_aggregated_by_ethnicity, + aggregate_all_eligible_kpi_fields, + all_registered_cases_for_cohort_and_abstraction_level, + calculate_kpis, +) +from epilepsy12.models import ( + Organisation, + Case, + KPI, + Registration, +) +from epilepsy12.constants import ( + SEX_TYPE, + DEPRIVATION_QUINTILES, + ETHNICITIES, + KPI_SCORE, +) +from epilepsy12.tests.common_view_functions_tests.CreateKPIMetrics import KPIMetric + + +@pytest.mark.django_db +def test_cases_aggregated_by_sex(e12_case_factory): + """Tests the cases_aggregated_by_sex fn returns correct count. + + NOTE: There is already 1 seeded Case in the test db. In this test setup, we seed 10 children per SEX_TYPE (n=4). + + Thus expected total count is 10 for each sex, except Male, which is 11. + """ + + # define constants + GOSH = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + # Create 10 cases of each available sex type + for sex_type in SEX_TYPE: + # For each sex, assign 10 cases + e12_case_factory.create_batch( + size=10, + sex=sex_type[0], + registration=None, # ensure related audit factories not generated + organisations__organisation=GOSH, + ) + + cases_queryset = cases_aggregated_by_sex(selected_organisation=GOSH) + + expected_counts = { + "Female": 10, + "Not Known": 10, + "Not Specified": 10, + "Male": 11, + } + + for aggregate in cases_queryset: + SEX = aggregate["sex_display"] + + assert ( + aggregate["sexes"] == expected_counts[SEX] + ), f"`cases_aggregated_by_sex` output does not match expected output for {SEX}. Output {aggregate['sexes']} but expected {expected_counts[SEX]}." + + +@pytest.mark.django_db +def test_cases_aggregated_by_deprivation_score(e12_case_factory, e12_site_factory): + """Tests the cases_aggregated_by_deprivation_score fn returns correct count.""" + + # define constants + CHELWEST = Organisation.objects.get( + ODSCode="RQM01", + ParentOrganisation_ODSCode="RQM", + ) + + # Loop through each deprivation quintile + for deprivation_type in DEPRIVATION_QUINTILES.deprivation_quintiles: + # For each deprivation, assign 10 cases, add to cases_list + e12_case_factory.create_batch( + size=10, + index_of_multiple_deprivation_quintile=deprivation_type, + registration=None, # ensure related audit factories not generated + organisations__organisation=CHELWEST, + ) + + expected_counts = [ + { + "index_of_multiple_deprivation_quintile_display": 1, + "cases_aggregated_by_deprivation": 10, + "index_of_multiple_deprivation_quintile_display_str": "1st quintile", + }, + { + "index_of_multiple_deprivation_quintile_display": 2, + "cases_aggregated_by_deprivation": 10, + "index_of_multiple_deprivation_quintile_display_str": "2nd quintile", + }, + { + "index_of_multiple_deprivation_quintile_display": 3, + "cases_aggregated_by_deprivation": 10, + "index_of_multiple_deprivation_quintile_display_str": "3rd quintile", + }, + { + "index_of_multiple_deprivation_quintile_display": 4, + "cases_aggregated_by_deprivation": 10, + "index_of_multiple_deprivation_quintile_display_str": "4th quintile", + }, + { + "index_of_multiple_deprivation_quintile_display": 5, + "cases_aggregated_by_deprivation": 10, + "index_of_multiple_deprivation_quintile_display_str": "5th quintile", + }, + { + "index_of_multiple_deprivation_quintile_display": 6, + "cases_aggregated_by_deprivation": 10, + "index_of_multiple_deprivation_quintile_display_str": "Not known", + }, + ] + + cases_queryset = cases_aggregated_by_deprivation_score(CHELWEST) + + for ix, aggregate in enumerate(cases_queryset): + assert ( + aggregate == expected_counts[ix] + ), f"Expected aggregate count for cases_aggregated_by_deprivation_score not matching output." + + +@pytest.mark.django_db +def test_cases_aggregated_by_ethnicity(e12_case_factory): + """Tests the cases_aggregated_by_ethnicity fn returns correct count.""" + + # define constants + GOSH = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + # Loop through each ethnicity + for ethnicity_type in ETHNICITIES: + # For each deprivation, assign 10 cases, add to cases_list + e12_case_factory.create_batch( + size=10, + ethnicity=ethnicity_type[0], + registration=None, # ensure related audit factories not generated + organisations__organisation=GOSH, + ) + + cases_queryset = cases_aggregated_by_ethnicity(selected_organisation=GOSH) + + expected_counts = [ + {"ethnicity_display": "Pakistani or British Pakistani", "ethnicities": 10}, + {"ethnicity_display": "Any other Asian background", "ethnicities": 10}, + {"ethnicity_display": "Any other Black background", "ethnicities": 10}, + {"ethnicity_display": "Any other ethnic group", "ethnicities": 10}, + {"ethnicity_display": "Any other mixed background", "ethnicities": 10}, + {"ethnicity_display": "Any other White background", "ethnicities": 10}, + {"ethnicity_display": "Bangladeshi or British Bangladeshi", "ethnicities": 10}, + {"ethnicity_display": "African", "ethnicities": 10}, + {"ethnicity_display": "Caribbean", "ethnicities": 10}, + {"ethnicity_display": "Chinese", "ethnicities": 10}, + {"ethnicity_display": "Indian or British Indian", "ethnicities": 10}, + {"ethnicity_display": "Irish", "ethnicities": 10}, + {"ethnicity_display": "Mixed (White and Asian)", "ethnicities": 10}, + {"ethnicity_display": "Mixed (White and Black African)", "ethnicities": 10}, + {"ethnicity_display": "Mixed (White and Black Caribbean)", "ethnicities": 10}, + {"ethnicity_display": "Not Stated", "ethnicities": 10}, + { + "ethnicity_display": "British, Mixed British", + "ethnicities": 11, + }, # 11 AS THERE IS ALREADY A SEEDED CASE IN TEST DB + ] + + for ix, aggregate in enumerate(cases_queryset): + assert ( + aggregate == expected_counts[ix] + ), f"Expected aggregate count for cases_aggregated_by_ethnicity not matching output: {aggregate} should be {expected_counts[ix]}" + + +@pytest.mark.django_db +def test_aggregate_all_eligible_kpi_fields_correct_fields_present(e12_case_factory): + """Tests the aggregate_all_eligible_kpi_fields fn returns all the KPI fields.""" + + # define constants + GOSH = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + COHORT = 6 + + # create a KPI object + kpi_metric_eligible_3_5_object = KPIMetric( + eligible_kpi_3_5=True, eligible_kpi_6_8_10=False + ) + + # generate answer set dict for e12_case_factory constructor + answers_eligible_3_5 = kpi_metric_eligible_3_5_object.generate_metrics( + kpi_1="PASS", + kpi_2="PASS", + kpi_3="PASS", + kpi_4="PASS", + kpi_5="PASS", + kpi_7="PASS", + kpi_9="PASS", + ) + + e12_case_factory.create_batch( + size=10, + organisations__organisation=GOSH, + **answers_eligible_3_5, + ) + + organisation_level = all_registered_cases_for_cohort_and_abstraction_level( + organisation_instance=GOSH, + cohort=COHORT, + case_complete=False, + abstraction_level="organisation", + ) + + aggregated_kpis = aggregate_all_eligible_kpi_fields(organisation_level) + + all_kpi_measures = [ + "paediatrician_with_expertise_in_epilepsies", + "epilepsy_specialist_nurse", + "tertiary_input", + "epilepsy_surgery_referral", + "ecg", + "mri", + "assessment_of_mental_health_issues", + "mental_health_support", + "sodium_valproate", + "comprehensive_care_planning_agreement", + "patient_held_individualised_epilepsy_document", + "patient_carer_parent_agreement_to_the_care_planning", + "care_planning_has_been_updated_when_necessary", + "comprehensive_care_planning_content", + "parental_prolonged_seizures_care_plan", + "water_safety", + "first_aid", + "general_participation_and_risk", + "service_contact_details", + "sudep", + "school_individual_healthcare_plan", + ] + + for kpi in all_kpi_measures: + assert ( + kpi in aggregated_kpis + ), f"{kpi} not present in aggregate_all_eligible_kpi_fields output." + + assert ( + f"{kpi}_average" + ), f"{kpi}_average not present in aggregate_all_eligible_kpi_fields output." + + assert ( + f"{kpi}_total" + ), f"{kpi}_total not present in aggregate_all_eligible_kpi_fields output." + + +@pytest.mark.django_db +def test_aggregate_all_eligible_kpi_fields_correct_kpi_scoring(e12_case_factory): + """Tests the aggregate_all_eligible_kpi_fields fn returns scoring of KPIs. This is a larger, more complex test. + + For Cases with known KPI scorings, assert the output is correct. + + NOTE: using a different organisation to Cases already seeded in test db. + + METHOD: + - define EXPECTED_KPI_SCORE_OUTPUT dict, all zeros initially + + - Over 50 iterations: + 1) Create a Case with attributes set according to KPI answers (automatically ineligible for KPIs 6,8,10) + 2) Create a Case with attributes set according to KPI answers (automatically ineligible for KPIs 3,5) + + NOTE: + - The Case constructor gets these attributes from get_ans_dict_update_expected_score_dict fn. This fn also updates the EXPECTED_KPI_SCORE_OUTPUT eg. if it determines KPI_1 should 'PASS', it adds 1 to 'paediatrician_with_expertise_in_epilepsies' and 'paediatrician_with_expertise_in_epilepsies_total' + - The Cases are all assigned to the same organisation + + 3) calculate KPIs for each Case + + - assert aggregated_kpis == EXPECTED_KPI_SCORE_OUTPUT + """ + + # define constants + CHELWEST = Organisation.objects.get( + ODSCode="RQM01", + ParentOrganisation_ODSCode="RQM", + ) + + KPI_MAP = { + "kpi_1": ["paediatrician_with_expertise_in_epilepsies"], + "kpi_2": ["epilepsy_specialist_nurse"], + "kpi_3": ["tertiary_input", "epilepsy_surgery_referral"], + "kpi_4": ["ecg"], + "kpi_5": ["mri"], + "kpi_6": ["assessment_of_mental_health_issues"], + "kpi_7": ["mental_health_support"], + "kpi_8": ["sodium_valproate"], + "kpi_9": [ + "comprehensive_care_planning_agreement", + "patient_held_individualised_epilepsy_document", + "patient_carer_parent_agreement_to_the_care_planning", + "care_planning_has_been_updated_when_necessary", + "comprehensive_care_planning_content", + "parental_prolonged_seizures_care_plan", + "water_safety", + "first_aid", + "general_participation_and_risk", + "service_contact_details", + "sudep", + ], + "kpi_10": ["school_individual_healthcare_plan"], + } + + EXPECTED_KPI_SCORE_OUTPUT = { + "paediatrician_with_expertise_in_epilepsies": 0, + "paediatrician_with_expertise_in_epilepsies_total": 0, + "epilepsy_specialist_nurse": 0, + "epilepsy_specialist_nurse_total": 0, + "tertiary_input": 0, + "tertiary_input_total": 0, + "epilepsy_surgery_referral": 0, + "epilepsy_surgery_referral_total": 0, + "ecg": 0, + "ecg_total": 0, + "mri": 0, + "mri_total": 0, + "assessment_of_mental_health_issues": 0, + "assessment_of_mental_health_issues_total": 0, + "mental_health_support": 0, + "mental_health_support_total": 0, + "sodium_valproate": 0, + "sodium_valproate_total": 0, + "comprehensive_care_planning_agreement": 0, + "comprehensive_care_planning_agreement_total": 0, + "patient_held_individualised_epilepsy_document": 0, + "patient_held_individualised_epilepsy_document_total": 0, + "patient_carer_parent_agreement_to_the_care_planning": 0, + "patient_carer_parent_agreement_to_the_care_planning_total": 0, + "care_planning_has_been_updated_when_necessary": 0, + "care_planning_has_been_updated_when_necessary_total": 0, + "comprehensive_care_planning_content": 0, + "comprehensive_care_planning_content_total": 0, + "parental_prolonged_seizures_care_plan": 0, + "parental_prolonged_seizures_care_plan_total": 0, + "water_safety": 0, + "water_safety_total": 0, + "first_aid": 0, + "first_aid_total": 0, + "general_participation_and_risk": 0, + "general_participation_and_risk_total": 0, + "service_contact_details": 0, + "service_contact_details_total": 0, + "sudep": 0, + "sudep_total": 0, + "school_individual_healthcare_plan": 0, + "school_individual_healthcare_plan_total": 0, + "total_number_of_cases": 0, + } + + # Generate KPIMetric objects + kpi_metric_eligible_3_5_object = KPIMetric( + eligible_kpi_3_5=True, eligible_kpi_6_8_10=False + ) + kpi_metric_eligible_6_8_10_object = KPIMetric( + eligible_kpi_3_5=False, eligible_kpi_6_8_10=True + ) + + + def get_ans_dict_update_expected_score_dict( + EXPECTED_KPI_SCORE_OUTPUT, eligible_3_5_only + ): + """ + Generates a random answer dict for the E12CaseFactory constructor (according to KPIMetric's constraints), setting whichever attributes in related models required for each KPI to pass. Also returns an updated EXPECTED_KPI_SCORE_OUTPUT so this function avoids 'side-effects'. + + For nuances regarding eligibile_3_5_only, please see the KPIMetric docstrings. + + """ + + # Dict to be returned + ans_dict = {} + + for kpi_num in range(1, 11): + if eligible_3_5_only: + # The kpi_metric_eligible_3_5_object automatically sets these to ineligible + if kpi_num in [6, 8, 10]: + continue + else: + # The kpi_metric_eligible_6_8_10_object automatically sets these to ineligible + if kpi_num in [3, 5]: + continue + + OUTCOME_CHOICES = ["PASS", "FAIL"] + + # These KPIs can be ineligible from the E12CaseFactory constructor + if kpi_num in [4, 7]: + OUTCOME_CHOICES += ["INELIGIBLE"] + + outcome = random.choice(OUTCOME_CHOICES) + + kpi = f"kpi_{kpi_num}" + kpi_names = KPI_MAP[ + kpi + ] # maps eg. kpi_1 -> paediatrician_with_expertise_in_epilepsies + for kpi_name in kpi_names: + # Update expected answer + if outcome == "PASS": + EXPECTED_KPI_SCORE_OUTPUT[kpi_name] += 1 + EXPECTED_KPI_SCORE_OUTPUT[f"{kpi_name}_total"] += 1 + elif outcome == "FAIL": + # Extra check for `parental_prolonged_seizures_care_plan` if kpi_9 = False => this sub-metric is set to INELIGIBLE in KPIMetric Class. Therefore, DON'T COUNT THIS in numerator nor denominator + if not kpi_name == "parental_prolonged_seizures_care_plan": + EXPECTED_KPI_SCORE_OUTPUT[f"{kpi_name}_total"] += 1 + + # Updated kpi answers for E12CaseFactory constructor + ans_dict.update({kpi: outcome}) + + kpi_metric_object = ( + kpi_metric_eligible_3_5_object + if eligible_3_5_only + else kpi_metric_eligible_6_8_10_object + ) + + ans_dict_return = kpi_metric_object.generate_metrics(**ans_dict) + + return ans_dict_return, EXPECTED_KPI_SCORE_OUTPUT + + for _ in range(50): + # Create and save child with these KPI answers (ELIGIBLE 3 + 5) + ( + answers_3_5_eligible, + EXPECTED_KPI_SCORE_OUTPUT, + ) = get_ans_dict_update_expected_score_dict( + EXPECTED_KPI_SCORE_OUTPUT, eligible_3_5_only=True + ) + + CHILD = e12_case_factory( + organisations__organisation=CHELWEST, **answers_3_5_eligible + ) + EXPECTED_KPI_SCORE_OUTPUT["total_number_of_cases"] += 1 + + registration = Registration.objects.get(case=CHILD) + + calculate_kpis(registration) + + # Create and save child with these KPI answers (ELIGIBLE 6 + 8 + 10) + ( + answers_6_8_10_eligible, + EXPECTED_KPI_SCORE_OUTPUT, + ) = get_ans_dict_update_expected_score_dict( + EXPECTED_KPI_SCORE_OUTPUT, eligible_3_5_only=False + ) + + CHILD = e12_case_factory( + organisations__organisation=CHELWEST, **answers_6_8_10_eligible + ) + EXPECTED_KPI_SCORE_OUTPUT["total_number_of_cases"] += 1 + + registration = Registration.objects.get(case=CHILD) + + calculate_kpis(registration) + + # Add average keys + only_numerators = [kpi for kpi in EXPECTED_KPI_SCORE_OUTPUT.keys() if not (kpi.endswith('_total') or kpi == 'total_number_of_cases')] + + for kpi in only_numerators: + EXPECTED_KPI_SCORE_OUTPUT[f"{kpi}_average"] = EXPECTED_KPI_SCORE_OUTPUT[kpi] / EXPECTED_KPI_SCORE_OUTPUT[f"{kpi}_total"] + + organisation_level = all_registered_cases_for_cohort_and_abstraction_level( + organisation_instance=CHELWEST, + cohort=6, + case_complete=False, + abstraction_level="organisation", + ) + + aggregated_kpis = aggregate_all_eligible_kpi_fields(organisation_level) + + assert aggregated_kpis == EXPECTED_KPI_SCORE_OUTPUT diff --git a/epilepsy12/tests/common_view_functions_tests/test_calculate_kpis.py b/epilepsy12/tests/common_view_functions_tests/test_calculate_kpis.py new file mode 100644 index 00000000..c2d826a8 --- /dev/null +++ b/epilepsy12/tests/common_view_functions_tests/test_calculate_kpis.py @@ -0,0 +1,147 @@ +""" +Tests for the calculate_kpi function. + +Tests +- [x] return None if child not registered in audit (registration.registration_date is None, or registration_eligibility_criteria_met is None or False, Site.site_is_primary_centre_of_epilepsy_care is None) + + +Measure 8 +- [ ] Measure 8 passed (registration.kpi.sodium_valproate == 1) if sex == 2 and on valproate and age >= 12 (line 378) and has_a_valproate_annual_risk_acknowledgement_form_been_completed +- [ ] Measure 8 failed (registration.kpi.sodium_valproate == 1) if sex == 2 and on valproate and age >= 12 (line 378) and not has_a_valproate_annual_risk_acknowledgement_form_been_completed +- [ ] Measure 8 ineligible (registration.kpi.sodium_valproate == 2) if sex == 2 and on valproate and age >= 12 (line 378) + +Measure 9A +- [ ] Measure 9A passed (registration.kpi.comprehensive_care_planning_agreement) if registration_instance.management.individualised_care_plan_in_place +- [ ] Measure 9A failed (registration.kpi.comprehensive_care_planning_agreement) if not registration_instance.management.individualised_care_plan_in_place + +Measure 9i +- [ ] Measure 9i (registration.kpi.patient_held_individualised_epilepsy_document == 1) if (registration_instance.management.individualised_care_plan_in_place and registration_instance.management.individualised_care_plan_has_parent_carer_child_agreement and registration_instance.management.has_individualised_care_plan_been_updated_in_the_last_year) +- [ ] Measure 9i (registration.kpi.patient_held_individualised_epilepsy_document == 0) if not (registration_instance.management.individualised_care_plan_in_place and registration_instance.management.individualised_care_plan_has_parent_carer_child_agreement and registration_instance.management.has_individualised_care_plan_been_updated_in_the_last_year) +- [ ] Measure 9i (registration.kpi.patient_held_individualised_epilepsy_document == 0) if not (registration_instance.management.individualised_care_plan_in_place) and registration_instance.management.individualised_care_plan_has_parent_carer_child_agreement and registration_instance.management.has_individualised_care_plan_been_updated_in_the_last_year) +- [ ] Measure 9i (registration.kpi.patient_held_individualised_epilepsy_document == 0) if registration_instance.management.individualised_care_plan_in_place) and not registration_instance.management.individualised_care_plan_has_parent_carer_child_agreement and registration_instance.management.has_individualised_care_plan_been_updated_in_the_last_year) +- [ ] Measure 9i (registration.kpi.patient_held_individualised_epilepsy_document == 2) if not (registration_instance.management.individualised_care_plan_in_place) + +Measure 9ii +- [ ] Measure 9ii passed (registration.kpi.patient_carer_parent_agreement_to_the_care_planning == 1) if (registration_instance.management.individualised_care_plan_has_parent_carer_child_agreement) +- [ ] Measure 9ii failed (registration.kpi.patient_carer_parent_agreement_to_the_care_planning == 0) if (Not registration_instance.management.individualised_care_plan_has_parent_carer_child_agreement) + +Measure 9iii +- [ ] Measure 9iii passed (registration.kpi.care_planning_has_been_updated_when_necessary == 1) if (registration_instance.management.has_individualised_care_plan_been_updated_in_the_last_year) +- [ ] Measure 9iii failed (registration.kpi.care_planning_has_been_updated_when_necessary == 0) if (Not registration_instance.management.has_individualised_care_plan_been_updated_in_the_last_year) + +Measure 9B +- [ ] Measure 9B passed (registration.kpi.comprehensive_care_planning_content == 1) if ( + registration_instance.management.has_rescue_medication_been_prescribed + and registration_instance.management.individualised_care_plan_parental_prolonged_seizure_care + and registration_instance.management.individualised_care_plan_include_first_aid + and registration_instance.management.individualised_care_plan_addresses_water_safety + and registration_instance.management.individualised_care_plan_includes_service_contact_details + and registration_instance.management.individualised_care_plan_includes_general_participation_risk + and registration_instance.management.individualised_care_plan_addresses_sudep + ) +- [ ] Measure 9B failed (registration.kpi.comprehensive_care_planning_content == 0) if ( + registration_instance.management.has_rescue_medication_been_prescribed + and registration_instance.management.individualised_care_plan_parental_prolonged_seizure_care + and registration_instance.management.individualised_care_plan_include_first_aid + and registration_instance.management.individualised_care_plan_addresses_water_safety + and registration_instance.management.individualised_care_plan_includes_service_contact_details + and registration_instance.management.individualised_care_plan_includes_general_participation_risk + and registration_instance.management.individualised_care_plan_addresses_sudep + ) == False + +Measure 9B i +- [ ] Measure 9B i passed (registration.kpi.parental_prolonged_seizures_care_plan == 1) if ( + registration_instance.management.has_rescue_medication_been_prescribed + and registration_instance.management.individualised_care_plan_parental_prolonged_seizure_care + ) is True +- [ ] Measure 9B i failed (registration.kpi.parental_prolonged_seizures_care_plan == 0) if ( + registration_instance.management.has_rescue_medication_been_prescribed + and registration_instance.management.individualised_care_plan_parental_prolonged_seizure_care + ) is False + +Measure 9B ii +- [ ] Measure 9B ii passed (registration.kpi.water_safety == 1) if (registration_instance.management.individualised_care_plan_addresses_water_safety) +- [ ] Measure 9B ii failed (registration.kpi.water_safety == 0) if not (registration_instance.management.individualised_care_plan_addresses_water_safety) + +Measure 9B iii +- [ ] Measure 9B iii passed (registration.kpi.first_aid == 1) if (if registration_instance.management.individualised_care_plan_include_first_aid) +- [ ] Measure 9B iii failed (registration.kpi.first_aid == 0) if not (if registration_instance.management.individualised_care_plan_include_first_aid) + +Measure 9B iv +- [ ] Measure 9B iv passed (registration.kpi.general_participation_and_risk == 1) if (registration_instance.management.individualised_care_plan_includes_general_participation_risk) +- [ ] Measure 9B iv failed (registration.kpi.general_participation_and_risk == 0) if not (registration_instance.management.individualised_care_plan_includes_general_participation_risk) + +Measure 9B v +- [ ] Measure 9B v passed (registration.kpi.service_contact_details == 1) if (registration_instance.management.individualised_care_plan_includes_service_contact_details) +- [ ] Measure 9B v failed (registration.kpi.service_contact_details == 0) if not (registration_instance.management.individualised_care_plan_includes_service_contact_details) + +Measure 9B vi +- [ ] Measure 9B vi passed (registration.kpi.sudep == 1) if ( + registration_instance.management.individualised_care_plan_parental_prolonged_seizure_care + and registration_instance.management.individualised_care_plan_addresses_sudep + ) +- [ ] Measure 9B vi failed (registration.kpi.sudep == 0) if not( + registration_instance.management.individualised_care_plan_parental_prolonged_seizure_care + and registration_instance.management.individualised_care_plan_addresses_sudep + ): + +Measure 10 +- [ ] Measure 10 passed (registration.kpi.school_individual_healthcare_plan == 1) if (age_at_first_paediatric_assessment >= 5) and ( + registration_instance.management.individualised_care_plan_includes_ehcp + ): +- [ ] Measure 10 failed (registration.kpi.school_individual_healthcare_plan == 0) if not (age_at_first_paediatric_assessment >= 5) and ( + registration_instance.management.individualised_care_plan_includes_ehcp + ): +- [ ] Measure 10 ineligible (registration.kpi.school_individual_healthcare_plan == 2) if not age_at_first_paediatric_assessment < 5 + ): + + +""" + +# Standard imports +import pytest +from datetime import date +from dateutil.relativedelta import relativedelta + +# Third party imports + +# RCPCH imports +from epilepsy12.common_view_functions import calculate_kpis +from epilepsy12.models import ( + Registration, + KPI, +) + + +@pytest.mark.django_db +def test_child_not_registered_in_audit_returns_none(e12_case_factory): + """ + Test that calculate_kpis() returns None if child is not registered in the audit. + """ + + # creates an case with all audit values filled with default values + case = e12_case_factory() + + # overwrite registration_date and eligibility criteria + case.registration.registration_date = None + case.registration.eligibility_criteria_met = None + case.save() + + registration = Registration.objects.get(case=case) + + assert calculate_kpis(registration) is None + + # repeat with eligibility_criteria_met = False + case.registration.registration_date = None + case.registration.eligibility_criteria_met = False + case.save() + + # get registration for the saved case model + registration = Registration.objects.get(case=case) + + assert calculate_kpis(registration) is None + + + + + diff --git a/epilepsy12/tests/conftest.py b/epilepsy12/tests/conftest.py new file mode 100644 index 00000000..24191d9f --- /dev/null +++ b/epilepsy12/tests/conftest.py @@ -0,0 +1,81 @@ +"""conftest.py +Configures pytest fixtures for epilepsy12 app tests. +""" + +# standard imports + +# third-party imports +from pytest_factoryboy import register +import pytest + + +# rcpch imports +from epilepsy12.tests.factories import ( + seed_groups_fixture, + seed_users_fixture, + seed_cases_fixture, + E12AntiEpilepsyMedicineFactory, + E12AssessmentFactory, + E12CaseFactory, + E12ComorbidityFactory, + E12EpilepsyContextFactory, + E12EpisodeFactory, + E12FirstPaediatricAssessmentFactory, + E12ManagementFactory, + E12MultiaxialDiagnosisFactory, + E12RegistrationFactory, + E12SiteFactory, + E12SyndromeFactory, + E12UserFactory, +) +from epilepsy12.models import Organisation, Case + + +# register factories to be used across test directory + +# factory object becomes lowercase-underscore form of the class name +register(E12AntiEpilepsyMedicineFactory) # => e12_anti_epilepsy_medicine_factory +register(E12AssessmentFactory) # => e12_assessment_factory +register(E12CaseFactory) # => e12_case_factory +register(E12ComorbidityFactory) # => e12_comborbidity_factory +register(E12EpilepsyContextFactory) # => e12_epilepsy_context +register(E12EpisodeFactory) # => e12_episode_factory +register( + E12FirstPaediatricAssessmentFactory +) # => e12_first_paediatric_assessment_factory +register(E12ManagementFactory) # => e12_management_factory +register(E12MultiaxialDiagnosisFactory) # => e12_multiaxial_diagnosis_factory +register(E12RegistrationFactory) # => e12_registration_factory +register(E12SiteFactory) # => e12_site_factory +register(E12SyndromeFactory) # => e12_syndrome_factory +register(E12UserFactory) # => e12_user_factory + + +@pytest.fixture +@pytest.mark.django_db +def GOSH(): + return Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + +@pytest.fixture +@pytest.mark.django_db +def CASE_GOSH(): + return Case.objects.get(first_name=f"child_{GOSH.OrganisationName}") + + +@pytest.fixture +@pytest.mark.django_db +def ADDENBROOKES(): + Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + +@pytest.fixture +@pytest.mark.django_db +def CASE_ADDENBROOKES(): + Case.objects.get(first_name=f"child_{ADDENBROOKES.OrganisationName}") diff --git a/epilepsy12/tests/factories/E12AntiEpilepsyMedicineFactory.py b/epilepsy12/tests/factories/E12AntiEpilepsyMedicineFactory.py new file mode 100644 index 00000000..e244fc70 --- /dev/null +++ b/epilepsy12/tests/factories/E12AntiEpilepsyMedicineFactory.py @@ -0,0 +1,29 @@ +"""Factory fn to create new E12 AntiEpilepsyMedicine, related to a management. +""" +# standard imports +from datetime import timedelta + +# third-party imports +import factory + +# rcpch imports +from epilepsy12.models import ( + AntiEpilepsyMedicine, + MedicineEntity, +) + + +class E12AntiEpilepsyMedicineFactory(factory.django.DjangoModelFactory): + """Dependency factory for creating a minimum viable E12Management. + + This Factory is generated AFTER a Management generated. + """ + + class Meta: + model = AntiEpilepsyMedicine + + # Once Management instance made, it will attach to this instance + management = None + + + diff --git a/epilepsy12/tests/factories/E12AssessmentFactory.py b/epilepsy12/tests/factories/E12AssessmentFactory.py new file mode 100644 index 00000000..d8451d9f --- /dev/null +++ b/epilepsy12/tests/factories/E12AssessmentFactory.py @@ -0,0 +1,100 @@ +"""Factory fn to create new E12 Assessments, related to a created Registration. +""" +# standard imports +from datetime import date +from dateutil.relativedelta import relativedelta + +# third-party imports +import factory + +# rcpch imports +from epilepsy12.models import ( + Assessment, + Site, +) + + +class E12AssessmentFactory(factory.django.DjangoModelFactory): + """Dependency factory for creating a minimum viable E12Registration. + + This Factory is generated AFTER a Registration is created. + + Effect on related Site model: + - if relevant referral is True ON CREATION, the relevant Site attribute related to that referral will be True e.g. if `consultant_paediatrician_referral_made=True`, then Site.site_is_general_paediatric_centre will be set to True. These Site attributes are FALSE by default. + + Flags: + - `pass_paediatrician_with_expertise_in_epilepsies` - if True, sets plausible answers so score passes this KPI + - `fail_paediatrician_with_expertise_in_epilepsies` - if True, sets plausible answers so score fails this KPI + """ + + class Meta: + model = Assessment + + # Once Registration instance made, it will attach to this instance + registration = None + + # once assessment filled, will set corresponding Site attributes depending on these values + @factory.post_generation + def set_site_attribute(self, create, extracted, **kwargs): + if create: + site = Site.objects.get(case=self.registration.case) + + if self.consultant_paediatrician_referral_made: + site.site_is_general_paediatric_centre = True + if self.paediatric_neurologist_referral_made: + site.site_is_paediatric_neurology_centre = True + if self.childrens_epilepsy_surgical_service_referral_made: + site.site_is_childrens_epilepsy_surgery_centre = True + + site.save() + + class Params: + + # KPI 1 + pass_paediatrician_with_expertise_in_epilepsies = factory.Trait( + consultant_paediatrician_referral_made=True, + consultant_paediatrician_referral_date=date(2023, 1, 1), + consultant_paediatrician_input_date=date(2023, 1, 2), + ) + + fail_paediatrician_with_expertise_in_epilepsies = factory.Trait( + consultant_paediatrician_referral_made=True, + consultant_paediatrician_referral_date=date(2023, 1, 1), + consultant_paediatrician_input_date=date(2023, 2, 1), + paediatric_neurologist_referral_made=True, + paediatric_neurologist_referral_date=date(2023, 1, 1), + paediatric_neurologist_input_date=date(2023, 2, 1), + ) + + # KPI 2 + pass_epilepsy_specialist_nurse = factory.Trait( + epilepsy_specialist_nurse_referral_made=True, + epilepsy_specialist_nurse_referral_date=factory.LazyAttribute( + lambda o: o.registration.registration_date + relativedelta(days=5) + ), + epilepsy_specialist_nurse_input_date=factory.LazyAttribute( + lambda o: o.epilepsy_specialist_nurse_referral_date + relativedelta(days=5) + ), + ) + + fail_epilepsy_specialist_nurse = factory.Trait( + epilepsy_specialist_nurse_referral_made=False, + ) + + # KPI 3 and 3b + pass_tertiary_input_AND_epilepsy_surgery_referral = factory.Trait( + childrens_epilepsy_surgical_service_referral_criteria_met = True, + paediatric_neurologist_referral_made = True, + childrens_epilepsy_surgical_service_referral_made = True, + ) + + fail_tertiary_input_AND_epilepsy_surgery_referral = factory.Trait( + childrens_epilepsy_surgical_service_referral_criteria_met = True, + paediatric_neurologist_referral_made = False, + childrens_epilepsy_surgical_service_referral_made = False, + ) + + ineligible_tertiary_input_AND_epilepsy_surgery_referral = factory.Trait( + childrens_epilepsy_surgical_service_referral_criteria_met = False, + ) + diff --git a/epilepsy12/tests/factories/E12AuditProgressFactory.py b/epilepsy12/tests/factories/E12AuditProgressFactory.py new file mode 100644 index 00000000..50077776 --- /dev/null +++ b/epilepsy12/tests/factories/E12AuditProgressFactory.py @@ -0,0 +1,43 @@ +""" +E12 Audit Progress Factory. +""" +# standard imports +import datetime + +# third-party imports +import pytest +import factory + +# rcpch imports +from epilepsy12.models import ( + AuditProgress, +) + + +class E12AuditProgressFactory(factory.django.DjangoModelFactory): + """Factory fn to create new E12 AuditProgress""" + + class Meta: + model = AuditProgress + + registration_complete = False + first_paediatric_assessment_complete = False + assessment_complete = False + epilepsy_context_complete = False + multiaxial_diagnosis_complete = False + management_complete = False + investigations_complete = False + registration_total_expected_fields = 3 + registration_total_completed_fields = 0 + first_paediatric_assessment_total_expected_fields = 0 + first_paediatric_assessment_total_completed_fields = 0 + assessment_total_expected_fields = 0 + assessment_total_completed_fields = 0 + epilepsy_context_total_expected_fields = 0 + epilepsy_context_total_completed_fields = 0 + multiaxial_diagnosis_total_expected_fields = 0 + multiaxial_diagnosis_total_completed_fields = 0 + investigations_total_expected_fields = 0 + investigations_total_completed_fields = 0 + management_total_expected_fields = 0 + management_total_completed_fields = 0 diff --git a/epilepsy12/tests/factories/E12CaseFactory.py b/epilepsy12/tests/factories/E12CaseFactory.py new file mode 100644 index 00000000..70211572 --- /dev/null +++ b/epilepsy12/tests/factories/E12CaseFactory.py @@ -0,0 +1,92 @@ +"""Factory fn to create new E12 Cases""" +# standard imports +from datetime import date + +# third-party imports +import factory + +# rcpch imports +from epilepsy12.models import Case +from .E12SiteFactory import E12SiteFactory +from .E12RegistrationFactory import E12RegistrationFactory +from epilepsy12.constants import ( + SEX_TYPE, + DEPRIVATION_QUINTILES + ) +import nhs_number + + +class E12CaseFactory(factory.django.DjangoModelFactory): + """Factory for making E12 Cases, with all associated models (with answers as model-defined defaults, usually None). As default values should be none, KPIs should be 'NOT_SCORED' if no specific flags are passed. + + Using FactoryBoy factories, Traits can be set on related factories, directly on creation of this e12_user_factory. + + Using the KPIMetric class, we take advantage of traits to generate completed audits which pass | fail specified KPIs. Please see CreateKPIMetrics module for full details. + """ + + class Meta: + model = Case + + class Params: + # helper eligibility flags to set age + eligible_kpi_3_5_ineligible_6_8_10 = factory.Trait( + date_of_birth=date(2022, 1, 1) # age = 1y + ) + + eligible_kpi_6_8_10_ineligible_3_5 = factory.Trait( + date_of_birth=date(2011, 1, 1) # age = 12y + ) + + pass_assessment_of_mental_health_issues = factory.Trait( + eligible_kpi_6_8_10_ineligible_3_5=True, + ) + fail_assessment_of_mental_health_issues = factory.Trait( + pass_assessment_of_mental_health_issues=True + ) + + pass_sodium_valproate = factory.Trait( + eligible_kpi_6_8_10_ineligible_3_5=True, + sex=SEX_TYPE[2][0], + ) + fail_sodium_valproate = factory.Trait( + pass_sodium_valproate=True, + ) + + pass_school_individual_healthcare_plan = factory.Trait( + eligible_kpi_6_8_10_ineligible_3_5=True, + ) + fail_school_individual_healthcare_plan = factory.Trait( + pass_school_individual_healthcare_plan=True, + ) + + @factory.lazy_attribute + def nhs_number(self): + """Returns a unique NHS number which has not been used in the db yet.""" + not_found_unique_nhs_num = True + + while not_found_unique_nhs_num: + candidate_num = nhs_number.generate()[0] + + if not Case.objects.filter(nhs_number=candidate_num).exists(): + not_found_unique_nhs_num = False + return candidate_num + + first_name = "Thomas" + surname = "Anderson" + sex = 1 + + # default date of birth (with no Params flags set) will be 2022,1,1 + date_of_birth = date(2022, 1, 1) + + ethnicity = "A" + index_of_multiple_deprivation_quintile=DEPRIVATION_QUINTILES.first + locked = False + + # once case created, create a Site, which acts as a link table between the Case and Organisation + organisations = factory.RelatedFactory(E12SiteFactory, factory_related_name="case") + + # reverse dependency + registration = factory.RelatedFactory( + E12RegistrationFactory, + factory_related_name="case", + ) diff --git a/epilepsy12/tests/factories/E12ComorbidityFactory.py b/epilepsy12/tests/factories/E12ComorbidityFactory.py new file mode 100644 index 00000000..3cebf18a --- /dev/null +++ b/epilepsy12/tests/factories/E12ComorbidityFactory.py @@ -0,0 +1,29 @@ +"""Factory fn to create new E12 Comorbidity, related to a created Registration. +""" +# standard imports +from datetime import timedelta + +# third-party imports +import factory + +# rcpch imports +from epilepsy12.models import ( + Comorbidity, +) + + +class E12ComorbidityFactory(factory.django.DjangoModelFactory): + """Dependency factory for creating a minimum viable E12MaDFactory. + + This Factory is generated AFTER a E12MaDFactory generated. + + """ + class Meta: + model = Comorbidity + + # Once MultiaxialDiagnosis instance made, it will attach to this instance + multiaxial_diagnosis = None + + + + \ No newline at end of file diff --git a/epilepsy12/tests/factories/E12EpilepsyContextFactory.py b/epilepsy12/tests/factories/E12EpilepsyContextFactory.py new file mode 100644 index 00000000..d84a52e3 --- /dev/null +++ b/epilepsy12/tests/factories/E12EpilepsyContextFactory.py @@ -0,0 +1,31 @@ +"""Factory fn to create new E12 EpilepsyContext +""" +# standard imports + + +# third-party imports +import factory + +# rcpch imports +from epilepsy12.models import ( + EpilepsyContext +) + +class E12EpilepsyContextFactory(factory.django.DjangoModelFactory): + + class Meta: + model = EpilepsyContext + + # when a registration instance created, it will attach to this instance + registration = None + + class Params: + pass_ecg = factory.Trait( + were_any_of_the_epileptic_seizures_convulsive = True, + ) + fail_ecg = factory.Trait( + pass_ecg = True, + ) + ineligible_ecg = factory.Trait( + were_any_of_the_epileptic_seizures_convulsive = False, + ) \ No newline at end of file diff --git a/epilepsy12/tests/factories/E12EpisodeFactory.py b/epilepsy12/tests/factories/E12EpisodeFactory.py new file mode 100644 index 00000000..fdde7e03 --- /dev/null +++ b/epilepsy12/tests/factories/E12EpisodeFactory.py @@ -0,0 +1,165 @@ +"""Factory fn to create new E12 Episodes, related to a Multiaxial Diagnosis. +""" +# standard imports +from datetime import date + +# third-party imports +import factory + +# rcpch imports +from epilepsy12.models import ( + Episode, +) +from epilepsy12.constants import ( + DATE_ACCURACY, + EPISODE_DEFINITION, + EPILEPSY_SEIZURE_TYPE, + GENERALISED_SEIZURE_TYPE, + NON_EPILEPSY_SEIZURE_ONSET, + NON_EPILEPSY_SEIZURE_TYPE, + NON_EPILEPSY_BEHAVIOURAL_ARREST_SYMPTOMS, + EPILEPSY_DIAGNOSIS_STATUS, +) + + +class E12EpisodeFactory(factory.django.DjangoModelFactory): + """Dependency factory for creating a minimum viable E12MaDFactory. + + This E12EpisodeFactory is generated AFTER a E12MaDFactory generated. + + Flags: + - `epileptic_seizure_onset_type_generalised`: if True, resets Focal Onset fields, sets Generalised onset with Tonic Clonic + - `epileptic_seizure_onset_type_unknown`: if True, resets Focal Onset fields, sets to unknown seizure type + - `epileptic_seizure_onset_type_Unclassified`: if True, resets Focal Onset fields, sets to unclassified seizure type + - `epilepsy_or_nonepilepsy_status_nonepilepsy`: if True, uses non-epilepsy responses, with first value defined in constants + - `epilepsy_or_nonepilepsy_status_uncertain`: if True, if True, uses uncertain, no further description + """ + + class Meta: + model = Episode + + # Once MultiaxialDiagnosis instance made, it will attach to this instance + multiaxial_diagnosis = None + + class Params: + # Helper flag to reset all fields, used by other traits to start with clean fields + reset = factory.Trait( + focal_onset_impaired_awareness=None, + focal_onset_automatisms=None, + focal_onset_atonic=None, + focal_onset_clonic=None, + focal_onset_left=None, + focal_onset_right=None, + focal_onset_epileptic_spasms=None, + focal_onset_hyperkinetic=None, + focal_onset_myoclonic=None, + focal_onset_tonic=None, + focal_onset_autonomic=None, + focal_onset_behavioural_arrest=None, + focal_onset_cognitive=None, + focal_onset_emotional=None, + focal_onset_sensory=None, + focal_onset_centrotemporal=None, + focal_onset_temporal=None, + focal_onset_frontal=None, + focal_onset_parietal=None, + focal_onset_occipital=None, + focal_onset_gelastic=None, + focal_onset_focal_to_bilateral_tonic_clonic=None, + ) + + # These values don't affect further answer choices + common_fields = factory.Trait( + **{ + "seizure_onset_date": date(2023, 1, 1), + "seizure_onset_date_confidence": DATE_ACCURACY[0][0], + "episode_definition": EPISODE_DEFINITION[0][0], + "has_description_of_the_episode_or_episodes_been_gathered": True, + "description": "The seizure happened when child was watching TV", + } + ) + + # Set fields for completely filled Focal Onset Epilepsy + complete_episode_focal_onset_seizure = factory.Trait( + common_fields=True, + **{ + "epilepsy_or_nonepilepsy_status": EPILEPSY_DIAGNOSIS_STATUS[0][0], + "epileptic_seizure_onset_type": EPILEPSY_SEIZURE_TYPE[0][0], + "focal_onset_left": True, + "focal_onset_impaired_awareness": True, # should not be counted! + } + ) + # Set fields for completely filled Generalised Onset Epilepsy + complete_episode_generalised_onset_seizure = factory.Trait( + common_fields=True, + **{ + "epilepsy_or_nonepilepsy_status": EPILEPSY_DIAGNOSIS_STATUS[0][0], + "epileptic_seizure_onset_type": EPILEPSY_SEIZURE_TYPE[1][0], + "epileptic_generalised_onset": GENERALISED_SEIZURE_TYPE[-3][0], + } + ) + # Set fields for completely filled Unclassified Onset Epilepsy + complete_episode_unclassified_onset_seizure = factory.Trait( + common_fields=True, + **{ + "epilepsy_or_nonepilepsy_status": EPILEPSY_DIAGNOSIS_STATUS[0][0], + "epileptic_seizure_onset_type": EPILEPSY_SEIZURE_TYPE[3][0], + } + ) + # Set fields for completely filled Unclassified Onset Epilepsy + complete_episode_unknown_onset_seizure = factory.Trait( + common_fields=True, + **{ + "epilepsy_or_nonepilepsy_status": EPILEPSY_DIAGNOSIS_STATUS[0][0], + "epileptic_seizure_onset_type": EPILEPSY_SEIZURE_TYPE[2][0], + } + ) + + # Set appropriate fields if Generalised onset type + epileptic_seizure_onset_type_generalised = factory.Trait( + reset=True, + **{ + # 'GO' Generalised onset + "epileptic_seizure_onset_type": EPILEPSY_SEIZURE_TYPE[1][0], + # TCl Tonic-clonic + "epileptic_generalised_onset": GENERALISED_SEIZURE_TYPE[-3][0], + } + ) + + # Set appropriate fields if unknown onset type + epileptic_seizure_onset_type_unknown = factory.Trait( + reset=True, + epileptic_seizure_onset_type=EPILEPSY_SEIZURE_TYPE[2][ + 0 + ], # 'UO' "Unknown onset onset + ) + + # Set appropriate fields if unclassified onset type + epileptic_seizure_onset_type_Unclassified = factory.Trait( + reset=True, + epileptic_seizure_onset_type=EPILEPSY_SEIZURE_TYPE[3][ + 0 + ], # 'UO' Unclassified onset onset + ) + + # Set appropriate fields if episode non-epileptic + epilepsy_or_nonepilepsy_status_nonepilepsy = factory.Trait( + reset=True, + epilepsy_or_nonepilepsy_status=EPILEPSY_DIAGNOSIS_STATUS[1][0], + nonepileptic_seizure_unknown_onset=NON_EPILEPSY_SEIZURE_ONSET[0][ + 0 + ], # Behavioural arrest + nonepileptic_seizure_type=NON_EPILEPSY_SEIZURE_TYPE[0][ + 0 + ], # behavioral psychological and psychiatric disorders + nonepileptic_seizure_behavioural=NON_EPILEPSY_BEHAVIOURAL_ARREST_SYMPTOMS[ + 0 + ][ + 0 + ], # daydreaming / inattention + ) + # Set appropriate fields if episode uncertain + epilepsy_or_nonepilepsy_status_uncertain = factory.Trait( + reset=True, + epilepsy_or_nonepilepsy_status=EPILEPSY_DIAGNOSIS_STATUS[2][0], + ) diff --git a/epilepsy12/tests/factories/E12FirstPaediatricAssessmentFactory.py b/epilepsy12/tests/factories/E12FirstPaediatricAssessmentFactory.py new file mode 100644 index 00000000..3ed45f01 --- /dev/null +++ b/epilepsy12/tests/factories/E12FirstPaediatricAssessmentFactory.py @@ -0,0 +1,22 @@ +"""Factory fn to create new E12 FPAs + +NOTE: calling this factory will cause dependencies to be created automatically e.g. Registration, Case related to an Organisation. +""" +# standard imports + + +# third-party imports +import factory + +# rcpch imports +from epilepsy12.models import ( + FirstPaediatricAssessment +) + +class E12FirstPaediatricAssessmentFactory(factory.django.DjangoModelFactory): + + class Meta: + model = FirstPaediatricAssessment + + # when a registration instance created, it will attach to this instance + registration = None diff --git a/epilepsy12/tests/factories/E12InvestigationsFactory.py b/epilepsy12/tests/factories/E12InvestigationsFactory.py new file mode 100644 index 00000000..8fa19175 --- /dev/null +++ b/epilepsy12/tests/factories/E12InvestigationsFactory.py @@ -0,0 +1,41 @@ +"""Factory fn to create new E12 Investigations, related to a created Registration. +""" +# standard imports +from datetime import date + +# third-party imports +import factory + +# rcpch imports +from epilepsy12.models import ( + Investigations +) + +class E12InvestigationsFactory(factory.django.DjangoModelFactory): + """Dependency factory for creating a minimum viable E12Investigations. + + This Factory is generated AFTER a Registration is created. + + + """ + class Meta: + model = Investigations + + # Once Registration instance made, it will attach to this instance + registration = None + + class Params: + pass_ecg = factory.Trait( + twelve_lead_ecg_status = True, + ) + fail_ecg = factory.Trait( + twelve_lead_ecg_status = False, + ) + pass_mri = factory.Trait( + mri_brain_requested_date=date(2023,2,1), + mri_brain_reported_date=date(2023,2,2), # 1 day later + ) + fail_mri = factory.Trait( + mri_brain_requested_date=date(2023,2,1), + mri_brain_reported_date=date(2023,5,1), # 3 months later (>42 days) + ) \ No newline at end of file diff --git a/epilepsy12/tests/factories/E12ManagementFactory.py b/epilepsy12/tests/factories/E12ManagementFactory.py new file mode 100644 index 00000000..be96196d --- /dev/null +++ b/epilepsy12/tests/factories/E12ManagementFactory.py @@ -0,0 +1,106 @@ +"""Factory fn to create new E12 Management, related to a created Registration. +""" +# standard imports +from datetime import timedelta + +# third-party imports +import factory + +# rcpch imports +from epilepsy12.models import Management +from .E12AntiEpilepsyMedicineFactory import E12AntiEpilepsyMedicineFactory + +from epilepsy12.models import ( + MedicineEntity, +) + + +class E12ManagementFactory(factory.django.DjangoModelFactory): + """Dependency factory for creating a minimum viable E12Management. + + This Factory is generated AFTER a Registration is created. + + Default: + - aed will be sodium valproate, with pregnancy fields automatically filled as True if childbearing age girl + """ + + class Meta: + model = Management + + class Params: + pass_mental_health_support = factory.Trait( + has_support_for_mental_health_support=True + ) + fail_mental_health_support = factory.Trait( + has_support_for_mental_health_support=False + ) + + pass_kpi_9 = factory.Trait( + individualised_care_plan_in_place=True, + individualised_care_plan_has_parent_carer_child_agreement=True, + has_individualised_care_plan_been_updated_in_the_last_year=True, + has_rescue_medication_been_prescribed=True, + individualised_care_plan_parental_prolonged_seizure_care=True, + individualised_care_plan_include_first_aid=True, + individualised_care_plan_addresses_water_safety=True, + individualised_care_plan_includes_service_contact_details=True, + individualised_care_plan_includes_general_participation_risk=True, + individualised_care_plan_addresses_sudep=True, + ) + fail_kpi_9 = factory.Trait( + individualised_care_plan_in_place=False, + individualised_care_plan_has_parent_carer_child_agreement=False, + has_individualised_care_plan_been_updated_in_the_last_year=False, + has_rescue_medication_been_prescribed=False, + individualised_care_plan_parental_prolonged_seizure_care=False, + individualised_care_plan_include_first_aid=False, + individualised_care_plan_addresses_water_safety=False, + individualised_care_plan_includes_service_contact_details=False, + individualised_care_plan_includes_general_participation_risk=False, + individualised_care_plan_addresses_sudep=False, + ) + + pass_school_individual_healthcare_plan = factory.Trait( + individualised_care_plan_includes_ehcp = True, + ) + fail_school_individual_healthcare_plan = factory.Trait( + individualised_care_plan_includes_ehcp = False, + ) + + # Once Registration instance made, it will attach to this instance + registration = None + + @factory.post_generation + def antiepilepsymedicine(self, create, extracted, **kwargs): + # default don't create any AEM + if not create: + return + + if kwargs: + if kwargs.get("sodium_valproate"): + # create a related Valproate AEM instance, with pregnancy prevention fields set depending on sodium_valproate flag. + + self.has_an_aed_been_given = True + + if kwargs["sodium_valproate"] == "pass": + E12AntiEpilepsyMedicineFactory( + management=self, + is_rescue_medicine=False, + medicine_entity=MedicineEntity.objects.get( + medicine_name="Sodium valproate" + ), + antiepilepsy_medicine_risk_discussed=True, + is_a_pregnancy_prevention_programme_needed=True, + has_a_valproate_annual_risk_acknowledgement_form_been_completed=True, + ) + elif kwargs["sodium_valproate"] == "fail": + E12AntiEpilepsyMedicineFactory( + management=self, + is_rescue_medicine=False, + medicine_entity=MedicineEntity.objects.get( + medicine_name="Sodium valproate" + ), + antiepilepsy_medicine_risk_discussed=False, + is_a_pregnancy_prevention_programme_needed=False, + has_a_valproate_annual_risk_acknowledgement_form_been_completed=False, + ) diff --git a/epilepsy12/tests/factories/E12MultiaxialDiagnosisFactory.py b/epilepsy12/tests/factories/E12MultiaxialDiagnosisFactory.py new file mode 100644 index 00000000..15ec70b8 --- /dev/null +++ b/epilepsy12/tests/factories/E12MultiaxialDiagnosisFactory.py @@ -0,0 +1,62 @@ +"""Factory fn to create new E12 Multiaxial diagnoses, related to a created Case. +""" +# standard imports + +# third-party imports +import factory + +# rcpch imports +from epilepsy12.models import ( + MultiaxialDiagnosis, +) +from .E12EpisodeFactory import E12EpisodeFactory +from .E12SyndromeFactory import E12SyndromeFactory + + + +class E12MultiaxialDiagnosisFactory(factory.django.DjangoModelFactory): + class Meta: + model = MultiaxialDiagnosis + + # Once Registration instance made, it will attach to this instance + registration = None + + # Reverse dependency + # comorbidity = factory.RelatedFactory( + # E12ComorbidityFactory, + # factory_related_name="multiaxial_diagnosis", + # ) + + # Reverse dependency + syndrome_entity = factory.RelatedFactory( + E12SyndromeFactory, + factory_related_name="multiaxial_diagnosis", + ) + + # Reverse dependency + episode = factory.RelatedFactory( + E12EpisodeFactory, + factory_related_name="multiaxial_diagnosis", + ) + + class Params: + ineligible_mri = factory.Trait( + syndrome_present=True, + ) + + pass_assessment_of_mental_health_issues = factory.Trait( + mental_health_screen=True + ) + fail_assessment_of_mental_health_issues = factory.Trait( + mental_health_screen=False + ) + + pass_mental_health_support = factory.Trait( + mental_health_issue_identified = True + ) + fail_mental_health_support = factory.Trait( + pass_mental_health_support = True + ) + ineligible_mental_health_support = factory.Trait( + mental_health_issue_identified = False + ) diff --git a/epilepsy12/tests/factories/E12RegistrationFactory.py b/epilepsy12/tests/factories/E12RegistrationFactory.py new file mode 100644 index 00000000..6241a60f --- /dev/null +++ b/epilepsy12/tests/factories/E12RegistrationFactory.py @@ -0,0 +1,108 @@ +"""Factory fn to create new E12 Registrations""" + +# standard imports +from datetime import date + +# third-party imports +import factory + +# rcpch imports +from epilepsy12.models import ( + Site, + Registration, + KPI, +) +from .E12AuditProgressFactory import E12AuditProgressFactory +from .E12MultiaxialDiagnosisFactory import E12MultiaxialDiagnosisFactory +from .E12InvestigationsFactory import E12InvestigationsFactory +from .E12ManagementFactory import E12ManagementFactory +from .E12AssessmentFactory import E12AssessmentFactory +from .E12FirstPaediatricAssessmentFactory import E12FirstPaediatricAssessmentFactory +from .E12EpilepsyContextFactory import E12EpilepsyContextFactory + + +class E12RegistrationFactory(factory.django.DjangoModelFactory): + class Meta: + model = Registration + + # Once Case instance made, it will attach to this instance + case = None + + # Sets the minimal 'required' fields for a registration to be valid + registration_date = date(2023, 1, 1) + eligibility_criteria_met = True + audit_progress = factory.SubFactory(E12AuditProgressFactory) + + # Getting the KPI organisation requires a more complex operation so we use the .lazy_attribute decorator. Once a Registration is .create()'d, filter Sites using the related Case to find the lead organisation - which is used to generate the kpi model. + @factory.lazy_attribute + def kpi(self): + lead_organisation = Site.objects.filter( + case=self.case, + site_is_primary_centre_of_epilepsy_care=True, + site_is_actively_involved_in_epilepsy_care=True, + ).get() + return KPI.objects.create( + organisation=lead_organisation.organisation, + parent_trust=lead_organisation.organisation.ParentOrganisation_OrganisationName, + paediatrician_with_expertise_in_epilepsies=0, + epilepsy_specialist_nurse=0, + tertiary_input=0, + epilepsy_surgery_referral=0, + ecg=0, + mri=0, + assessment_of_mental_health_issues=0, + mental_health_support=0, + sodium_valproate=0, + comprehensive_care_planning_agreement=0, + patient_held_individualised_epilepsy_document=0, + patient_carer_parent_agreement_to_the_care_planning=0, + care_planning_has_been_updated_when_necessary=0, + comprehensive_care_planning_content=0, + parental_prolonged_seizures_care_plan=0, + water_safety=0, + first_aid=0, + general_participation_and_risk=0, + service_contact_details=0, + sudep=0, + school_individual_healthcare_plan=0, + ) + + # Reverse dependencies + first_paediatric_assessment = factory.RelatedFactory( + E12FirstPaediatricAssessmentFactory, factory_related_name="registration" + ) + epilepsy_context = factory.RelatedFactory( + E12EpilepsyContextFactory, factory_related_name="registration" + ) + multiaxial_diagnosis = factory.RelatedFactory( + E12MultiaxialDiagnosisFactory, factory_related_name="registration" + ) + assessment = factory.RelatedFactory( + E12AssessmentFactory, + factory_related_name="registration", + ) + investigations = factory.RelatedFactory( + E12InvestigationsFactory, + factory_related_name="registration", + ) + + @factory.post_generation + def management(self, create, extracted, **kwargs): + if not create: + return None + + sodium_valproate = kwargs.pop('sodium_valproate', None) + + E12ManagementFactory( + registration=self, + antiepilepsymedicine__sodium_valproate=sodium_valproate if sodium_valproate else None, + **kwargs, + ) + + class Params: + ineligible_mri = factory.Trait(registration_date=date(2023, 1, 1)) + + pass_assessment_of_mental_health_issues = factory.Trait(ineligible_mri=True) + fail_assessment_of_mental_health_issues = factory.Trait( + pass_assessment_of_mental_health_issues=True + ) diff --git a/epilepsy12/tests/factories/E12SiteFactory.py b/epilepsy12/tests/factories/E12SiteFactory.py new file mode 100644 index 00000000..952801d7 --- /dev/null +++ b/epilepsy12/tests/factories/E12SiteFactory.py @@ -0,0 +1,29 @@ +"""Factory fn to create new E12 Sites + +A new site is create automatically once `E12CaseFactory.create()` is called. +""" +# standard imports + + +# third-party imports +import factory + +# rcpch imports +from epilepsy12.models import ( + Organisation, + Site, +) + + +class E12SiteFactory(factory.django.DjangoModelFactory): + class Meta: + model = Site + + # define many to many relationship + case = None + + # define many to many relationship + organisation = factory.Iterator(Organisation.objects.all()) + + site_is_actively_involved_in_epilepsy_care = True + site_is_primary_centre_of_epilepsy_care = True diff --git a/epilepsy12/tests/factories/E12SyndromeFactory.py b/epilepsy12/tests/factories/E12SyndromeFactory.py new file mode 100644 index 00000000..0a4651dc --- /dev/null +++ b/epilepsy12/tests/factories/E12SyndromeFactory.py @@ -0,0 +1,32 @@ +"""Factory fn to create new E12 Syndromes, related to a multiaxial diagnosis. +""" +# standard imports +from datetime import timedelta + +# third-party imports +import factory + +# rcpch imports +from epilepsy12.models import Syndrome, SyndromeEntity +from epilepsy12.constants import ( + SYNDROMES, +) + + +class E12SyndromeFactory(factory.django.DjangoModelFactory): + """Dependency factory for creating a minimum viable E12MaDFactory. + + This Factory is generated AFTER a E12MaDFactory generated. + + """ + + class Meta: + model = Syndrome + + # Once MultiaxialDiagnosis instance made, it will attach to this instance + multiaxial_diagnosis = None + + class Params: + ineligible_mri = factory.Trait( + syndrome=factory.LazyAttribute(lambda o: SyndromeEntity.objects.get(syndrome_name=SYNDROMES[18][1])) + ) diff --git a/epilepsy12/tests/factories/E12UserFactory.py b/epilepsy12/tests/factories/E12UserFactory.py new file mode 100644 index 00000000..15336ada --- /dev/null +++ b/epilepsy12/tests/factories/E12UserFactory.py @@ -0,0 +1,53 @@ +"""Factory fn to create new E12 Users. + +Note default values include: + - organisation = GOSH + - is_superuser = False + +The following parameters must be specified: + + - is_staff + - is_rcpch_audit_team_member + - role + +""" +# standard imports + +# third-party imports +import factory +from epilepsy12.models import Epilepsy12User + +# rcpch imports +from epilepsy12.models import ( + Organisation, +) + + +class E12UserFactory(factory.django.DjangoModelFactory): + class Meta: + model = Epilepsy12User # returns the Epilepsy12User object + + email = factory.Sequence(lambda n: f"e12_test_user_{n}@nhs.net") + first_name = "Mandel" + surname = "Brot" + is_active = True + is_superuser = False + email_confirmed = True + organisation_employer = factory.LazyFunction( + lambda: Organisation.objects.filter(ODSCode="RP401").first() + ) + + # Add Groups + @factory.post_generation + def groups(self, create, extracted, **kwargs): + if not create: + return + + # hook into post gen hook to set pass + self.set_password('pw') + + if extracted: + for group in extracted: + self.groups.add(group) + + self.save() diff --git a/epilepsy12/tests/factories/__init__.py b/epilepsy12/tests/factories/__init__.py new file mode 100644 index 00000000..c88d7364 --- /dev/null +++ b/epilepsy12/tests/factories/__init__.py @@ -0,0 +1,17 @@ +from .seed_groups_permissions import seed_groups_fixture +from .seed_cases import seed_cases_fixture +from .seed_users import seed_users_fixture +from .E12AntiEpilepsyMedicineFactory import E12AntiEpilepsyMedicineFactory +from .E12AssessmentFactory import E12AssessmentFactory +from .E12AuditProgressFactory import E12AuditProgressFactory +from .E12CaseFactory import E12CaseFactory +from .E12ComorbidityFactory import E12ComorbidityFactory +from .E12EpisodeFactory import E12EpisodeFactory +from .E12EpilepsyContextFactory import E12EpilepsyContextFactory +from .E12FirstPaediatricAssessmentFactory import E12FirstPaediatricAssessmentFactory +from .E12ManagementFactory import E12ManagementFactory +from .E12MultiaxialDiagnosisFactory import E12MultiaxialDiagnosisFactory +from .E12RegistrationFactory import E12RegistrationFactory +from .E12SiteFactory import E12SiteFactory +from .E12SyndromeFactory import E12SyndromeFactory +from .E12UserFactory import E12UserFactory diff --git a/epilepsy12/tests/factories/seed_cases.py b/epilepsy12/tests/factories/seed_cases.py new file mode 100644 index 00000000..02e4a654 --- /dev/null +++ b/epilepsy12/tests/factories/seed_cases.py @@ -0,0 +1,37 @@ +""" +Seeds 2 E12 Cases in test db once per session. One user in GOSH. One user in different Trust (Addenbrooke's) +""" + +# Standard imports +import pytest + +# 3rd Party imports + +# E12 Imports +from epilepsy12.models import Organisation, Case +from .E12CaseFactory import E12CaseFactory + + +@pytest.mark.django_db +@pytest.fixture(scope="session") +def seed_cases_fixture(django_db_setup, django_db_blocker): + with django_db_blocker.unblock(): + # prevent repeat seed + if not Case.objects.all().exists(): + GOSH = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + ADDENBROOKES = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + for organisation in [GOSH, ADDENBROOKES]: + E12CaseFactory( + first_name=f"child_{organisation.OrganisationName}", + organisations__organisation=organisation, + ) + else: + print("Test cases seeded. Skipping") diff --git a/epilepsy12/tests/factories/seed_groups_permissions.py b/epilepsy12/tests/factories/seed_groups_permissions.py new file mode 100644 index 00000000..b89bf13a --- /dev/null +++ b/epilepsy12/tests/factories/seed_groups_permissions.py @@ -0,0 +1,23 @@ +import pytest + +from django.contrib.auth.models import Group + +from epilepsy12.management.commands.create_groups import groups_seeder + +@pytest.mark.django_db +@pytest.fixture(scope="session") +def seed_groups_fixture(django_db_setup, django_db_blocker): + """ + Fixture which runs once per session to seed groups + verbose=False + """ + with django_db_blocker.unblock(): + + if not Group.objects.all().exists(): + groups_seeder( + run_create_groups=True, + verbose=False, + ) + else: + print('Groups already seeded. Skipping') + diff --git a/epilepsy12/tests/factories/seed_users.py b/epilepsy12/tests/factories/seed_users.py new file mode 100644 index 00000000..314fff73 --- /dev/null +++ b/epilepsy12/tests/factories/seed_users.py @@ -0,0 +1,79 @@ +""" +Seeds E12 Users in test db once per session. +""" + +# Standard imports +import pytest + +# 3rd Party imports +from django.contrib.auth.models import Group + +# E12 Imports +from epilepsy12.tests.UserDataClasses import ( + test_user_audit_centre_administrator_data, + test_user_audit_centre_clinician_data, + test_user_audit_centre_lead_clinician_data, + test_user_rcpch_audit_team_data, + test_user_clinicial_audit_team_data, +) +from epilepsy12.models import ( + Epilepsy12User, + Organisation, +) +from .E12UserFactory import E12UserFactory +from epilepsy12.constants.user_types import ( + RCPCH_AUDIT_TEAM, +) + + +@pytest.mark.django_db +@pytest.fixture(scope="session") +def seed_users_fixture(django_db_setup, django_db_blocker): + users = [ + test_user_audit_centre_administrator_data, + test_user_audit_centre_clinician_data, + test_user_audit_centre_lead_clinician_data, + test_user_rcpch_audit_team_data, + test_user_clinicial_audit_team_data, + ] + + with django_db_blocker.unblock(): + # Don't repeat seed + if not Epilepsy12User.objects.exists(): + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + is_active = True + is_staff = False + is_rcpch_audit_team_member = False + is_rcpch_staff = False + + # seed a user of each type at GOSH + for user in users: + + first_name=user.role_str + + # set RCPCH AUDIT TEAM MEMBER ATTRIBUTE + if user.role == RCPCH_AUDIT_TEAM: + is_rcpch_audit_team_member = True + is_rcpch_staff = True + + if user.is_clinical_audit_team: + is_rcpch_audit_team_member = True + first_name='CLINICAL_AUDIT_TEAM' + + E12UserFactory( + first_name=first_name, + role=user.role, + # Assign flags based on user role + is_active=is_active, + is_staff=is_staff, + is_rcpch_audit_team_member=is_rcpch_audit_team_member, + is_rcpch_staff=is_rcpch_staff, + organisation_employer=TEST_USER_ORGANISATION, + groups=[user.group_name], + ) + else: + print("Test users already seeded. Skipping") diff --git a/epilepsy12/tests/general_functions_tests/test_nhs_number.py b/epilepsy12/tests/general_functions_tests/test_nhs_number.py new file mode 100644 index 00000000..64863639 --- /dev/null +++ b/epilepsy12/tests/general_functions_tests/test_nhs_number.py @@ -0,0 +1,37 @@ +"""Tests for the NHS Number functions used in E12 +""" + +import pytest + +from epilepsy12.constants import VALID_NHS_NUMS +from epilepsy12.general_functions import validate_nhs_number + + +def test_constants_valid_nhs_nums(): + """Runs `validate_nhs_number` through the VALID_NHS_NUMS constants list, ensuring all are valid""" + for num in VALID_NHS_NUMS: + assert validate_nhs_number(num)["valid"] + + +def test_valid_nhs_number_function_works(): + """Runs `validate_nhs_number` through 2 lists: + 1) valid_nhs_numbers, asserts ALL return True + 2) invalid_nhs_numbers, asserts ALL return False + """ + valid_nhs_numbers = VALID_NHS_NUMS[:9] + invalid_nhs_numbers = [ + "969 003 9564", + "969 003 9565", + "969 003 9566", + "434 151 9744", + "434 151 9745", + "434 151 9746", + "025 845 8594", + "025 845 8595", + "025 845 8596", + ] + + for num in valid_nhs_numbers: + assert validate_nhs_number(num)["valid"] + for num in invalid_nhs_numbers: + assert not validate_nhs_number(num)["valid"] diff --git a/epilepsy12/tests/misc_py_shell_code.py b/epilepsy12/tests/misc_py_shell_code.py new file mode 100644 index 00000000..9c6c8016 --- /dev/null +++ b/epilepsy12/tests/misc_py_shell_code.py @@ -0,0 +1,55 @@ +""" +This file houses code to be copied and pasted easily into the Django Python shell. +""" + + +# Seeds test db users according to role + permissions. + +from django.contrib.auth.models import Group +from epilepsy12.tests.UserDataClasses import ( + test_user_audit_centre_administrator_data, + test_user_audit_centre_clinician_data, + test_user_audit_centre_lead_clinician_data, + test_user_rcpch_audit_team_data, + test_user_clinicial_audit_team_data, +) + +from epilepsy12.models import Organisation +from epilepsy12.tests.factories.E12UserFactory import E12UserFactory +from epilepsy12.constants.user_types import RCPCH_AUDIT_TEAM + +users = [ + test_user_audit_centre_administrator_data, + test_user_audit_centre_clinician_data, + test_user_audit_centre_lead_clinician_data, + test_user_rcpch_audit_team_data, + test_user_clinicial_audit_team_data, +] + +TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", +) + +E12UserFactory( + first_name=test_user_audit_centre_administrator_data.role_str, + role=test_user_audit_centre_administrator_data.role, + # Assign flags based on user role + is_active=True, + is_staff=False, + is_rcpch_audit_team_member=False, + is_rcpch_staff=False, + organisation_employer=TEST_USER_ORGANISATION, + groups=[test_user_audit_centre_administrator_data.group_name], +) +E12UserFactory( + first_name=test_user_rcpch_audit_team_data.role_str, + role=test_user_rcpch_audit_team_data.role, + # Assign flags based on user role + is_active=True, + is_staff=False, + is_rcpch_audit_team_member=False, + is_rcpch_staff=False, + organisation_employer=TEST_USER_ORGANISATION, + groups=[test_user_rcpch_audit_team_data.group_name], +) diff --git a/epilepsy12/tests/model_tests/test_antiepilepsy_medicine.py b/epilepsy12/tests/model_tests/test_antiepilepsy_medicine.py new file mode 100644 index 00000000..35b5769f --- /dev/null +++ b/epilepsy12/tests/model_tests/test_antiepilepsy_medicine.py @@ -0,0 +1,14 @@ +"""Tests for AntieEpilepsyMedicine model +""" +# Standard imports +from datetime import date + +# Third party imports +import pytest + +# RCPCH imports +from epilepsy12.models import ( + AntiEpilepsyMedicine, +) + + diff --git a/epilepsy12/tests/model_tests/test_assessment.py b/epilepsy12/tests/model_tests/test_assessment.py new file mode 100644 index 00000000..db8060b8 --- /dev/null +++ b/epilepsy12/tests/model_tests/test_assessment.py @@ -0,0 +1,307 @@ +""" +Tests the Assessment model +""" + +# Standard imports +from datetime import date +from dateutil.relativedelta import relativedelta +import pytest +from unittest.mock import patch + +# Third party imports +from django.core.exceptions import ValidationError + +# RCPCH imports +from epilepsy12.models import ( + Assessment, +) + +""" +- [x] Assessment.consultant_paediatrician_referral_date and Assessment.consultant_paediatrician_input_date are both None if Assessment.consultant_paediatrician_referral_made is False +- [x] Assessment.consultant_paediatrician_referral_date and Assessment.consultant_paediatrician_input_date cannot be in the future +- [x] Assessment.consultant_paediatrician_referral_date cannot be after Assessment.consultant_paediatrician_input_date +- [x] Neither Assessment.consultant_paediatrician_referral_date nor Assessment.consultant_paediatrician_input_date can be before Registration.registration_date or Case.date_of_birth +- [x] Assessment.paediatric_neurologist_input_date and Assessment.paediatric_neurologist_referral_date are both None if Assessment.paediatric_neurologist_referral_made is False +- [x] Assessment.paediatric_neurologist_input_date and Assessment.paediatric_neurologist_referral_date cannot be in the future +- [x] Assessment.paediatric_neurologist_input_date cannot be after Assessment.paediatric_neurologist_referral_date +- [x] Neither Assessment.paediatric_neurologist_input_date nor Assessment.paediatric_neurologist_referral_date can be before Registration.registration_date or Case.date_of_birth +- [x] Assessment.childrens_epilepsy_surgical_service_input_date and Assessment.childrens_epilepsy_surgical_service_referral_date are both None if Assessment.childrens_epilepsy_surgical_service_referral_made is False +- [x] Assessment.childrens_epilepsy_surgical_service_input_date and Assessment.childrens_epilepsy_surgical_service_referral_date cannot be in the future +- [x] Assessment.childrens_epilepsy_surgical_service_input_date cannot be after Assessment.childrens_epilepsy_surgical_service_referral_date +- [x] Neither Assessment.childrens_epilepsy_surgical_service_input_date nor Assessment.childrens_epilepsy_surgical_service_referral_date can be before Registration.registration_date or Case.date_of_birth +- [x] Assessment.epilepsy_specialist_nurse_input_date and Assessment.epilepsy_specialist_nurse_referral_date are both None if Assessment.epilepsy_specialist_nurse_referral_made is False +- [x] Assessment.epilepsy_specialist_nurse_input_date and Assessment.epilepsy_specialist_nurse_referral_date cannot be in the future +- [x] Assessment.epilepsy_specialist_nurse_input_date cannot be after Assessment.epilepsy_specialist_nurse_referral_date +- [x] Neither Assessment.epilepsy_specialist_nurse_input_date nor Assessment.epilepsy_specialist_nurse_referral_date can be before Registration.registration_date or Case.date_of_birth +""" + + +@pytest.mark.django_db +def test_validation_referral_date_and_input_date_both_none_when_referral_made_false( + e12_case_factory, +): + """ + Tests: + - Assessment.consultant_paediatrician_referral_date and Assessment.consultant_paediatrician_input_date are both None if Assessment.consultant_paediatrician_referral_made is False. + - Assessment.paediatric_neurologist_referral_date and Assessment.paediatric_neurologist_input_date are both None if Assessment.paediatric_neurologist_referral_made is False. + - Assessment.childrens_epilepsy_surgical_service_referral_date and Assessment.childrens_epilepsy_surgical_service_input_date are both None if Assessment.childrens_epilepsy_surgical_service_referral_made is False. + - Assessment.epilepsy_specialist_nurse_referral_date and Assessment.epilepsy_specialist_nurse_input_date are both None if Assessment.epilepsy_specialist_nurse_referral_made is False. + """ + + assessment = e12_case_factory( + registration__assessment__consultant_paediatrician_referral_made=False, + registration__assessment__paediatric_neurologist_referral_made=False, + registration__assessment__childrens_epilepsy_surgical_service_referral_made=False, + registration__assessment__epilepsy_specialist_nurse_referral_made=False, + ).registration.assessment + + assert (assessment.consultant_paediatrician_referral_date is None) and ( + assessment.consultant_paediatrician_input_date is None + ) + assert (assessment.paediatric_neurologist_referral_date is None) and ( + assessment.paediatric_neurologist_input_date is None + ) + assert (assessment.childrens_epilepsy_surgical_service_referral_date is None) and ( + assessment.childrens_epilepsy_surgical_service_input_date is None + ) + assert (assessment.epilepsy_specialist_nurse_referral_date is None) and ( + assessment.epilepsy_specialist_nurse_input_date is None + ) + + +@pytest.mark.xfail +@patch.object(Assessment, "get_current_date", return_value=date(2023, 10, 1)) +@pytest.mark.django_db +def test_validation_referral_date_and_input_date_cant_be_future( + mocked_get_current_date, + e12_case_factory, +): + """ + Tests: + - Assessment.consultant_paediatrician_referral_date and Assessment.consultant_paediatrician_input_date cannot be in the future relative to today. + - Assessment.paediatric_neurologist_referral_date and Assessment.paediatric_neurologist_input_date cannot be in the future relative to today. + - Assessment.childrens_epilepsy_surgical_service_referral_date and Assessment.childrens_epilepsy_surgical_service_input_date cannot be in the future relative to today. + - Assessment.epilepsy_specialist_nurse_referral_date and Assessment.epilepsy_specialist_nurse_input_date cannot be in the future relative to today. + + Patches .get_current_date method. + """ + + # try saving referral and input date which are 1 month ahead of patched today + referral_date = date(2023, 11, 1) + input_date = date(2023, 11, 1) + + with pytest.raises(ValidationError): + assessment = e12_case_factory( + registration__assessment__consultant_paediatrician_referral_date=referral_date, + registration__assessment__consultant_paediatrician_input_date=input_date, + ).registration.assessment + with pytest.raises(ValidationError): + assessment = e12_case_factory( + registration__assessment__paediatric_neurologist_referral_date=referral_date, + registration__assessment__paediatric_neurologist_input_date=input_date, + ).registration.assessment + with pytest.raises(ValidationError): + assessment = e12_case_factory( + registration__assessment__childrens_epilepsy_surgical_service_referral_date=referral_date, + registration__assessment__childrens_epilepsy_surgical_service_input_date=input_date, + ).registration.assessment + with pytest.raises(ValidationError): + assessment = e12_case_factory( + registration__assessment__epilepsy_specialist_nurse_referral_date=referral_date, + registration__assessment__epilepsy_specialist_nurse_input_date=input_date, + ).registration.assessment + + +@pytest.mark.xfail +@pytest.mark.django_db +def test_validation_consultant_paediatrician_input_date_cant_be_after_referral_date( + e12_case_factory, +): + """ + - Tests Assessment.consultant_paediatrician_referral_date cannot be after Assessment.consultant_paediatrician_input_date. + - Tests Assessment.paediatric_neurologist_referral_date cannot be after Assessment.paediatric_neurologist_input_date. + - Tests Assessment.childrens_epilepsy_surgical_service_input_date cannot be after Assessment.childrens_epilepsy_surgical_service_referral_date. + - Tests Assessment.epilepsy_specialist_nurse_input_date cannot be after Assessment.epilepsy_specialist_nurse_referral_date. + + """ + + referral_date = date(2023, 1, 1) + input_date = date(2023, 2, 1) + + with pytest.raises(ValidationError): + assessment = e12_case_factory( + registration__assessment__consultant_paediatrician_referral_date=referral_date, + registration__assessment__consultant_paediatrician_input_date=input_date, + ).registration.assessment + with pytest.raises(ValidationError): + assessment = e12_case_factory( + registration__assessment__paediatric_neurologist_referral_date=referral_date, + registration__assessment__paediatric_neurologist_input_date=input_date, + ).registration.assessment + with pytest.raises(ValidationError): + assessment = e12_case_factory( + registration__assessment__childrens_epilepsy_surgical_service_referral_date=referral_date, + registration__assessment__childrens_epilepsy_surgical_service_input_date=input_date, + ).registration.assessment + with pytest.raises(ValidationError): + assessment = e12_case_factory( + registration__assessment__epilepsy_specialist_nurse_referral_date=referral_date, + registration__assessment__epilepsy_specialist_nurse_input_date=input_date, + ).registration.assessment + + +@pytest.mark.xfail +@pytest.mark.django_db +def test_validation_consultant_paediatrician_referral_date_nor_input_date_before_registration_date_or_dob( + e12_case_factory, +): + """ + Tests + - neither Assessment.consultant_paediatrician_referral_date nor Assessment.consultant_paediatrician_input_date can be before Registration.registration_date or Case.date_of_birth + - neither Assessment.paediatric_neurologist_referral_date nor Assessment.paediatric_neurologist_input_date can be before Registration.registration_date or Case.date_of_birth + - neither Assessment.childrens_epilepsy_surgical_service_referral_date nor Assessment.childrens_epilepsy_surgical_service_input_date can be before Registration.registration_date or Case.date_of_birth + - neither Assessment.epilepsy_specialist_nurse_referral_date nor Assessment.epilepsy_specialist_nurse_input_date can be before Registration.registration_date or Case.date_of_birth + """ + + date_of_birth = date(2020, 1, 1) + registration_date = date(2022, 1, 1) + referral_date = date_of_birth - relativedelta( + days=1 + ) # one day before date of birth + input_date = registration_date - relativedelta( + days=1 + ) # one day before registration date + + with pytest.raises(ValidationError): + assessment = e12_case_factory( + date_of_birth=date_of_birth, + registration_date=registration_date, + registration__assessment__consultant_paediatrician_referral_date=referral_date, + registration__assessment__consultant_paediatrician_input_date=input_date, + ).registration.assessment + with pytest.raises(ValidationError): + assessment = e12_case_factory( + date_of_birth=date_of_birth, + registration_date=registration_date, + registration__assessment__paediatric_neurologist_referral_date=referral_date, + registration__assessment__paediatric_neurologist_input_date=input_date, + ).registration.assessment + with pytest.raises(ValidationError): + assessment = e12_case_factory( + date_of_birth=date_of_birth, + registration_date=registration_date, + registration__assessment__childrens_epilepsy_surgical_service_referral_date=referral_date, + registration__assessment__childrens_epilepsy_surgical_service_input_date=input_date, + ).registration.assessment + with pytest.raises(ValidationError): + assessment = e12_case_factory( + date_of_birth=date_of_birth, + registration_date=registration_date, + registration__assessment__epilepsy_specialist_nurse_referral_date=referral_date, + registration__assessment__epilepsy_specialist_nurse_input_date=input_date, + ).registration.assessment + + +# The following tests check if the calculated wait times for each service are correct. +# They all use the same function (epilepsy12.general_functions.time_elapsed.stringify_time_elapsed) +# behind the scenes, so the tests are designed to cover the range of cases handled by the logic in that function. + + +@pytest.mark.xfail +@pytest.mark.django_db +def test_consultant_paediatrician_wait_only_one_date(e12_case_factory): + """ + Tests for an error when only one date is supplied + """ + # Create an Assessment object with referral and input dates for a consultant paediatrician + assessment = e12_case_factory( + registration__assessment__consultant_paediatrician_input_date=None + ).registration.assessment + + # Only one date was supplied so an error should be raised + with pytest.raises(ValueError): + assessment.consultant_paediatrician_wait() + + +@pytest.mark.django_db +def test_consultant_paediatrician_wait_days(e12_case_factory): + """ + Tests output when the wait is in days + """ + assessment = e12_case_factory( + registration__assessment__consultant_paediatrician_referral_date=date( + 2023, 1, 1 + ), + registration__assessment__consultant_paediatrician_input_date=date(2023, 1, 3), + ).registration.assessment + + # Check if the calculated wait time for the consultant paediatrician is correct + assert assessment.consultant_paediatrician_wait() == 2 + + +@pytest.mark.django_db +def test_consultant_paediatrician_wait_weeks(e12_case_factory): + """ + Tests output when the wait is in weeks + """ + referral_date = date(2023, 1, 1) + input_date = referral_date + relativedelta(weeks=2) + + assessment = e12_case_factory( + registration__assessment__consultant_paediatrician_referral_date=referral_date, + registration__assessment__consultant_paediatrician_input_date=input_date, + ).registration.assessment + + # Check if the calculated wait time for the consultant paediatrician is correct + assert assessment.consultant_paediatrician_wait() == 14 + + +@pytest.mark.django_db +def test_paediatric_neurologist_wait_months(e12_case_factory): + """ + Tests output when the wait is in months + """ + referral_date = date(2023, 1, 1) + input_date = referral_date + relativedelta(months=4) + + assessment = e12_case_factory( + registration__assessment__paediatric_neurologist_referral_date=referral_date, + registration__assessment__paediatric_neurologist_input_date=input_date, + ).registration.assessment + + # Check if the calculated wait time for the paediatric neurologist is correct + assert assessment.paediatric_neurologist_wait() == 120 + + +@pytest.mark.django_db +def test_childrens_epilepsy_surgery_wait_years(e12_case_factory): + """ + Tests output when the wait is in years + """ + referral_date = date(2023, 1, 1) + input_date = referral_date + relativedelta(years=2, months=2) + + assessment = e12_case_factory( + registration__assessment__childrens_epilepsy_surgical_service_referral_date=referral_date, + registration__assessment__childrens_epilepsy_surgical_service_input_date=input_date, + ).registration.assessment + + # Check if the calculated wait time for the children's epilepsy surgery service is correct + assert assessment.childrens_epilepsy_surgery_wait() == 790 + + +@pytest.mark.django_db +def test_epilepsy_nurse_specialist_wait_same_day(e12_case_factory): + """ + Tests output when the wait is in 0 days (same day) + """ + referral_date = date(2023, 1, 1) + input_date = referral_date + + assessment = e12_case_factory( + registration__assessment__epilepsy_specialist_nurse_referral_date=referral_date, + registration__assessment__epilepsy_specialist_nurse_input_date=input_date, + ).registration.assessment + + # Check if the calculated wait time for the epilepsy nurse specialist is correct + assert assessment.epilepsy_nurse_specialist_wait() == 0 diff --git a/epilepsy12/tests/model_tests/test_audit_progress.py b/epilepsy12/tests/model_tests/test_audit_progress.py new file mode 100644 index 00000000..663b41e1 --- /dev/null +++ b/epilepsy12/tests/model_tests/test_audit_progress.py @@ -0,0 +1,73 @@ +""" +Tests the Audit Progress model +""" + +# Standard imports +import pytest + +# Third party imports + +# RCPCH imports +from epilepsy12.models import AuditProgress, Registration +from epilepsy12.common_view_functions.recalculate_form_generate_response import ( + update_audit_progress, +) + +@pytest.mark.django_db +def test_audit_progress_creation( + e12_case_factory, +): + """Creates an audit progress with audit values filled using default values. + """ + case = e12_case_factory().registration.audit_progress + + assert case.registration.audit_progress + +# @pytest.mark.django_db +# def test_audit_progress_creation(e12Registration_2022): +# """ +# Tests that an empty AuditProgress object can be created and has correct initial values +# """ +# # Create an AuditProgress object +# audit_progress = AuditProgress.objects.create() + +# # Check that the AuditProgress has the correct initial values +# assert audit_progress.registration_complete is False +# assert audit_progress.registration_total_expected_fields == 0 +# assert audit_progress.registration_total_completed_fields == 0 +# assert audit_progress.audit_complete is False + +# # Add the Registration object to the AuditProgress object +# # Update the AuditProgress object +# update_audit_progress(e12Registration_2022) + +# assert e12Registration_2022.audit_progress.registration_complete is False +# assert e12Registration_2022.audit_progress.registration_total_expected_fields == 3 + + +# first_paediatric_assessment_complete +# first_paediatric_assessment_total_expected_fields +# first_paediatric_assessment_total_completed_fields + +# assessment_complete +# assessment_total_expected_fields +# assessment_total_completed_fields + +# epilepsy_context_complete +# epilepsy_context_total_expected_fields +# epilepsy_context_total_completed_fields + +# multiaxial_diagnosis_complete +# multiaxial_diagnosis_total_expected_fields +# multiaxial_diagnosis_total_completed_fields + +# investigations_complete +# investigations_total_expected_fields +# investigations_total_completed_fields + +# management_complete +# management_total_expected_fields +# management_total_completed_fields + +# total_completed_fields +# total_expected_fields diff --git a/epilepsy12/tests/model_tests/test_case.py b/epilepsy12/tests/model_tests/test_case.py new file mode 100644 index 00000000..bafee16f --- /dev/null +++ b/epilepsy12/tests/model_tests/test_case.py @@ -0,0 +1,72 @@ +""" +Tests the Case model +""" + +# Standard imports +import pytest +from datetime import date + +# Third party imports + +# RCPCH imports + + +@pytest.mark.django_db +def test_case_age_days_calculation(e12_case_factory): + # Test that the age function works as expected + + e12Case = e12_case_factory(date_of_birth=date(2018, 5, 11)) + + fixed_testing_date = date(2023, 6, 17) + + assert ( + e12Case.age_days(fixed_testing_date) == 1863 + ), "Incorrect age" # "5 years, 1 month" + + +@pytest.mark.django_db +def test_case_age_calculation(e12_case_factory): + # Test that the age function works as expected + + e12Case = e12_case_factory(date_of_birth=date(2018, 5, 11)) + + fixed_testing_date = date(2023, 6, 17) + + assert ( + e12Case.age(fixed_testing_date) == "5 years, 1 month" + ), "Incorrect calendar age" + + +@pytest.mark.django_db +def test_case_organisation_assignment(e12_case_factory): + """Test that case has organisation(s) assigned""" + e12Case = e12_case_factory() + assert e12Case.organisations.count() > 0 + + +@pytest.mark.django_db +def test_case_save_unknown_postcode(e12_case_factory): + # Tests that the save method works as expected using one of the 'unknown' postcodes + e12Case = e12_case_factory(index_of_multiple_deprivation_quintile=None) + e12Case.postcode = "ZZ99 3CZ" + e12Case.save() + assert e12Case.index_of_multiple_deprivation_quintile is None + + +@pytest.mark.django_db +def test_case_save_postcode_obtain_imdq(e12_case_factory): + # Tests that the save method works as expected using a known postcode IMD + e12Case = e12_case_factory() + e12Case.postcode = "WC1X 8SH" # RCPCH address + e12Case.save() + assert e12Case.index_of_multiple_deprivation_quintile == 4 + + +@pytest.mark.django_db +def test_case_save_invalid_postcode(e12_case_factory): + # Tests that the save method works as expected using an invalid postcode + e12Case = e12_case_factory() + e12Case.postcode = "GARBAGE" + e12Case.save() + assert e12Case.postcode == "GARBAGE" + assert e12Case.index_of_multiple_deprivation_quintile is None diff --git a/epilepsy12/tests/model_tests/test_comorbidity.py b/epilepsy12/tests/model_tests/test_comorbidity.py new file mode 100644 index 00000000..a0de6a75 --- /dev/null +++ b/epilepsy12/tests/model_tests/test_comorbidity.py @@ -0,0 +1,14 @@ +""" +Tests the Comorbidity model +""" + +# Standard imports +import pytest + +# Third party imports + +# RCPCH imports +from epilepsy12.models import Comorbidity + + + \ No newline at end of file diff --git a/epilepsy12/tests/model_tests/test_entities.py b/epilepsy12/tests/model_tests/test_entities.py new file mode 100644 index 00000000..3aa0f70b --- /dev/null +++ b/epilepsy12/tests/model_tests/test_entities.py @@ -0,0 +1,45 @@ +""" +Tests for the entities. + +These should all be seeded during test db migrations. +""" + +# Standard imports +import pytest + + +# Third party imports + +# RCPCH imports +from epilepsy12.models import ( + ComorbidityEntity, + EpilepsyCauseEntity, + IntegratedCareBoardEntity, + Keyword, + MedicineEntity, + NHSRegionEntity, + ONSCountryEntity, + OPENUKNetworkEntity, + Organisation, + SyndromeEntity, +) + + +@pytest.mark.parametrize( + "entity", + [ + ComorbidityEntity, + EpilepsyCauseEntity, + IntegratedCareBoardEntity, + Keyword, + MedicineEntity, + NHSRegionEntity, + ONSCountryEntity, + OPENUKNetworkEntity, + Organisation, + SyndromeEntity, + ], +) +@pytest.mark.django_db +def test_entity_exists(entity): + assert entity.objects.exists(), f"{entity} not found in database" diff --git a/epilepsy12/tests/model_tests/test_epilepsy_context.py b/epilepsy12/tests/model_tests/test_epilepsy_context.py new file mode 100644 index 00000000..ead01768 --- /dev/null +++ b/epilepsy12/tests/model_tests/test_epilepsy_context.py @@ -0,0 +1,26 @@ +""" +Tests the Epilepsy Context model. + +Tests: + + - +""" + +# Standard imports + +# Third party imports +import pytest + +# RCPCH imports +from epilepsy12.models import ( + Case +) + +@pytest.mark.django_db +def test_epilepsy_context_valid_creation( + e12_case_factory, +): + + case = e12_case_factory() + + assert Case.objects.filter(registration = case.registration).exists() diff --git a/epilepsy12/tests/model_tests/test_first_paediatric_assessment.py b/epilepsy12/tests/model_tests/test_first_paediatric_assessment.py new file mode 100644 index 00000000..ff0d7adf --- /dev/null +++ b/epilepsy12/tests/model_tests/test_first_paediatric_assessment.py @@ -0,0 +1,23 @@ +""" +Tests the FPA model. + +Tests: + + - [ ] +""" + +# Standard imports + +# Third party imports +import pytest + +# RCPCH imports +from epilepsy12.models import FirstPaediatricAssessment + +@pytest.mark.django_db +def test_fpa_valid_creation( + e12_case_factory, +): + case = e12_case_factory() + + assert FirstPaediatricAssessment.objects.filter(registration=case.registration).exists() diff --git a/epilepsy12/tests/model_tests/test_investigations.py b/epilepsy12/tests/model_tests/test_investigations.py new file mode 100644 index 00000000..52b629f3 --- /dev/null +++ b/epilepsy12/tests/model_tests/test_investigations.py @@ -0,0 +1,194 @@ +""" +Tests the Investigations model. + +TODO -> refactor Investigations EEG + MRI field names. Currently: + +1 eeg_indicated +2 eeg_request_date +3 eeg_performed_date +4 mri_indicated +5 mri_brain_requested_date +6 mri_brain_reported_date + +Change to: + +1 SAME +2 eeg_requested_date +3 eeg_reported_date +4 mri_brain_indicated +5 SAME +6 SAME + +Tests: += [x] Investigations.eeg_performed_date should be None if Investigations.eeg_indicated is False or None +- [x] Investigations.eeg_request_date cannot be after Investigations.eeg_performed_date +- [x] Investigations.eeg_request_date cannot be before Registration.registration_date +- [x] Neither Investigations.eeg_request_date nor Investigations.eeg_performed_date can be in the future +- [x] Investigations.mri_brain_requested_date Investigations.mri_brain_reported_date should be None if Investigations.mri_indicated is False or None +- [x] Investigations.mri_brain_requested_date cannot be after Investigations.mri_brain_reported_date +- [x] Investigations.mri_brain_requested_date cannot be before Registration.registration_date +- [x] Neither Investigations.mri_brain_requested_date nor Investigations.mri_brain_reported_date can be in the future +- [x] Investigations.mri_wait is only calculated if both Investigations.mri_brain_requested_date and Investigations.mri_brain_reported_date are present and valid (including MRI declined date not set) +- [x] Investigations.mri_wait is only calculated if both Investigations.eeg_request_date and Investigations.eeg_performed_date are present and valid (including EEG declined date not set) + +""" + +# Standard imports +from datetime import date +from dateutil.relativedelta import relativedelta +from unittest.mock import patch + +# Third party imports +import pytest +from django.core.exceptions import ValidationError + +# RCPCH imports +from epilepsy12.models import Investigations + + +@pytest.mark.xfail +@pytest.mark.django_db +def test_validation_no_performed_date_when_not_indicated( + e12_case_factory, +): + """Tests: + - Investigations.eeg_performed_date should be None if Investigations.eeg_indicated is False or None + - Investigations.mri_brain_requested_date Investigations.mri_brain_reported_date should be None if Investigations.mri_indicated is False or None + """ + + performed_date = date(2023, 10, 1) + + with pytest.raises(ValidationError): + # Create an Investigations object where eeg_indicated = False, and related fields None, then .eeg_performed_date to a date, attempts to save it. + e12_case_factory( + registration__investigations__eeg_not_requested=True, + registration__investigations__eeg_performed_date=performed_date, + ) + with pytest.raises(ValidationError): + # Create an Investigations object where mri_brain_indicated = False, and related fields None, then .mri_brain_reported_date to a date, attempts to save it. + e12_case_factory( + registration__investigations__mri_brain_not_requested=True, + registration__investigations__mri_brain_reported_date=performed_date, + ) + + +@pytest.mark.xfail +@pytest.mark.django_db +def test_validation_request_date_cant_be_after_performed_date( + e12_case_factory, +): + """Tests: + - Investigations.eeg_request_date cannot be after Investigations.eeg_performed_date + - Investigations.mri_brain_requested_date cannot be after Investigations.mri_brain_reported_date + """ + + request_date = date(2023, 2, 1) + performed_date = date(2023, 1, 1) + + with pytest.raises(ValidationError): + # Create an Investigations object where request date is after performed date + e12_case_factory( + registration__investigations__eeg_request_date=request_date, + registration__investigations__eeg_performed_date=performed_date, + ) + with pytest.raises(ValidationError): + # Create an Investigations object where request date is after performed date + e12_case_factory( + registration__investigations__mri_brain_requested_date_request_date=request_date, + registration__investigations__mri_brain_reported_date_performed_date=performed_date, + ) + + +@pytest.mark.xfail +@pytest.mark.django_db +def test_validation_request_date_cant_be_before_registration( + e12_case_factory, +): + """Tests: + - Investigations.eeg_request_date cannot be before Registration.registration_date + - Investigations.mri_brain_requested_date cannot be before Registration.registration_date + """ + + # assign request date to 10 days before registration + registration_date = date(2023, 1, 1) + request_date = registration_date - relativedelta(days=10) + + with pytest.raises(ValidationError): + # Create an Investigations object where request date is after performed date + case = e12_case_factory( + registration__registration_date=registration_date, + registration__investigations__eeg_request_date=request_date, + ) + + with pytest.raises(ValidationError): + # Create an Investigations object where request date is after performed date + case = e12_case_factory( + registration__registration_date=registration_date, + registration__investigations__mri_brain_requested_date=request_date, + ) + + +@patch.object(Investigations, "get_current_date", return_value=date(2023, 10, 1)) +@pytest.mark.xfail +@pytest.mark.django_db +def test_validation_request_or_performed_in_future( + mocked_get_current_date, + e12_case_factory, +): + """Tests: + - Neither Investigations.eeg_request_date nor Investigations.eeg_performed_date can be in the future + - Neither Investigations.mri_brain_requested_date nor Investigations.mri_brain_reported_date can be in the future + + Patches today to always be 2023-10-1 + """ + + # assign request date to 1 month after today + request_date = date(2023, 10, 1) + relativedelta(months=1) + # assign performed date to 2 months after today + performed_date = date(2023, 9, 15) + relativedelta(months=2) + + print(Investigations.get_current_date()) + + with pytest.raises(ValidationError): + # try to save an investigation whose request date is after today + case = e12_case_factory( + registration__investigations__eeg_request_date=request_date, + ) + with pytest.raises(ValidationError): + # try to save an investigation whose request date is after today + case = e12_case_factory( + registration__investigations__mri_brain_requested_date=request_date, + ) + with pytest.raises(ValidationError): + # try to save an investigation whose performed date is after today + case = e12_case_factory( + registration__investigations__eeg_per_date=performed_date, + ) + with pytest.raises(ValidationError): + # try to save an investigation whose performed date is after today + case = e12_case_factory( + registration__investigations__mri_brain_reported_date=performed_date, + ) + + +@pytest.mark.xfail +@pytest.mark.django_db +def test_validation_wait_calculation( + e12_case_factory, +): + """Tests: + - Investigations.eeg_wait is only calculated if both Investigations.eeg_request_date and Investigations.eeg_performed_date are present and valid (including EEG declined date not set) + - Investigations.mri_wait is only calculated if both Investigations.mri_brain_requested_date and Investigations.mri_brain_reported_date are present and valid (including MRI declined date not set) + """ + + # Save an investigation where eeg and mri were not requested + corresponding fields are None + case = e12_case_factory( + registration__investigations__eeg_not_requested=True, + registration__investigations__mri_not_requested=True, + ) + + # Try calculating investigation wait + with pytest.raises(ValidationError): + case.registration.investigations.eeg_wait() + with pytest.raises(ValidationError): + case.registration.investigations.mri_wait() diff --git a/epilepsy12/tests/model_tests/test_kpi.py b/epilepsy12/tests/model_tests/test_kpi.py new file mode 100644 index 00000000..17b91ea3 --- /dev/null +++ b/epilepsy12/tests/model_tests/test_kpi.py @@ -0,0 +1,12 @@ +""" +Tests the KPI model +""" + +# Standard imports + + +# Third party imports + + +# RCPCH imports + diff --git a/epilepsy12/tests/model_tests/test_multiaxial_diagnosis.py b/epilepsy12/tests/model_tests/test_multiaxial_diagnosis.py new file mode 100644 index 00000000..78dc7b15 --- /dev/null +++ b/epilepsy12/tests/model_tests/test_multiaxial_diagnosis.py @@ -0,0 +1,135 @@ +""" + +TODO MOVE TO DIFFERENT TEST FILE: + - [ ] all MultiaxialDiagnosis.Episode.calculated_score == MultiaxialDiagnosis.Episode.expected_score when all fields completed + + +Tests the Multiaxial Diagnosis model +More complicated than the others as has 3 related models, each with a one to many relationship +These are: +Episode +Syndrome +Comorbidity + +Epilepsy onset fields can be thought of as a group and are stored in constants: +FOCAL_EPILEPSY_FIELDS, GENERALISED_ONSET_EPILEPSY_FIELDS +NONEPILEPSY_FIELDS + +Test cases ensure: + Episode + - [x] at least one MultiaxialDiagnosis.Episode.epilepsy_or_nonepilepsy_status is epileptic ('E') + - [x] a MultiaxialDiagnosis.Episode.description is present if MultiaxialDiagnosis.Episode.has_description_of_the_episode_or_episodes_been_gathered is True + - [x] a MultiaxialDiagnosis.Episode.description_keywords are correct for the given description + - [ ] all nonepilepsy fields including nonepilepsy seizure type fields are None if MultiaxialDiagnosis.Episode.epilepsy_or_nonepilepsy_status == 'E' (epilepsy) + - [ ] all epilepsy fields are None if MultiaxialDiagnosis.Episode.epilepsy_or_nonepilepsy_status == 'E' (epilepsy) or U (Unknown) + - [ ] all generalised onset epilepsy fields are None if MultiaxialDiagnosis.Episode.epileptic_seizure_onset_type == 'FO' (focal onset) + - [ ] all focal onset epilepsy fields are None if MultiaxialDiagnosis.Episode.epileptic_seizure_onset_type == 'GO' (generalised onset) + - [ ] all focal and generalised onset epilepsy fields are None if MultiaxialDiagnosis.Episode.epileptic_seizure_onset_type == 'UO' or 'UC' (unknow onset or unclassified) + Nonepilepsy seizure types - choices are NON_EPILEPSY_SEIZURE_TYPE + - [ ] all nonepilepsy seizure fields are None except MultiaxialDiagnosis.Episode.nonepileptic_seizure_behavioural + if MultiaxialDiagnosis.Episode.nonepileptic_seizure_type == 'BPP' (Behavioural, Psychological and Psychiatric) + - [ ] all nonepilepsy seizure fields are None except MultiaxialDiagnosis.Episode.nonepileptic_seizure_migraine + if MultiaxialDiagnosis.Episode.nonepileptic_seizure_type == 'MAD' (Migraine Associated Disorders) + - [ ] all nonepilepsy seizure fields are None except MultiaxialDiagnosis.Episode.nonepileptic_seizure_miscellaneous + if MultiaxialDiagnosis.Episode.nonepileptic_seizure_type == 'ME' (Miscellaneous Events) + - [ ] all nonepilepsy seizure fields are None except MultiaxialDiagnosis.Episode.nonepileptic_seizure_sleep + if MultiaxialDiagnosis.Episode.nonepileptic_seizure_type == 'SRC' (Sleep Related Conditions) + - [ ] all nonepilepsy seizure fields are None except MultiaxialDiagnosis.Episode.nonepileptic_seizure_syncope + if MultiaxialDiagnosis.Episode.nonepileptic_seizure_type == 'SAS' (Syncope and Anoxic Seizures) + - [ ] all nonepilepsy seizure fields are None except MultiaxialDiagnosis.Episode.nonepileptic_seizure_paroxysmal + if MultiaxialDiagnosis.Episode.nonepileptic_seizure_type == 'PMD' (Paroxysmal Movement Disorders) + - [ ] all nonepilepsy seizure fields are None except MultiaxialDiagnosis.Episode.nonepileptic_seizure_unknown_onset + if MultiaxialDiagnosis.Episode.nonepileptic_seizure_type == 'Oth' (Other) + Syndromes + - [ ] there is at least one MultiaxialDiagnosis.syndrome.pk if MultiaxialDiagnosis.syndrome_present is True + - [ ] each instance of MultiaxialDiagnosis.syndrome has valid syndrome_diagnosis_date + - [ ] each instance of MultiaxialDiagnosis.syndrome has valid syndrome + - [ ] each instance of MultiaxialDiagnosis.syndrome has both syndrome_diagnosis_date and syndrome completed + Epilepsy Causes + - [ ] there is one instance of MultiaxialDiagnosis.epilepsy_cause if MultiaxialDiagnosis.epilepsy_cause_known is True + - [ ] there is at least one item in the array of MultiaxialDiagnosis.epilepsy_cause_categories if MultiaxialDiagnosis.epilepsy_cause_known is True + Comorbidities + - [ ] there is one instance of MultiaxialDiagnosis.comorbidity if MultiaxialDiagnosis.relevant_impairments_behavioural_educational is True + - [ ] for every instance of MultiaxialDiagnosis.comorbidity there is a MultiaxialDiagnosis.comorbidity.comorbidity_diagnosis_date + - [ ] for every instance of MultiaxialDiagnosis.comorbidity there is an instance of MultiaxialDiagnosis.comorbidity.comorbidity_entity + Multiaxial diagnosis + - [ ] MultiaxialDiagnosis.global_developmental_delay_or_learning_difficulties_severity is not None if MultiaxialDiagnosis.global_developmental_delay_or_learning_difficulties is True + - [ ] MultiaxialDiagnosis.mental_health_issue_identified is not None if MultiaxialDiagnosis.mental_health_screen is True + +""" +# Standard imports + +# Third party imports +import pytest +from django.core.exceptions import ValidationError + +# RCPCH imports +from epilepsy12.models import ( + Episode, + Syndrome, + Comorbidity, + Keyword, +) +from epilepsy12.constants import ( + EPILEPSY_DIAGNOSIS_STATUS, +) + + +@pytest.mark.xfail +@pytest.mark.django_db +def test_at_least_one_MultiaxialDiagnosis__Episode_is_epileptic( + e12_case_factory, +): + + # creates multiaxial diagnosis where only episode is non epileptic. Assert validation error on save (called automatically when creating subFactories) + with pytest.raises(ValidationError): + e12_case_factory( + registration__multiaxial_diagnosis__episode__epilepsy_or_nonepilepsy_status=EPILEPSY_DIAGNOSIS_STATUS[ + 1 + ][ + 0 + ] + ) + +@pytest.mark.xfail +@pytest.mark.django_db +def test_Episode_validation_description_must_be_present_when_DescriptionOfEpisode_True( + e12_case_factory, +): + with pytest.raises(ValidationError): + e12_case_factory( + registration__multiaxial_diagnosis__episode__has_description_of_the_episode_or_episodes_been_gathered=True, + registration__multiaxial_diagnosis__episode__description=None, + ) + +@pytest.mark.xfail +@pytest.mark.django_db +def test_Episode_validation_description_keywords_correct_for_description( + e12_case_factory, +): + KEYWORDS = Keyword.objects.all() + description = ( + f"Patient was running when they {KEYWORDS[0]}, {KEYWORDS[1]}, and {KEYWORDS[2]}" + ) + + # create an Episode with the wrong keywords stored, try to save, should raise validation error + # ALL WRONG + with pytest.raises(ValidationError): + e12_case_factory( + registration__multiaxial_diagnosis__episode__description=description, + registration__multiaxial_diagnosis__episode__description_keywords=[ + KEYWORDS[3], + KEYWORDS[4], + KEYWORDS[5], + ], + ) + # 1 WRONG + with pytest.raises(ValidationError): + e12_case_factory( + registration__multiaxial_diagnosis__episode__description=description, + registration__multiaxial_diagnosis__episode__description_keywords=[ + KEYWORDS[0], + KEYWORDS[1], + KEYWORDS[5], + ], + ) diff --git a/epilepsy12/tests/model_tests/test_registration.py b/epilepsy12/tests/model_tests/test_registration.py new file mode 100644 index 00000000..b0ccf37f --- /dev/null +++ b/epilepsy12/tests/model_tests/test_registration.py @@ -0,0 +1,202 @@ +""" +Tests the Registration model. + +Tests: + + - [x] Test a valid Registration + - [x] Test for DOFPA in the future + - [x] Test for DOFPA before E12 began + - [x] Test for DOFPA before the child's DOB + +""" + +# Standard imports +from datetime import date +from dateutil.relativedelta import relativedelta +from unittest.mock import patch + +# Third party imports +import pytest +from django.core.exceptions import ValidationError + +# RCPCH imports +from epilepsy12.models import ( + Registration, +) + + +@pytest.mark.django_db +def test_registration_custom_method_audit_submission_date_calculation( + e12_case_factory, +): + """ + Tests the `audit_submission_date_calculation` accurately calculates audit submission date, depending on different registration dates. + + This is always the second Tuesday of January, following 1 year after the first paediatric assessment. + + If registration date + 1 year IS the 2nd Tues of Jan, the submission date is the same as registration + 1 year. + """ + + registration_dates = [ + # (registration date, expected audit submission date) + (date(2022, 11, 1), date(2024, 1, 9)), + (date(2022, 12, 31), date(2024, 1, 9)), + (date(2022, 1, 9), date(2023, 1, 10)), + (date(2022, 1, 10), date(2023, 1, 10)), + (date(2022, 1, 11), date(2024, 1, 9)), + ] + + for expected_input_output in registration_dates: + registration = e12_case_factory( + registration__registration_date=expected_input_output[0] + ).registration + + assert registration.audit_submission_date == expected_input_output[1] + + +@pytest.mark.django_db +def test_registration_custom_method_registration_date_one_year_on( + e12_case_factory, +): + """ + Tests the `registration_date_one_year_on` accurately calculates one year on (registration close date). + + This is always 1 year after `registration_date`. + """ + + expected_inputs_outputs = [ + # (registration date, expected audit close date) + (date(2022, 11, 1), date(2023, 11, 1)), + (date(2022, 12, 31), date(2023, 12, 31)), + (date(2022, 1, 9), date(2023, 1, 9)), + (date(2022, 1, 10), date(2023, 1, 10)), + (date(2022, 1, 11), date(2023, 1, 11)), + ] + + for expected_input_output in expected_inputs_outputs: + registration = e12_case_factory( + registration__registration_date=expected_input_output[0] + ).registration + + assert registration.registration_close_date == expected_input_output[1] + + +@pytest.mark.django_db +def test_registration_cohort( + e12_case_factory, +): + """ + Tests cohort number is set accurately, dependent on registration_date. + + Cohorts are defined between 1st December year and 30th November in the subsequent year. + + Examples of cohort numbers: + Cohort 4: 1 December 2020 - 30 November 2021 + Cohort 5: 1 December 2021 - 30 November 2022 + Cohort 6: 1 December 2022 - 30 November 2023 + Cohort 7: 1 December 2023 - 30 November 2024 + + Dates which are too early (< 2020) should return `None`. + """ + + expected_inputs_outputs = [ + # (registration date, expected cohort) + (date(2019, 11, 1), None), + (date(2020, 11, 30), None), + (date(2020, 12, 1), 4), + (date(2021, 11, 30), 4), + (date(2021, 12, 1), 5), + ] + + for expected_input_output in expected_inputs_outputs: + registration = e12_case_factory( + registration__registration_date=expected_input_output[0] + ).registration + + assert registration.cohort == expected_input_output[1] + + +@patch.object(Registration, "get_current_date", return_value=date(2022, 11, 30)) +@pytest.mark.django_db +def test_registration_days_remaining_before_submission( + mocked_get_current_date, + e12_case_factory, +): + """ + Tests `days_remaining_before_submission` property calculated properly. + + Calculated as submission date - current date, return number of days left days as an int. + + Test patches "today" - patches the example Registration instance's `.get_current_date`'s return value to always return 30 Nov 2022. + + NOTE: if `audit_submission_date` is before today, returns 0. + """ + + # submission date = 2023-01-10, 41 days after today + registration = e12_case_factory( + registration__registration_date=date(2022, 1, 10) + ).registration + assert registration.days_remaining_before_submission == 41 + + # submission date = 2023-01-10, 41 days after today + registration = e12_case_factory( + registration__registration_date=date(2021, 1, 1) + ).registration + assert registration.days_remaining_before_submission == 0 + + +@pytest.mark.xfail +@patch.object(Registration, "get_current_date", return_value=date(2023, 1, 1)) +@pytest.mark.django_db +def test_registration_validate_dofpa_not_future( + mocked_current_date, + e12_case_factory, +): + """ + # TODO - add validation + + Test related to ensuring model-level validation of inputted Date of First Paediatric Assessment (registration_date). + + Patches Registration's .get_current_date() method to always be 1 Jan 2023. + + """ + + # Tests that dofpa (registration_date) can't be in the future (relative to today). Tries to create and save a Registration which is 30 days ahead of today. + future_date = Registration.get_current_date() + relativedelta(days=30) + with pytest.raises(ValidationError): + e12_case_factory(registration__registration_date=future_date) + + +@pytest.mark.xfail +@pytest.mark.django_db +def test_registration_validate_dofpa_not_before_2009(e12_case_factory): + """ + # TODO - add validation + + Tests related to ensuring model-level validation of inputted Date of First Paediatric Assessment (registration_date). + + """ + + # Tests that dofpa (registration_date) can't be before E12 began in 2009. + with pytest.raises(ValidationError): + e12_case_factory(registration__registration_date=date(2007, 8, 9)) + + +@pytest.mark.xfail +@pytest.mark.django_db +def test_registration_validate_dofpa_not_before_child_dob(e12_case_factory): + """ + # TODO - add validation + + Tests related to ensuring model-level validation of inputted Date of First Paediatric Assessment (registration_date). + + """ + date_of_birth = date(2023, 1, 1) + registration_date = date_of_birth + relativedelta(days=10) + + # Tests that dofpa (registration_date) can't be before the child's DoB + with pytest.raises(ValidationError): + case = e12_case_factory( + date_of_birth=date_of_birth, + registration__registration_date=registration_date, + ) diff --git a/epilepsy12/tests/model_tests/test_visitactivity.py b/epilepsy12/tests/model_tests/test_visitactivity.py new file mode 100644 index 00000000..a06256b1 --- /dev/null +++ b/epilepsy12/tests/model_tests/test_visitactivity.py @@ -0,0 +1,25 @@ +import time + +import pytest + +from epilepsy12.models import ( + VisitActivity, +) + + +# @pytest.mark.django_db +# def test_visitActivity(e12User_GOSH, client): +# """ +# Test that visit activity logging works, using e12User_GOSH fixture +# """ + +# client.force_login(e12User_GOSH) + +# visit_activity = VisitActivity.objects.all() + +# time.sleep(1) + +# client.force_login(e12User_GOSH) + +# assert len(visit_activity) == 2 +# assert visit_activity[1].activity_datetime > visit_activity[0].activity_datetime diff --git a/epilepsy12/tests/test_meta_seed_test_db.py b/epilepsy12/tests/test_meta_seed_test_db.py new file mode 100644 index 00000000..6dde129e --- /dev/null +++ b/epilepsy12/tests/test_meta_seed_test_db.py @@ -0,0 +1,17 @@ +import pytest + +from django.contrib.auth.models import Group + +from epilepsy12.models import Epilepsy12User, Case, EpilepsyCauseEntity + + +@pytest.mark.django_db +def test__seed_test_db( + seed_groups_fixture, + seed_users_fixture, + seed_cases_fixture, +): + assert Group.objects.all().exists() + assert Case.objects.all().exists() + assert Epilepsy12User.objects.all().exists() + assert EpilepsyCauseEntity.objects.all().exists() \ No newline at end of file diff --git a/epilepsy12/tests/view_tests/__init__.py b/epilepsy12/tests/view_tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/epilepsy12/tests/view_tests/db_actions/__init__.py b/epilepsy12/tests/view_tests/db_actions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/epilepsy12/tests/view_tests/db_actions/test_update_actions.py b/epilepsy12/tests/view_tests/db_actions/test_update_actions.py new file mode 100644 index 00000000..52d9d443 --- /dev/null +++ b/epilepsy12/tests/view_tests/db_actions/test_update_actions.py @@ -0,0 +1,1460 @@ +""" +# Epilepsy12Users + [] Assert user can change user title + [] Assert user can change user first_name + [] Assert user can change user surname + [] Assert user can change user email + [] Assert user can change user role + [] Assert user can resend create_user email + + +# Cases + [] Assert user can change child first_name + [] Assert user can change child surname + [] Assert user can change child date_of_birth + [] Assert user can change child sex + [] Assert user can change child postcode + [] Assert user can change child postcode to unknown + [] Assert user can change child postcode to address unspecified + [] Assert user can change child postcode to no fixed abode + [] Assert user can change child nhs_number + [] Assert user can change child ethnicity + [] Assert user can opt child out of Epilepsy12 + + +# First Paediatric Assessment + for field in fields: [ + 'first_paediatric_assessment_in_acute_or_nonacute_setting', single_choice_multiple_toggle_button + 'has_number_of_episodes_since_the_first_been_documented', toggle_button + 'general_examination_performed', toggle_button + 'neurological_examination_performed', toggle_button + 'developmental_learning_or_schooling_problems', toggle_button + 'behavioural_or_emotional_problems' toggle_button + ] + [x] Assert user can change 'first_paediatric_assessment_in_acute_or_nonacute_setting' to acute (CHRONICITY[0][0]==1) + [x] Assert user can change 'first_paediatric_assessment_in_acute_or_nonacute_setting' to non-acute (CHRONICITY[0][0]==2) + [x] Assert user can change 'first_paediatric_assessment_in_acute_or_nonacute_setting' to don't know (CHRONICITY[0][0]==3) + [x] Assert user can change 'has_number_of_episodes_since_the_first_been_documented' to True + [x] Assert user can change 'has_number_of_episodes_since_the_first_been_documented' to False + [x] Assert user can change 'general_examination_performed' to True + [x] Assert user can change 'general_examination_performed' to False + [x] Assert user can change 'neurological_examination_performed' to True + [x] Assert user can change 'neurological_examination_performed' to False + [x] Assert user can change 'developmental_learning_or_schooling_problems' to True + [x] Assert user can change 'developmental_learning_or_schooling_problems' to False + [x] Assert user can change 'behavioural_or_emotional_problems' to True + [x] Assert user can change 'behavioural_or_emotional_problems' to False + +# Epilepsy Context + for field in fields: [ + 'previous_febrile_seizure', single_choice_multiple_toggle_button + 'previous_acute_symptomatic_seizure', single_choice_multiple_toggle_button + 'is_there_a_family_history_of_epilepsy', single_choice_multiple_toggle_button + 'previous_neonatal_seizures', single_choice_multiple_toggle_button + 'were_any_of_the_epileptic_seizures_convulsive', toggle_button + 'experienced_prolonged_generalized_convulsive_seizures', single_choice_multiple_toggle_button + 'experienced_prolonged_focal_seizures', single_choice_multiple_toggle_button + 'diagnosis_of_epilepsy_withdrawn', toggle_button + ] + + [x] Assert user can change 'previous_febrile_seizure' to Yes (OPT_OUT_UNCERTAIN[0][0] == 'Y') + [x] Assert user can change 'previous_febrile_seizure' to No (OPT_OUT_UNCERTAIN[1][0] == 'N') + [x] Assert user can change 'previous_febrile_seizure' to Uncertain (OPT_OUT_UNCERTAIN[2][0] == 'U') + [x] Assert user can change 'previous_acute_symptomatic_seizure' to Yes (OPT_OUT_UNCERTAIN[0][0] == 'Y') + [x] Assert user can change 'previous_acute_symptomatic_seizure' to No (OPT_OUT_UNCERTAIN[1][0] == 'N') + [x] Assert user can change 'previous_acute_symptomatic_seizure' to Uncertain (OPT_OUT_UNCERTAIN[2][0] == 'U') + [x] Assert user can change 'is_there_a_family_history_of_epilepsy' to Yes (OPT_OUT_UNCERTAIN[0][0] == 'Y') + [x] Assert user can change 'is_there_a_family_history_of_epilepsy' to No (OPT_OUT_UNCERTAIN[1][0] == 'N') + [x] Assert user can change 'is_there_a_family_history_of_epilepsy' to Uncertain (OPT_OUT_UNCERTAIN[2][0] == 'U') + [x] Assert user can change 'previous_neonatal_seizures' to Yes (OPT_OUT_UNCERTAIN[0][0] == 'Y') + [x] Assert user can change 'previous_neonatal_seizures' to No (OPT_OUT_UNCERTAIN[1][0] == 'N') + [x] Assert user can change 'previous_neonatal_seizures' to Uncertain (OPT_OUT_UNCERTAIN[2][0] == 'U') + [x] Assert user can change 'were_any_of_the_epileptic_seizures_convulsive' to True + [x] Assert user can change 'were_any_of_the_epileptic_seizures_convulsive' to False + [x] Assert user can change 'experienced_prolonged_generalized_convulsive_seizures' to Yes (OPT_OUT_UNCERTAIN[0][0] == 'Y') + [x] Assert user can change 'experienced_prolonged_generalized_convulsive_seizures' to No (OPT_OUT_UNCERTAIN[1][0] == 'N') + [x] Assert user can change 'experienced_prolonged_generalized_convulsive_seizures' to Uncertain (OPT_OUT_UNCERTAIN[2][0] == 'U') + [x] Assert user can change 'experienced_prolonged_focal_seizures' to Yes (OPT_OUT_UNCERTAIN[0][0] == 'Y') + [x] Assert user can change 'experienced_prolonged_focal_seizures' to No (OPT_OUT_UNCERTAIN[1][0] == 'N') + [x] Assert user can change 'experienced_prolonged_focal_seizures' to Uncertain (OPT_OUT_UNCERTAIN[2][0] == 'U') + [x] Assert user can change 'diagnosis_of_epilepsy_withdrawn' to True + [x] Assert user can change 'diagnosis_of_epilepsy_withdrawn' to False + + + +# Multiaxial Diagnosis + for field in fields: [ + 'epilepsy_cause_known', toggle_button + 'epilepsy_cause', select + 'epilepsy_cause_categories', multiple_choice_multiple_toggle_button + 'relevant_impairments_behavioural_educational', toggle_button + 'mental_health_screen', toggle_button + 'mental_health_issue_identified', toggle_button + 'mental_health_issue', single_choice_multiple_toggle_button + 'global_developmental_delay_or_learning_difficulties', toggle_button + 'global_developmental_delay_or_learning_difficulties_severity', single_choice_multiple_toggle_button + 'autistic_spectrum_disorder', toggle_button + ] + + [x] Assert user can change 'epilepsy_cause_known' to True + [x] Assert user can change 'epilepsy_cause_known' to False + [x] Assert user can change 'epilepsy_cause' to Aicardi's Syndrome (pk=134) + [x] Assert user can change 'epilepsy_cause_categories' to array of EPILEPSY_CAUSES[0][0]=='Gen' and EPILEPSY_CAUSES[1][0]=='Imm' and and EPILEPSY_CAUSES[5][0]=='Othe' + [x] Assert user can change 'epilepsy_cause_categories' to array of EPILEPSY_CAUSES[2][0]=='Inf' and EPILEPSY_CAUSES[3][0]=='Met' and EPILEPSY_CAUSES[4][0]=='Str' + [x] Assert user can change 'relevant_impairments_behavioural_educational' to True + [x] Assert user can change 'relevant_impairments_behavioural_educational' to False + [x] Assert user can change 'mental_health_issue' to NEUROPSYCHIATRIC[0][0]=='AxD' ('Anxiety disorder') + [x] Assert user can change 'mental_health_issue' to NEUROPSYCHIATRIC[0][0]=='EmB' ('Emotional/ behavioural') + [x] Assert user can change 'mental_health_issue' to NEUROPSYCHIATRIC[0][0]=='MoD' ('Mood disorder') + [x] Assert user can change 'mental_health_issue' to NEUROPSYCHIATRIC[0][0]=='SHm' ('Self harm') + [x] Assert user can change 'mental_health_issue' to NEUROPSYCHIATRIC[0][0]=='Oth' ('Other') + [x] Assert user can change 'mental_health_screen' to True + [x] Assert user can change 'mental_health_screen' to False + [x] Assert user can change 'mental_health_issue_identified' to True + [x] Assert user can change 'mental_health_issue_identified' to False + [x] Assert user can change 'autistic_spectrum_disorder' to True + [x] Assert user can change 'autistic_spectrum_disorder' to False + [x] Assert user can change 'global_developmental_delay_or_learning_difficulties' to True + [x] Assert user can change 'global_developmental_delay_or_learning_difficulties' to False + [x] Assert user can change 'global_developmental_delay_or_learning_difficulties_severity' to SEVERITY[0][0]=='mild' + [x] Assert user can change 'global_developmental_delay_or_learning_difficulties_severity' to SEVERITY[1][0]=='moderate' + [x] Assert user can change 'global_developmental_delay_or_learning_difficulties_severity' to SEVERITY[2][0]=='severe' + [x] Assert user can change 'global_developmental_delay_or_learning_difficulties_severity' to SEVERITY[3][0]=='profound' + [x] Assert user can change 'global_developmental_delay_or_learning_difficulties_severity' to SEVERITY[4][0]=='uncertain' + +# Episode + for field in fields: [ + 'seizure_onset_date', date_field + 'seizure_onset_date_confidence', single_choice_multiple_toggle_button + 'episode_definition', select + 'has_description_of_the_episode_or_episodes_been_gathered', toggle_button + 'edit_description', string - updated in view function + 'delete_description_keyword', Keyword id - updated in view function + 'epilepsy_or_nonepilepsy_status', single_choice_multiple_toggle_button + 'epileptic_seizure_onset_type', single_choice_multiple_toggle_button + 'focal_onset_epilepsy_checked_changed', updated in view function + 'epileptic_generalised_onset', select + 'nonepilepsy_generalised_onset', multiple_choice_multiple_toggle_button + 'nonepileptic_seizure_type', select + 'nonepileptic_seizure_subtype', select + ] + [] Assert user can change 'seizure_onset_date' to today + [x] Assert user can change 'seizure_onset_date_confidence' to DATE_ACCURACY[0][0]=='Apx' (Approximate) + [x] Assert user can change 'seizure_onset_date_confidence' to DATE_ACCURACY[1][0]=='Exc' (Exact) + [x] Assert user can change 'seizure_onset_date_confidence' to DATE_ACCURACY[2][0]=='NK' (Not Known) + [x] Assert user can change 'episode_definition' to EPISODE_DEFINITION[0][0]=='a' ('This was a single episode') + [x] Assert user can change 'episode_definition' to EPISODE_DEFINITION[1][0]=='b' ('This was a cluster within 24 hours') + [x] Assert user can change 'episode_definition' to EPISODE_DEFINITION[2][0]=='c' ('These were 2 or more episodes more than 24 hours apart') + [x] Assert user can change 'has_description_of_the_episode_or_episodes_been_gathered' to True + [x] Assert user can delete 'gelastic' in 'delete_description_keyword' from ['gelastic', 'left'] + [] Assert user can change 'edit_description' to "Jacob fell to the floor and shook the left side of his body." + [] Assert user can change 'edit_description' to "Jacob fell to the floor and shook the left side of his body." + [x] Assert user can change 'epilepsy_or_nonepilepsy_status' to EPILEPSY_DIAGNOSIS_STATUS[0][0]=='E' (Epilepsy) + [x] Assert user can change 'epilepsy_or_nonepilepsy_status' to EPILEPSY_DIAGNOSIS_STATUS[1][0]=='NE' (Epilepsy) + [x] Assert user can change 'epilepsy_or_nonepilepsy_status' to EPILEPSY_DIAGNOSIS_STATUS[2][0]=='U' (Uncertain) + [x] Assert user can change 'epileptic_seizure_onset_type' to EPILEPSY_SEIZURE_TYPE[0][0]=='FO' (Focal Onset) + [x] Assert user can change 'epileptic_seizure_onset_type' to EPILEPSY_SEIZURE_TYPE[1][0]=='GO' (Generalised Onset) + [x] Assert user can change 'epileptic_seizure_onset_type' to EPILEPSY_SEIZURE_TYPE[2][0]=='UO' (Unknown Onset) + [x] Assert user can change 'epileptic_seizure_onset_type' to EPILEPSY_SEIZURE_TYPE[3][0]=='UC' (Unclassified) + [] Assert user can change 'focal_onset_epilepsy_checked_changed' to ........ + [x] Assert user can change 'epileptic_generalised_onset' to GENERALISED_SEIZURE_TYPE[0][0]=='AEM' ('Absence with eyelid myoclonia') + [x] Assert user can change 'epileptic_generalised_onset' to GENERALISED_SEIZURE_TYPE[1][0]=='Ato' ('Atonic')] + [x] Assert user can change 'epileptic_generalised_onset' to GENERALISED_SEIZURE_TYPE[2][0]=='Aab' ('Atypical absence') + [x] Assert user can change 'epileptic_generalised_onset' to GENERALISED_SEIZURE_TYPE[3][0]=='Clo' ('Clonic') + [x] Assert user can change 'epileptic_generalised_onset' to GENERALISED_SEIZURE_TYPE[4][0]=='EpS' ('Epileptic spasms') + [x] Assert user can change 'epileptic_generalised_onset' to GENERALISED_SEIZURE_TYPE[5][0]=='MyC' ('Myoclonic') + [x] Assert user can change 'epileptic_generalised_onset' to GENERALISED_SEIZURE_TYPE[6][0]=='MAb' ('Myoclonic absence') + [x] Assert user can change 'epileptic_generalised_onset' to GENERALISED_SEIZURE_TYPE[7][0]=='MTC' ('Myoclonic-tonic-clonic') + [x] Assert user can change 'epileptic_generalised_onset' to GENERALISED_SEIZURE_TYPE[8][0]=='MAt' ('Myoclonic-atonic') + [x] Assert user can change 'epileptic_generalised_onset' to GENERALISED_SEIZURE_TYPE[9][0]=='Ton' ('Tonic') + [x] Assert user can change 'epileptic_generalised_onset' to GENERALISED_SEIZURE_TYPE[10][0]=='TCl' ('Tonic-clonic') + [x] Assert user can change 'epileptic_generalised_onset' to GENERALISED_SEIZURE_TYPE[11][0]=='TAb' ('Typical absence') + [x] Assert user can change 'epileptic_generalised_onset' to GENERALISED_SEIZURE_TYPE[12][0]=='Oth' ('Other') + [x] Assert user can change 'nonepileptic_seizure_type' to NON_EPILEPSY_SEIZURE_ONSET[0][0]=='BAr' ('Behaviour arrest') + [x] Assert user can change 'nonepileptic_seizure_type' to NON_EPILEPSY_SEIZURE_ONSET[1][0]=='EpS' ('Epileptic spasms') + [x] Assert user can change 'nonepileptic_seizure_type' to NON_EPILEPSY_SEIZURE_ONSET[2][0]=='TCl' ('Tonic-clonic') + [x] Assert user can change 'nonepileptic_seizure_type' to NON_EPILEPSY_SEIZURE_ONSET[3][0]=='Oth' ('Other') + [x] Assert user can change 'nonepileptic_seizure_subtype' to NON_EPILEPSY_SEIZURE_TYPE[0][0]=='BPP' ('Behavioral Psychological And Psychiatric Disorders') + [x] Assert user can change 'nonepileptic_seizure_subtype' to NON_EPILEPSY_SEIZURE_TYPE[1][0]=='MAD' ('Migraine Associated Disorders') + [x] Assert user can change 'nonepileptic_seizure_subtype' to NON_EPILEPSY_SEIZURE_TYPE[2][0]=='ME' ('Miscellaneous Events') + [x] Assert user can change 'nonepileptic_seizure_subtype' to NON_EPILEPSY_SEIZURE_TYPE[3][0]=='SRC' ('Sleep Related Conditions') + [x] Assert user can change 'nonepileptic_seizure_subtype' to NON_EPILEPSY_SEIZURE_TYPE[4][0]=='SAS' ('Syncope And Anoxic Seizures') + [x] Assert user can change 'nonepileptic_seizure_subtype' to NON_EPILEPSY_SEIZURE_TYPE[5][0]=='PMD' ('Paroxysmal Movement Disorders')]], tuple[Literal['Oth'], Literal['Other']]] + [x] Assert user can change 'nonepileptic_seizure_subtype' to NON_EPILEPSY_SEIZURE_TYPE[6][0]=='Oth' ('Other') + + + [] Assert user cannot change 'seizure_onset_date' to before Case.date_of_birth (raise ValidationError) + [] Assert user cannot change 'seizure_onset_date' to before Registration.registration_date (raise ValidationError) + [] Assert user cannot change 'seizure_onset_date' to future date (raise ValidationError) + + +# Comorbidity + for field in fields: [ + 'comorbidity_diagnosis_date', date_field + 'comorbidity_diagnosis', select + ] + [] Assert user can change 'comorbidity_diagnosis_date' .. + [x] Assert user can change 'comorbidity_diagnosis' .. + +# Syndrome + for field in fields: [ + add_syndrome (multiaxial_diagnosis_id) button click + edit_syndrome (syndrome_id) button click + remove_syndrome (syndrome_id) button click + close_syndrome (syndrome_id) button click + syndrome_present (multiaxial_diagnosis_id) button click + syndrome_diagnosis_date (syndrome_id) date_field + syndrome_name (syndrome_id) select + ] + [] Assert user can change .. + [] Assert user can change .. + + +# Assessment + for field in fields: [ + 'consultant_paediatrician_referral_made', toggle_button + 'consultant_paediatrician_referral_date', date_field + 'consultant_paediatrician_input_date', date_field + 'general_paediatric_centre', button click + 'edit_general_paediatric_centre', button click + 'update_general_paediatric_centre_pressed', button click (action:edit/cancel) + 'paediatric_neurologist_referral_made', toggle_button + 'paediatric_neurologist_referral_date', date_field + 'paediatric_neurologist_input_date', date_field + 'paediatric_neurology_centre', button click + 'edit_paediatric_neurology_centre', button click + 'update_paediatric_neurology_centre_pressed', button click (action:edit/cancel) + 'childrens_epilepsy_surgical_service_referral_criteria_met', toggle_button + 'childrens_epilepsy_surgical_service_referral_made', toggle_button + 'childrens_epilepsy_surgical_service_referral_date', date_field + 'childrens_epilepsy_surgical_service_input_date', date_field + 'epilepsy_surgery_centre', button click + 'edit_epilepsy_surgery_centre', button click + 'update_epilepsy_surgery_centre_pressed', button click (action:edit/cancel) + 'epilepsy_specialist_nurse_referral_made', toggle_button + 'epilepsy_specialist_nurse_referral_date', date_field + 'epilepsy_specialist_nurse_input_date', date_field + ] + [x] Assert user can change 'consultant_paediatrician_referral_made' to True + [x] Assert user can change 'consultant_paediatrician_referral_made' to False + [] Assert user can change 'consultant_paediatrician_referral_date' .. + [] Assert user can change 'general_paediatric_centre'... + [] Assert user can change 'edit_general_paediatric_centre' .. + [] Assert user can change 'update_general_paediatric_centre_pressed'.. + [x] Assert user can change 'paediatric_neurologist_referral_made' to True + [x] Assert user can change 'paediatric_neurologist_referral_made' to False + [] Assert user can change 'paediatric_neurologist_referral_date' .. + [] Assert user can change 'paediatric_neurologist_referral_date' .. + [] Assert user can change 'paediatric_neurologist_input_date' .. + [] Assert user can change 'paediatric_neurology_centre' .. + [] Assert user can change 'edit_paediatric_neurology_centre' .. + [] Assert user can change 'update_paediatric_neurology_centre_pressed'.. + [x] Assert user can change 'childrens_epilepsy_surgical_service_referral_criteria_met' to True + [x] Assert user can change 'childrens_epilepsy_surgical_service_referral_criteria_met' to False + [] Assert user can change 'childrens_epilepsy_surgical_service_referral_made' to True + [] Assert user can change 'childrens_epilepsy_surgical_service_referral_made' to False + [] Assert user can change 'childrens_epilepsy_surgical_service_referral_date' .. + [] Assert user can change 'childrens_epilepsy_surgical_service_input_date' .. + [] Assert user can change 'epilepsy_surgery_centre' .. + [] Assert user can change 'edit_epilepsy_surgery_centre'.. + [] Assert user can change 'update_epilepsy_surgery_centre_pressed' .. + [] Assert user can change 'update_epilepsy_surgery_centre_pressed' .. + [x] Assert user can change 'epilepsy_specialist_nurse_referral_made' to True + [x] Assert user can change 'epilepsy_specialist_nurse_referral_made' to False + [] Assert user can change 'epilepsy_specialist_nurse_referral_date' .. + [] Assert user can change 'epilepsy_specialist_nurse_input_date' .. + +# Investigations + for field in fields: [ + 'eeg_indicated', toggle_button + 'eeg_request_date', date_field + 'eeg_performed_date', date_field + 'eeg_declined', button click (confirm:edit/decline) + 'twelve_lead_ecg_status', toggle_button + 'ct_head_scan_status', toggle_button + 'mri_indicated', toggle_button + 'mri_brain_requested_date', date_field + 'mri_brain_reported_date', date_field + 'mri_brain_declined', button click (confirm:edit/decline) + ] + [x] Assert user can change 'eeg_indicated' to True + [x] Assert user can change 'eeg_indicated' to False + [] Assert user can change 'eeg_request_date' .. + [] Assert user can change 'eeg_performed_date' .. + [] Assert user can change 'eeg_declined' .. + [x] Assert user can change 'twelve_lead_ecg_status' to True + [x] Assert user can change 'twelve_lead_ecg_status' to False + [x] Assert user can change 'ct_head_scan_status' to True + [x] Assert user can change 'ct_head_scan_status' to False + [x] Assert user can change 'mri_indicated' to True + [x] Assert user can change 'mri_indicated' to False + [] Assert user can change 'mri_brain_requested_date' .. + [] Assert user can change 'mri_brain_reported_date' .. + [] Assert user can change 'mri_brain_declined' .. + +# Management + for field in fields: [ + 'individualised_care_plan_in_place', toggle_button + 'individualised_care_plan_date', date_field + 'individualised_care_plan_has_parent_carer_child_agreement', toggle_button + 'individualised_care_plan_includes_service_contact_details', toggle_button + 'individualised_care_plan_include_first_aid', toggle_button + 'individualised_care_plan_parental_prolonged_seizure_care', toggle_button + 'individualised_care_plan_includes_general_participation_risk', toggle_button + 'individualised_care_plan_addresses_water_safety', toggle_button + 'individualised_care_plan_addresses_sudep', toggle_button + 'individualised_care_plan_includes_ehcp', toggle_button + 'has_individualised_care_plan_been_updated_in_the_last_year', toggle_button + 'has_been_referred_for_mental_health_support', toggle_button + 'has_support_for_mental_health_support', toggle_button + 'has_an_aed_been_given', toggle_button + 'has_rescue_medication_been_prescribed', toggle_button + ] + [x] Assert user can change 'individualised_care_plan_in_place' to True + [x] Assert user can change 'individualised_care_plan_in_place' to False + [] Assert user can change 'individualised_care_plan_date' .. + [x] Assert user can change 'individualised_care_plan_has_parent_carer_child_agreement' to True + [x] Assert user can change 'individualised_care_plan_has_parent_carer_child_agreement' to False + [x] Assert user can change 'individualised_care_plan_includes_service_contact_details' to True + [x] Assert user can change 'individualised_care_plan_includes_service_contact_details' to False + [x] Assert user can change 'individualised_care_plan_include_first_aid' to True + [x] Assert user can change 'individualised_care_plan_include_first_aid' to False + [x] Assert user can change 'individualised_care_plan_parental_prolonged_seizure_care' to True + [x] Assert user can change 'individualised_care_plan_parental_prolonged_seizure_care' to False + [x] Assert user can change 'individualised_care_plan_includes_general_participation_risk' to True + [x] Assert user can change 'individualised_care_plan_includes_general_participation_risk' to False + [x] Assert user can change 'individualised_care_plan_addresses_water_safety' to True + [x] Assert user can change 'individualised_care_plan_addresses_water_safety' to False + [x] Assert user can change 'individualised_care_plan_addresses_sudep' to True + [x] Assert user can change 'individualised_care_plan_addresses_sudep' to False + [x] Assert user can change 'individualised_care_plan_includes_ehcp' to True + [x] Assert user can change 'individualised_care_plan_includes_ehcp' to False + [x] Assert user can change 'has_individualised_care_plan_been_updated_in_the_last_year' to True + [x] Assert user can change 'has_individualised_care_plan_been_updated_in_the_last_year' to False + [x] Assert user can change 'has_been_referred_for_mental_health_support' to True + [x] Assert user can change 'has_been_referred_for_mental_health_support' to False + [x] Assert user can change 'has_support_for_mental_health_support' to True + [x] Assert user can change 'has_support_for_mental_health_support' to False + [x] Assert user can change 'has_an_aed_been_given' to True + [x] Assert user can change 'has_an_aed_been_given' to False + [x] Assert user can change 'has_rescue_medication_been_prescribed' to True + [x] Assert user can change 'has_rescue_medication_been_prescribed' to False + +# Antiepilepsy Medicine + for field in fields: [ + 'edit_antiepilepsy_medicine', button click (antiepilepsy_medicine_id) + 'medicine_id', post on select change handled in view + 'antiepilepsy_medicine_start_date', date_field + 'antiepilepsy_medicine_add_stop_date', button click (antiepilepsy_medicine_id) + 'antiepilepsy_medicine_remove_stop_date', button click (antiepilepsy_medicine_id) + 'antiepilepsy_medicine_stop_date', date_field + 'antiepilepsy_medicine_risk_discussed', toggle_button + 'is_a_pregnancy_prevention_programme_in_place', toggle_button + 'has_a_valproate_annual_risk_acknowledgement_form_been_completed', toggle_button + ] + [] Assert user can change 'edit_antiepilepsy_medicine' .. + [] Assert user can change 'medicine_id' .. + [] Assert user can change 'antiepilepsy_medicine_start_date' .. + [] Assert user can change 'antiepilepsy_medicine_add_stop_date' .. + [] Assert user can change 'antiepilepsy_medicine_remove_stop_date' .. + [] Assert user can change 'antiepilepsy_medicine_stop_date' .. + [x] Assert user can change 'antiepilepsy_medicine_risk_discussed' to True + [x] Assert user can change 'antiepilepsy_medicine_risk_discussed' to False + [x] Assert user can change 'is_a_pregnancy_prevention_programme_in_place' to True + [x] Assert user can change 'is_a_pregnancy_prevention_programme_in_place' to False + [x] Assert user can change 'has_a_valproate_annual_risk_acknowledgement_form_been_completed' to True + [x] Assert user can change 'has_a_valproate_annual_risk_acknowledgement_form_been_completed' to False + +""" +# Python imports +from datetime import date +from dateutil import relativedelta + +# Django imports +from django.urls import reverse +from django.apps import apps + +# third party imports +import pytest +import factory + +# E12 imports +from epilepsy12.tests.UserDataClasses import ( + test_user_rcpch_audit_team_data, +) +from epilepsy12.tests.factories import ( + E12UserFactory, + E12CaseFactory, + E12SiteFactory, + E12AntiEpilepsyMedicineFactory, +) + +# E12 imports +from epilepsy12.models import ( + Epilepsy12User, + Organisation, + Case, + Episode, + Keyword, + EpilepsyCauseEntity, + MultiaxialDiagnosis, + ComorbidityEntity, + Comorbidity, + MedicineEntity, + AntiEpilepsyMedicine, + Syndrome, + SyndromeEntity, +) + +from epilepsy12.constants import ( + SEX_TYPE, + CHRONICITY, + OPT_OUT_UNCERTAIN, + SEVERITY, + EPILEPSY_CAUSES, + NEUROPSYCHIATRIC, + DATE_ACCURACY, + EPILEPSY_DIAGNOSIS_STATUS, + EPILEPSY_SEIZURE_TYPE, + GENERALISED_SEIZURE_TYPE, + EPISODE_DEFINITION, + NON_EPILEPSY_SEIZURE_ONSET, + NON_EPILEPSY_SEIZURE_TYPE, +) + + +SINGLE_CHOICE_MULTIPLE_TOGGLES = ( + { + "field_name": "first_paediatric_assessment_in_acute_or_nonacute_setting", + "param": "first_paediatric_assessment_id", + "choices": CHRONICITY, + "model": "firstpaediatricassessment", + }, + { + "field_name": "previous_febrile_seizure", + "choices": OPT_OUT_UNCERTAIN, + "param": "epilepsy_context_id", + "model": "epilepsycontext", + }, + { + "field_name": "previous_acute_symptomatic_seizure", + "choices": OPT_OUT_UNCERTAIN, + "param": "epilepsy_context_id", + "model": "epilepsycontext", + }, + { + "field_name": "is_there_a_family_history_of_epilepsy", + "choices": OPT_OUT_UNCERTAIN, + "param": "epilepsy_context_id", + "model": "epilepsycontext", + }, + { + "field_name": "previous_neonatal_seizures", + "choices": OPT_OUT_UNCERTAIN, + "param": "epilepsy_context_id", + "model": "epilepsycontext", + }, + { + "field_name": "experienced_prolonged_generalized_convulsive_seizures", + "choices": OPT_OUT_UNCERTAIN, + "param": "epilepsy_context_id", + "model": "epilepsycontext", + }, + { + "field_name": "experienced_prolonged_focal_seizures", + "choices": OPT_OUT_UNCERTAIN, + "param": "epilepsy_context_id", + "model": "epilepsycontext", + }, + { + "field_name": "mental_health_issue", + "choices": NEUROPSYCHIATRIC, + "param": "multiaxial_diagnosis_id", + "model": "multiaxialdiagnosis", + }, + { + "field_name": "global_developmental_delay_or_learning_difficulties_severity", + "choices": SEVERITY, + "param": "multiaxial_diagnosis_id", + "model": "multiaxialdiagnosis", + }, + { + "field_name": "seizure_onset_date_confidence", + "choices": DATE_ACCURACY, + "param": "episode_id", + "model": "episode", + }, + { + "field_name": "epilepsy_or_nonepilepsy_status", + "choices": EPILEPSY_DIAGNOSIS_STATUS, + "param": "episode_id", + "model": "episode", + }, + { + "field_name": "epileptic_seizure_onset_type", + "choices": EPILEPSY_SEIZURE_TYPE, + "param": "episode_id", + "model": "episode", + }, +) + +TOGGLES = ( + { + "field_name": "has_number_of_episodes_since_the_first_been_documented", + "param": "first_paediatric_assessment_id", + "model": "firstpaediatricassessment", + }, + { + "field_name": "general_examination_performed", + "param": "first_paediatric_assessment_id", + "model": "firstpaediatricassessment", + }, + { + "field_name": "neurological_examination_performed", + "param": "first_paediatric_assessment_id", + "model": "firstpaediatricassessment", + }, + { + "field_name": "developmental_learning_or_schooling_problems", + "param": "first_paediatric_assessment_id", + "model": "firstpaediatricassessment", + }, + { + "field_name": "behavioural_or_emotional_problems", + "param": "first_paediatric_assessment_id", + "model": "firstpaediatricassessment", + }, + { + "field_name": "were_any_of_the_epileptic_seizures_convulsive", + "param": "epilepsy_context_id", + "model": "epilepsycontext", + }, + { + "field_name": "diagnosis_of_epilepsy_withdrawn", + "param": "epilepsy_context_id", + "model": "epilepsycontext", + }, + { + "field_name": "epilepsy_cause_known", + "param": "multiaxial_diagnosis_id", + "model": "multiaxialdiagnosis", + }, + { + "field_name": "relevant_impairments_behavioural_educational", + "param": "multiaxial_diagnosis_id", + "model": "multiaxialdiagnosis", + }, + { + "field_name": "mental_health_screen", + "param": "multiaxial_diagnosis_id", + "model": "multiaxialdiagnosis", + }, + { + "field_name": "mental_health_issue_identified", + "param": "multiaxial_diagnosis_id", + "model": "multiaxialdiagnosis", + }, + { + "field_name": "global_developmental_delay_or_learning_difficulties", + "param": "multiaxial_diagnosis_id", + "model": "multiaxialdiagnosis", + }, + { + "field_name": "autistic_spectrum_disorder", + "param": "multiaxial_diagnosis_id", + "model": "multiaxialdiagnosis", + }, + { + "field_name": "has_description_of_the_episode_or_episodes_been_gathered", + "param": "episode_id", + "model": "episode", + }, + { + "field_name": "consultant_paediatrician_referral_made", + "param": "assessment_id", + "model": "assessment", + }, + { + "field_name": "paediatric_neurologist_referral_made", + "param": "assessment_id", + "model": "assessment", + }, + { + "field_name": "childrens_epilepsy_surgical_service_referral_criteria_met", + "param": "assessment_id", + "model": "assessment", + }, + { + "field_name": "childrens_epilepsy_surgical_service_referral_made", + "param": "assessment_id", + "model": "assessment", + }, + { + "field_name": "epilepsy_specialist_nurse_referral_made", + "param": "assessment_id", + "model": "assessment", + }, + { + "field_name": "eeg_indicated", + "param": "investigations_id", + "model": "investigations", + }, + { + "field_name": "twelve_lead_ecg_status", + "param": "investigations_id", + "model": "investigations", + }, + { + "field_name": "ct_head_scan_status", + "param": "investigations_id", + "model": "investigations", + }, + { + "field_name": "mri_indicated", + "param": "investigations_id", + "model": "investigations", + }, + { + "field_name": "individualised_care_plan_in_place", + "param": "management_id", + "model": "management", + }, + { + "field_name": "individualised_care_plan_has_parent_carer_child_agreement", + "param": "management_id", + "model": "management", + }, + { + "field_name": "individualised_care_plan_includes_service_contact_details", + "param": "management_id", + "model": "management", + }, + { + "field_name": "individualised_care_plan_include_first_aid", + "param": "management_id", + "model": "management", + }, + { + "field_name": "individualised_care_plan_parental_prolonged_seizure_care", + "param": "management_id", + "model": "management", + }, + { + "field_name": "individualised_care_plan_includes_general_participation_risk", + "param": "management_id", + "model": "management", + }, + { + "field_name": "individualised_care_plan_addresses_water_safety", + "param": "management_id", + "model": "management", + }, + { + "field_name": "individualised_care_plan_addresses_sudep", + "param": "management_id", + "model": "management", + }, + { + "field_name": "individualised_care_plan_includes_ehcp", + "param": "management_id", + "model": "management", + }, + { + "field_name": "has_individualised_care_plan_been_updated_in_the_last_year", + "param": "management_id", + "model": "management", + }, + { + "field_name": "has_been_referred_for_mental_health_support", + "param": "management_id", + "model": "management", + }, + { + "field_name": "has_support_for_mental_health_support", + "param": "management_id", + "model": "management", + }, + { + "field_name": "has_an_aed_been_given", + "param": "management_id", + "model": "management", + }, + { + "field_name": "has_rescue_medication_been_prescribed", + "param": "management_id", + "model": "management", + }, + { + "field_name": "antiepilepsy_medicine_risk_discussed", + "param": "antiepilepsy_medicine_id", + "model": "antiepilepsymedicine", + }, + { + "field_name": "is_a_pregnancy_prevention_programme_in_place", + "param": "antiepilepsy_medicine_id", + "model": "antiepilepsymedicine", + }, + { + "field_name": "has_a_valproate_annual_risk_acknowledgement_form_been_completed", + "param": "antiepilepsy_medicine_id", + "model": "antiepilepsymedicine", + }, +) + +SELECTS = ( + { + "field_name": "epilepsy_cause", + "param": "multiaxial_diagnosis_id", + "model": "multiaxialdiagnosis", + "choices": None, + }, + { + "field_name": "comorbidity_diagnosis", + "param": "comorbidity_id", + "model": "comorbidity", + "choices": None, + }, + { + "field_name": "episode_definition", + "param": "episode_id", + "model": "episode", + "choices": EPISODE_DEFINITION, + }, + { + "field_name": "nonepileptic_seizure_type", + "param": "episode_id", + "model": "episode", + "choices": NON_EPILEPSY_SEIZURE_ONSET, + }, + { + "field_name": "nonepileptic_seizure_subtype", + "param": "episode_id", + "model": "episode", + "choices": NON_EPILEPSY_SEIZURE_TYPE, + }, + { + "field_name": "epileptic_generalised_onset", + "choices": GENERALISED_SEIZURE_TYPE, + "param": "episode_id", + "model": "episode", + }, + { + "field_name": "syndrome_name", + "choices": None, + "param": "syndrome_id", + "model": "syndrome", + }, +) + + +@pytest.mark.django_db +def test_user_updates_single_choice_multiple_toggle_success( + client, seed_groups_fixture, seed_users_fixture, seed_cases_fixture +): + """ + Assert for each single_choice_multiple_toggle choice selection, value stored in model is correct selection value + """ + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + CASE_FROM_TEST_USER_ORGANISATION = E12CaseFactory.create( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}", + organisations__organisation=TEST_USER_ORGANISATION, + ) + + test_user = Epilepsy12User.objects.get( + first_name=test_user_rcpch_audit_team_data.role_str + ) + + client.force_login(test_user) + + for index, url in enumerate(SINGLE_CHOICE_MULTIPLE_TOGGLES): + for item in enumerate(url.get("choices")): + model = get_model_from_model( + case=CASE_FROM_TEST_USER_ORGANISATION, model_name=url.get("model") + ) + client.post( + reverse( + url.get("field_name"), + kwargs={url.get("param"): model.id}, + ), + headers={"Hx-Trigger-Name": item[0], "Hx-Request": "true"}, + ) + updated_model = get_model_from_model( + case=CASE_FROM_TEST_USER_ORGANISATION, model_name=url.get("model") + ) + validate_single_choice_multiple_toggle_button( + field_name=url.get("field_name"), + model_instance=updated_model, + expected_result=item[0], + assert_pass=True, + ) + + +@pytest.mark.django_db +def test_user_updates_single_choice_multiple_toggle_fail( + client, seed_groups_fixture, seed_users_fixture, seed_cases_fixture +): + """ + Assert for each single_choice_multiple_toggle choice selection, value stored in model is correct selection value + """ + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + CASE_FROM_TEST_USER_ORGANISATION = E12CaseFactory.create( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}", + organisations__organisation=TEST_USER_ORGANISATION, + ) + + test_user = Epilepsy12User.objects.get( + first_name=test_user_rcpch_audit_team_data.role_str + ) + + client.force_login(test_user) + + for index, url in enumerate(SINGLE_CHOICE_MULTIPLE_TOGGLES): + for item in enumerate(url.get("choices")): + model = get_model_from_model( + case=CASE_FROM_TEST_USER_ORGANISATION, model_name=url.get("model") + ) + client.post( + reverse( + url.get("field_name"), + kwargs={url.get("param"): model.id}, + ), + headers={"Hx-Trigger-Name": item[0], "Hx-Request": "true"}, + ) + updated_model = get_model_from_model( + case=CASE_FROM_TEST_USER_ORGANISATION, model_name=url.get("model") + ) + validate_single_choice_multiple_toggle_button( + field_name=url.get("field_name"), + model_instance=updated_model, + expected_result="dummy data", + assert_pass=False, + ) + + +@pytest.mark.django_db +def test_user_updates_toggles_true_success(client): + """ + Assert for each toggle selection, value stored in model is correct selection value + """ + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + CASE_FROM_TEST_USER_ORGANISATION = E12CaseFactory.create( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}", + organisations__organisation=TEST_USER_ORGANISATION, + ) + + test_user = Epilepsy12User.objects.get( + first_name=test_user_rcpch_audit_team_data.role_str + ) + + client.force_login(test_user) + + for index, url in enumerate(TOGGLES): + print(url.get("field_name")) + model = get_model_from_model( + case=CASE_FROM_TEST_USER_ORGANISATION, model_name=url.get("model") + ) + client.post( + reverse( + url.get("field_name"), + kwargs={url.get("param"): model.id}, + ), + headers={"Hx-Trigger-Name": "button-true", "Hx-Request": "true"}, + ) + updated_model = get_model_from_model( + case=CASE_FROM_TEST_USER_ORGANISATION, model_name=url.get("model") + ) + validate_toggle_button( + field_name=url.get("field_name"), + model_instance=updated_model, + expected_result=True, + assert_pass=True, + ) + + +@pytest.mark.django_db +def test_user_updates_toggles_false_success(client): + """ + Assert for each toggle selection, value stored in model is correct selection value + """ + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + CASE_FROM_TEST_USER_ORGANISATION = E12CaseFactory.create( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}", + organisations__organisation=TEST_USER_ORGANISATION, + ) + + test_user = Epilepsy12User.objects.get( + first_name=test_user_rcpch_audit_team_data.role_str + ) + + client.force_login(test_user) + + for index, url in enumerate(TOGGLES): + model = get_model_from_model( + case=CASE_FROM_TEST_USER_ORGANISATION, model_name=url.get("model") + ) + client.post( + reverse( + url.get("field_name"), + kwargs={url.get("param"): model.id}, + ), + headers={"Hx-Trigger-Name": "button-false", "Hx-Request": "true"}, + ) + updated_model = get_model_from_model( + case=CASE_FROM_TEST_USER_ORGANISATION, model_name=url.get("model") + ) + validate_toggle_button( + field_name=url.get("field_name"), + model_instance=updated_model, + expected_result=False, + assert_pass=True, + ) + + +@pytest.mark.django_db +def test_user_updates_toggles_true_fail(client): + """ + Assert for each toggle selection, value stored in model is correct selection value + """ + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + CASE_FROM_TEST_USER_ORGANISATION = E12CaseFactory.create( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}", + organisations__organisation=TEST_USER_ORGANISATION, + ) + + test_user = Epilepsy12User.objects.get( + first_name=test_user_rcpch_audit_team_data.role_str + ) + + client.force_login(test_user) + + for index, url in enumerate(TOGGLES): + model = get_model_from_model( + case=CASE_FROM_TEST_USER_ORGANISATION, model_name=url.get("model") + ) + client.post( + reverse( + url.get("field_name"), + kwargs={url.get("param"): model.id}, + ), + headers={"Hx-Trigger-Name": "button-true", "Hx-Request": "true"}, + ) + updated_model = get_model_from_model( + case=CASE_FROM_TEST_USER_ORGANISATION, model_name=url.get("model") + ) + validate_toggle_button( + field_name=url.get("field_name"), + model_instance=updated_model, + expected_result=False, + assert_pass=False, + ) + + +@pytest.mark.django_db +def test_user_updates_toggles_false_fail(client): + """ + Assert for each toggle selection, value stored in model is correct selection value + """ + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + CASE_FROM_TEST_USER_ORGANISATION = E12CaseFactory.create( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}", + organisations__organisation=TEST_USER_ORGANISATION, + ) + + test_user = Epilepsy12User.objects.get( + first_name=test_user_rcpch_audit_team_data.role_str + ) + + client.force_login(test_user) + + for index, url in enumerate(TOGGLES): + model = get_model_from_model( + case=CASE_FROM_TEST_USER_ORGANISATION, model_name=url.get("model") + ) + client.post( + reverse( + url.get("field_name"), + kwargs={url.get("param"): model.id}, + ), + headers={"Hx-Trigger-Name": "button-false", "Hx-Request": "true"}, + ) + updated_model = get_model_from_model( + case=CASE_FROM_TEST_USER_ORGANISATION, model_name=url.get("model") + ) + validate_toggle_button( + field_name=url.get("field_name"), + model_instance=updated_model, + expected_result=True, + assert_pass=False, + ) + + +@pytest.mark.skip(reason="unfinished test") +@pytest.mark.django_db +def test_user_updates_select_success( + client, +): + """ + Assert for each single_choice_multiple_toggle choice selection, value stored in model is correct selection value + """ + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + CASE_FROM_TEST_USER_ORGANISATION = E12CaseFactory.create( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}", + organisations__organisation=TEST_USER_ORGANISATION, + ) + + test_user = Epilepsy12User.objects.get( + first_name=test_user_rcpch_audit_team_data.role_str + ) + + client.force_login(test_user) + + for index, url in enumerate(SELECTS): + if url.get("choices") is not None: + for choice in url.get("choices"): + model = get_model_from_model( + case=CASE_FROM_TEST_USER_ORGANISATION, + model_name=url.get("model"), + ) + data = {url.get("field_name"): choice} + + client.post( + reverse( + url.get("field_name"), + kwargs={url.get("param"): model.id}, + ), + headers={ + "Hx-Trigger-Name": choice, + "Hx-Request": "true", + }, + data=data, + ) + updated_model = get_model_from_model( + case=CASE_FROM_TEST_USER_ORGANISATION, model_name=url.get("model") + ) + validate_select( + field_name=url.get("field_name"), + model_instance=updated_model, + expected_result=EpilepsyCauseEntity.objects.get( + pk=134 + ), # Aicardi's sy. + assert_pass=True, + ) + else: + model = get_model_from_model( + case=CASE_FROM_TEST_USER_ORGANISATION, + model_name=url.get("model"), + ) + + if url.get("field_name") == "epilepsy_cause": + data = {"epilepsy_cause": 134} + expected_result = EpilepsyCauseEntity.objects.get( + pk=134 + ) # Aicardi's sy + htmx_trigger = "epilepsy_cause" + elif url.get("field_name") == "comorbidity": + data = {"comorbidityentity": 134} + expected_result = ComorbidityEntity.objects.get( + pk=34 + ) # specific learning difficulty + htmx_trigger = "comorbidityentity" + elif url.get("field_name") == "syndrome_name": + data = {"syndrome": 35} + expected_result = SyndromeEntity.objects.get( + pk=35 + ) # Self-limited (familial) neonatal epilepsy + htmx_trigger = "syndrome" + + client.post( + reverse( + url.get("field_name"), + kwargs={url.get("param"): model.id}, + ), + headers={"Hx-Trigger-Name": htmx_trigger, "Hx-Request": "true"}, + data=data, + ) + updated_model = get_model_from_model( + case=CASE_FROM_TEST_USER_ORGANISATION, model_name=url.get("model") + ) + validate_select( + field_name=url.get("field_name"), + model_instance=updated_model, + expected_result=expected_result, + assert_pass=True, + ) + + +@pytest.mark.django_db +def test_user_updates_select_fail( + client, +): + """ + Assert for each single_choice_multiple_toggle choice selection, value stored in model is correct selection value + """ + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + CASE_FROM_TEST_USER_ORGANISATION = E12CaseFactory.create( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}", + organisations__organisation=TEST_USER_ORGANISATION, + ) + + test_user = Epilepsy12User.objects.get( + first_name=test_user_rcpch_audit_team_data.role_str + ) + + client.force_login(test_user) + + for index, url in enumerate(SELECTS): + if url.get("choices") is not None: + for choice in url.get("choices"): + model = get_model_from_model( + case=CASE_FROM_TEST_USER_ORGANISATION, + model_name=url.get("model"), + ) + data = {url.get("field_name"): choice} + + client.post( + reverse( + url.get("field_name"), + kwargs={url.get("param"): model.id}, + ), + headers={ + "Hx-Trigger-Name": choice, + "Hx-Request": "true", + }, + data=data, + ) + updated_model = get_model_from_model( + case=CASE_FROM_TEST_USER_ORGANISATION, model_name=url.get("model") + ) + validate_select( + field_name=url.get("field_name"), + model_instance=updated_model, + expected_result=EpilepsyCauseEntity.objects.get( + pk=135 + ), # Dysmorphic sialidosis with renal involvement + assert_pass=False, + ) + else: + model = get_model_from_model( + case=CASE_FROM_TEST_USER_ORGANISATION, + model_name=url.get("model"), + ) + + if url.get("field_name") == "epilepsy_cause": + data = {"epilepsy_cause": 134} # Aicardi's sy. + expected_result = EpilepsyCauseEntity.objects.get( + pk=135 + ) # Dysmorphic sialidosis with renal involvement + htmx_trigger = "epilepsy_cause" + elif url.get("field_name") == "comorbidity_diagnosis": + data = {"comorbidityentity": 134} + expected_result = ComorbidityEntity.objects.get( + pk=35 + ) # Meets criteria for referral to Children's Epilepsy Surgery Service + htmx_trigger = "comorbidityentity" + elif url.get("field_name") == "syndrome_name": + data = {"syndrome": 35} + expected_result = SyndromeEntity.objects.get( + pk=34 + ) # Self-limited (familial) neonatal epilepsy + htmx_trigger = "syndrome" + + client.post( + reverse( + url.get("field_name"), + kwargs={url.get("param"): model.id}, + ), + headers={"Hx-Trigger-Name": htmx_trigger, "Hx-Request": "true"}, + data=data, + ) + updated_model = get_model_from_model( + case=CASE_FROM_TEST_USER_ORGANISATION, model_name=url.get("model") + ) + validate_select( + field_name=url.get("field_name"), + model_instance=updated_model, + expected_result=expected_result, + assert_pass=False, + ) + + +@pytest.mark.django_db +def test_user_updates_registation_date_not_over_24_years_PASS( + client, +): + """ + Assert date of first paediatric assessment cannot be after 24th birthday + """ + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + CASE_FROM_TEST_USER_ORGANISATION = E12CaseFactory.create( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}", + organisations__organisation=TEST_USER_ORGANISATION, + date_of_birth=date(year=2000, month=3, day=2), + ) + + test_user = Epilepsy12User.objects.get( + first_name=test_user_rcpch_audit_team_data.role_str + ) + + client.force_login(test_user) + + client.post( + reverse( + "registration_date", + kwargs={"case_id": CASE_FROM_TEST_USER_ORGANISATION.id}, + ), + headers={"Hx-Trigger-Name": "registration_date", "Hx-Request": "true"}, + data={"registration_date": date(year=2024, month=2, day=2)}, + ) + + assert ( + relativedelta.relativedelta( + CASE_FROM_TEST_USER_ORGANISATION.registration.registration_date, + CASE_FROM_TEST_USER_ORGANISATION.date_of_birth, + ).years + < 24 + ), f"{CASE_FROM_TEST_USER_ORGANISATION} is not over 24 years. Expected Pass." + + +@pytest.mark.skip(reason="Unfinished test. Test needs further work.") +def test_user_updates_registation_date_not_over_24_years_FAIL( + client, +): + """ + Assert date of first paediatric assessment cannot be after 24th birthday + """ + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + CASE_FROM_TEST_USER_ORGANISATION = E12CaseFactory.create( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}", + organisations__organisation=TEST_USER_ORGANISATION, + date_of_birth=date(year=2000, month=1, day=2), + ) + + test_user = Epilepsy12User.objects.get( + first_name=test_user_rcpch_audit_team_data.role_str + ) + + client.force_login(test_user) + + client.post( + reverse( + "registration_date", + kwargs={"case_id": CASE_FROM_TEST_USER_ORGANISATION.id}, + ), + headers={"Hx-Trigger-Name": "registration_date", "Hx-Request": "true"}, + data={"registration_date": date(year=2024, month=2, day=2)}, + ) + + assert ( + relativedelta.relativedelta( + CASE_FROM_TEST_USER_ORGANISATION.registration.registration_date, + CASE_FROM_TEST_USER_ORGANISATION.date_of_birth, + ).years + >= 24 + ), f"{CASE_FROM_TEST_USER_ORGANISATION} is over 24 years. Expected Fail." + + +# Test helper methods - there is one for each page_element +def validate_date_assertions( + field_name: str, + model_instance, + case, + second_date: date = None, + is_initial_date=False, + assert_pass=True, +): + """ + Tests all dates + """ + + date_to_test: date = getattr(model_instance, field_name, None) + + if date_to_test is None or type(date_to_test) is not date: + raise Exception("This field either does not exist or is not a date.") + + if assert_pass: + assert ( + date_to_test <= date.today() + ), f"{field_name} - {date_to_test} is not in the future - Expected PASS" + assert ( + date_to_test >= case.date_of_birth + ), f"{field_name} - {date_to_test} is not before {case}'s date of birth - Expected PASS" + assert ( + date_to_test >= case.registration.registration_date + ), f"{date_to_test} is not before {case}'s first paediatric assessment date ({case.registration.registration_date}) - Expected PASS" + else: + assert ( + date_to_test > date.today() + ), f"{field_name} - {date_to_test} is in the future - Expected FAIL" + assert ( + date_to_test < case.date_of_birth + ), f"{field_name} - {date_to_test} is before {case}'s date of birth - Expected FAIL" + assert ( + date_to_test < case.registration.registration_date + ), f"{field_name} - {date_to_test} is before {case}'s first paediatric assessment date ({case.registration.registration_date}) - Expected FAIL" + + if second_date is not None: + if assert_pass: + if is_initial_date: + assert ( + date_to_test <= second_date + ), f"{field_name} - {date_to_test} is before {second_date} - Expected PASS" + else: + assert ( + date_to_test >= second_date + ), f"{field_name} - {date_to_test} is after {second_date} - Expected PASS" + else: + if is_initial_date: + assert ( + date_to_test > second_date + ), f"{field_name} - {date_to_test} is not before {second_date} - Expected FAIL" + else: + assert ( + date_to_test < second_date + ), f"{field_name} - {date_to_test} is not after {second_date} - Expected FAIL" + + +def validate_toggle_button( + field_name: str, model_instance, expected_result: bool, assert_pass=True +): + """ + Asserts whether the result stored in the model matches that expected + """ + field_value = getattr(model_instance, field_name, None) + + if field_value is not None: + if assert_pass: + assert ( + expected_result == field_value + ), f"{field_name} - result stored in model is {field_value}. Result expected: {expected_result} - Expected PASS" + else: + assert ( + expected_result != field_value + ), f"{field_name} - result stored in model is {field_value}. Result expected: {expected_result} - Expected Fail" + + +def validate_single_choice_multiple_toggle_button( + field_name: str, model_instance, expected_result, assert_pass=True +): + """ + Asserts whether the result stored in the model matches that expected + """ + field_value = getattr(model_instance, field_name, None) + + if field_value is not None: + if assert_pass: + assert ( + f"{expected_result}" == f"{field_value}" + ), f"{field_name} - result stored in model is {field_value}. Result expected: {expected_result} - Expected PASS" + else: + assert ( + f"{expected_result}" != f"{field_value}" + ), f"{field_name} - result stored in model is {field_value}. Result expected: {expected_result} - Expected Fail" + + +def validate_select(field_name: str, model_instance, expected_result, assert_pass=True): + """ + Asserts whether the result stored in the model matches that expected + """ + field_value = getattr(model_instance, field_name, None) + + if field_value is not None: + if assert_pass: + assert ( + f"{expected_result}" == f"{field_value}" + ), f"{field_name} - result stored in model is {field_value}. Result expected: {expected_result} - Expected PASS" + else: + assert ( + f"{expected_result}" != f"{field_value}" + ), f"{field_name} - result stored in model is {field_value}. Result expected: {expected_result} - Expected Fail" + + +def get_model_from_model(case, model_name): + """ + Return model instance related to Case + """ + if model_name == "episode": + return Episode.objects.filter( + multiaxial_diagnosis=case.registration.multiaxialdiagnosis + ).first() + elif model_name == "comorbidity": + comorbidity, created = Comorbidity.objects.get_or_create( + multiaxial_diagnosis=case.registration.multiaxialdiagnosis, + comorbidityentity=ComorbidityEntity.objects.get( + pk=34 + ), # specific learning difficulty + ) + return comorbidity + elif model_name == "syndrome": + syndrome, created = Syndrome.objects.get_or_create( + multiaxial_diagnosis=case.registration.multiaxialdiagnosis, + syndrome=SyndromeEntity.objects.get( + pk=35 + ), # Self-limited (familial) neonatal epilepsy + ) + return syndrome + elif model_name == "epilepsycauseentity": + return EpilepsyCauseEntity.objects.get(pk=135) # Aicardi's syndrome + elif model_name == "antiepilepsymedicine": + return AntiEpilepsyMedicine.objects.create( + management=case.registration.management, + is_rescue_medicine=False, + medicine_entity=MedicineEntity.objects.get( + medicine_name="Sodium valproate" + ), + ) + elif model_name == "multiaxialdiagnosis": + return MultiaxialDiagnosis.objects.get( + pk=case.registration.multiaxialdiagnosis.pk + ) # + else: + refresh_case = Case.objects.get(pk=case.pk) + return getattr(refresh_case.registration, model_name, None) diff --git a/epilepsy12/tests/view_tests/form_calculations/__init__.py b/epilepsy12/tests/view_tests/form_calculations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/epilepsy12/tests/view_tests/form_calculations/test_completed_fields.py b/epilepsy12/tests/view_tests/form_calculations/test_completed_fields.py new file mode 100644 index 00000000..11fca26b --- /dev/null +++ b/epilepsy12/tests/view_tests/form_calculations/test_completed_fields.py @@ -0,0 +1,946 @@ +""" +# Tests for `completed_fields` fn + +These tests occur on a model-basis. + +For each MODEL: + + 1) + - fill in ALL number of attributes + - assert completed_fields(MODEL) == COUNT(ALL) + 2) + - fill in random(X) number of attributes + - assert completed_fields(MODEL) == X + +- `Registration` - DONE + +- `FirstPaediatricAssessment` - DONE + + completed_fields(MODEL) == 6 + 1. first_paediatric_assessment_in_acute_or_nonacute_setting + 2. has_number_of_episodes_since_the_first_been_documented + 3. general_examination_performed + 4. neurological_examination_performed + 5. developmental_learning_or_schooling_problems + 6. behavioural_or_emotional_problems + +- `EpilepsyContext` - DONE + + completed_fields(MODEL) == 8 + 1. previous_febrile_seizure + 2. previous_acute_symptomatic_seizure + 3. is_there_a_family_history_of_epilepsy + 4. previous_neonatal_seizures + 5. diagnosis_of_epilepsy_withdrawn + 6. were_any_of_the_epileptic_seizures_convulsive + 7. experienced_prolonged_generalized_convulsive_seizures + 8. experienced_prolonged_focal_seizures + +- `Assessment` - DONE + + EXPECTED_SCORE=completed_fields(MODEL) == 5 + 1. "childrens_epilepsy_surgical_service_referral_criteria_met" + 2. "consultant_paediatrician_referral_made" + 3. "paediatric_neurologist_referral_made" + 4. "childrens_epilepsy_surgical_service_referral_made" + 5. "epilepsy_specialist_nurse_referral_made" + + if "consultant_paediatrician_referral_made" == True + EXPECTED_SCORE+=3 + 'general_paediatric_centre' + 'consultant_paediatrician_referral_date' + 'consultant_paediatrician_input_date' + + number_of_completed_fields_in_related_models = 1 + general_paediatric_centre + + if "paediatric_neurologist_referral_made" == True + EXPECTED_SCORE+=3 + 'paediatric_neurology_centre' + 'paediatric_neurologist_referral_date' + 'paediatric_neurologist_input_date' + + number_of_completed_fields_in_related_models = 1 + 'paediatric_neurology_centre' + + if "childrens_epilepsy_surgical_service_referral_made" == True + EXPECTED_SCORE+=3 + 'epilepsy_surgery_centre' + 'childrens_epilepsy_surgical_service_referral_date' + 'childrens_epilepsy_surgical_service_input_date' + + number_of_completed_fields_in_related_models = 1 + 'epilepsy_surgery_centre' + + if "epilepsy_specialist_nurse_referral_made" == True + EXPECTED_SCORE+=2 + 'epilepsy_specialist_nurse_referral_date' + 'epilepsy_specialist_nurse_input_date' + + +- `Investigations` - DONE + + EXPECTED_SCORE=completed_fields(MODEL) == 4 + 'eeg_indicated' + 'twelve_lead_ecg_status' + 'ct_head_scan_status' + 'mri_indicated' + + if 'eeg_indicated' == True: + EXPECTED_SCORE+=2 + 'eeg_request_date' + 'eeg_performed_date' + if mri_indicated == True: + EXPECTED_SCORE+=2 + 'mri_brain_requested_date' + 'mri_brain_reported_date' + +- `Management` - DONE + EXPECTED_SCORE = 5 + 'has_an_aed_been_given' + 'has_rescue_medication_been_prescribed' + 'individualised_care_plan_in_place' + 'has_been_referred_for_mental_health_support' + 'has_support_for_mental_health_support' + + if 'has_rescue_medication_been_prescribed' == True + for medicine in AntiepilepsyMedicine.all(is_rescue_medicine=True) + number_of_completed_fields_in_related_models += 3 + is_rescue_medicine + antiepilepsy_medicine_start_date + antiepilepsy_medicine_risk_discussed + + if 'has_an_aed_been_given' == True + for medicine in AntiepilepsyMedicine.all(is_rescue_medicine=False) + number_of_completed_fields_in_related_models += 3 + is_rescue_medicine + antiepilepsy_medicine_start_date + antiepilepsy_medicine_risk_discussed + + if case.sex == 2 and age >= 12 and is_a_pregnancy_prevention_programme_needed==True: + number_of_completed_fields_in_related_models += 2 + has_a_valproate_annual_risk_acknowledgement_form_been_completed + is_a_pregnancy_prevention_programme_in_place + + + if 'individualised_care_plan_in_place' == True + EXPECTED_SCORE += 10 + 'individualised_care_plan_date' + 'individualised_care_plan_has_parent_carer_child_agreement' + 'individualised_care_plan_includes_service_contact_details' + 'individualised_care_plan_include_first_aid' + 'individualised_care_plan_parental_prolonged_seizure_care' + 'individualised_care_plan_includes_general_participation_risk' + 'individualised_care_plan_addresses_water_safety' + 'individualised_care_plan_addresses_sudep' + 'individualised_care_plan_includes_ehcp' + 'has_individualised_care_plan_been_updated_in_the_last_year' + + +- `MultiaxialDiagnosis` - DONE + EXPECTED_SCORE = 7 + 'epilepsy_cause_known' + 'relevant_impairments_behavioural_educational' + 'mental_health_screen' + 'mental_health_issue_identified' + 'global_developmental_delay_or_learning_difficulties' + 'autistic_spectrum_disorder' + + if 'mental_health_issue_identified': + EXPECTED_SCORE += 1 + 'mental_health_issue' + + if 'global_developmental_delay_or_learning_difficulties': + EXPECTED_SCORE += 1 + 'global_developmental_delay_or_learning_difficulties_severity' + + + if multiaxial_diagnosis.'epilepsy_cause_known': + 'epilepsy_cause' + 'epilepsy_cause_categories' (len(multiaxial_diagnosis.epilepsy_cause_categories)>0) + + +""" + +# python imports +import pytest +from datetime import date +import random + +# django imports +from django.urls import reverse + +# E12 imports +from epilepsy12.models import ( + Epilepsy12User, + Organisation, + Case, + Registration, + FirstPaediatricAssessment, + EpilepsyContext, + Assessment, + Investigations, + Management, + MultiaxialDiagnosis, +) +from epilepsy12.common_view_functions import completed_fields +from epilepsy12.constants import ( + CHRONICITY, + OPT_OUT_UNCERTAIN, + SEVERITY, + NEUROPSYCHIATRIC, +) + + +@pytest.mark.django_db +def test_completed_fields_registration_all_fields(e12_case_factory, GOSH): + """ + Simulating completed_fields(model_instance=Registration) returns correct counter when all fields have an answer. + """ + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + registration__registration_date=None, + registration__eligibility_criteria_met=None, + ) + + registration = Registration.objects.get(case=CASE) + + assert ( + completed_fields(registration) == 0 + ), f"Empty registration, `completed_fields(registration)` should return 0. Instead returned {completed_fields(registration)}" + + registration.registration_date = date(2023, 1, 1) + registration.eligibility_criteria_met = True + registration.save() + + assert ( + completed_fields(registration) == 2 + ), f"Completed registration, `completed_fields(registration)` should return 2. . Instead returned {completed_fields(registration)}" + + +@pytest.mark.django_db +def test_completed_fields_registration_random_fields(e12_case_factory, GOSH): + """ + Simulating completed_fields(model_instance=Registration) returns correct counter when fields randomly have an answer or left None. + """ + EXPECTED_SCORE = 0 + + REGISTRATION_DATE = random.choice([None, date(2023, 1, 1)]) + if REGISTRATION_DATE is not None: + EXPECTED_SCORE += 1 + + ELIGIBILITY_CRITERIA_MET = random.choice([None, True]) + if ELIGIBILITY_CRITERIA_MET is not None: + EXPECTED_SCORE += 1 + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + registration__registration_date=REGISTRATION_DATE, + registration__eligibility_criteria_met=ELIGIBILITY_CRITERIA_MET, + ) + + registration = Registration.objects.get(case=CASE) + + assert ( + completed_fields(registration) == EXPECTED_SCORE + ), f"Randomly completed registration, `completed_fields(registration)` should return {EXPECTED_SCORE}. Instead returned {completed_fields(registration)}" + + +@pytest.mark.django_db +def test_completed_fields_first_paediatric_assessment_all_fields( + e12_case_factory, GOSH +): + """ + Simulating completed_fields(model_instance=first_paediatric_assessment) returns correct counter when all fields have an answer. + """ + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + ) + + first_paediatric_assessment = FirstPaediatricAssessment.objects.get( + registration=CASE.registration + ) + + assert ( + completed_fields(first_paediatric_assessment) == 0 + ), f"Empty first_paediatric_assessment, `completed_fields(first_paediatric_assessment)` should return 0. Instead returned {completed_fields(first_paediatric_assessment)}" + + fields_and_answers = { + "first_paediatric_assessment_in_acute_or_nonacute_setting": CHRONICITY[0][0], + "has_number_of_episodes_since_the_first_been_documented": True, + "general_examination_performed": True, + "neurological_examination_performed": True, + "developmental_learning_or_schooling_problems": True, + "behavioural_or_emotional_problems": True, + } + factory_attributes = { + f"registration__first_paediatric_assessment__{field}": answer + for field, answer in fields_and_answers.items() + } + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + **factory_attributes, + ) + + first_paediatric_assessment = FirstPaediatricAssessment.objects.get( + registration=CASE.registration + ) + + assert completed_fields(first_paediatric_assessment) == len( + fields_and_answers + ), f"Completed first_paediatric_assessment, `completed_fields(first_paediatric_assessment)` should return {len(fields_and_answers)}. Instead returned {completed_fields(first_paediatric_assessment)}" + + +@pytest.mark.django_db +def test_completed_fields_first_paediatric_assessment_random_fields( + e12_case_factory, GOSH +): + """ + Simulating completed_fields(model_instance=first_paediatric_assessment) returns correct counter when random fields have an answer or None. + """ + + fields_and_answers = { + "first_paediatric_assessment_in_acute_or_nonacute_setting": random.choice( + [None, CHRONICITY[0][0]] + ), + "has_number_of_episodes_since_the_first_been_documented": random.choice( + [None, True] + ), + "general_examination_performed": random.choice([None, True]), + "neurological_examination_performed": random.choice([None, True]), + "developmental_learning_or_schooling_problems": random.choice([None, True]), + "behavioural_or_emotional_problems": random.choice([None, True]), + } + EXPECTED_SCORE = 0 + for answer in fields_and_answers.values(): + if answer is not None: + EXPECTED_SCORE += 1 + + factory_attributes = { + f"registration__first_paediatric_assessment__{field}": answer + for field, answer in fields_and_answers.items() + } + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + **factory_attributes, + ) + + first_paediatric_assessment = FirstPaediatricAssessment.objects.get( + registration=CASE.registration + ) + + assert ( + completed_fields(first_paediatric_assessment) == EXPECTED_SCORE + ), f"Randomly completed first_paediatric_assessment, `completed_fields(first_paediatric_assessment)` should return {EXPECTED_SCORE}. Instead returned {completed_fields(first_paediatric_assessment)}. Answers: {fields_and_answers}" + + +@pytest.mark.django_db +def test_completed_fields_epilepsy_context_all_fields(e12_case_factory, GOSH): + """ + Simulating completed_fields(model_instance=epilepsy_context) returns correct counter when all fields have an answer. + """ + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + ) + + epilepsy_context = FirstPaediatricAssessment.objects.get( + registration=CASE.registration + ) + + assert ( + completed_fields(epilepsy_context) == 0 + ), f"Empty epilepsy_context, `completed_fields(epilepsy_context)` should return 0. Instead returned {completed_fields(epilepsy_context)}" + + fields_and_answers = { + "previous_febrile_seizure": OPT_OUT_UNCERTAIN[0][0], + "previous_acute_symptomatic_seizure": OPT_OUT_UNCERTAIN[0][0], + "is_there_a_family_history_of_epilepsy": OPT_OUT_UNCERTAIN[0][0], + "previous_neonatal_seizures": OPT_OUT_UNCERTAIN[0][0], + "diagnosis_of_epilepsy_withdrawn": True, + "were_any_of_the_epileptic_seizures_convulsive": True, + "experienced_prolonged_generalized_convulsive_seizures": OPT_OUT_UNCERTAIN[0][ + 0 + ], + "experienced_prolonged_focal_seizures": OPT_OUT_UNCERTAIN[0][0], + } + factory_attributes = { + f"registration__epilepsy_context__{field}": answer + for field, answer in fields_and_answers.items() + } + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + **factory_attributes, + ) + + epilepsy_context = EpilepsyContext.objects.get(registration=CASE.registration) + + assert completed_fields(epilepsy_context) == len( + fields_and_answers + ), f"Completed epilepsy_context, `completed_fields(epilepsy_context)` should return {len(fields_and_answers)}. Instead returned {completed_fields(epilepsy_context)}" + + +@pytest.mark.django_db +def test_completed_fields_epilepsy_context_random_fields(e12_case_factory, GOSH): + """ + Simulating completed_fields(model_instance=epilepsy_context) returns correct counter when random fields have an answer. + """ + + CHAR_CHOICE_FIELDS = [ + "previous_febrile_seizure", + "previous_acute_symptomatic_seizure", + "is_there_a_family_history_of_epilepsy", + "previous_neonatal_seizures", + "experienced_prolonged_generalized_convulsive_seizures", + "experienced_prolonged_focal_seizures", + ] + BOOL_FIELDS = [ + "diagnosis_of_epilepsy_withdrawn", + "were_any_of_the_epileptic_seizures_convulsive", + ] + + factory_attributes = {} + EXPECTED_SCORE = 0 + + for field in CHAR_CHOICE_FIELDS: + BASE_KEY_NAME = f"registration__epilepsy_context__{field}" + + answer = random.choice([None, OPT_OUT_UNCERTAIN]) + if answer is not None: + answer = answer[0][0] + EXPECTED_SCORE += 1 + + factory_attributes.update({BASE_KEY_NAME: answer}) + + for field in BOOL_FIELDS: + BASE_KEY_NAME = f"registration__epilepsy_context__{field}" + + answer = random.choice([None, True]) + if answer is not None: + EXPECTED_SCORE += 1 + + factory_attributes.update({BASE_KEY_NAME: answer}) + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + **factory_attributes, + ) + + epilepsy_context = EpilepsyContext.objects.get(registration=CASE.registration) + + assert ( + completed_fields(epilepsy_context) == EXPECTED_SCORE + ), f"Randomly completed epilepsy_context, `completed_fields(epilepsy_context)` should return {EXPECTED_SCORE}. Instead returned {completed_fields(epilepsy_context)}. Answers: {factory_attributes}" + + +@pytest.mark.django_db +def test_completed_fields_assessment_all_fields(e12_case_factory, GOSH): + """ + Simulating completed_fields(model_instance=assessment) returns correct counter when all fields have an answer. + """ + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + ) + + assessment = Assessment.objects.get(registration=CASE.registration) + + assert ( + completed_fields(assessment) == 0 + ), f"Empty assessment, `completed_fields(assessment)` should return 0. Instead returned {completed_fields(assessment)}" + + fields_and_answers = { + "childrens_epilepsy_surgical_service_referral_criteria_met": True, + "consultant_paediatrician_referral_made": True, + "consultant_paediatrician_referral_date": date(2023, 1, 1), + "consultant_paediatrician_input_date": date(2023, 1, 2), + "paediatric_neurologist_referral_made": True, + "paediatric_neurologist_referral_date": date(2023, 1, 1), + "paediatric_neurologist_input_date": date(2023, 1, 2), + "childrens_epilepsy_surgical_service_referral_made": True, + "childrens_epilepsy_surgical_service_referral_date": date(2023, 1, 1), + "childrens_epilepsy_surgical_service_input_date": date(2023, 1, 2), + "epilepsy_specialist_nurse_referral_made": True, + "epilepsy_specialist_nurse_referral_date": date(2023, 1, 1), + "epilepsy_specialist_nurse_input_date": date(2023, 1, 2), + } + + factory_attributes = { + f"registration__assessment__{field}": answer + for field, answer in fields_and_answers.items() + } + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + **factory_attributes, + ) + + assessment = Assessment.objects.get(registration=CASE.registration) + + assert completed_fields(assessment) == len( + fields_and_answers + ), f"Completed assessment, `completed_fields(assessment)` should return {len(fields_and_answers)}. Instead returned {completed_fields(assessment)}" + + +@pytest.mark.django_db +def test_completed_fields_assessment_random_fields(e12_case_factory, GOSH): + """ + Simulating completed_fields(model_instance=assessment) returns correct counter when random fields have an answer. + """ + + factory_attributes = {} + EXPECTED_SCORE = 0 + BASE_KEY_NAME = "registration__assessment__" + + # This field has no dependent date fields + KEY_NAME = ( + BASE_KEY_NAME + "childrens_epilepsy_surgical_service_referral_criteria_met" + ) + + ANSWER = random.choice([None, True]) + factory_attributes.update({KEY_NAME: ANSWER}) + if ANSWER is not None: + print(f"Adding 1 because {KEY_NAME} is not None") + EXPECTED_SCORE += 1 + + # All other bool fields have dependent date fields + BOOL_FIELDS = [ + "consultant_paediatrician", + "paediatric_neurologist", + "childrens_epilepsy_surgical_service", + "epilepsy_specialist_nurse", + ] + DATE_1 = date(2023, 1, 1) + DATE_2 = date(2023, 1, 2) + + for bool_field in BOOL_FIELDS: + KEY_NAME = BASE_KEY_NAME + f"{bool_field}_referral_made" + ANSWER = random.choice([None, True]) + factory_attributes.update({KEY_NAME: ANSWER}) + + DATE_1_ANSWER_OPTIONS = [None] + DATE_2_ANSWER_OPTIONS = [None] + + if ANSWER is not None: + # Opens up 2 date options + EXPECTED_SCORE += 1 + + DATE_1_ANSWER_OPTIONS += [DATE_1] + DATE_2_ANSWER_OPTIONS += [DATE_2] + + REFERRAL_KEY_NAME = BASE_KEY_NAME + f"{bool_field}_referral_date" + REFERRAL_ANSWER = random.choice(DATE_1_ANSWER_OPTIONS) + if REFERRAL_ANSWER is not None: + EXPECTED_SCORE += 1 + + INPUT_KEY_NAME = BASE_KEY_NAME + f"{bool_field}_input_date" + INPUT_ANSWER = random.choice(DATE_2_ANSWER_OPTIONS) + if INPUT_ANSWER is not None: + EXPECTED_SCORE += 1 + + date_answers = { + REFERRAL_KEY_NAME: REFERRAL_ANSWER, + INPUT_KEY_NAME: INPUT_ANSWER, + } + factory_attributes.update(date_answers) + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + **factory_attributes, + ) + + assessment = Assessment.objects.get(registration=CASE.registration) + + assert ( + completed_fields(assessment) == EXPECTED_SCORE + ), f"Randomly completed assessment, `completed_fields(assessment)` should return {EXPECTED_SCORE}. Instead returned {completed_fields(assessment)}. Answers: {factory_attributes}" + + +@pytest.mark.django_db +def test_completed_fields_investigations_all_fields(e12_case_factory, GOSH): + """ + Simulating completed_fields(model_instance=investigations) returns correct counter when all fields have an answer. + """ + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + ) + + investigations = Investigations.objects.get(registration=CASE.registration) + + assert ( + completed_fields(investigations) == 0 + ), f"Empty investigations, `completed_fields(investigations)` should return 0. Instead returned {completed_fields(investigations)}" + + DATE_1 = date(2023, 1, 1) + DATE_2 = date(2023, 1, 2) + + fields_and_answers = { + "eeg_indicated": True, + "eeg_request_date": DATE_1, + "eeg_performed_date": DATE_2, + "twelve_lead_ecg_status": True, + "ct_head_scan_status": True, + "mri_indicated": True, + "mri_brain_requested_date": DATE_1, + "mri_brain_reported_date": DATE_2, + } + + factory_attributes = { + f"registration__investigations__{field}": answer + for field, answer in fields_and_answers.items() + } + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + **factory_attributes, + ) + + investigations = Investigations.objects.get(registration=CASE.registration) + + assert completed_fields(investigations) == len( + fields_and_answers + ), f"Completed investigations, `completed_fields(investigations)` should return {len(fields_and_answers)}. Instead returned {completed_fields(investigations)}" + + +@pytest.mark.django_db +def test_completed_fields_investigations_random_fields(e12_case_factory, GOSH): + """ + Simulating completed_fields(model_instance=investigations) returns correct counter when random fields have an answer. + """ + + factory_attributes = {} + EXPECTED_SCORE = 0 + BASE_KEY_NAME = "registration__investigations__" + + # These fields have no dependent date fields + SIMPLE_BOOL_FIELDS = [ + "ct_head_scan_status", + "twelve_lead_ecg_status", + ] + for simple_bool_field in SIMPLE_BOOL_FIELDS: + KEY_NAME = BASE_KEY_NAME + simple_bool_field + + ANSWER = random.choice([None, True]) + factory_attributes.update({KEY_NAME: ANSWER}) + if ANSWER is not None: + EXPECTED_SCORE += 1 + + # All other bool fields have dependent date fields + BOOL_FIELDS = [ + "eeg", + "mri", + ] + + DATE_1 = date(2023, 1, 1) + DATE_2 = date(2023, 1, 2) + INVESTIGATION_DECLINED = date( + year=1915, month=4, day=15 + ) # Specific date value to signify investigation was declined, set in investigation_views.investigations + + for bool_field in BOOL_FIELDS: + KEY_NAME = BASE_KEY_NAME + f"{bool_field}_indicated" + ANSWER = random.choice([True]) + factory_attributes.update({KEY_NAME: ANSWER}) + + DATE_1_ANSWER_OPTIONS = [None] + DATE_2_ANSWER_OPTIONS = [None] + + if ANSWER is not None: + # Opens up 2 date options + + EXPECTED_SCORE += 1 + DATE_1_ANSWER_OPTIONS += [DATE_1] + DATE_2_ANSWER_OPTIONS += [DATE_2, INVESTIGATION_DECLINED] + + REQUEST_KEY_NAME = BASE_KEY_NAME + ( + f"{bool_field}_request_date" + if bool_field == "eeg" + else f"{bool_field}_brain_requested_date" + ) + REQUEST_ANSWER = random.choice(DATE_1_ANSWER_OPTIONS) + if REQUEST_ANSWER is not None: + EXPECTED_SCORE += 1 + + REPORTED_KEY_NAME = BASE_KEY_NAME + ( + f"{bool_field}_performed_date" + if bool_field == "eeg" + else f"{bool_field}_brain_reported_date" + ) + REPORTED_ANSWER = random.choice(DATE_2_ANSWER_OPTIONS) + if REPORTED_ANSWER is not None: + EXPECTED_SCORE += 1 + + date_answers = { + REQUEST_KEY_NAME: REQUEST_ANSWER, + REPORTED_KEY_NAME: REPORTED_ANSWER, + } + factory_attributes.update(date_answers) + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + **factory_attributes, + ) + + investigations = Investigations.objects.get(registration=CASE.registration) + + assert ( + completed_fields(investigations) == EXPECTED_SCORE + ), f"Randomly completed investigations, `completed_fields(investigations)` should return {EXPECTED_SCORE}. Instead returned {completed_fields(investigations)}. Answers: {factory_attributes}" + + +@pytest.mark.django_db +def test_completed_fields_management_all_fields(e12_case_factory, GOSH): + """ + Simulating completed_fields(model_instance=management) returns correct counter when all fields have an answer. + """ + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + ) + + management = Management.objects.get(registration=CASE.registration) + + assert ( + completed_fields(management) == 0 + ), f"Empty management, `completed_fields(management)` should return 0. Instead returned {completed_fields(management)}" + + BOOL_FIELDS = [ + "has_an_aed_been_given", + "has_rescue_medication_been_prescribed", + "individualised_care_plan_in_place", + "individualised_care_plan_has_parent_carer_child_agreement", + "individualised_care_plan_includes_service_contact_details", + "individualised_care_plan_include_first_aid", + "individualised_care_plan_parental_prolonged_seizure_care", + "individualised_care_plan_includes_general_participation_risk", + "individualised_care_plan_addresses_water_safety", + "individualised_care_plan_addresses_sudep", + "individualised_care_plan_includes_ehcp", + "has_individualised_care_plan_been_updated_in_the_last_year", + "has_been_referred_for_mental_health_support", + "has_support_for_mental_health_support", + ] + + factory_attributes = { + f"registration__management__{field}": True for field in BOOL_FIELDS + } + DATE_1 = date(2023, 1, 1) + DATE_FIELD = "individualised_care_plan_date" + factory_attributes.update({f"registration__management__{DATE_FIELD}": DATE_1}) + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + **factory_attributes, + ) + + management = Management.objects.get(registration=CASE.registration) + + assert completed_fields(management) == len( + factory_attributes + ), f"Completed management, `completed_fields(management)` should return {len(factory_attributes)}. Instead returned {completed_fields(management)}" + + +@pytest.mark.django_db +def test_completed_fields_management_random_fields(e12_case_factory, GOSH): + """ + Simulating completed_fields(model_instance=management) returns correct counter when random fields have an answer. + """ + + factory_attributes = {} + EXPECTED_SCORE = 0 + BASE_KEY_NAME = "registration__management__" + + BOOL_FIELDS = [ + "has_an_aed_been_given", + "has_rescue_medication_been_prescribed", + "individualised_care_plan_in_place", + "individualised_care_plan_has_parent_carer_child_agreement", + "individualised_care_plan_includes_service_contact_details", + "individualised_care_plan_include_first_aid", + "individualised_care_plan_parental_prolonged_seizure_care", + "individualised_care_plan_includes_general_participation_risk", + "individualised_care_plan_addresses_water_safety", + "individualised_care_plan_addresses_sudep", + "individualised_care_plan_includes_ehcp", + "has_individualised_care_plan_been_updated_in_the_last_year", + "has_been_referred_for_mental_health_support", + "has_support_for_mental_health_support", + ] + for bool_field in BOOL_FIELDS: + KEY_NAME = BASE_KEY_NAME + bool_field + ANSWER = random.choice([None, True]) + factory_attributes.update({KEY_NAME: ANSWER}) + if ANSWER is not None: + EXPECTED_SCORE += 1 + + DATE_1 = date(2023, 1, 1) + DATE_FIELD = "individualised_care_plan_date" + DATE_KEY_NAME = BASE_KEY_NAME + DATE_FIELD + DATE_ANSWER = random.choice([None, DATE_1]) + if DATE_ANSWER is not None: + EXPECTED_SCORE += 1 + factory_attributes.update({DATE_KEY_NAME: DATE_ANSWER}) + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + **factory_attributes, + ) + + management = Management.objects.get(registration=CASE.registration) + + assert ( + completed_fields(management) == EXPECTED_SCORE + ), f"Randomly completed management, `completed_fields(management)` should return {EXPECTED_SCORE}. Instead returned {completed_fields(management)}. Answers: {factory_attributes}" + + +@pytest.mark.django_db +def test_completed_fields_multiaxial_diagnosis_all_fields(e12_case_factory, GOSH): + """ + Simulating completed_fields(model_instance=multiaxial_diagnosis) returns correct counter when all fields have an answer. + """ + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + ) + + multiaxial_diagnosis = MultiaxialDiagnosis.objects.get( + registration=CASE.registration + ) + + assert ( + completed_fields(multiaxial_diagnosis) == 0 + ), f"Empty multiaxial_diagnosis, `completed_fields(multiaxial_diagnosis)` should return 0. Instead returned {completed_fields(multiaxial_diagnosis)}" + + BASE_KEY_NAME = "registration__multiaxial_diagnosis__" + + BOOL_FIELDS = [ + "syndrome_present", + "epilepsy_cause_known", + "relevant_impairments_behavioural_educational", + "global_developmental_delay_or_learning_difficulties", + "autistic_spectrum_disorder", + "mental_health_screen", + "mental_health_issue_identified", + ] + factory_attributes = {f"{BASE_KEY_NAME}{field}": True for field in BOOL_FIELDS} + + # ArrayField + EPILEPSY_CAUSES_KEY_NAME = BASE_KEY_NAME + "epilepsy_cause_categories" + EPILEPSY_CAUSES_ANSWER = ["Gen"] + factory_attributes.update({EPILEPSY_CAUSES_KEY_NAME: EPILEPSY_CAUSES_ANSWER}) + + # CharFields + char_fields_and_answers = { + "global_developmental_delay_or_learning_difficulties_severity": SEVERITY[0][0], + "mental_health_issue": NEUROPSYCHIATRIC[0][0], + } + char_fields_factory_attributes = { + f"{BASE_KEY_NAME}{field}": answer + for field, answer in char_fields_and_answers.items() + } + factory_attributes.update(char_fields_factory_attributes) + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + **factory_attributes, + ) + + multiaxial_diagnosis = MultiaxialDiagnosis.objects.get( + registration=CASE.registration + ) + + assert completed_fields(multiaxial_diagnosis) == len( + factory_attributes + ), f"Completed multiaxial_diagnosis, `completed_fields(multiaxial_diagnosis)` should return {len(factory_attributes)}. Instead returned {completed_fields(multiaxial_diagnosis)}" + + +@pytest.mark.django_db +def test_completed_fields_multiaxial_diagnosis_random_fields(e12_case_factory, GOSH): + """ + Simulating completed_fields(model_instance=multiaxial_diagnosis) returns correct counter when random fields have an answer. + """ + + factory_attributes = {} + EXPECTED_SCORE = 0 + BASE_KEY_NAME = "registration__multiaxial_diagnosis__" + + BASE_KEY_NAME = "registration__multiaxial_diagnosis__" + + BOOL_FIELDS = [ + "syndrome_present", + "epilepsy_cause_known", + "relevant_impairments_behavioural_educational", + "global_developmental_delay_or_learning_difficulties", + "autistic_spectrum_disorder", + "mental_health_screen", + "mental_health_issue_identified", + ] + for bool_field in BOOL_FIELDS: + KEY_NAME = BASE_KEY_NAME + bool_field + ANSWER = random.choice([None, True]) + factory_attributes.update({KEY_NAME: ANSWER}) + if ANSWER is not None: + EXPECTED_SCORE += 1 + + # ArrayField + EPILEPSY_CAUSES_KEY_NAME = BASE_KEY_NAME + "epilepsy_cause_categories" + EPILEPSY_CAUSES_ANSWER = random.choice([None, ["Gen"]]) + if EPILEPSY_CAUSES_ANSWER is not None: + EXPECTED_SCORE += 1 + factory_attributes.update({EPILEPSY_CAUSES_KEY_NAME: EPILEPSY_CAUSES_ANSWER}) + + # CharFields + GLOBAL_DEV_DELAY_KEY_NAME = ( + BASE_KEY_NAME + "global_developmental_delay_or_learning_difficulties_severity" + ) + GLOBAL_DEV_DELAY_ANSWER = random.choice([None, SEVERITY[0][0]]) + if GLOBAL_DEV_DELAY_ANSWER is not None: + EXPECTED_SCORE += 1 + factory_attributes.update({GLOBAL_DEV_DELAY_KEY_NAME: GLOBAL_DEV_DELAY_ANSWER}) + + MENTAL_HEALTH_ISSUE_KEY_NAME = BASE_KEY_NAME + "mental_health_issue" + MENTAL_HEALTH_ISSUE_ANSWER = random.choice([None, NEUROPSYCHIATRIC[0][0]]) + if MENTAL_HEALTH_ISSUE_ANSWER is not None: + EXPECTED_SCORE += 1 + factory_attributes.update( + {MENTAL_HEALTH_ISSUE_KEY_NAME: MENTAL_HEALTH_ISSUE_ANSWER} + ) + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + **factory_attributes, + ) + + multiaxial_diagnosis = MultiaxialDiagnosis.objects.get( + registration=CASE.registration + ) + + assert ( + completed_fields(multiaxial_diagnosis) == EXPECTED_SCORE + ), f"Randomly completed multiaxial_diagnosis, `completed_fields(multiaxial_diagnosis)` should return {EXPECTED_SCORE}. Instead returned {completed_fields(multiaxial_diagnosis)}. Answers: {factory_attributes}" diff --git a/epilepsy12/tests/view_tests/form_calculations/test_number_of_completed_fields_in_related_models.py b/epilepsy12/tests/view_tests/form_calculations/test_number_of_completed_fields_in_related_models.py new file mode 100644 index 00000000..c167391b --- /dev/null +++ b/epilepsy12/tests/view_tests/form_calculations/test_number_of_completed_fields_in_related_models.py @@ -0,0 +1,592 @@ +""" Tests for `number_of_completed_fields_in_related_models` fn. + + MultiaxialDiagnosis - DONE + - `Episode` + for episode in Episodes: + EXPECTED_SCORE += 5 (if at least one episode is epileptic) + 'seizure_onset_date' + 'seizure_onset_date_confidence' + 'episode_definition' + 'has_description_of_the_episode_or_episodes_been_gathered' + 'epilepsy_or_nonepilepsy_status' + + if episode.has_description_of_the_episode_or_episodes_been_gathered: + EXPECTED_SCORE += 1 + description + if episode.epilepsy_or_nonepilepsy_status == "E": + EXPECTED_SCORE += 1 + + if episode.epileptic_seizure_onset_type == "GO": + # 'generalised' onset: essential fields + # 'epileptic_generalised_onset' + EXPECTED_SCORE += 1 + elif episode.epileptic_seizure_onset_type == "FO": + # focal onset + # minimum score is laterality + EXPECTED_SCORE += 1 + else: + # either unclassified or unknown onset + # no further score + EXPECTED_SCORE += 0 + elif episode.epilepsy_or_nonepilepsy_status == "NE": + # nonepileptic seizure - essential fields: + # nonepileptic_seizure_unknown_onset + # nonepileptic_seizure_type + # AND ONE of behavioural/migraine/misc/paroxysmal/sleep related/syncope - essential fields: + # nonepileptic_seizure_behavioural or + # nonepileptic_seizure_migraine or + # nonepileptic_seizure_miscellaneous or + # nonepileptic_seizure_paroxysmal or + # nonepileptic_seizure_sleep + # nonepileptic_seizure_syncope + + if episode.nonepileptic_seizure_type == "Oth": + EXPECTED_SCORE += 2 + else: + EXPECTED_SCORE += 3 + elif episode.epilepsy_or_nonepilepsy_status == "U": + # uncertain status + EXPECTED_SCORE += 0 + + - `Syndrome` + for syndrome in Syndromes: + EXPECTED_SCORE += 2 + "syndrome_diagnosis_date" + "syndrome__syndrome_name" + + - `Comorbidity` + for comorbidity in Comorbidities: + EXPECTED_SCORE += 2 + comorbidity_diagnosis_date" + "comorbidity__comorbidityentity__conceptId" +Management +Registration + +""" + +# python imports +import pytest +from datetime import date +import random + +# django imports + +# E12 imports +from epilepsy12.models import ( + SyndromeEntity, + ComorbidityEntity, + Site, + MedicineEntity, +) +from epilepsy12.common_view_functions.recalculate_form_generate_response import ( + number_of_completed_fields_in_related_models, +) +from epilepsy12.constants import ( + EPILEPSY_DIAGNOSIS_STATUS, + EPILEPSY_SEIZURE_TYPE, + GENERALISED_SEIZURE_TYPE, + NON_EPILEPSY_SEIZURE_TYPE, + NON_EPILEPSY_SEIZURE_ONSET, + NON_EPILEPSY_BEHAVIOURAL_ARREST_SYMPTOMS, + DATE_ACCURACY, + EPISODE_DEFINITION, + SEX_TYPE, +) + + +def get_random_answers_update_counter(answer_set: dict, counter: int): + """Helper fn to return a random answer (None or valid value), and update counter of expected score. + + Args: + answer_set (dict): current answer_set + counter (int): current counter state + + Returns: + answer_set (dict) : answer_set which can be provided to factory constructor + counter (int): updated counter + """ + + for key, val in answer_set.items(): + answer = random.choice([None, val]) + answer_set[key] = answer + + # update counter + if answer is not None: + counter += 1 + + return answer_set, counter + + +@pytest.mark.django_db +def test_related_model_fields_count_all_episode_fully_completed( + e12_case_factory, e12_episode_factory, GOSH +): + """ + Simulating number_of_completed_fields_in_related_models(model_instance=multiaxialdiagnosis) returns correct counter when all Episode fields have an answer. + """ + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + ) + multiaxial_diagnosis = CASE.registration.multiaxialdiagnosis + return_value = number_of_completed_fields_in_related_models(multiaxial_diagnosis) + assert ( + return_value == 0 + ), f"Empty episode, `number_of_completed_fields_in_related_models(multiaxial_diagnosis)` should return 0. Instead returned {return_value}" + + # Specific answer doesn't matter for these fields - just need an answer + COMMON_FIELDS = { + "seizure_onset_date": date(2023, 1, 1), + "seizure_onset_date_confidence": DATE_ACCURACY[0][0], + "episode_definition": EPISODE_DEFINITION[0][0], + "has_description_of_the_episode_or_episodes_been_gathered": True, + "description": "The seizure happened when child was watching TV", + } + + EPILEPTIC_FOCAL_ONSET = { + "epilepsy_or_nonepilepsy_status": EPILEPSY_DIAGNOSIS_STATUS[0][0], + "epileptic_seizure_onset_type": EPILEPSY_SEIZURE_TYPE[0][0], + "focal_onset_left": True, + "focal_onset_impaired_awareness": True, # should not be counted! + } + EPILEPTIC_GENERALISED_ONSET = { + "epilepsy_or_nonepilepsy_status": EPILEPSY_DIAGNOSIS_STATUS[0][0], + "epileptic_seizure_onset_type": EPILEPSY_SEIZURE_TYPE[1][0], + "epileptic_generalised_onset": GENERALISED_SEIZURE_TYPE[0][0], + } + EPILEPTIC_UNKNOWN_ONSET = { + "epilepsy_or_nonepilepsy_status": EPILEPSY_DIAGNOSIS_STATUS[0][0], + "epileptic_seizure_onset_type": EPILEPSY_SEIZURE_TYPE[2][0], + } + EPILEPTIC_UNCLASSIFIED_ONSET = { + "epilepsy_or_nonepilepsy_status": EPILEPSY_DIAGNOSIS_STATUS[0][0], + "epileptic_seizure_onset_type": EPILEPSY_SEIZURE_TYPE[3][0], + } + NON_EPILEPTIC = { + "epilepsy_or_nonepilepsy_status": EPILEPSY_DIAGNOSIS_STATUS[1][0], + "nonepileptic_seizure_type": NON_EPILEPSY_SEIZURE_TYPE[0][0], + "nonepileptic_seizure_unknown_onset": NON_EPILEPSY_SEIZURE_ONSET[0][0], + "nonepileptic_seizure_behavioural": NON_EPILEPSY_BEHAVIOURAL_ARREST_SYMPTOMS[0][ + 0 + ], + } + UNCERTAIN = { + "epilepsy_or_nonepilepsy_status": EPILEPSY_DIAGNOSIS_STATUS[2][0], + } + + # These are each answer sets which, combined with COMMON_FIELDS, will create a fully completed episode + SEIZURE_TYPE_OPTIONS = [ + EPILEPTIC_FOCAL_ONSET, + EPILEPTIC_GENERALISED_ONSET, + EPILEPTIC_UNKNOWN_ONSET, + EPILEPTIC_UNCLASSIFIED_ONSET, + NON_EPILEPTIC, + UNCERTAIN, + ] + + # For each SEIZURE_TYPE_OPTION, create an Episode with COMMON_FIELDS and that SEIZURE_TYPE, make assertion + factory_attributes = {**COMMON_FIELDS} + for SEIZURE_TYPE in SEIZURE_TYPE_OPTIONS: + episode = e12_episode_factory( + multiaxial_diagnosis=multiaxial_diagnosis, + **factory_attributes, + **SEIZURE_TYPE, + ) + + return_value = number_of_completed_fields_in_related_models( + multiaxial_diagnosis + ) + + expected_value = len(factory_attributes) + len(SEIZURE_TYPE) + + # DON'T COUNT OTHER RADIO BUTTONS FOR FOCAL ONSET + if "focal_onset_impaired_awareness" in SEIZURE_TYPE: + expected_value -= 1 + + assert ( + return_value == expected_value + ), f"Fully completed episodes run through `number_of_completed_fields_in_related_models(multiaxial_diagnosis)`. Expected {expected_value=} but received {return_value=}. Inserted Episode answer fields were: {factory_attributes}+{SEIZURE_TYPE}" + + # Reset for next seizure type + episode.delete() + + +@pytest.mark.django_db +def test_related_model_fields_count_all_episode_random_answers( + e12_case_factory, e12_episode_factory, GOSH +): + """ + Simulating number_of_completed_fields_in_related_models(model_instance=multiaxialdiagnosis) returns correct counter when Episode fields' answers are randomly either valid value or None. + """ + + counter = 0 + + # Specific answer doesn't matter for these fields - just need an answer + COMMON_FIELDS = { + "seizure_onset_date": date(2023, 1, 1), + "seizure_onset_date_confidence": DATE_ACCURACY[0][0], + "episode_definition": EPISODE_DEFINITION[0][0], + "has_description_of_the_episode_or_episodes_been_gathered": True, + "description": "The seizure happened when child was watching TV", + } + + EPILEPTIC_FOCAL_ONSET = { + "epilepsy_or_nonepilepsy_status": EPILEPSY_DIAGNOSIS_STATUS[0][0], + "epileptic_seizure_onset_type": EPILEPSY_SEIZURE_TYPE[0][0], + "focal_onset_left": True, + "focal_onset_impaired_awareness": True, # should not be counted! + } + EPILEPTIC_GENERALISED_ONSET = { + "epilepsy_or_nonepilepsy_status": EPILEPSY_DIAGNOSIS_STATUS[0][0], + "epileptic_seizure_onset_type": EPILEPSY_SEIZURE_TYPE[1][0], + "epileptic_generalised_onset": GENERALISED_SEIZURE_TYPE[0][0], + } + EPILEPTIC_UNKNOWN_ONSET = { + "epilepsy_or_nonepilepsy_status": EPILEPSY_DIAGNOSIS_STATUS[0][0], + "epileptic_seizure_onset_type": EPILEPSY_SEIZURE_TYPE[2][0], + } + EPILEPTIC_UNCLASSIFIED_ONSET = { + "epilepsy_or_nonepilepsy_status": EPILEPSY_DIAGNOSIS_STATUS[0][0], + "epileptic_seizure_onset_type": EPILEPSY_SEIZURE_TYPE[3][0], + } + NON_EPILEPTIC = { + "epilepsy_or_nonepilepsy_status": EPILEPSY_DIAGNOSIS_STATUS[1][0], + "nonepileptic_seizure_type": NON_EPILEPSY_SEIZURE_TYPE[0][0], + "nonepileptic_seizure_unknown_onset": NON_EPILEPSY_SEIZURE_ONSET[0][0], + "nonepileptic_seizure_behavioural": NON_EPILEPSY_BEHAVIOURAL_ARREST_SYMPTOMS[0][ + 0 + ], + } + UNCERTAIN = { + "epilepsy_or_nonepilepsy_status": EPILEPSY_DIAGNOSIS_STATUS[2][0], + } + + SEIZURE_TYPE_OPTIONS = [ + EPILEPTIC_FOCAL_ONSET, + EPILEPTIC_GENERALISED_ONSET, + EPILEPTIC_UNKNOWN_ONSET, + EPILEPTIC_UNCLASSIFIED_ONSET, + NON_EPILEPTIC, + UNCERTAIN, + ] + + # Get random answer set for common fields + COMMON_FIELDS, counter = get_random_answers_update_counter( + answer_set=COMMON_FIELDS, counter=counter + ) + + # Create 5 randomly filled episodes, for each seizure type - ensures covers various different scenarios. + for _ in range(5): + for SEIZURE_TYPE in SEIZURE_TYPE_OPTIONS: + SEIZURE_TYPE, expected_value = get_random_answers_update_counter( + answer_set=SEIZURE_TYPE, counter=counter + ) + + # DON'T COUNT OTHER RADIO BUTTONS FOR FOCAL ONSET, regardless of answer + if SEIZURE_TYPE.get("focal_onset_impaired_awareness") is not None: + expected_value -= 1 + + factory_attributes = {**COMMON_FIELDS, **SEIZURE_TYPE} + + # Need a case to make an episode + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + ) + multiaxial_diagnosis = CASE.registration.multiaxialdiagnosis + + episode = e12_episode_factory( + multiaxial_diagnosis=multiaxial_diagnosis, + **factory_attributes, + ) + + return_value = number_of_completed_fields_in_related_models( + multiaxial_diagnosis + ) + + assert ( + return_value == expected_value + ), f"Randomly completed episodes run through `number_of_completed_fields_in_related_models(multiaxial_diagnosis)`. Expected {expected_value=} but received {return_value=}. Inserted Episode answer fields: {factory_attributes}+{SEIZURE_TYPE}" + + # Reset for next seizure type + episode.delete() + + +@pytest.mark.django_db +def test_related_model_fields_count_all_syndrome_fully_completed( + e12_case_factory, e12_syndrome_factory, GOSH +): + """ + Simulating number_of_completed_fields_in_related_models(model_instance=multiaxialdiagnosis) returns correct counter when all syndrome fields have an answer. + """ + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + ) + multiaxial_diagnosis = CASE.registration.multiaxialdiagnosis + return_value = number_of_completed_fields_in_related_models(multiaxial_diagnosis) + assert ( + return_value == 0 + ), f"Empty syndrome, `number_of_completed_fields_in_related_models(multiaxial_diagnosis)` should return 0. Instead returned {return_value}" + + factory_attributes = { + "syndrome_diagnosis_date": date(2023, 1, 1), + "syndrome": SyndromeEntity.objects.get(syndrome_name="Rasmussen syndrome"), + } + + syndrome = e12_syndrome_factory( + multiaxial_diagnosis=multiaxial_diagnosis, **factory_attributes + ) + + return_value = number_of_completed_fields_in_related_models(multiaxial_diagnosis) + + expected_value = len(factory_attributes) + + assert ( + return_value == expected_value + ), f"Fully completed Syndrome run through `number_of_completed_fields_in_related_models(multiaxial_diagnosis)`. Expected {expected_value=} but received {return_value=}. Inserted answer fields were: {factory_attributes}" + + +@pytest.mark.django_db +def test_related_model_fields_count_all_syndrome_random_answers( + e12_case_factory, e12_syndrome_factory, GOSH +): + """ + Simulating number_of_completed_fields_in_related_models(model_instance=multiaxialdiagnosis) returns correct counter when syndrome fields' answers are randomly either valid value or None. + """ + + counter = 0 + + factory_attributes_list = [] + SYNDROME_NAMES = SyndromeEntity.objects.all()[:5] + # Create 5 randomly filled syndromes - ensures covers various different scenarios. + for i in range(5): + inital_answer = { + "syndrome_diagnosis_date": date(2023, 1, 1), + "syndrome": SYNDROME_NAMES[i], + } + # Get random answer set for fields + ANSWER_SET, expected_value = get_random_answers_update_counter( + answer_set=inital_answer, counter=counter + ) + factory_attributes_list.append((ANSWER_SET, expected_value)) + + for factory_attributes, expected_value in factory_attributes_list: + # Need a case to make an syndrome + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + ) + multiaxial_diagnosis = CASE.registration.multiaxialdiagnosis + + syndrome = e12_syndrome_factory( + multiaxial_diagnosis=multiaxial_diagnosis, + **factory_attributes, + ) + + return_value = number_of_completed_fields_in_related_models( + multiaxial_diagnosis + ) + + assert ( + return_value == expected_value + ), f"Randomly completed syndromes run through `number_of_completed_fields_in_related_models(multiaxial_diagnosis)`. Expected {expected_value=} but received {return_value=}. Inserted answer fields: {factory_attributes}" + + # Reset for next seizure type + syndrome.delete() + + +@pytest.mark.django_db +def test_related_model_fields_count_all_comorbidity_fully_completed( + e12_case_factory, e12_comorbidity_factory, GOSH +): + """ + Simulating number_of_completed_fields_in_related_models(model_instance=multiaxialdiagnosis) returns correct counter when all comorbidity fields have an answer. + """ + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + ) + multiaxial_diagnosis = CASE.registration.multiaxialdiagnosis + return_value = number_of_completed_fields_in_related_models(multiaxial_diagnosis) + assert ( + return_value == 0 + ), f"Empty comorbidity, `number_of_completed_fields_in_related_models(multiaxial_diagnosis)` should return 0. Instead returned {return_value}" + + factory_attributes = { + "comorbidity_diagnosis_date": date(2023, 1, 1), + "comorbidityentity": ComorbidityEntity.objects.first(), + } + + comorbidity = e12_comorbidity_factory( + multiaxial_diagnosis=multiaxial_diagnosis, **factory_attributes + ) + + return_value = number_of_completed_fields_in_related_models(multiaxial_diagnosis) + + expected_value = len(factory_attributes) + + assert ( + return_value == expected_value + ), f"Fully completed comorbidity run through `number_of_completed_fields_in_related_models(multiaxial_diagnosis)`. Expected {expected_value=} but received {return_value=}. Inserted answer fields were: {factory_attributes}" + + +@pytest.mark.django_db +def test_related_model_fields_count_all_comorbidity_random_answers( + e12_case_factory, e12_comorbidity_factory, GOSH +): + """ + Simulating number_of_completed_fields_in_related_models(model_instance=multiaxialdiagnosis) returns correct counter when comorbidity fields' answers are randomly either valid value or None. + """ + + counter = 0 + + factory_attributes_list = [] + COMORBIDITY_NAMES = ComorbidityEntity.objects.all()[:5] + # Create 5 randomly filled comorbiditys - ensures covers various different scenarios. + for i in range(len(COMORBIDITY_NAMES)): + # Comorbidity.comorbidityentity CANNOT be None, so only have diagnosis date's random answer options include None + inital_answer = { + "comorbidity_diagnosis_date": date(2023, 1, 1), + } + # Get random answer set for fields + ANSWER_SET, expected_value = get_random_answers_update_counter( + answer_set=inital_answer, counter=counter + ) + ANSWER_SET.update({"comorbidityentity": COMORBIDITY_NAMES[i]}) + expected_value += 1 + + factory_attributes_list.append((ANSWER_SET, expected_value)) + + for factory_attributes, expected_value in factory_attributes_list: + # Need a case to make an comorbidity + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + ) + multiaxial_diagnosis = CASE.registration.multiaxialdiagnosis + + comorbidity = e12_comorbidity_factory( + multiaxial_diagnosis=multiaxial_diagnosis, + **factory_attributes, + ) + + return_value = number_of_completed_fields_in_related_models( + multiaxial_diagnosis + ) + + assert ( + return_value + == expected_value # Have to add 1 as comorbidityentity is always set, but outside the helper fn to updated expected_value + ), f"Randomly completed comorbiditys run through `number_of_completed_fields_in_related_models(multiaxial_diagnosis)`. Expected {expected_value=} but received {return_value=}. Inserted answer fields: {factory_attributes}" + + # Reset for next seizure type + comorbidity.delete() + + +@pytest.mark.django_db +def test_related_model_fields_count_assessment(e12_case_factory, GOSH): + """ + Simulating number_of_completed_fields_in_related_models(model_instance=assessment) returns correct counter when Site fields' answers are filled. + """ + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + ) + assessment = CASE.registration.assessment + return_value = number_of_completed_fields_in_related_models(assessment) + assert ( + return_value == 0 + ), f"Empty assessment with relevant Site vars False, `number_of_completed_fields_in_related_models(assessment)` should return 0. Instead returned {return_value}" + + site = Site.objects.get(case=CASE) + site.site_is_childrens_epilepsy_surgery_centre = True + site.site_is_general_paediatric_centre = True + site.site_is_paediatric_neurology_centre = True + site.save() + + return_value = number_of_completed_fields_in_related_models(assessment) + expected_value = 3 + + assert ( + return_value == expected_value + ), f"Site values all True for `number_of_completed_fields_in_related_models(assessment)`. Expected {expected_value=} but received {return_value=}" + + +@pytest.mark.django_db +def test_related_model_fields_count_management( + e12_case_factory, e12_anti_epilepsy_medicine_factory, GOSH +): + """ + Simulating number_of_completed_fields_in_related_models(model_instance=management) returns correct counter when AED fields' answers are filled. + """ + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + sex=SEX_TYPE[2][0], + ) + management = CASE.registration.management + return_value = number_of_completed_fields_in_related_models(management) + assert ( + return_value == 0 + ), f"Empty management, `number_of_completed_fields_in_related_models(management)` should return 0. Instead returned {return_value}" + + management.has_an_aed_been_given = True + management.has_rescue_medication_been_prescribed = True + management.save() + + aed_answers = { + "medicine_entity": MedicineEntity.objects.get(medicine_name="Sodium valproate"), + "antiepilepsy_medicine_start_date": date(2023, 1, 1), + "antiepilepsy_medicine_risk_discussed": True, + "is_a_pregnancy_prevention_programme_in_place": True, + "has_a_valproate_annual_risk_acknowledgement_form_been_completed": True, + } + aed = e12_anti_epilepsy_medicine_factory( + management=management, + is_rescue_medicine=False, + **aed_answers, + ) + rescue_medicine_answers = { + "medicine_entity": MedicineEntity.objects.get(medicine_name="Levetiracetam"), + "antiepilepsy_medicine_start_date": date(2023, 1, 1), + "antiepilepsy_medicine_risk_discussed": True, + } + rescue_medicine = e12_anti_epilepsy_medicine_factory( + management=management, + is_rescue_medicine=True, + **rescue_medicine_answers, + ) + return_value = number_of_completed_fields_in_related_models(management) + + expected_value = len(rescue_medicine_answers) + len(aed_answers) + + assert ( + return_value == expected_value + ), f"AED values all filled and valid for `number_of_completed_fields_in_related_models(management)`. Expected {expected_value=} but received {return_value=}" + + +@pytest.mark.django_db +def test_related_model_fields_count_registration(e12_case_factory, GOSH): + """ + Simulating number_of_completed_fields_in_related_models(model_instance=registration) returns correct counter when Site fields' answers are filled. + + NOTE: expected_value == 1 as default factory has: + - site_is_primary_centre_of_epilepsy_care=True + - site_is_actively_involved_in_epilepsy_care=True + """ + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + ) + + return_value = number_of_completed_fields_in_related_models(CASE.registration) + + expected_value = 1 + + assert ( + return_value == expected_value + ), f"Site values all True for `number_of_completed_fields_in_related_models(registration)`. Expected {expected_value=} but received {return_value=}" diff --git a/epilepsy12/tests/view_tests/form_calculations/test_total_fields_expected.py b/epilepsy12/tests/view_tests/form_calculations/test_total_fields_expected.py new file mode 100644 index 00000000..084229a7 --- /dev/null +++ b/epilepsy12/tests/view_tests/form_calculations/test_total_fields_expected.py @@ -0,0 +1,508 @@ +""" Tests for `total_fields_expected` fn and `scoreable_fields_for_model_class_name` helper fn. + +All models EXCEPT the following 5 simply return the output of `scoreable_fields_for_model_class_name`. + +Additionally, these 5 use `scoreable_fields_for_model_class_name` += some extra fields in related models. + + 1. MultiaxialDiagnosis + 2. Assessment + 3. Investigations + 4. Management + 5. Registration +""" + +# Python imports +import pytest +from datetime import date +from dateutil.relativedelta import relativedelta +import random + +# Django imports + +# E12 imports +from epilepsy12.models import ( + Episode, + SyndromeEntity, + MedicineEntity, +) +from epilepsy12.common_view_functions.recalculate_form_generate_response import ( + scoreable_fields_for_model_class_name, + total_fields_expected, + count_episode_fields, +) +from epilepsy12.constants import ( + Registration_minimum_scorable_fields, + EpilepsyContext_minimum_scorable_fields, + FirstPaediatricAssessment_minimum_scorable_fields, + MultiaxialDiagnosis_minimum_scorable_fields, + Episode_minimum_scorable_fields, + Syndrome_minimum_scorable_fields, + Comorbidity_minimum_scorable_fields, + Assessment_minimum_scorable_fields, + Investigations_minimum_scorable_fields, + Management_minimum_scorable_fields, + AntiEpilepsyMedicine_minimum_scorable_fields, + SEX_TYPE, + EPILEPSY_DIAGNOSIS_STATUS, + NON_EPILEPSY_SEIZURE_ONSET, + NON_EPILEPSY_SEIZURE_TYPE, +) +from epilepsy12.tests.view_tests.form_calculations.test_number_of_completed_fields_in_related_models import ( + get_random_answers_update_counter, +) + + +def test_correct_output_scoreable_fields_for_model_class_name(): + """ + Tests the scoreable_fields_for_model_class_name function returns correct score for all models. + """ + + model_expected_fields = [ + Registration_minimum_scorable_fields, + EpilepsyContext_minimum_scorable_fields, + FirstPaediatricAssessment_minimum_scorable_fields, + MultiaxialDiagnosis_minimum_scorable_fields, + Episode_minimum_scorable_fields, + Syndrome_minimum_scorable_fields, + Comorbidity_minimum_scorable_fields, + Assessment_minimum_scorable_fields, + Investigations_minimum_scorable_fields, + Management_minimum_scorable_fields, + AntiEpilepsyMedicine_minimum_scorable_fields, + ] + + for model in model_expected_fields: + return_value = scoreable_fields_for_model_class_name(model.model_name) + + expected_value = len(model.all_fields) + + assert ( + return_value == expected_value + ), f"scoreable_fields_for_model_class_name({model.model_name}) should return {expected_value}, instead returned {return_value}." + + +@pytest.mark.django_db +def test_count_episode_fields_epileptic_focal_onset(e12_case_factory, GOSH): + """ + Tests count_episode_fields with single Episode queryset returns correct expected output, with a completed episode. + """ + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + registration__multiaxial_diagnosis__episode__complete_episode_focal_onset_seizure=True, + ) + + multiaxial_diagnosis = CASE.registration.multiaxialdiagnosis + + episode_queryset = Episode.objects.filter(multiaxial_diagnosis=multiaxial_diagnosis) + + return_value = count_episode_fields(episode_queryset) + + assert ( + return_value == 8 + ), f"Single completely filled Focal Onset Episode ({episode_queryset=}) inserted into count_episode_fields() fn. Should return 8. Instead, returned {return_value}" + + +@pytest.mark.django_db +def test_count_episode_fields_epileptic_generalised_onset(e12_case_factory, GOSH): + """ + Tests count_episode_fields with single Episode queryset returns correct expected output, with a completed episode. + """ + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + registration__multiaxial_diagnosis__episode__complete_episode_generalised_onset_seizure=True, + ) + + multiaxial_diagnosis = CASE.registration.multiaxialdiagnosis + + episode_queryset = Episode.objects.filter(multiaxial_diagnosis=multiaxial_diagnosis) + + return_value = count_episode_fields(episode_queryset) + + assert ( + return_value == 8 + ), f"Single completely filled Generalised Onset Episode ({episode_queryset=}) inserted into count_episode_fields() fn. Should return 8. Instead, returned {return_value}" + + +@pytest.mark.django_db +def test_count_episode_fields_epileptic_unclassified_onset(e12_case_factory, GOSH): + """ + Tests count_episode_fields with single Episode queryset returns correct expected output, with a completed episode. + """ + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + registration__multiaxial_diagnosis__episode__complete_episode_unclassified_onset_seizure=True, + ) + + multiaxial_diagnosis = CASE.registration.multiaxialdiagnosis + + episode_queryset = Episode.objects.filter(multiaxial_diagnosis=multiaxial_diagnosis) + + return_value = count_episode_fields(episode_queryset) + + assert ( + return_value == 7 + ), f"Single completely filled unclassified Onset Episode ({episode_queryset=}) inserted into count_episode_fields() fn. Should return 7. Instead, returned {return_value}" + + +@pytest.mark.django_db +def test_count_episode_fields_epileptic_unknown_onset(e12_case_factory, GOSH): + """ + Tests count_episode_fields with single Episode queryset returns correct expected output, with a completed episode. + """ + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + registration__multiaxial_diagnosis__episode__complete_episode_unknown_onset_seizure=True, + ) + + multiaxial_diagnosis = CASE.registration.multiaxialdiagnosis + + episode_queryset = Episode.objects.filter(multiaxial_diagnosis=multiaxial_diagnosis) + + return_value = count_episode_fields(episode_queryset) + + assert ( + return_value == 7 + ), f"Single completely filled unknown Onset Episode ({episode_queryset=}) inserted into count_episode_fields() fn. Should return 7. Instead, returned {return_value}" + + +@pytest.mark.django_db +def test_count_episode_fields_nonepileptic(e12_case_factory, GOSH): + """ + Tests count_episode_fields with single non epileptic Episode queryset returns correct expected output, with a completed episode. + """ + expected_value = 11 # 5 for incomplete epileptic episode, ++6 for non epileptic episode base value incomplete fields + + # Either BPP (++3 to expected score) OR Oth (++2 to expected score) + seizure_type_answer = random.choice( + [NON_EPILEPSY_SEIZURE_TYPE[0][0], NON_EPILEPSY_SEIZURE_TYPE[-1][0]] + ) + + if seizure_type_answer == NON_EPILEPSY_SEIZURE_TYPE[0][0]: + expected_value += 3 + else: + expected_value += 2 + answer_set = { + "registration__multiaxial_diagnosis__episode__epilepsy_or_nonepilepsy_status": EPILEPSY_DIAGNOSIS_STATUS[ + 1 + ][ + 0 + ], + "registration__multiaxial_diagnosis__episode__nonepileptic_seizure_unknown_onset": NON_EPILEPSY_SEIZURE_ONSET[ + 0 + ][ + 0 + ], + "registration__multiaxial_diagnosis__episode__nonepileptic_seizure_type": seizure_type_answer, + } + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + registration__multiaxial_diagnosis__episode__common_fields=True, + **answer_set, + ) + + multiaxial_diagnosis = CASE.registration.multiaxialdiagnosis + + episode_queryset = Episode.objects.filter(multiaxial_diagnosis=multiaxial_diagnosis) + + return_value = count_episode_fields(episode_queryset) + + assert ( + return_value == expected_value + ), f"Single completely filled non epileptic Episode ({episode_queryset=}) inserted into count_episode_fields() fn. Should return {expected_value} (as inserted nonepileptic_seizure_type=={seizure_type_answer}). Instead, returned {return_value}" + + +@pytest.mark.django_db +def test_count_episode_fields_uncertain(e12_case_factory, GOSH): + """ + Tests count_episode_fields with single uncertain epileptic Episode queryset returns correct expected output, with a completed episode. + """ + expected_value = 11 # 5 for incomplete epileptic episode, ++6 for uncertain episode base value incomplete fields + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + registration__multiaxial_diagnosis__episode__common_fields=True, + **{ + "registration__multiaxial_diagnosis__episode__epilepsy_or_nonepilepsy_status": EPILEPSY_DIAGNOSIS_STATUS[ + 2 + ][ + 0 + ], + }, + ) + + multiaxial_diagnosis = CASE.registration.multiaxialdiagnosis + + episode_queryset = Episode.objects.filter(multiaxial_diagnosis=multiaxial_diagnosis) + + return_value = count_episode_fields(episode_queryset) + + assert ( + return_value == expected_value + ), f"Single completely filled uncertain epileptic Episode ({episode_queryset=}) inserted into count_episode_fields() fn. Should return {expected_value}. Instead, returned {return_value}" + + +@pytest.mark.django_db +def test_total_fields_expected_multiaxial_diagnosis_episode_fields( + e12_case_factory, GOSH +): + """ + Tests total_fields_expected(multiaxial_diagnosis) returns correct expected output, with a completed Focal Onset episode. + """ + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + registration__multiaxial_diagnosis__episode__complete_episode_focal_onset_seizure=True, + ) + + multiaxial_diagnosis = CASE.registration.multiaxialdiagnosis + + # Multiaxial diagnosis fields minimum == 7 ++ focal onset episode fields == 8 + expected_value = 15 + return_value = total_fields_expected(multiaxial_diagnosis) + + assert ( + return_value == expected_value + ), f"total_fields_expected(multiaxial_diagnosis) with fully completed Focal Onset episode should return {expected_value}, instead returned {return_value}" + + +@pytest.mark.django_db +def test_total_fields_expected_multiaxial_diagnosis_syndrome_fields( + e12_case_factory, GOSH, e12_syndrome_factory +): + """ + Tests total_fields_expected(multiaxial_diagnosis) returns correct expected output, with 3 completed Syndromes. Each syndrome registered has 2 fields. + + """ + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + registration__multiaxial_diagnosis__episode=None, + registration__multiaxial_diagnosis__syndrome_present=True, + registration__multiaxial_diagnosis__syndrome_entity=None, + ) + + multiaxial_diagnosis = CASE.registration.multiaxialdiagnosis + + # Initial value because Multiaxial diagnosis fields minimum == 7 ++ no episodes == 5 + expected_value = 12 + + ADD_SYNDROMES = random.choice([None, True]) + if ADD_SYNDROMES is not None: + # Create 3 syndromes + SYNDROMES_LIST = SyndromeEntity.objects.all()[:3] + + for i in range(3): + e12_syndrome_factory( + multiaxial_diagnosis=multiaxial_diagnosis, + syndrome_diagnosis_date=date(2023, 1, 1), + syndrome=SYNDROMES_LIST[i], + ) + + # Each syndrome has 2 fields to complete + expected_value += 2 + else: + expected_value += 2 # syndrome_present==True, so minimum value adds 2 expected + + return_value = total_fields_expected(multiaxial_diagnosis) + + assert ( + return_value == expected_value + ), f"total_fields_expected(multiaxial_diagnosis) with {'3 syndromes registered' if ADD_SYNDROMES else 'syndrome_present==True but no syndromes entered'} expects return value of {expected_value} but received {return_value}" + + +@pytest.mark.django_db +def test_total_fields_expected_multiaxial_diagnosis_general_fields( + e12_case_factory, GOSH +): + """ + Tests total_fields_expected(multiaxial_diagnosis) returns correct expected output, with all general fields all True. + + """ + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + registration__multiaxial_diagnosis__epilepsy_cause_known=True, # +2 + registration__multiaxial_diagnosis__relevant_impairments_behavioural_educational=True, # +2 + registration__multiaxial_diagnosis__mental_health_issue_identified=True, # +1 + registration__multiaxial_diagnosis__global_developmental_delay_or_learning_difficulties=True, # +1 + ) + + multiaxial_diagnosis = CASE.registration.multiaxialdiagnosis + + # Initial value because Multiaxial diagnosis fields minimum == 7; ++ no episodes == 5; ++ general_fields all true == 6; + expected_value = 23 + + return_value = total_fields_expected(multiaxial_diagnosis) + + assert ( + return_value == expected_value + ), f"total_fields_expected(multiaxial_diagnosis) with general fields all True. Expected {expected_value} but got {return_value}" + + +@pytest.mark.django_db +def test_total_fields_expected_assessment(e12_case_factory, GOSH): + """ + Tests total_fields_expected(assessment) returns correct expected output, with all general fields all True. + """ + + answer_set = {} + expected_value = 5 # minimum value when no fields complete + fields = [ + "consultant_paediatrician_referral_made", + "paediatric_neurologist_referral_made", + "childrens_epilepsy_surgical_service_referral_made", + "epilepsy_specialist_nurse_referral_made", + ] + for field in fields: + answer = random.choice([None, True]) + + BASE_KEY_NAME = "registration__assessment__" + answer_set.update({f"{BASE_KEY_NAME}{field}": answer}) + + if answer is not None: + if field == "epilepsy_specialist_nurse_referral_made": + expected_value += 2 + else: + expected_value += 3 + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + **answer_set, + ) + + assessment = CASE.registration.assessment + + return_value = total_fields_expected(assessment) + + assert ( + return_value == expected_value + ), f"total_fields_expected(assessment) expected {expected_value} but got {return_value}. Used answers: {answer_set}" + + +@pytest.mark.django_db +def test_total_fields_expected_investigations(e12_case_factory, GOSH): + """ + Tests total_fields_expected(investigations) returns correct expected output, with all fields all True. + """ + + answer_set = {} + expected_value = 4 # minimum value when no fields complete + fields = [ + "eeg_indicated", + "mri_indicated", + ] + for field in fields: + answer = random.choice([None, True]) + + BASE_KEY_NAME = "registration__investigations__" + answer_set.update({f"{BASE_KEY_NAME}{field}": answer}) + + if answer is not None: + expected_value += 2 + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + **answer_set, + ) + + investigations = CASE.registration.investigations + + return_value = total_fields_expected(investigations) + + assert ( + return_value == expected_value + ), f"total_fields_expected(investigations) expected {expected_value} but got {return_value}. Used answers: {answer_set}" + + +@pytest.mark.django_db +def test_total_fields_expected_registration(e12_case_factory, GOSH): + """ + Tests total_fields_expected(registration) returns correct expected output, with all fields all True. + """ + + answer_set = {} + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + organisations__organisation=GOSH, + ) + + registration = CASE.registration + + expected_value = 3 # minimum value when no fields complete + return_value = total_fields_expected(registration) + + assert ( + return_value == expected_value + ), f"total_fields_expected(registration) expected {expected_value} but got {return_value}. Used answers: {answer_set}" + + +@pytest.mark.django_db +def test_total_fields_expected_management( + e12_case_factory, e12_anti_epilepsy_medicine_factory, GOSH +): + """ + Tests total_fields_expected(management) returns correct expected output, with all fields all True. + """ + + CASE = e12_case_factory( + first_name=f"temp_child_{GOSH.OrganisationName}", + date_of_birth=date.today() + - relativedelta(years=13), # to test sodium valproate + organisations__organisation=GOSH, + sex=SEX_TYPE[2][0], + registration__management__has_an_aed_been_given=True, + registration__management__has_rescue_medication_been_prescribed=True, + ) + + management = CASE.registration.management + + # score +3 for medicine present, +2 as valproate in childbearing female + aed_answers = { + "medicine_entity": MedicineEntity.objects.get(medicine_name="Sodium valproate"), + "antiepilepsy_medicine_start_date": date(2023, 1, 1), + "antiepilepsy_medicine_risk_discussed": True, + "is_a_pregnancy_prevention_programme_needed": True, + "is_a_pregnancy_prevention_programme_in_place": True, + "has_a_valproate_annual_risk_acknowledgement_form_been_completed": True, + } + aed = e12_anti_epilepsy_medicine_factory( + management=management, + is_rescue_medicine=False, + **aed_answers, + ) + + # score +3 for medicine present + rescue_medicine_answers = { + "medicine_entity": MedicineEntity.objects.get(medicine_name="Levetiracetam"), + "antiepilepsy_medicine_start_date": date(2023, 1, 1), + "antiepilepsy_medicine_risk_discussed": True, + } + rescue_medicine = e12_anti_epilepsy_medicine_factory( + management=management, + is_rescue_medicine=True, + **rescue_medicine_answers, + ) + + expected_value = 13 # minimum = 5; ++3 for Keppra; ++5 for valproate + return_value = total_fields_expected(management) + + assert ( + return_value == expected_value + ), f"total_fields_expected(management) expected {expected_value} but got {return_value}. 13yo girl registered with Valproate AED and Keppra as rescue" diff --git a/epilepsy12/tests/view_tests/permissions_tests/__init__.py b/epilepsy12/tests/view_tests/permissions_tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/epilepsy12/tests/view_tests/permissions_tests/test_custom_permissions.py b/epilepsy12/tests/view_tests/permissions_tests/test_custom_permissions.py new file mode 100644 index 00000000..83569c21 --- /dev/null +++ b/epilepsy12/tests/view_tests/permissions_tests/test_custom_permissions.py @@ -0,0 +1,109 @@ +""" + +## Custom Permissions Tests +TODO #511 + +Opt out + [x] Assert an Audit Centre Administrator CANNOT let a child opt out of Epilepsy12 + [x] Assert an audit centre clinician CANNOT let a child opt out of Epilepsy12 + [x] Assert an Audit Centre Lead Clinician CANNOT let a child outside their own Trust opt out of Epilepsy12 + + [] Assert an Audit Centre Lead Clinician can let a child within their own Trust opt out of Epilepsy12 + [] Assert RCPCH Audit Team can let a child opt out of Epilepsy12 + [] Assert Clinical Audit Team can let a child opt out of Epilepsy12 + +Locking +[] Assert an Audit Centre Administrator CANNOT lock a child from being edited +[] Assert an audit centre clinician CANNOT unlock a child for editing +[] Assert an Audit Centre Clinician CAN lock a child from being edited +[] Assert an Audit Centre Lead Clinician CAN lock a child from being edited +[] Assert an Audit Centre Lead Clinician CANNOT unlock a child for editing +[] Assert RCPCH Audit Team can lock a child from being edited +[] Assert RCPCH Audit Team can unlock a child from being edited + +can_consent_to_audit_participation +[] Assert an Audit Centre Administrator CANNOT consent for a child to be included in Epilepsy12 +[] Assert an audit centre clinician CANNOT consent for a child to be included in Epilepsy12 +[] Assert an Audit Centre Lead Clinician CANNOT consent for a child to be included in Epilepsy12 +[] Assert RCPCH Audit Team CANNOT consent for a child to be included in Epilepsy12 +[] Assert a Child, Young Person or family can member consent for a child to be included in Epilepsy12 + +can_register_child_in_epilepsy12 +[] Assert an Audit Centre Administrator CANNOT register a child in Epilepsy12 +[] Assert an audit centre clinician CAN register a child within their own Trust in Epilepsy12 +[] Assert an audit centre clinician CANNOT register a child outside their own Trust in Epilepsy12 +[] Assert an Audit Centre Lead Clinician CAN register a child within their own Trust in Epilepsy12 +[] Assert an Audit Centre Lead Clinician CANNOT register a child outside their own Trust in Epilepsy12 +[] Assert RCPCH Audit Team CAN register a child within any Trust in Epilepsy12 + +can_delete_epilepsy12_lead_centre +[] Assert an Audit Centre Administrator CANNOT 'delete_lead_site' +[] Assert an audit centre clinician CANNOT delete_lead_site +[] Assert an Audit Centre Lead Clinician CANNOT delete_lead_site +[] Assert RCPCH Audit Team CAN delete_lead_site + +""" +# python imports +import pytest + +# django imports +from django.urls import reverse + +# E12 imports +# E12 imports +from epilepsy12.models import Epilepsy12User, Organisation, Case + + +@pytest.mark.skip(reason="Unfinished test. Awaiting E12 advice re custom permissions.") +@pytest.mark.django_db +def test_users_opt_out_forbidden( + client, + seed_groups_fixture, + seed_users_fixture, + seed_cases_fixture, +): + """ + Simulating different E12 Users attempting to opt children out of Epilepsy12 + + Assert these users cannot opt child out of Epilepsy12 + """ + + # set up constants + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + # ADDENBROOKE'S + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + CASE_FROM_SAME_ORG = Case.objects.get( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}" + ) + + users = Epilepsy12User.objects.all().exclude( + first_name__in=["RCPCH_AUDIT_TEAM", "CLINICAL_AUDIT_TEAM"] + ) + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + response = client.get( + reverse( + "opt_out", + kwargs={ + "organisation_id": DIFF_TRUST_DIFF_ORGANISATION.id, + "case_id": CASE_FROM_SAME_ORG.id, + }, + ) + ) + + assert ( + response.status_code == 403 + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested opt out for {CASE_FROM_SAME_ORG} in {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()} Expected 403 response status code, received {response.status_code}" diff --git a/epilepsy12/tests/view_tests/permissions_tests/test_permissions_create.py b/epilepsy12/tests/view_tests/permissions_tests/test_permissions_create.py new file mode 100644 index 00000000..7bdfd075 --- /dev/null +++ b/epilepsy12/tests/view_tests/permissions_tests/test_permissions_create.py @@ -0,0 +1,711 @@ +""" + +## Create Tests + + [x] Assert an Audit Centre Lead Clinician can create users inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can create users nationally, inside own Trust, and outside - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can create users nationally, inside own Trust, and outside - response.status_code == HTTPStatus.OK + + [x] Assert an Audit Centre Administrator CANNOT create users - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an audit centre clinician CANNOT create users - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician CANNOT create users outside own Trust - response.status_code == HTTPStatus.FORBIDDEN + + + [x] Assert an Audit Centre Administrator can create patients inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Clinician can create patients inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can create patients inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can create patients inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can create patients inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can create patients outside own Trust - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can create patients outside own Trust - response.status_code == HTTPStatus.OK + + [x] Assert an Audit Centre Administrator CANNOT create patients outside own Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an audit centre clinician CANNOT create patients outside own Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician CANNOT create patients outside own Trust - response.status_code == HTTPStatus.FORBIDDEN + + + [ ] Assert an Audit Centre Clinician can create patient records inside own Trust - response.status_code == HTTPStatus.OK + [ ] Assert an Audit Centre Lead Clinician can create patient records inside own Trust - response.status_code == HTTPStatus.OK + [ ] Assert RCPCH Audit Team can create patient records nationally, inside own Trust, and outside - response.status_code == HTTPStatus.OK + [ ] Assert Clinical Audit Team can create patient records nationally, inside own Trust, and outside - response.status_code == HTTPStatus.OK + + [ ] Assert an Audit Centre Administrator CANNOT create patient records - response.status_code == HTTPStatus.FORBIDDEN + [ ] Assert an audit centre clinician CANNOT create patient records outside own Trust - response.status_code == HTTPStatus.FORBIDDEN + [ ] Assert an Audit Centre Lead Clinician CANNOT create patient records outside own Trust - response.status_code == HTTPStatus.FORBIDDEN + + + +# Episode + + [x] Assert an Audit Centre Clinician can 'add_episode' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can 'add_episode' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can 'add_episode' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can 'add_episode' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can 'add_episode' inside different Trust - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can 'add_episode' inside different Trust - response.status_code == HTTPStatus.OK + + [x] Assert an Audit Centre Administrator CANNOT 'add_episode' - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Clinician CANNOT 'add_episode' outside own Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician CANNOT 'add_episode' outside own Trust - response.status_code == HTTPStatus.FORBIDDEN + +# Comorbidity + + [x] Assert an Audit Centre Clinician can 'add_comorbidity' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can 'add_comorbidity' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can 'add_comorbidity' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can 'add_comorbidity' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can 'add_comorbidity' inside different Trust - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can 'add_comorbidity' inside different Trust - response.status_code == HTTPStatus.OK + + [x] Assert an Audit Centre Administrator CANNOT 'add_comorbidity' - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Clinician CANNOT 'add_comorbidity' outside own Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician CANNOT 'add_comorbidity' outside own Trust - response.status_code == HTTPStatus.FORBIDDEN + +# Syndrome + + [x] Assert an Audit Centre Clinician can 'add_syndrome' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can 'add_syndrome' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can 'add_syndrome' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can 'add_syndrome' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can 'add_syndrome' inside different Trust - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can 'add_syndrome' inside different Trust - response.status_code == HTTPStatus.OK + + [x] Assert an Audit Centre Administrator CANNOT 'add_syndrome' - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Clinician CANNOT 'add_syndrome' outside own Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician CANNOT 'add_syndrome' outside own Trust - response.status_code == HTTPStatus.FORBIDDEN + + +# Antiepilepsy Medicine + + [x] Assert an Audit Centre Clinician can 'add_antiepilepsy_medicine' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can 'add_antiepilepsy_medicine' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can 'add_antiepilepsy_medicine' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can 'add_antiepilepsy_medicine' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can 'add_antiepilepsy_medicine' inside different Trust - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can 'add_antiepilepsy_medicine' inside different Trust - response.status_code == HTTPStatus.OK + + [x] Assert an Audit Centre Administrator CANNOT 'add_antiepilepsy_medicine' - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Clinician CANNOT 'add_antiepilepsy_medicine' outside own Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician CANNOT 'add_antiepilepsy_medicine' outside own Trust - response.status_code == HTTPStatus.FORBIDDEN +""" + +# python imports +import pytest +from http import HTTPStatus +from datetime import date + +# django imports +from django.urls import reverse + +# E12 Imports +from epilepsy12.tests.UserDataClasses import ( + test_user_audit_centre_administrator_data, + test_user_audit_centre_clinician_data, + test_user_audit_centre_lead_clinician_data, + test_user_clinicial_audit_team_data, + test_user_rcpch_audit_team_data, +) +from epilepsy12.models import ( + Epilepsy12User, + Organisation, + Case, +) + + +@pytest.mark.django_db +def test_user_create_same_org_success( + client, + seed_groups_fixture, + seed_users_fixture, + seed_cases_fixture, +): + """Integration test checking functionality of view and form. + + Simulating different E12 users with different roles attempting to create Users inside own trust. + """ + + # set up constants + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + TEMP_CREATED_USER_FIRST_NAME = "TEMP_CREATED_USER_FIRST_NAME" + + selected_users = [ + test_user_audit_centre_lead_clinician_data.role_str, + test_user_rcpch_audit_team_data.role_str, + test_user_clinicial_audit_team_data.role_str, + ] + + users = Epilepsy12User.objects.filter(first_name__in=selected_users) + + if len(users) != len(selected_users): + assert ( + False + ), f"Incorrect number of users selected. Requested {len(selected_users)} but queryset contains {len(users)}: {users}" + + for test_user in users: + client.force_login(test_user) + + url = reverse( + "create_epilepsy12_user", + kwargs={ + "organisation_id": TEST_USER_ORGANISATION.id, + "user_type": "organisation-staff", + }, + ) + + response = client.get(url) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested `{url}` of {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()}. Expected 200 response status code, received {response.status_code}" + + data = { + "title": 1, + "email": f"{test_user.first_name}@test.com", + "role": 1, + "organisation_employer": TEST_USER_ORGANISATION.id, + "first_name": TEMP_CREATED_USER_FIRST_NAME, + "surname": "User", + "is_rcpch_audit_team_member": True, + "is_rcpch_staff": False, + "email_confirmed": True, + } + + response = client.post(url, data=data) + + # This is valid form data, should redirect + assert ( + response.status_code == HTTPStatus.FOUND + ), f"Valid E12User form data POSTed by {test_user}, expected status_code 302, received {response.status_code}" + + assert ( + Epilepsy12User.objects.filter(first_name=TEMP_CREATED_USER_FIRST_NAME).count() + == 3 + ), f"Logged in as 3 different people and created an e12 user with first_name = {TEMP_CREATED_USER_FIRST_NAME}. Should be 3 matches in db for this filter." + + +@pytest.mark.django_db +def test_user_create_diff_org_success( + client, +): + """Integration test checking functionality of view and form. + + RCPCH Audit Team and Clinical Audit Team roles should be able to create user in different trust. + """ + + # set up constants + + # ADDENBROOKE'S + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + TEMP_CREATED_USER_FIRST_NAME = "TEMP_CREATED_USER_FIRST_NAME" + + selected_users = [ + test_user_rcpch_audit_team_data.role_str, + test_user_clinicial_audit_team_data.role_str, + ] + + users = Epilepsy12User.objects.filter(first_name__in=selected_users) + + if len(users) != len(selected_users): + assert ( + False + ), f"Incorrect number of users selected. Requested {len(selected_users)} but queryset contains {len(users)}: {users}" + + for test_user in users: + client.force_login(test_user) + + url = reverse( + "create_epilepsy12_user", + kwargs={ + "organisation_id": DIFF_TRUST_DIFF_ORGANISATION.id, + "user_type": "organisation-staff", + }, + ) + + data = { + "title": 1, + "email": f"{test_user.first_name}@test.com", + "role": 1, + "organisation_employer": DIFF_TRUST_DIFF_ORGANISATION.id, + "first_name": TEMP_CREATED_USER_FIRST_NAME, + "surname": "User", + "is_rcpch_audit_team_member": True, + "is_rcpch_staff": False, + "email_confirmed": True, + } + + response = client.post(url, data=data) + + # This is valid form data, should redirect + assert ( + response.status_code == HTTPStatus.FOUND + ), f"Valid E12User form data POSTed by {test_user}, expected status_code 302, received {response.status_code}" + + assert ( + Epilepsy12User.objects.filter(first_name=TEMP_CREATED_USER_FIRST_NAME).count() + == 2 + ), f"Logged in as 2 different people and created an e12 user with first_name = {TEMP_CREATED_USER_FIRST_NAME}. Should be 2 matches in db for this filter." + + +@pytest.mark.django_db +def test_user_creation_forbidden( + client, +): + """Integration test checking functionality of view and form. + + Simulating unpermitted E12 users attempting to create Users inside own trust. + + Additionally, AUDIT_CENTRE_LEAD_CLINICIAN role CANNOT create user in different trust. + """ + + # set up constants + + # ADDENBROOKE'S + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + TEMP_CREATED_USER_FIRST_NAME = "TEMP_CREATED_USER_FIRST_NAME" + + selected_users = [ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + ] + + users = Epilepsy12User.objects.filter(first_name__in=selected_users) + + if len(users) != len(selected_users): + assert ( + False + ), f"Incorrect number of users selected. Requested {len(selected_users)} but queryset contains {len(users)}: {users}" + + for test_user in users: + client.force_login(test_user) + + url = reverse( + "create_epilepsy12_user", + kwargs={ + "organisation_id": DIFF_TRUST_DIFF_ORGANISATION.id, + "user_type": "organisation-staff", + }, + ) + + response = client.get(url) + + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested `{url}` of {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()}. Expected {HTTPStatus.FORBIDDEN} response status code, received {response.status_code}" + + data = { + "title": 1, + "email": f"{test_user.first_name}@test.com", + "role": 1, + "organisation_employer": DIFF_TRUST_DIFF_ORGANISATION.id, + "first_name": TEMP_CREATED_USER_FIRST_NAME, + "surname": "User", + "is_rcpch_audit_team_member": True, + "is_rcpch_staff": False, + "email_confirmed": True, + } + + response = client.post(url, data=data) + + # This is valid form data, should redirect + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"Unpermitted E12User {test_user} attempted to create an E12User. expected status_code {HTTPStatus.FORBIDDEN}, received {response.status_code}" + + assert ( + Epilepsy12User.objects.filter(first_name=TEMP_CREATED_USER_FIRST_NAME).count() + == 0 + ), f"Logged in as 3 different unpermitted Users and attempted to create an e12 user with first_name = {TEMP_CREATED_USER_FIRST_NAME}. Should be 0 matches in db for this filter." + + +@pytest.mark.django_db +def test_patient_create_success( + client, +): + """Integration test checking functionality of view and form. + + Simulating different E12 users with different roles attempting to create Patients inside own trust. + """ + + # set up constants + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + # ADDENBROOKE'S + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + TEST_FIRST_NAME = "TEST_FIRST_NAME" + + selected_users = [ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + test_user_rcpch_audit_team_data.role_str, + test_user_clinicial_audit_team_data.role_str, + ] + + users = Epilepsy12User.objects.filter(first_name__in=selected_users) + + if len(users) != len(selected_users): + assert ( + False + ), f"Incorrect number of users selected. Requested {len(selected_users)} but queryset contains {len(users)}: {users}" + + for test_user in users: + client.force_login(test_user) + + url = reverse( + "create_case", + kwargs={ + "organisation_id": TEST_USER_ORGANISATION.id, + }, + ) + + response = client.get(url) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested `{url}` of {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()}. Expected 200 response status code, received {response.status_code}" + + data = { + "first_name": TEST_FIRST_NAME, + "surname": "Chandran", + "date_of_birth": date(2023, 6, 15), + "sex": "1", + "nhs_number": "400 0000 012", + "postcode": "SW1A 1AA", + "ethnicity": "N", + } + + response = client.post(url, data=data) + + # This is valid form data, should redirect + assert ( + response.status_code == HTTPStatus.FOUND + ), f"Valid Case form data POSTed by {test_user}, expected status_code {HTTPStatus.FOUND}, received {response.status_code}" + + assert Case.objects.filter( + first_name=TEST_FIRST_NAME + ).exists(), f"Logged in as {test_user} and created Case at {url}. Created Case not found in db." + + # Remove Case for next user + Case.objects.filter(first_name=TEST_FIRST_NAME).delete() + + # Additionally RCPCH_AUDIT_TEAM and CLINICAL_AUDIT_TEAM can create Case in different Trust + if test_user.first_name in [ + test_user_rcpch_audit_team_data.role_str, + test_user_clinicial_audit_team_data.role_str, + ]: + url = reverse( + "create_case", + kwargs={ + "organisation_id": DIFF_TRUST_DIFF_ORGANISATION.id, + }, + ) + + response = client.get(url) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested `{url}` of {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()}. Expected 200 response status code, received {response.status_code}" + + data = { + "first_name": TEST_FIRST_NAME, + "surname": "Chandran", + "date_of_birth": date(2023, 6, 15), + "sex": "1", + "nhs_number": "400 0000 012", + "postcode": "SW1A 1AA", + "ethnicity": "N", + } + + response = client.post(url, data=data) + + # This is valid form data, should redirect + assert ( + response.status_code == HTTPStatus.FOUND + ), f"Valid Case form data POSTed by {test_user}, expected status_code 302, received {response.status_code}" + + assert Case.objects.filter( + first_name=TEST_FIRST_NAME + ).exists(), f"Logged in as {test_user} and created Case at {url}. Created Case not found in db." + + # Remove Case for next user + Case.objects.filter(first_name=TEST_FIRST_NAME).delete() + + +@pytest.mark.django_db +def test_patient_creation_forbidden( + client, +): + """Integration test checking functionality of view and form. + + Simulating unpermitted E12 Users attempting to create patients. + + Additionally, AUDIT_CENTRE_LEAD_CLINICIAN role only NOT ALLOWED to create patient in different trust. + """ + + # set up constants + + # ADDENBROOKE'S + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + TEST_FIRST_NAME = "TEST_FIRST_NAME" + + selected_users = [ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + ] + + users = Epilepsy12User.objects.filter(first_name__in=selected_users) + + if len(users) != len(selected_users): + assert ( + False + ), f"Incorrect number of users selected. Requested {len(selected_users)} but queryset contains {len(users)}: {users}" + + for test_user in users: + client.force_login(test_user) + + url = reverse( + "create_case", + kwargs={ + "organisation_id": DIFF_TRUST_DIFF_ORGANISATION.id, + }, + ) + + response = client.get(url) + + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested `{url}` of {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()}. Expected {HTTPStatus.FORBIDDEN} status code, received {response.status_code}" + + data = { + "first_name": TEST_FIRST_NAME, + "surname": "Chandran", + "date_of_birth": date(2023, 6, 15), + "sex": "1", + "nhs_number": "400 0000 012", + "postcode": "SW1A 1AA", + "ethnicity": "N", + } + + response = client.post(url, data=data) + + # This is valid form data but should be forbidden + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"Valid Case form data POSTed by unpermitted {test_user}, expected status_code {HTTPStatus.FORBIDDEN}, received {response.status_code}" + + assert not Case.objects.filter( + first_name=TEST_FIRST_NAME + ).exists(), f"Logged in as {test_user} and attempted to Case at {url}. Unpermitted so Case should not be created." + + +@pytest.mark.django_db +def test_add_episode_comorbidity_syndrome_aem_success(client): + """ + Simulating different permitted E12 Roles request.POSTing to the following htmx urls: + + - `add_episode` + - `add_comorbidity` + - `add_syndrome` + - `add_antiepilepsy_medicine` + + Additionally, RCPCH_AUDIT_TEAM and CLINICAL_AUDIT_TEAM can add Episode in different Trust. + """ + # set up constants + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + # ADDENBROOKE'S + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + CASE_FROM_SAME_ORG = Case.objects.get( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}" + ) + + URLS = [ + "add_episode", + "add_comorbidity", + "add_syndrome", + "add_antiepilepsy_medicine", + ] + + selected_users = [ + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + test_user_rcpch_audit_team_data.role_str, + test_user_clinicial_audit_team_data.role_str, + ] + + users = Epilepsy12User.objects.filter(first_name__in=selected_users) + + if len(users) != len(selected_users): + assert ( + False + ), f"Incorrect number of users selected. Requested {len(selected_users)} but queryset contains {len(users)}: {users}" + + for test_user in users: + client.force_login(test_user) + + for url in URLS: + if url == "add_antiepilepsy_medicine": + kwargs = { + "management_id": CASE_FROM_SAME_ORG.registration.management.id, + "is_rescue_medicine": "is_rescue_medicine", + } + else: + kwargs = { + "multiaxial_diagnosis_id": CASE_FROM_SAME_ORG.registration.multiaxialdiagnosis.id + } + + response = client.post( + reverse( + url, + kwargs=kwargs, + ), + headers={"Hx-Request": "true"}, + ) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user} from {test_user.organisation_employer} with perms {test_user.groups.all()} request.POSTed to {url} for Case from {TEST_USER_ORGANISATION}. Expected {HTTPStatus.OK}, received {response.status_code}" + + # Additional test for RCPCH_AUDIT_TEAM and CLINICAL_AUDIT_TEAM adding in different Trust + if test_user.first_name in [ + test_user_rcpch_audit_team_data.role_str, + test_user_clinicial_audit_team_data.role_str, + ]: + response = client.post( + reverse( + url, + kwargs=kwargs, + ), + headers={"Hx-Request": "true"}, + ) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user} from {test_user.organisation_employer} with perms {test_user.groups.all()} request.POSTed to {url} for Case from {DIFF_TRUST_DIFF_ORGANISATION}. Expected {HTTPStatus.OK}, received {response.status_code}" + + +@pytest.mark.django_db +def test_add_episode_comorbidity_syndrome_aem_forbidden(client): + """ + Simulating different unauthorized E12 Roles adding Episodes for Case in same / diff Trust. + + - `add_episode` + - `add_comorbidity` + - `add_syndrome` + - `add_antiepilepsy_medicine` + + """ + # set up constants + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + # ADDENBROOKE'S + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + CASE_FROM_SAME_ORG = Case.objects.get( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}" + ) + + CASE_FROM_DIFF_ORG = Case.objects.get( + first_name=f"child_{DIFF_TRUST_DIFF_ORGANISATION.OrganisationName}" + ) + + selected_users = [ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + ] + + users = Epilepsy12User.objects.filter(first_name__in=selected_users) + + if len(users) != len(selected_users): + assert ( + False + ), f"Incorrect number of users selected. Requested {len(selected_users)} but queryset contains {len(users)}: {users}" + + for test_user in users: + client.force_login(test_user) + + URLS = [ + "add_episode", + "add_comorbidity", + "add_syndrome", + "add_antiepilepsy_medicine", + ] + + for url in URLS: + if ( + test_user.first_name + == test_user_audit_centre_administrator_data.role_str + ): + CASE = CASE_FROM_SAME_ORG + + # Other users only forbidden from doing action in different Trust + else: + CASE = CASE_FROM_DIFF_ORG + + if url == "add_antiepilepsy_medicine": + kwargs = { + "management_id": CASE.registration.management.id, + "is_rescue_medicine": "is_rescue_medicine", + } + else: + kwargs = { + "multiaxial_diagnosis_id": CASE.registration.multiaxialdiagnosis.id + } + + response = client.post( + reverse( + url, + kwargs=kwargs, + ), + headers={"Hx-Request": "true"}, + ) + + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user} from {test_user.organisation_employer} with perms {test_user.groups.all()} request.POSTed to `{url}` for Case from {CASE.organisations.all()}. Expected {HTTPStatus.FORBIDDEN}, received {response.status_code}" diff --git a/epilepsy12/tests/view_tests/permissions_tests/test_permissions_delete.py b/epilepsy12/tests/view_tests/permissions_tests/test_permissions_delete.py new file mode 100644 index 00000000..cab1bd5c --- /dev/null +++ b/epilepsy12/tests/view_tests/permissions_tests/test_permissions_delete.py @@ -0,0 +1,608 @@ +""" +## Delete Tests + + [x] Assert an Audit Centre Lead Clinician can delete users inside own Trust - HTTPStatus.OK + [x] Assert RCPCH Audit Team can delete users inside own Trust - HTTPStatus.OK + [x] Assert RCPCH Audit Team can delete users outside own Trust - HTTPStatus.OK + [x] Assert Clinical Audit Team can delete users inside own Trust - HTTPStatus.OK + [x] Assert Clinical Audit Team can delete users outside own Trust - HTTPStatus.OK + + [x] Assert an Audit Centre Administrator CANNOT delete users - HTTPStatus.FORBIDDEN + [x] Assert an audit centre clinician CANNOT delete users - HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician CANNOT delete users outside own Trust - HTTPStatus.FORBIDDEN + + + + [x] Assert an Audit Centre Lead Clinician can delete patients inside own Trust - HTTPStatus.OK + [x] Assert RCPCH Audit Team can delete patients inside own Trust - HTTPStatus.OK + [x] Assert RCPCH Audit Team can delete patients outside own Trust - HTTPStatus.OK + [x] Assert Clinical Audit Team can delete patients inside own Trust - HTTPStatus.OK + [x] Assert Clinical Audit Team can delete patients outside own Trust - HTTPStatus.OK + + [x] Assert an Audit Centre Administrator CANNOT delete patients - HTTPStatus.FORBIDDEN + [x] Assert an audit centre clinician CANNOT delete patients outside own Trust - HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician CANNOT delete patients outside own Trust - HTTPStatus.FORBIDDEN + + +# Episode + + [x] Assert an Audit Centre Lead Clinician can 'remove_episode' inside own Trust - HTTPStatus.OK + [x] Assert RCPCH Audit Team can 'remove_episode' inside own Trust - HTTPStatus.OK + [x] Assert RCPCH Audit Team can 'remove_episode' outside own Trust - HTTPStatus.OK + [x] Assert Clinical Audit Team can 'remove_episode' inside own Trust - HTTPStatus.OK + [x] Assert Clinical Audit Team can 'remove_episode' outside own Trust - HTTPStatus.OK + + [x] Assert an Audit Centre Administrator CANNOT 'remove_episode' - HTTPStatus.FORBIDDEN + [x] Assert an audit centre clinician CANNOT 'remove_episode' outside own Trust - HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician CANNOT 'remove_episode' outside own Trust - HTTPStatus.FORBIDDEN + +# Syndrome + + [x] Assert an Audit Centre Lead Clinician can 'remove_syndrome' inside own Trust - HTTPStatus.OK + [x] Assert RCPCH Audit Team can 'remove_syndrome' inside own Trust - HTTPStatus.OK + [x] Assert RCPCH Audit Team can 'remove_syndrome' outside own Trust - HTTPStatus.OK + [x] Assert Clinical Audit Team can 'remove_syndrome' inside own Trust - HTTPStatus.OK + [x] Assert Clinical Audit Team can 'remove_syndrome' outside own Trust - HTTPStatus.OK + + [x] Assert an Audit Centre Administrator CANNOT 'remove_syndrome' - HTTPStatus.FORBIDDEN + [x] Assert an audit centre clinician CANNOT 'remove_syndrome' outside own Trust - HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician CANNOT 'remove_syndrome' outside own Trust - HTTPStatus.FORBIDDEN + +# Comorbidity + + [x] Assert an Audit Centre Lead Clinician can 'remove_comorbidity' inside own Trust - HTTPStatus.OK + [x] Assert RCPCH Audit Team can 'remove_comorbidity' inside own Trust - HTTPStatus.OK + [x] Assert RCPCH Audit Team can 'remove_comorbidity' outside own Trust - HTTPStatus.OK + [x] Assert Clinical Audit Team can 'remove_comorbidity' inside own Trust - HTTPStatus.OK + [x] Assert Clinical Audit Team can 'remove_comorbidity' outside own Trust - HTTPStatus.OK + + [x] Assert an Audit Centre Administrator CANNOT 'remove_comorbidity' - HTTPStatus.FORBIDDEN + [x] Assert an audit centre clinician CANNOT 'remove_comorbidity' outside own Trust - HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician CANNOT 'remove_comorbidity' outside own Trust - HTTPStatus.FORBIDDEN + + +# Antiepilepsy Medicine + + [x] Assert an Audit Centre Lead Clinician can 'remove_antiepilepsy_medicine' inside own Trust - HTTPStatus.OK + [x] Assert RCPCH Audit Team can 'remove_antiepilepsy_medicine' inside own Trust - HTTPStatus.OK + [x] Assert RCPCH Audit Team can 'remove_antiepilepsy_medicine' outside own Trust - HTTPStatus.OK + [x] Assert Clinical Audit Team can 'remove_antiepilepsy_medicine' inside own Trust - HTTPStatus.OK + [x] Assert Clinical Audit Team can 'remove_antiepilepsy_medicine' outside own Trust - HTTPStatus.OK + + [x] Assert an Audit Centre Administrator CANNOT 'remove_antiepilepsy_medicine' - HTTPStatus.FORBIDDEN + [x] Assert an audit centre clinician CANNOT 'remove_antiepilepsy_medicine' outside own Trust - HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician CANNOT 'remove_antiepilepsy_medicine' outside own Trust - HTTPStatus.FORBIDDEN +""" + +# python imports +import pytest +from http import HTTPStatus +from datetime import date + +# django imports +from django.urls import reverse + +# E12 Imports +from epilepsy12.tests.factories import ( + E12UserFactory, + E12CaseFactory, +) +from epilepsy12.tests.UserDataClasses import ( + test_user_audit_centre_administrator_data, + test_user_audit_centre_clinician_data, + test_user_audit_centre_lead_clinician_data, + test_user_clinicial_audit_team_data, + test_user_rcpch_audit_team_data, +) +from epilepsy12.models import ( + Epilepsy12User, + Organisation, + Episode, + Syndrome, + Comorbidity, + AntiEpilepsyMedicine, + MedicineEntity, + ComorbidityEntity, +) + + +@pytest.mark.django_db +def test_user_delete_success( + client, + seed_groups_fixture, + seed_users_fixture, + seed_cases_fixture, +): + """Simulating different E12 users with different roles attempting to delete Users inside own trust. + + Additionally, RCPCH Audit Team and Clinical Audit Team roles should be able to delete user in different trust. + """ + + # set up constants + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + user_first_names_for_test = [ + test_user_audit_centre_lead_clinician_data.role_str, + test_user_rcpch_audit_team_data.role_str, + test_user_clinicial_audit_team_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + for test_user in users: + client.force_login(test_user) + + # Seed a temp User to be deleted + temp_user_same_org = E12UserFactory( + first_name="temp_user", + email=f"temp_{test_user.first_name}@temp.com", + role=test_user.role, + is_active=1, + organisation_employer=TEST_USER_ORGANISATION, + groups=[test_user_audit_centre_administrator_data.group_name], + ) + + url = reverse( + "delete_epilepsy12_user", + kwargs={ + "organisation_id": TEST_USER_ORGANISATION.id, + "epilepsy12_user_id": temp_user_same_org.id, + }, + ) + + response = client.get(url) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested `{url}` for User from {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()}. Expected 200 response status code, received {response.status_code}" + + # Additional test for deleting users outside of own Trust + if test_user.first_name in [ + test_user_clinicial_audit_team_data.role_str, + test_user_rcpch_audit_team_data.role_str, + ]: + # Seed a temp User to be deleted + temp_user_same_org = E12UserFactory( + first_name="temp_user", + email=f"temp_{test_user.first_name}@temp.com", + role=test_user.role, + is_active=1, + organisation_employer=DIFF_TRUST_DIFF_ORGANISATION, + groups=[test_user_audit_centre_administrator_data.group_name], + ) + + url = reverse( + "delete_epilepsy12_user", + kwargs={ + "organisation_id": DIFF_TRUST_DIFF_ORGANISATION.id, + "epilepsy12_user_id": temp_user_same_org.id, + }, + ) + + response = client.get(url) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested `{url}` for User from {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()}. Expected 200 response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_user_delete_forbidden( + client, +): + """Simulating different E12 users with different roles attempting to delete Users inside own trust. + + Audit Centre Lead Clinician role CANNOT delete user in different trust. + """ + + # set up constants + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + user_first_names_for_test = [ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + # Seed a temp User for attempt to delete + temp_user_same_org = E12UserFactory( + first_name="temp_user", + email=f"temp_user_same_org@temp.com", + role=test_user_audit_centre_administrator_data.role, + is_active=1, + organisation_employer=TEST_USER_ORGANISATION, + groups=[test_user_audit_centre_administrator_data.group_name], + ) + # Seed a temp User to be deleted + temp_user_same_org = E12UserFactory( + first_name="temp_user", + email=f"temp_user_diff_org@temp.com", + role=test_user_audit_centre_administrator_data.role, + is_active=1, + organisation_employer=DIFF_TRUST_DIFF_ORGANISATION, + groups=[test_user_audit_centre_administrator_data.group_name], + ) + + for test_user in users: + client.force_login(test_user) + + if test_user.first_name in [ + test_user_audit_centre_lead_clinician_data.role_str, + ]: + url = reverse( + "delete_epilepsy12_user", + kwargs={ + "organisation_id": DIFF_TRUST_DIFF_ORGANISATION.id, + "epilepsy12_user_id": temp_user_same_org.id, + }, + ) + + response = client.get(url) + + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested `{url}` for User from {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()}. Expected {HTTPStatus.FORBIDDEN} response status code, received {response.status_code}" + + else: + url = reverse( + "delete_epilepsy12_user", + kwargs={ + "organisation_id": TEST_USER_ORGANISATION.id, + "epilepsy12_user_id": temp_user_same_org.id, + }, + ) + + response = client.get(url) + + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested `{url}` for User from {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()}. Expected {HTTPStatus.FORBIDDEN} response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_patient_delete_success( + client, +): + """Simulating different E12 users with different roles attempting to delete Patients inside own trust. + + Additionally, RCPCH Audit Team and Clinical Audit Team roles should be able to delete patient in different trust. + """ + + # set up constants + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + user_first_names_for_test = [ + test_user_audit_centre_lead_clinician_data.role_str, + test_user_rcpch_audit_team_data.role_str, + test_user_clinicial_audit_team_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + for test_user in users: + client.force_login(test_user) + + # Seed a temp pt to be deleted + temp_pt_same_org = E12CaseFactory( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}", + organisations__organisation=TEST_USER_ORGANISATION, + ) + + url = reverse( + "update_case", + kwargs={ + "organisation_id": TEST_USER_ORGANISATION.id, + "case_id": temp_pt_same_org.id, + }, + ) + + response = client.post( + url, + data={"delete": "Delete"}, + ) + + assert ( + response.status_code == HTTPStatus.FOUND + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested `{url}` with DELETE for Case from {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()}. Expected {HTTPStatus.FOUND} response status code, received {response.status_code}" + + # Additional test for deleting users outside of own Trust + if test_user.first_name in [ + test_user_clinicial_audit_team_data.role_str, + test_user_rcpch_audit_team_data.role_str, + ]: + # Seed a temp pt to be deleted + temp_pt_diff_org = E12CaseFactory( + first_name=f"child_{DIFF_TRUST_DIFF_ORGANISATION.OrganisationName}", + organisations__organisation=DIFF_TRUST_DIFF_ORGANISATION, + ) + + url = reverse( + "update_case", + kwargs={ + "organisation_id": DIFF_TRUST_DIFF_ORGANISATION.id, + "case_id": temp_pt_diff_org.id, + }, + ) + + response = client.post( + url, + data={"delete": "Delete"}, + ) + + assert ( + response.status_code == HTTPStatus.FOUND + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested `{url}`with DELETE for Case from {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()}. Expected {HTTPStatus.FOUND} response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_patient_delete_forbidden( + client, +): + """Simulating different E12 users with different roles attempting to delete Patients inside own trust. + + Audit Centre Clinician & Audit Centre Lead Clinician role CANNOT delete patient in different trust. + """ + + # set up constants + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + user_first_names_for_test = [ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + # Seed a temp pt to be deleted + temp_pt_same_org = E12CaseFactory( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}", + organisations__organisation=TEST_USER_ORGANISATION, + ) + # Seed a temp pt to be deleted + temp_pt_diff_org = E12CaseFactory( + first_name=f"child_{DIFF_TRUST_DIFF_ORGANISATION.OrganisationName}", + organisations__organisation=DIFF_TRUST_DIFF_ORGANISATION, + ) + + for test_user in users: + client.force_login(test_user) + + if test_user.first_name in [ + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + ]: + url = reverse( + "update_case", + kwargs={ + "organisation_id": DIFF_TRUST_DIFF_ORGANISATION.id, + "case_id": temp_pt_diff_org.id, + }, + ) + else: + url = reverse( + "update_case", + kwargs={ + "organisation_id": TEST_USER_ORGANISATION.id, + "case_id": temp_pt_same_org.id, + }, + ) + + response = client.post( + url, + data={"delete": "Delete"}, + ) + + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested `{url}` with DELETE for Case from {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()}. Expected {HTTPStatus.FORBIDDEN} response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_episode_delete_success( + client, +): + """Simulating different E12 users with different roles attempting to delete Episode inside own trust. + + Additionally, RCPCH Audit Team and Clinical Audit Team roles should be able to delete Episode in different trust. + """ + + # set up constants + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + user_first_names_for_test = [ + test_user_audit_centre_lead_clinician_data.role_str, + test_user_rcpch_audit_team_data.role_str, + test_user_clinicial_audit_team_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + URLS = [ + "episode", + "comorbidity", + "syndrome", + "antiepilepsy_medicine", + ] + + for test_user in users: + client.force_login(test_user) + + # Create a Case with Episode + CASE_FROM_SAME_ORG = E12CaseFactory( + first_name=f"temp_{TEST_USER_ORGANISATION}", + organisations__organisation=TEST_USER_ORGANISATION, + ) + # Create objs to search for + episode = Episode.objects.create( + episode_definition="a", + multiaxial_diagnosis=CASE_FROM_SAME_ORG.registration.multiaxialdiagnosis, + ) + + syndrome = Syndrome.objects.create( + syndrome_diagnosis_date=date( + 2023, 2, 1 + ), # arbitrary answer just to ensure at least 1 completed field so not removed inside close_syndrome view + multiaxial_diagnosis=CASE_FROM_SAME_ORG.registration.multiaxialdiagnosis, + ) + + comorbidity = Comorbidity.objects.create( + multiaxial_diagnosis=CASE_FROM_SAME_ORG.registration.multiaxialdiagnosis, + comorbidityentity=ComorbidityEntity.objects.filter( + conceptId="1148757008" + ).first(), + ) + + aem = AntiEpilepsyMedicine.objects.create( + management=CASE_FROM_SAME_ORG.registration.management, + medicine_entity=MedicineEntity.objects.get( + medicine_name="Sodium valproate" + ), + ) + + for url in URLS: + if url == "episode": + OBJ_TO_DEL = episode + elif url == "comorbidity": + OBJ_TO_DEL = syndrome + elif url == "syndrome": + OBJ_TO_DEL = comorbidity + else: + OBJ_TO_DEL = aem + + url = reverse( + f"remove_{url}", + kwargs={f"{url}_id": OBJ_TO_DEL.id}, + ) + + response = client.post( + url, + ) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested `{url}` with DELETE for Case from {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()}. Expected {HTTPStatus.OK} response status code, received {response.status_code}" + + # Reset Case for next User + CASE_FROM_SAME_ORG.delete() + + # Additional test for deleting Episode outside of own Trust + if test_user.first_name in [ + test_user_clinicial_audit_team_data.role_str, + test_user_rcpch_audit_team_data.role_str, + ]: + # Create a Case with Episode + CASE_FROM_DIFF_ORG = E12CaseFactory( + first_name=f"temp_{DIFF_TRUST_DIFF_ORGANISATION}", + organisations__organisation=DIFF_TRUST_DIFF_ORGANISATION, + ) + # Create objs to search for + episode = Episode.objects.create( + episode_definition="a", + multiaxial_diagnosis=CASE_FROM_DIFF_ORG.registration.multiaxialdiagnosis, + ) + + syndrome = Syndrome.objects.create( + syndrome_diagnosis_date=date( + 2023, 2, 1 + ), # arbitrary answer just to ensure at least 1 completed field so not removed inside close_syndrome view + multiaxial_diagnosis=CASE_FROM_DIFF_ORG.registration.multiaxialdiagnosis, + ) + + comorbidity = Comorbidity.objects.create( + multiaxial_diagnosis=CASE_FROM_DIFF_ORG.registration.multiaxialdiagnosis, + comorbidityentity=ComorbidityEntity.objects.filter( + conceptId="1148757008" + ).first(), + ) + + aem = AntiEpilepsyMedicine.objects.create( + management=CASE_FROM_DIFF_ORG.registration.management, + medicine_entity=MedicineEntity.objects.get( + medicine_name="Sodium valproate" + ), + ) + + for url in URLS: + if url == "episode": + OBJ_TO_DEL = episode + elif url == "comorbidity": + OBJ_TO_DEL = syndrome + elif url == "syndrome": + OBJ_TO_DEL = comorbidity + else: + OBJ_TO_DEL = aem + + url = reverse( + f"remove_{url}", + kwargs={f"{url}_id": OBJ_TO_DEL.id}, + ) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested `{url}`with DELETE for Case from {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()}. Expected {HTTPStatus.OK} response status code, received {response.status_code}" + + # Reset Case and Episode + CASE_FROM_DIFF_ORG.delete() diff --git a/epilepsy12/tests/view_tests/permissions_tests/test_permissions_transfer.py b/epilepsy12/tests/view_tests/permissions_tests/test_permissions_transfer.py new file mode 100644 index 00000000..f57df8ae --- /dev/null +++ b/epilepsy12/tests/view_tests/permissions_tests/test_permissions_transfer.py @@ -0,0 +1,9 @@ +""" +## Transfer Tests + +['allocate_lead_site', 'edit_lead_site', 'transfer_lead_site'] +[ ] Assert an Audit Centre Administrator CANNOT transfer patients +[ ] Assert an audit centre clinician CANNOT transfer patients +[ ] Assert an Audit Centre Lead Clinician can transfer patients +[ ] Assert RCPCH Audit Team can transfer patients +""" diff --git a/epilepsy12/tests/view_tests/permissions_tests/test_permissions_update.py b/epilepsy12/tests/view_tests/permissions_tests/test_permissions_update.py new file mode 100644 index 00000000..a4419efd --- /dev/null +++ b/epilepsy12/tests/view_tests/permissions_tests/test_permissions_update.py @@ -0,0 +1,2503 @@ +""" +## Update Tests + +# Epilepsy12Users + [x] Assert an Audit Centre Administrator CANNOT update users inside own Trust + [x] Assert an Audit Centre Administrator CANNOT update users from outside own Trust + [x] Assert an audit centre clinician CANNOT update users inside own Trust + [x] Assert an audit centre clinician CANNOT update users from outside own Trust + [x] Assert an Audit Centre Lead Clinician CANNOT update users outside own Trust + + [x] Assert an Audit Centre Lead Clinician can update users inside own Trust + [x] Assert RCPCH Audit Team can update users in any trust + [x] Assert Clinical Audit Team can update users in own trust + [x] Assert Clinical Audit Team can update users in different trusts + +# Cases + [x] Assert an Audit Centre Administrator CANNOT update patient records outside own Trust + [x] Assert an audit centre clinician CANNOT update patient records outside own Trust + [x] Assert an Audit Centre Lead Clinician CANNOT update patient records outside own Trust + + [x] Assert an Audit Centre Administrator CAN update patient records inside own Trust + [x] Assert an audit centre clinician can update patient records within own organisation + [x] Assert an Audit Centre Lead Clinician can update patient records within own Trust + [x] Assert RCPCH Audit Team can update patient records within an organisation + [x] Assert Clinical Audit Team can update patient records within an organisation + +# First Paediatric Assessment + for field in fields: [ + 'first_paediatric_assessment_in_acute_or_nonacute_setting', + 'has_number_of_episodes_since_the_first_been_documented', + 'general_examination_performed', + 'neurological_examination_performed', + 'developmental_learning_or_schooling_problems', + 'behavioural_or_emotional_problems' + ] + [x] Assert an Audit Centre Administrator cannot change 'field' inside own Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Administrator cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Clinician cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + + [x] Assert an Audit Centre Clinician can change 'field' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can change 'field' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can change 'field' - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can change 'field' - response.status_code == HTTPStatus.OK + +# Epilepsy Context + for field in fields: [ + 'previous_febrile_seizure', single_choice_multiple_toggle_button + 'previous_acute_symptomatic_seizure', single_choice_multiple_toggle_button + 'is_there_a_family_history_of_epilepsy', toggle_button + 'previous_neonatal_seizures', single_choice_multiple_toggle_button + 'were_any_of_the_epileptic_seizures_convulsive', toggle_button + 'experienced_prolonged_generalized_convulsive_seizures', single_choice_multiple_toggle_button + 'experienced_prolonged_focal_seizures', single_choice_multiple_toggle_button + 'diagnosis_of_epilepsy_withdrawn', toggle_button + ] + + [x] Assert an Audit Centre Administrator cannot change 'field' inside own Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Administrator cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Clinician cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + + [x] Assert an Audit Centre Clinician can change 'field' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can change 'field' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can change 'field' - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can change 'field' - response.status_code == HTTPStatus.OK + + +# Multiaxial Diagnosis + for field in fields: [ + 'epilepsy_cause_known', toggle_button + 'epilepsy_cause', select + 'epilepsy_cause_categories', multiple_choice_multiple_toggle_button + 'relevant_impairments_behavioural_educational', toggle_button + 'mental_health_screen', toggle_button + 'mental_health_issue_identified', toggle_button + 'mental_health_issue', single_choice_multiple_toggle_button + 'global_developmental_delay_or_learning_difficulties', toggle_button + 'global_developmental_delay_or_learning_difficulties_severity', single_choice_multiple_toggle_button + 'autistic_spectrum_disorder', toggle_button + ] + + [x] Assert an Audit Centre Administrator cannot change 'field' inside own Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Administrator cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Clinician cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + + [x] Assert an Audit Centre Clinician can change 'field' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can change 'field' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can change 'field' - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can change 'field' - response.status_code == HTTPStatus.OK + +# Episode + for field in fields: [ + seizure_onset_date', date_field + seizure_onset_date_confidence', single_choice_multiple_toggle_button + episode_definition', select + has_description_of_the_episode_or_episodes_been_gathered', toggle_button + edit_description', string - updated in view function + delete_description_keyword', Keyword id - updated in view function + epilepsy_or_nonepilepsy_status', single_choice_multiple_toggle_button + epileptic_seizure_onset_type', single_choice_multiple_toggle_button + focal_onset_epilepsy_checked_changed', updated in view function + epileptic_generalised_onset', single_choice_multiple_toggle_button + nonepilepsy_generalised_onset', single_choice_multiple_toggle_button + nonepileptic_seizure_type', select + nonepileptic_seizure_subtype', select + ] + [x] Assert an Audit Centre Administrator cannot change 'field' inside own Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Administrator cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Clinician cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + + [x] Assert an Audit Centre Clinician can change 'field' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can change 'field' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can change 'field' - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can change 'field' - response.status_code == HTTPStatus.OK + +# Comorbidity + for field in fields: [ + 'comorbidity_diagnosis_date', date_field + 'comorbidity_diagnosis', select + ] + [x] Assert an Audit Centre Administrator cannot change 'field' inside own Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Administrator cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Clinician cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + + [x] Assert an Audit Centre Clinician can change 'field' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can change 'field' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can change 'field' - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can change 'field' - response.status_code == HTTPStatus.OK + +# Assessment + for field in fields: [ + 'consultant_paediatrician_referral_made', toggle_button + 'consultant_paediatrician_referral_date', date_field + 'consultant_paediatrician_input_date', date_field + 'general_paediatric_centre', button click + 'edit_general_paediatric_centre', button click + 'update_general_paediatric_centre_pressed', button click (action:edit/cancel) + 'paediatric_neurologist_referral_made', toggle_button + 'paediatric_neurologist_referral_date', date_field + 'paediatric_neurologist_input_date', date_field + 'paediatric_neurology_centre', button click + 'edit_paediatric_neurology_centre', button click + 'update_paediatric_neurology_centre_pressed', button click (action:edit/cancel) + 'childrens_epilepsy_surgical_service_referral_criteria_met', toggle_button + 'childrens_epilepsy_surgical_service_referral_made', toggle_button + 'childrens_epilepsy_surgical_service_referral_date', date_field + 'childrens_epilepsy_surgical_service_input_date', date_field + 'epilepsy_surgery_centre', button click + 'edit_epilepsy_surgery_centre', button click + 'update_epilepsy_surgery_centre_pressed', button click (action:edit/cancel) + 'epilepsy_specialist_nurse_referral_made', toggle_button + 'epilepsy_specialist_nurse_referral_date', date_field + 'epilepsy_specialist_nurse_input_date', date_field + ] + [x] Assert an Audit Centre Administrator cannot change 'field' inside own Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Administrator cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Clinician cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + + [x] Assert an Audit Centre Clinician can change 'field' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can change 'field' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can change 'field' - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can change 'field' - response.status_code == HTTPStatus.OK + +# Investigations + for field in fields: [ + 'eeg_indicated', toggle_button + 'eeg_request_date', date_field + 'eeg_performed_date', date_field + 'eeg_declined', button click (confirm:edit/decline) + 'twelve_lead_ecg_status', toggle_button + 'ct_head_scan_status', toggle_button + 'mri_indicated', toggle_button + 'mri_brain_requested_date', date_field + 'mri_brain_reported_date', date_field + 'mri_brain_declined', button click (confirm:edit/decline) + ] + [x] Assert an Audit Centre Administrator cannot change 'field' inside own Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Administrator cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Clinician cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + + [x] Assert an Audit Centre Clinician can change 'field' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can change 'field' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can change 'field' - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can change 'field' - response.status_code == HTTPStatus.OK + +# Management + for field in fields: [ + 'individualised_care_plan_in_place', toggle_button + 'individualised_care_plan_date', date_field + 'individualised_care_plan_has_parent_carer_child_agreement', toggle_button + 'individualised_care_plan_includes_service_contact_details', toggle_button + 'individualised_care_plan_include_first_aid', toggle_button + 'individualised_care_plan_parental_prolonged_seizure_care', toggle_button + 'individualised_care_plan_includes_general_participation_risk', toggle_button + 'individualised_care_plan_addresses_water_safety', toggle_button + 'individualised_care_plan_addresses_sudep', toggle_button + 'individualised_care_plan_includes_ehcp', toggle_button + 'has_individualised_care_plan_been_updated_in_the_last_year', toggle_button + 'has_been_referred_for_mental_health_support', toggle_button + 'has_support_for_mental_health_support', toggle_button + 'has_an_aed_been_given', toggle_button + 'has_rescue_medication_been_prescribed', toggle_button + ] + [x] Assert an Audit Centre Administrator cannot change 'field' inside own Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Administrator cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Clinician cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + + [x] Assert an Audit Centre Clinician can change 'field' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can change 'field' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can change 'field' - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can change 'field' - response.status_code == HTTPStatus.OK + +# Antiepilepsy Medicine + for field in fields: [ + 'edit_antiepilepsy_medicine', button click (antiepilepsy_medicine_id) + 'medicine_id', post on select change handled in view + 'antiepilepsy_medicine_start_date', date_field + 'antiepilepsy_medicine_add_stop_date', button click (antiepilepsy_medicine_id) + 'antiepilepsy_medicine_remove_stop_date', button click (antiepilepsy_medicine_id) + 'antiepilepsy_medicine_stop_date', date_field + 'antiepilepsy_medicine_risk_discussed', toggle_button + 'is_a_pregnancy_prevention_programme_in_place', toggle_button + 'has_a_valproate_annual_risk_acknowledgement_form_been_completed', toggle_button + ] + [x] Assert an Audit Centre Administrator cannot change 'field' inside own Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Administrator cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Clinician cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician cannot change 'field' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + + [x] Assert an Audit Centre Clinician can change 'field' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can change 'field' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can change 'field' - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can change 'field' - response.status_code == HTTPStatus.OK + + +""" +# python imports +import pytest +from http import HTTPStatus +from datetime import date + +# django imports +from django.urls import reverse +from django.contrib.auth.models import Group + +import factory + +# E12 imports +from epilepsy12.models import ( + Epilepsy12User, + Organisation, + Case, + Episode, + Keyword, + EpilepsyCauseEntity, + MultiaxialDiagnosis, + ComorbidityEntity, + Comorbidity, + MedicineEntity, +) +from epilepsy12.tests.UserDataClasses import ( + test_user_audit_centre_administrator_data, + test_user_audit_centre_clinician_data, + test_user_audit_centre_lead_clinician_data, + test_user_clinicial_audit_team_data, + test_user_rcpch_audit_team_data, +) +from epilepsy12.tests.factories import ( + E12UserFactory, + E12SiteFactory, + E12AntiEpilepsyMedicineFactory, +) + + +@pytest.mark.django_db +def test_users_update_users_forbidden( + client, + seed_groups_fixture, + seed_users_fixture, + seed_cases_fixture, +): + """ + Simulating different E12 Users attempting to update users in Epilepsy12 + + Assert these users cannot change Epilepsy12Users + """ + + # set up constants + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + # ADDENBROOKE'S + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + # Seed Test user to be updated + USER_FROM_DIFFERENT_ORG = E12UserFactory( + email=f"{DIFF_TRUST_DIFF_ORGANISATION}_ADMINISTRATOR@email.com", + first_name=f"{DIFF_TRUST_DIFF_ORGANISATION}_ADMINISTRATOR", + role=test_user_audit_centre_administrator_data.role, + # Assign flags based on user role + is_active=test_user_audit_centre_administrator_data.is_active, + is_staff=test_user_audit_centre_administrator_data.is_staff, + is_rcpch_audit_team_member=test_user_audit_centre_administrator_data.is_rcpch_audit_team_member, + is_rcpch_staff=test_user_audit_centre_administrator_data.is_rcpch_staff, + organisation_employer=TEST_USER_ORGANISATION, + groups=[ + Group.objects.get(name=test_user_audit_centre_administrator_data.group_name) + ], + ) + + user_first_names_for_test = [ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + response = client.get( + reverse( + "edit_epilepsy12_user", + kwargs={ + "organisation_id": DIFF_TRUST_DIFF_ORGANISATION.id, + "epilepsy12_user_id": USER_FROM_DIFFERENT_ORG.id, + }, + ) + ) + + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested update user {USER_FROM_DIFFERENT_ORG} in {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.FORBIDDEN} response status code, received {response.status_code}" + + # These users can't update any users, including same Trust + if test_user.first_name in [ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + ]: + response = client.get( + reverse( + "edit_epilepsy12_user", + kwargs={ + "organisation_id": TEST_USER_ORGANISATION.id, + "epilepsy12_user_id": test_user.id, + }, + ) + ) + + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested update user {test_user} in {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.FORBIDDEN} response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_users_update_users_success( + client, +): + """ + Simulating different E12 Users attempting to update users in Epilepsy12 + + Assert these users are allowed to change Epilepsy12Users + """ + + # set up constants + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + # ADDENBROOKE'S + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + USER_FROM_DIFFERENT_ORG = E12UserFactory( + email=f"{DIFF_TRUST_DIFF_ORGANISATION}_ADMINISTRATOR@email.com", + first_name=f"{DIFF_TRUST_DIFF_ORGANISATION}_ADMINISTRATOR", + role=test_user_audit_centre_administrator_data.role, + # Assign flags based on user role + is_active=test_user_audit_centre_administrator_data.is_active, + is_staff=test_user_audit_centre_administrator_data.is_staff, + is_rcpch_audit_team_member=test_user_audit_centre_administrator_data.is_rcpch_audit_team_member, + is_rcpch_staff=test_user_audit_centre_administrator_data.is_rcpch_staff, + organisation_employer=DIFF_TRUST_DIFF_ORGANISATION, + groups=[ + Group.objects.get(name=test_user_audit_centre_administrator_data.group_name) + ], + ) + + selected_users = [ + test_user_audit_centre_lead_clinician_data.role_str, + test_user_rcpch_audit_team_data.role_str, + test_user_clinicial_audit_team_data.role_str, + ] + + users = Epilepsy12User.objects.filter(first_name__in=selected_users) + + if len(users) != len(selected_users): + assert ( + False + ), f"Incorrect number of users selected. Requested {len(selected_users)} but queryset contains {len(users)}" + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + response = client.get( + reverse( + "edit_epilepsy12_user", + kwargs={ + "organisation_id": TEST_USER_ORGANISATION.id, + "epilepsy12_user_id": test_user.id, + }, + ) + ) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested update user {test_user} in {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.OK} response status code, received {response.status_code}" + + if test_user.first_name in [ + test_user_rcpch_audit_team_data, + test_user_clinicial_audit_team_data, + ]: + response = client.get( + reverse( + "edit_epilepsy12_user", + kwargs={ + "organisation_id": DIFF_TRUST_DIFF_ORGANISATION.id, + "epilepsy12_user_id": USER_FROM_DIFFERENT_ORG.id, + }, + ) + ) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested update user {USER_FROM_DIFFERENT_ORG} in {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.OK} response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_users_update_cases_forbidden( + client, +): + """ + Simulating different E12 Users attempting to update cases in Epilepsy12 + + Assert these users cannot change cases + """ + + # set up constants + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + CASE_FROM_DIFF_ORG = Case.objects.get( + first_name=f"child_{DIFF_TRUST_DIFF_ORGANISATION.OrganisationName}" + ) + + user_first_names_for_test = [ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + response = client.get( + reverse( + "update_case", + kwargs={ + "organisation_id": TEST_USER_ORGANISATION.id, + "case_id": CASE_FROM_DIFF_ORG.id, + }, + ) + ) + + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested update case {CASE_FROM_DIFF_ORG} in {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()} Expected 403 response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_users_update_cases_success( + client, +): + """ + Simulating different E12 Users attempting to update cases in Epilepsy12 + + Assert these users can change cases + """ + + # set up constants + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + CASE_FROM_SAME_ORG = Case.objects.get( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}" + ) + + users = Epilepsy12User.objects.filter( + first_name__in=[ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + test_user_clinicial_audit_team_data.role_str, + test_user_rcpch_audit_team_data.role_str, + ] + ) + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + response = client.get( + reverse( + "update_case", + kwargs={ + "organisation_id": TEST_USER_ORGANISATION.id, + "case_id": CASE_FROM_SAME_ORG.id, + }, + ) + ) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested update case {CASE_FROM_SAME_ORG} in {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.OK} response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_users_update_first_paediatric_assessment_forbidden(client): + """ + Simulating different E12 Users attempting to update first paediatric assessment in Epilepsy12 + + Assert these users cannot change first paediatric assessment + """ + + # set up constants + # GOSH + + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + CASE_FROM_DIFF_ORG = Case.objects.get( + first_name=f"child_{DIFF_TRUST_DIFF_ORGANISATION.OrganisationName}" + ) + + user_first_names_for_test = [ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + URLS = [ + "first_paediatric_assessment_in_acute_or_nonacute_setting", + "has_number_of_episodes_since_the_first_been_documented", + "general_examination_performed", + "neurological_examination_performed", + "developmental_learning_or_schooling_problems", + "behavioural_or_emotional_problems", + ] + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + for url in URLS: + response = client.get( + reverse( + url, + kwargs={ + "first_paediatric_assessment_id": CASE_FROM_DIFF_ORG.registration.firstpaediatricassessment.id, + }, + ) + ) + + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested update case {url} for {CASE_FROM_DIFF_ORG} in {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()} Expected 403 response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_users_update_first_paediatric_assessment_success(client): + """ + Simulating different E12 Users attempting to update first paediatric assessment in Epilepsy12 + + Assert these users can change first paediatric assessment + """ + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + CASE_FROM_SAME_ORG = Case.objects.get( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}" + ) + + user_first_names_for_test = [ + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + test_user_clinicial_audit_team_data.role_str, + test_user_rcpch_audit_team_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + URLS = [ + "first_paediatric_assessment_in_acute_or_nonacute_setting", + "has_number_of_episodes_since_the_first_been_documented", + "general_examination_performed", + "neurological_examination_performed", + "developmental_learning_or_schooling_problems", + "behavioural_or_emotional_problems", + ] + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + for URL in URLS: + if URL == "first_paediatric_assessment_in_acute_or_nonacute_setting": + # this is single_choice_multiple_toggle_button - select option 1 + response = client.get( + reverse( + URL, + kwargs={ + "first_paediatric_assessment_id": CASE_FROM_SAME_ORG.registration.firstpaediatricassessment.id, + }, + ), + headers={"Hx-Trigger-Name": "1", "Hx-Request": "true"}, + ) + else: + # all other options are toggle buttons: select True + response = client.get( + reverse( + URL, + kwargs={ + "first_paediatric_assessment_id": CASE_FROM_SAME_ORG.registration.firstpaediatricassessment.id, + }, + ), + headers={"Hx-Trigger-Name": "button-false", "Hx-Request": "true"}, + ) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested to update first paediatric assessment ({URL}) for {CASE_FROM_SAME_ORG} in {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.OK} response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_users_update_first_epilepsy_context_forbidden(client): + """ + Simulating different E12 Users attempting to update epilepsy context in Epilepsy12 + + Assert these users cannot change epilepsy context + """ + + # set up constants + + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + CASE_FROM_DIFF_ORG = Case.objects.get( + first_name=f"child_{DIFF_TRUST_DIFF_ORGANISATION.OrganisationName}" + ) + + user_first_names_for_test = [ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + URLS = [ + "previous_febrile_seizure", + "previous_acute_symptomatic_seizure", + "is_there_a_family_history_of_epilepsy", + "previous_neonatal_seizures", + "were_any_of_the_epileptic_seizures_convulsive", + "experienced_prolonged_generalized_convulsive_seizures", + "experienced_prolonged_focal_seizures", + "diagnosis_of_epilepsy_withdrawn", + ] + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + for URL in URLS: + response = client.get( + reverse( + URL, + kwargs={ + "epilepsy_context_id": CASE_FROM_DIFF_ORG.registration.epilepsycontext.id, + }, + ) + ) + + assert ( + response.status_code == response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested update epilepsy context {CASE_FROM_DIFF_ORG} in {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.FORBIDDEN} response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_users_update_epilepsy_context_success(client): + """ + Simulating different E12 Users attempting to update epilepsy context in Epilepsy12 + + Assert these users can change epilepsy context + """ + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + CASE_FROM_SAME_ORG = Case.objects.get( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}" + ) + + user_first_names_for_test = [ + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + test_user_clinicial_audit_team_data.role_str, + test_user_rcpch_audit_team_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + URLS = [ + "previous_febrile_seizure", + "previous_acute_symptomatic_seizure", + "is_there_a_family_history_of_epilepsy", + "previous_neonatal_seizures", + "were_any_of_the_epileptic_seizures_convulsive", + "experienced_prolonged_generalized_convulsive_seizures", + "experienced_prolonged_focal_seizures", + "diagnosis_of_epilepsy_withdrawn", + ] + + single_choice_multiple_toggle_fields = [ + "previous_febrile_seizure", + "previous_acute_symptomatic_seizure", + "is_there_a_family_history_of_epilepsy", + "previous_neonatal_seizures", + "experienced_prolonged_generalized_convulsive_seizures", + "experienced_prolonged_focal_seizures", + ] + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + for URL in URLS: + if URL in single_choice_multiple_toggle_fields: + # this is single_choice_multiple_toggle_button - select option 1 + response = client.get( + reverse( + URL, + kwargs={ + "epilepsy_context_id": CASE_FROM_SAME_ORG.registration.epilepsycontext.id, + }, + ), + headers={"Hx-Trigger-Name": "1", "Hx-Request": "true"}, + ) + else: + # all other options are toggle buttons: select True + response = client.get( + reverse( + URL, + kwargs={ + "epilepsy_context_id": CASE_FROM_SAME_ORG.registration.epilepsycontext.id, + }, + ), + headers={"Hx-Trigger-Name": "button-false", "Hx-Request": "true"}, + ) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested to update epilepsy context for {CASE_FROM_SAME_ORG} in {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.OK} response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_users_update_first_multiaxial_diagnosis_forbidden(client): + """ + Simulating different E12 Users attempting to update multiaxial diagnosis in Epilepsy12 + + Assert these users cannot change multiaxial diagnosis + """ + + # set up constants + + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + CASE_FROM_DIFF_ORG = Case.objects.get( + first_name=f"child_{DIFF_TRUST_DIFF_ORGANISATION.OrganisationName}" + ) + + user_first_names_for_test = [ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + URLS = [ + "epilepsy_cause_known", + "epilepsy_cause", + "epilepsy_cause_categories", + "relevant_impairments_behavioural_educational", + "mental_health_screen", + "mental_health_issue_identified", + "mental_health_issue", + "global_developmental_delay_or_learning_difficulties", + "global_developmental_delay_or_learning_difficulties_severity", + "autistic_spectrum_disorder", + ] + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + for URL in URLS: + response = client.get( + reverse( + URL, + kwargs={ + "multiaxial_diagnosis_id": CASE_FROM_DIFF_ORG.registration.multiaxialdiagnosis.id, + }, + ) + ) + + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested update multiaxial diagnosis {CASE_FROM_DIFF_ORG} in {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.FORBIDDEN} response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_users_update_multiaxial_diagnosis_success(client): + """ + Simulating different E12 Users attempting to update multiaxial diagnosis in Epilepsy12 + + Assert these users can change multiaxial diagnosis + """ + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + CASE_FROM_SAME_ORG = Case.objects.get( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}" + ) + + user_first_names_for_test = [ + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + test_user_clinicial_audit_team_data.role_str, + test_user_rcpch_audit_team_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + URLS = [ + "epilepsy_cause_known", + "epilepsy_cause", + "epilepsy_cause_categories", + "relevant_impairments_behavioural_educational", + "mental_health_screen", + "mental_health_issue_identified", + "mental_health_issue", + "global_developmental_delay_or_learning_difficulties", + "global_developmental_delay_or_learning_difficulties_severity", + "autistic_spectrum_disorder", + ] + + toggle_fields = [ + "epilepsy_cause_known", + "relevant_impairments_behavioural_educational", + "mental_health_screen", + "mental_health_issue_identified", + "global_developmental_delay_or_learning_difficulties", + "autistic_spectrum_disorder", + ] + + single_choice_multiple_toggle_button_fields = [ + "mental_health_issue", + "global_developmental_delay_or_learning_difficulties_severity", + ] + + # select_fields = ["epilepsy_cause"] tested in separate function + + multiple_choice_multiple_toggle_button_fields = ["epilepsy_cause_categories"] + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + for URL in URLS: + if URL in toggle_fields: + # all other options are toggle buttons: select True + response = client.post( + reverse( + URL, + kwargs={ + "multiaxial_diagnosis_id": CASE_FROM_SAME_ORG.registration.multiaxialdiagnosis.id, + }, + ), + headers={"Hx-Trigger-Name": "button-true", "Hx-Request": "true"}, + ) + elif ( + URL in single_choice_multiple_toggle_button_fields + or URL in multiple_choice_multiple_toggle_button_fields + ): + # this is single_choice_multiple_toggle_button - select option 1 + response = client.get( + reverse( + URL, + kwargs={ + "multiaxial_diagnosis_id": CASE_FROM_SAME_ORG.registration.epilepsycontext.id, + }, + ), + headers={"Hx-Trigger-Name": "1", "Hx-Request": "true"}, + ) + else: + # all other options are selects: select True + response = client.post( + reverse( + URL, + kwargs={ + "multiaxial_diagnosis_id": CASE_FROM_SAME_ORG.registration.multiaxialdiagnosis.id, + }, + ), + headers={"Hx-Trigger-Name": "epilepsy_cause", "Hx-Request": "true"}, + data={"epilepsy_cause": "179"}, + ) + + assert ( + response.status_code == response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested to update epilepsy context for {CASE_FROM_SAME_ORG} in {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()} Expected 200 response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_update_multiaxial_diagnosis_cause_success(client): + """ + Assert different E12 Users can update Cause section of multiaxial diagnosis. + + Endpoint url names: + + 'epilepsy_cause_known', + 'epilepsy_cause_categories', + 'epilepsy_cause' + """ + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + CASE_FROM_SAME_ORG = Case.objects.get( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}" + ) + + user_first_names_for_test = [ + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + test_user_clinicial_audit_team_data.role_str, + test_user_rcpch_audit_team_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + # Fryns macrocephaly + EPILEPSY_CAUSE_ENTITY = EpilepsyCauseEntity.objects.get(id=179) + + for test_user in users: + client.force_login(test_user) + + response_epilepsy_cause_known = client.post( + reverse( + "epilepsy_cause_known", + kwargs={ + "multiaxial_diagnosis_id": CASE_FROM_SAME_ORG.registration.multiaxialdiagnosis.id, + }, + ), + headers={"Hx-Trigger-Name": "button-true", "Hx-Request": "true"}, + ) + + assert ( + MultiaxialDiagnosis.objects.get( + registration=CASE_FROM_SAME_ORG.registration + ).epilepsy_cause_known + is True + ), f"{test_user} from {test_user.organisation_employer} attempted POST True to epilepsy_cause_known but model did not update." + + response_epilepsy_cause_categories = client.post( + reverse( + "epilepsy_cause_categories", + kwargs={ + "multiaxial_diagnosis_id": CASE_FROM_SAME_ORG.registration.multiaxialdiagnosis.id, + }, + ), + headers={"Hx-Trigger-Name": "Gen", "Hx-Request": "true"}, + ) + + assert MultiaxialDiagnosis.objects.get( + registration=CASE_FROM_SAME_ORG.registration + ).epilepsy_cause_categories == [ + "Gen" + ], f"{test_user} from {test_user.organisation_employer} attempted POST `Gen` to epilepsy_cause_categories but model did not update." + + response_epilepsy_cause = client.post( + reverse( + "epilepsy_cause", + kwargs={ + "multiaxial_diagnosis_id": CASE_FROM_SAME_ORG.registration.multiaxialdiagnosis.id, + }, + ), + headers={"Hx-Trigger-Name": "epilepsy_cause", "Hx-Request": "true"}, + data={"epilepsy_cause": f"{EPILEPSY_CAUSE_ENTITY.id}"}, + ) + + assert ( + MultiaxialDiagnosis.objects.get( + registration=CASE_FROM_SAME_ORG.registration + ).epilepsy_cause + == EPILEPSY_CAUSE_ENTITY + ), f"{test_user} from {test_user.organisation_employer} attempted POST `epilepsy_cause:{EPILEPSY_CAUSE_ENTITY.id}` but MultiaxialDiagnosis model field did not update." + + # Reset answers for next User + MultiaxialDiagnosis.objects.filter( + registration=CASE_FROM_SAME_ORG.registration + ).update( + epilepsy_cause_known=None, + epilepsy_cause_categories=[], + epilepsy_cause=None, + ) + + +@pytest.mark.django_db +def test_users_update_episode_forbidden(client): + """ + Simulating different E12 Users attempting to update episode in Epilepsy12 + + Assert these users cannot change episode + """ + + # set up constants + + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + CASE_FROM_DIFF_ORG = Case.objects.get( + first_name=f"child_{DIFF_TRUST_DIFF_ORGANISATION.OrganisationName}" + ) + + user_first_names_for_test = [ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + # Create objs to search for + episode = Episode.objects.create( + episode_definition="a", + multiaxial_diagnosis=CASE_FROM_DIFF_ORG.registration.multiaxialdiagnosis, + ) + + URLS = [ + "seizure_onset_date", + "seizure_onset_date_confidence", + "episode_definition", + "has_description_of_the_episode_or_episodes_been_gathered", + "edit_description", + "delete_description_keyword", + "epilepsy_or_nonepilepsy_status", + "epileptic_seizure_onset_type", + "focal_onset_epilepsy_checked_changed", + "epileptic_generalised_onset", + "nonepilepsy_generalised_onset", + "nonepileptic_seizure_type", + "nonepileptic_seizure_subtype", + ] + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + for URL in URLS: + if URL == "delete_description_keyword": + response = client.get( + reverse( + URL, + kwargs={ + "episode_id": episode.id, + "description_keyword_id": Keyword.objects.all().first().id, + }, + ) + ) + else: + response = client.get( + reverse( + URL, + kwargs={ + "episode_id": episode.id, + }, + ) + ) + + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested update episode {URL} for {CASE_FROM_DIFF_ORG} in {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.FORBIDDEN} response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_users_update_episode_success(client): + """ + Simulating different E12 Users attempting to update episode in Epilepsy12 + + Assert these users can change episode + """ + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + CASE_FROM_SAME_ORG = Case.objects.get( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}" + ) + + user_first_names_for_test = [ + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + test_user_clinicial_audit_team_data.role_str, + test_user_rcpch_audit_team_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + date_fields = ["seizure_onset_date"] + + toggle_fields = ["has_description_of_the_episode_or_episodes_been_gathered"] + + single_choice_multiple_toggle_button_fields = [ + "seizure_onset_date_confidence", + "epilepsy_or_nonepilepsy_status", + "epileptic_seizure_onset_type", + "epileptic_generalised_onset", + "nonepilepsy_generalised_onset", + ] + + select_fields = [ + "episode_definition", + "nonepileptic_seizure_type", + "nonepileptic_seizure_subtype", + ] + + # Create objs to search for + episode = Episode.objects.create( + episode_definition="a", + multiaxial_diagnosis=CASE_FROM_SAME_ORG.registration.multiaxialdiagnosis, + ) + + URLS = [ + "seizure_onset_date", + "seizure_onset_date_confidence", + "episode_definition", + "has_description_of_the_episode_or_episodes_been_gathered", + "edit_description", + "delete_description_keyword", + "epilepsy_or_nonepilepsy_status", + "epileptic_seizure_onset_type", + "focal_onset_epilepsy_checked_changed", + "epileptic_generalised_onset", + "nonepilepsy_generalised_onset", + "nonepileptic_seizure_type", + "nonepileptic_seizure_subtype", + ] + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + for URL in URLS: + if URL == "delete_description_keyword": + keyword = Keyword.objects.all().first() + description_keyword_list = [keyword.keyword] + episode.description_keywords = description_keyword_list + episode.save() + + response = client.post( + reverse( + URL, + kwargs={ + "episode_id": episode.id, + "description_keyword_id": 0, # remove first item in list + }, + ) + ) + elif URL in single_choice_multiple_toggle_button_fields: + response = client.post( + reverse( + URL, + kwargs={ + "episode_id": episode.id, + }, + ), + headers={"Hx-Trigger-Name": "1", "Hx-Request": "true"}, + ) + elif URL in toggle_fields: + response = client.post( + reverse( + URL, + kwargs={ + "episode_id": episode.id, + }, + ), + headers={"Hx-Trigger-Name": "button-true", "Hx-Request": "true"}, + ) + elif URL in select_fields: + post_body = None + if URL == "episode_definition": + post_body = "a" + elif URL == "nonepileptic_seizure_type": + post_body = "MAD" + elif URL == "nonepileptic_seizure_subtype": + post_body = "c" + else: + raise ValueError("No select chosen") + response = client.post( + reverse( + URL, + kwargs={ + "episode_id": episode.id, + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + data={URL: post_body}, + ) + elif URL in date_fields: + response = client.post( + reverse( + URL, + kwargs={ + "episode_id": episode.id, + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + data={URL: date.today()}, + ) + elif URL == "edit_description": + # remaining values are strings + response = client.post( + reverse( + URL, + kwargs={ + "episode_id": episode.id, + }, + ), + headers={"Hx-Trigger-Name": "description", "Hx-Request": "true"}, + data={"description": "This is a description"}, + ) + else: + # this is the choice for focal epilepsy + response = client.post( + reverse( + URL, + kwargs={ + "episode_id": episode.id, + }, + ), + headers={"Hx-Trigger-Name": "LATERALITY", "Hx-Request": "true"}, + data={URL: "focal_onset_left"}, + ) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested update episode {URL} for {CASE_FROM_SAME_ORG} in {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.OK} response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_users_update_comorbidity_forbidden(client): + """ + Simulating different E12 Users attempting to update comorbidity in Epilepsy12 + + Assert these users cannot change comorbidity + """ + + # set up constants + + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + CASE_FROM_DIFF_ORG = Case.objects.get( + first_name=f"child_{DIFF_TRUST_DIFF_ORGANISATION.OrganisationName}" + ) + + user_first_names_for_test = [ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + URLS = [ + "comorbidity_diagnosis_date", + "comorbidity_diagnosis", + ] + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + for URL in URLS: + comorbidity, created = Comorbidity.objects.update_or_create( + multiaxial_diagnosis=CASE_FROM_DIFF_ORG.registration.multiaxialdiagnosis, + comorbidity_diagnosis_date=date.today(), + comorbidityentity=ComorbidityEntity.objects.all().first(), + ) + comorbidity.save() + + if URL == "comorbidity_diagnosis_date": + response = client.post( + reverse( + URL, + kwargs={ + "comorbidity_id": comorbidity.pk, + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + data={URL: date.today()}, + ) + elif URL == "comorbidity_diagnosis": + response = client.post( + reverse( + URL, + kwargs={ + "comorbidity_id": comorbidity.pk, + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + data={URL: ComorbidityEntity.objects.all().first().id}, + ) + + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested update comorbidity {URL} for {CASE_FROM_DIFF_ORG} in {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.FORBIDDEN} response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_users_update_comorbidity_success(client): + """ + Simulating different E12 Users attempting to update comorbidity in Epilepsy12 + + Assert these users can change comorbidity + """ + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + CASE_FROM_SAME_ORG = Case.objects.get( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}" + ) + + user_first_names_for_test = [ + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + test_user_clinicial_audit_team_data.role_str, + test_user_rcpch_audit_team_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + URLS = [ + "comorbidity_diagnosis_date", + "comorbidity_diagnosis", + ] + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + for URL in URLS: + comorbidity, created = Comorbidity.objects.update_or_create( + multiaxial_diagnosis=CASE_FROM_SAME_ORG.registration.multiaxialdiagnosis, + comorbidity_diagnosis_date=date.today(), + comorbidityentity=ComorbidityEntity.objects.all().first(), + ) + comorbidity.save() + + if URL == "comorbidity_diagnosis_date": + response = client.post( + reverse( + URL, + kwargs={ + "comorbidity_id": comorbidity.id, + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + data={URL: date.today()}, + ) + elif URL == "comorbidity_diagnosis": + response = client.post( + reverse( + URL, + kwargs={ + "comorbidity_id": comorbidity.pk, + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + data={URL: ComorbidityEntity.objects.all().first().id}, + ) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested to update comorbidities {URL} for {CASE_FROM_SAME_ORG} in {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.OK} response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_users_update_assessment_forbidden(client): + """ + Simulating different E12 Users attempting to update assessment in Epilepsy12 + + Assert these users cannot change assessment + """ + + # set up constants + + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + CASE_FROM_DIFF_ORG = Case.objects.get( + first_name=f"child_{DIFF_TRUST_DIFF_ORGANISATION.OrganisationName}" + ) + + user_first_names_for_test = [ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + # fields + + toggle_buttons = [ + "consultant_paediatrician_referral_made", + "paediatric_neurologist_referral_made", + "childrens_epilepsy_surgical_service_referral_criteria_met", + "childrens_epilepsy_surgical_service_referral_made", + "epilepsy_specialist_nurse_referral_made", + ] + + date_fields = [ + "consultant_paediatrician_referral_date", + "consultant_paediatrician_input_date", + "paediatric_neurologist_referral_date", + "paediatric_neurologist_input_date", + "childrens_epilepsy_surgical_service_referral_date", + "childrens_epilepsy_surgical_service_input_date", + "epilepsy_specialist_nurse_referral_date", + "epilepsy_specialist_nurse_input_date", + ] + + URLS = [ + "consultant_paediatrician_referral_made", + "consultant_paediatrician_referral_date", + "consultant_paediatrician_input_date", + "general_paediatric_centre", + "edit_general_paediatric_centre", + "update_general_paediatric_centre_pressed", + "paediatric_neurologist_referral_made", + "paediatric_neurologist_referral_date", + "paediatric_neurologist_input_date", + "paediatric_neurology_centre", + "edit_paediatric_neurology_centre", + "update_paediatric_neurology_centre_pressed", + "childrens_epilepsy_surgical_service_referral_criteria_met", + "childrens_epilepsy_surgical_service_referral_made", + "childrens_epilepsy_surgical_service_referral_date", + "childrens_epilepsy_surgical_service_input_date", + "epilepsy_surgery_centre", + "edit_epilepsy_surgery_centre", + "update_epilepsy_surgery_centre_pressed", + "epilepsy_specialist_nurse_referral_made", + "epilepsy_specialist_nurse_referral_date", + "epilepsy_specialist_nurse_input_date", + ] + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + for URL in URLS: + if URL in toggle_buttons: + response = client.post( + reverse( + URL, + kwargs={ + "assessment_id": CASE_FROM_DIFF_ORG.registration.assessment.id, + }, + ), + headers={"Hx-Trigger-Name": "button-true", "Hx-Request": "true"}, + ) + elif URL in date_fields: + response = client.post( + reverse( + URL, + kwargs={ + "assessment_id": CASE_FROM_DIFF_ORG.registration.assessment.id, + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + data={URL: date.today()}, + ) + else: + # these are all button clicks + # 'general_paediatric_centre', button click assessment_id + # 'edit_general_paediatric_centre', button click assessment_id, site_id + # 'update_general_paediatric_centre_pressed', button click (edit/cancel) assessment_id, site_id + # 'paediatric_neurology_centre', button click assessment_id + # 'edit_paediatric_neurology_centre', button click assessment_id, site_id + # 'update_paediatric_neurology_centre_pressed', button click (edit/cancel) assessment_id, site_id + # 'epilepsy_surgery_centre', button click assessment_id + # 'edit_epilepsy_surgery_centre', button click assessment_id, site_id + # 'update_epilepsy_surgery_centre_pressed', button click (edit/cancel) assessment_id, site_id + if URL in [ + "edit_general_paediatric_centre", + "update_general_paediatric_centre_pressed", + "edit_paediatric_neurology_centre", + "update_paediatric_neurology_centre_pressed", + "edit_epilepsy_surgery_centre", + "update_epilepsy_surgery_centre_pressed", + ]: + # these all need assessment_id and site_id + current_site = E12SiteFactory( + case=CASE_FROM_DIFF_ORG, + organisation=DIFF_TRUST_DIFF_ORGANISATION, + ) + if URL in [ + "update_general_paediatric_centre_pressed", + "update_paediatric_neurology_centre_pressed", + "update_epilepsy_surgery_centre_pressed", + ]: + # these need accept a cancel or an edit param - testing the cancels here + response = client.post( + reverse( + URL, + kwargs={ + "assessment_id": CASE_FROM_DIFF_ORG.registration.assessment.id, + "site_id": current_site.pk, + "action": "cancel", + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + data={URL: 177}, # new organisation_id northampton general + ) + # assert cancel + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested update assessment for {CASE_FROM_DIFF_ORG} in {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.FORBIDDEN} response status code, received {response.status_code}" + # assert edit + response = client.post( + reverse( + URL, + kwargs={ + "assessment_id": CASE_FROM_DIFF_ORG.registration.assessment.id, + "site_id": current_site.pk, + "action": "edit", + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + data={URL: 177}, # new organisation_id northampton general + ) + else: + response = client.post( + reverse( + URL, + kwargs={ + "assessment_id": CASE_FROM_DIFF_ORG.registration.assessment.id, + "site_id": current_site.pk, + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + data={URL: 177}, # new organisation_id northampton general + ) + else: + response = client.post( + reverse( + URL, + kwargs={ + "assessment_id": CASE_FROM_DIFF_ORG.registration.assessment.id, + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + data={URL: 177}, # new organisation_id northampton general + ) + + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested update assessment {URL} for {CASE_FROM_DIFF_ORG} in {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.FORBIDDEN} response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_users_update_assessment_success(client): + """ + Simulating different E12 Users attempting to update assessment in Epilepsy12 + + Assert these users can change assessment + """ + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + CASE_FROM_SAME_ORG = Case.objects.get( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}" + ) + + user_first_names_for_test = [ + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + test_user_clinicial_audit_team_data.role_str, + test_user_rcpch_audit_team_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + # fields + + toggle_buttons = [ + "consultant_paediatrician_referral_made", + "paediatric_neurologist_referral_made", + "childrens_epilepsy_surgical_service_referral_criteria_met", + "childrens_epilepsy_surgical_service_referral_made", + "epilepsy_specialist_nurse_referral_made", + ] + + date_fields = [ + "consultant_paediatrician_referral_date", + "consultant_paediatrician_input_date", + "paediatric_neurologist_referral_date", + "paediatric_neurologist_input_date", + "childrens_epilepsy_surgical_service_referral_date", + "childrens_epilepsy_surgical_service_input_date", + "epilepsy_specialist_nurse_referral_date", + "epilepsy_specialist_nurse_input_date", + ] + + URLS = [ + "consultant_paediatrician_referral_made", + "consultant_paediatrician_referral_date", + "consultant_paediatrician_input_date", + "general_paediatric_centre", + "edit_general_paediatric_centre", + "update_general_paediatric_centre_pressed", + "paediatric_neurologist_referral_made", + "paediatric_neurologist_referral_date", + "paediatric_neurologist_input_date", + "paediatric_neurology_centre", + "edit_paediatric_neurology_centre", + "update_paediatric_neurology_centre_pressed", + "childrens_epilepsy_surgical_service_referral_criteria_met", + "childrens_epilepsy_surgical_service_referral_made", + "childrens_epilepsy_surgical_service_referral_date", + "childrens_epilepsy_surgical_service_input_date", + "epilepsy_surgery_centre", + "edit_epilepsy_surgery_centre", + "update_epilepsy_surgery_centre_pressed", + "epilepsy_specialist_nurse_referral_made", + "epilepsy_specialist_nurse_referral_date", + "epilepsy_specialist_nurse_input_date", + ] + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + for URL in URLS: + if URL in toggle_buttons: + response = client.post( + reverse( + URL, + kwargs={ + "assessment_id": CASE_FROM_SAME_ORG.registration.assessment.id, + }, + ), + headers={"Hx-Trigger-Name": "button-true", "Hx-Request": "true"}, + ) + elif URL in date_fields: + response = client.post( + reverse( + URL, + kwargs={ + "assessment_id": CASE_FROM_SAME_ORG.registration.assessment.id, + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + data={URL: date.today()}, + ) + else: + # these are all button clicks + # 'general_paediatric_centre', button click assessment_id + # 'edit_general_paediatric_centre', button click assessment_id, site_id + # 'update_general_paediatric_centre_pressed', button click (edit/cancel) assessment_id, site_id + # 'paediatric_neurology_centre', button click assessment_id + # 'edit_paediatric_neurology_centre', button click assessment_id, site_id + # 'update_paediatric_neurology_centre_pressed', button click (edit/cancel) assessment_id, site_id + # 'epilepsy_surgery_centre', button click assessment_id + # 'edit_epilepsy_surgery_centre', button click assessment_id, site_id + # 'update_epilepsy_surgery_centre_pressed', button click (edit/cancel) assessment_id, site_id + if URL in [ + "edit_general_paediatric_centre", + "update_general_paediatric_centre_pressed", + "edit_paediatric_neurology_centre", + "update_paediatric_neurology_centre_pressed", + "edit_epilepsy_surgery_centre", + "update_epilepsy_surgery_centre_pressed", + ]: + # these all need assessment_id and site_id + current_site = E12SiteFactory( + case=CASE_FROM_SAME_ORG, + organisation=TEST_USER_ORGANISATION, + ) + if URL in [ + "update_general_paediatric_centre_pressed", + "update_paediatric_neurology_centre_pressed", + "update_epilepsy_surgery_centre_pressed", + ]: + # these need accept a cancel or an edit param - testing the cancels here + response = client.post( + reverse( + URL, + kwargs={ + "assessment_id": CASE_FROM_SAME_ORG.registration.assessment.id, + "site_id": current_site.pk, + "action": "cancel", + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + data={URL: 177}, # new organisation_id northampton general + ) + # assert cancel + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested update assessment for {CASE_FROM_SAME_ORG} in {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.OK} response status code, received {response.status_code}" + # assert edit + response = client.post( + reverse( + URL, + kwargs={ + "assessment_id": CASE_FROM_SAME_ORG.registration.assessment.id, + "site_id": current_site.pk, + "action": "edit", + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + data={URL: 177}, # new organisation_id northampton general + ) + else: + response = client.post( + reverse( + URL, + kwargs={ + "assessment_id": CASE_FROM_SAME_ORG.registration.assessment.id, + "site_id": current_site.pk, + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + data={URL: 177}, # new organisation_id northampton general + ) + else: + response = client.post( + reverse( + URL, + kwargs={ + "assessment_id": CASE_FROM_SAME_ORG.registration.assessment.id, + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + data={URL: 177}, # new organisation_id northampton general + ) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested to update Assessment {URL} for {CASE_FROM_SAME_ORG} in {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.OK} response status code, received {response.status_code}" + + + +@pytest.mark.django_db +def test_users_update_investigations_forbidden(client): + """ + Simulating different E12 Users attempting to update investigations in Epilepsy12 + + Assert these users cannot change investigations + """ + + # set up constants + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + CASE_FROM_DIFF_ORG = Case.objects.get( + first_name=f"child_{DIFF_TRUST_DIFF_ORGANISATION.OrganisationName}" + ) + + user_first_names_for_test = [ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + # fields + date_fields = [ + "eeg_request_date", + "eeg_performed_date", + "mri_brain_requested_date", + "mri_brain_reported_date", + ] + + toggle_buttons = [ + "eeg_indicated", + "twelve_lead_ecg_status", + "ct_head_scan_status", + "mri_indicated", + ] + + URLS = [ + "eeg_indicated", + "eeg_request_date", + "eeg_performed_date", + "eeg_declined", + "twelve_lead_ecg_status", + "ct_head_scan_status", + "mri_indicated", + "mri_brain_requested_date", + "mri_brain_reported_date", + "mri_brain_declined", + ] + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + for URL in URLS: + if URL in toggle_buttons: + response = client.post( + reverse( + URL, + kwargs={ + "investigations_id": CASE_FROM_DIFF_ORG.registration.investigations.id, + }, + ), + headers={"Hx-Trigger-Name": "button-true", "Hx-Request": "true"}, + ) + elif URL in date_fields: + response = client.post( + reverse( + URL, + kwargs={ + "investigations_id": CASE_FROM_DIFF_ORG.registration.investigations.id, + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + data={URL: date.today()}, + ) + else: + # these are all button clicks + # these need accept an edit or a decline param - testing the confirm here + response = client.post( + reverse( + URL, + kwargs={ + "investigations_id": CASE_FROM_DIFF_ORG.registration.investigations.id, + "confirm": "edit", + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + ) + # assert edit + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested update assessment for {CASE_FROM_DIFF_ORG} in {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()} Expected 403 response status code, received {response.status_code}" + # assert decline + response = client.post( + reverse( + URL, + kwargs={ + "investigations_id": CASE_FROM_DIFF_ORG.registration.investigations.id, + "confirm": "decline", + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + ) + + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested update assessment {URL} for {CASE_FROM_DIFF_ORG} in {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.FORBIDDEN} response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_users_update_investigations_success(client): + """ + Simulating different E12 Users attempting to update investigations in Epilepsy12 + + Assert these users can change investigations + """ + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + CASE_FROM_SAME_ORG = Case.objects.get( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}" + ) + + user_first_names_for_test = [ + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + test_user_clinicial_audit_team_data.role_str, + test_user_rcpch_audit_team_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + # fields + date_fields = [ + "eeg_request_date", + "eeg_performed_date", + "mri_brain_requested_date", + "mri_brain_reported_date", + ] + + toggle_buttons = [ + "eeg_indicated", + "twelve_lead_ecg_status", + "ct_head_scan_status", + "mri_indicated", + ] + + URLS = [ + "eeg_indicated", + "eeg_request_date", + "eeg_performed_date", + "eeg_declined", + "twelve_lead_ecg_status", + "ct_head_scan_status", + "mri_indicated", + "mri_brain_requested_date", + "mri_brain_reported_date", + "mri_brain_declined", + ] + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + for URL in URLS: + if URL in toggle_buttons: + response = client.post( + reverse( + URL, + kwargs={ + "investigations_id": CASE_FROM_SAME_ORG.registration.investigations.id, + }, + ), + headers={"Hx-Trigger-Name": "button-true", "Hx-Request": "true"}, + ) + elif URL in date_fields: + response = client.post( + reverse( + URL, + kwargs={ + "investigations_id": CASE_FROM_SAME_ORG.registration.investigations.id, + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + data={URL: date.today()}, + ) + else: + # these are all button clicks + # these need accept an edit or a decline param - testing the edit here + response = client.post( + reverse( + URL, + kwargs={ + "investigations_id": CASE_FROM_SAME_ORG.registration.investigations.id, + "confirm": "edit", + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + ) + # assert edit + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested update assessment for {CASE_FROM_SAME_ORG} in {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.OK} response status code, received {response.status_code}" + # assert decline + response = client.post( + reverse( + URL, + kwargs={ + "investigations_id": CASE_FROM_SAME_ORG.registration.investigations.id, + "confirm": "decline", + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + ) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested to update Assessment {URL} for {CASE_FROM_SAME_ORG} in {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.OK} response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_users_update_management_forbidden(client): + """ + Simulating different E12 Users attempting to update management in Epilepsy12 + + Assert these users cannot change management + """ + + # set up constants + + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + CASE_FROM_DIFFERENT_ORG = Case.objects.get( + first_name=f"child_{DIFF_TRUST_DIFF_ORGANISATION.OrganisationName}" + ) + + user_first_names_for_test = [ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + # fields + date_fields = ["individualised_care_plan_date"] + + URLS = [ + "has_an_aed_been_given", + "has_rescue_medication_been_prescribed", + "individualised_care_plan_in_place", + "individualised_care_plan_date", + "individualised_care_plan_has_parent_carer_child_agreement", + "individualised_care_plan_includes_service_contact_details", + "individualised_care_plan_include_first_aid", + "individualised_care_plan_parental_prolonged_seizure_care", + "individualised_care_plan_includes_general_participation_risk", + "individualised_care_plan_addresses_water_safety", + "individualised_care_plan_addresses_sudep", + "individualised_care_plan_includes_ehcp", + "has_individualised_care_plan_been_updated_in_the_last_year", + "has_been_referred_for_mental_health_support", + "has_support_for_mental_health_support", + ] + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + for URL in URLS: + if URL in date_fields: + response = client.post( + reverse( + URL, + kwargs={ + "management_id": CASE_FROM_DIFFERENT_ORG.registration.management.id, + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + data={URL: date.today()}, + ) + else: + # these are all toggle buttons + response = client.post( + reverse( + URL, + kwargs={ + "management_id": CASE_FROM_DIFFERENT_ORG.registration.management.id, + }, + ), + headers={"Hx-Trigger-Name": "button-true", "Hx-Request": "true"}, + ) + + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested update management {URL} for {CASE_FROM_DIFFERENT_ORG} in {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.FORBIDDEN} response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_users_update_management_success(client): + """ + Simulating different E12 Users attempting to update management in Epilepsy12 + + Assert these users can change management + """ + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + CASE_FROM_SAME_ORG = Case.objects.get( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}" + ) + + user_first_names_for_test = [ + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + test_user_clinicial_audit_team_data.role_str, + test_user_rcpch_audit_team_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + # fields + date_fields = ["individualised_care_plan_date"] + + URLS = [ + "has_an_aed_been_given", + "has_rescue_medication_been_prescribed", + "individualised_care_plan_in_place", + "individualised_care_plan_date", + "individualised_care_plan_has_parent_carer_child_agreement", + "individualised_care_plan_includes_service_contact_details", + "individualised_care_plan_include_first_aid", + "individualised_care_plan_parental_prolonged_seizure_care", + "individualised_care_plan_includes_general_participation_risk", + "individualised_care_plan_addresses_water_safety", + "individualised_care_plan_addresses_sudep", + "individualised_care_plan_includes_ehcp", + "has_individualised_care_plan_been_updated_in_the_last_year", + "has_been_referred_for_mental_health_support", + "has_support_for_mental_health_support", + ] + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + for URL in URLS: + if URL in date_fields: + response = client.post( + reverse( + URL, + kwargs={ + "management_id": CASE_FROM_SAME_ORG.registration.management.id, + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + data={URL: date.today()}, + ) + else: + # these are all toggle buttons + response = client.post( + reverse( + URL, + kwargs={ + "management_id": CASE_FROM_SAME_ORG.registration.management.id, + }, + ), + headers={"Hx-Trigger-Name": "button-true", "Hx-Request": "true"}, + ) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested to update Management {URL} for {CASE_FROM_SAME_ORG} in {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.OK} response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_users_update_antiepilepsymedicine_forbidden(client): + """ + Simulating different E12 Users attempting to update antiepilepsymedicine in Epilepsy12 + + Assert these users cannot change antiepilepsymedicine + """ + + # set up constants + + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + CASE_FROM_DIFFERENT_ORG = Case.objects.get( + first_name=f"child_{DIFF_TRUST_DIFF_ORGANISATION.OrganisationName}" + ) + + user_first_names_for_test = [ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + # fields + date_fields = [ + "antiepilepsy_medicine_start_date", + "antiepilepsy_medicine_stop_date", + ] + + toggle_buttons = [ + "antiepilepsy_medicine_risk_discussed", + "is_a_pregnancy_prevention_programme_in_place", + "has_a_valproate_annual_risk_acknowledgement_form_been_completed", + ] + + URLS = [ + "edit_antiepilepsy_medicine", + "medicine_id", + "antiepilepsy_medicine_start_date", + "antiepilepsy_medicine_add_stop_date", + "antiepilepsy_medicine_remove_stop_date", + "antiepilepsy_medicine_stop_date", + "antiepilepsy_medicine_risk_discussed", + "is_a_pregnancy_prevention_programme_in_place", + "has_a_valproate_annual_risk_acknowledgement_form_been_completed", + ] + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + for URL in URLS: + antiepilepsy_medicine = E12AntiEpilepsyMedicineFactory( + management=CASE_FROM_DIFFERENT_ORG.registration.management, + is_rescue_medicine=True, + medicine_entity=MedicineEntity.objects.get(pk=4), # lorazepam + ) + if URL in date_fields: + response = client.post( + reverse( + URL, + kwargs={ + "antiepilepsy_medicine_id": antiepilepsy_medicine.pk, + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + data={URL: date.today()}, + ) + elif URL in toggle_buttons: + # these are all toggle buttons + response = client.post( + reverse( + URL, + kwargs={ + "antiepilepsy_medicine_id": antiepilepsy_medicine.pk, + }, + ), + headers={"Hx-Trigger-Name": "button-true", "Hx-Request": "true"}, + ) + else: + # these are all button clicks + response = client.post( + reverse( + URL, + kwargs={"antiepilepsy_medicine_id": antiepilepsy_medicine.pk}, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + ) + + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested update antiepilepsymedicine {URL} for {CASE_FROM_DIFFERENT_ORG} in {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.FORBIDDEN} response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_users_update_antiepilepsymedicine_success(client): + """ + Simulating different E12 Users attempting to update antiepilepsymedicine in Epilepsy12 + + Assert these users can change antiepilepsymedicine + """ + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + CASE_FROM_SAME_ORG = Case.objects.get( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}" + ) + + user_first_names_for_test = [ + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + test_user_clinicial_audit_team_data.role_str, + test_user_rcpch_audit_team_data.role_str, + ] + users = Epilepsy12User.objects.filter(first_name__in=user_first_names_for_test) + + assert len(users) == len( + user_first_names_for_test + ), f"Incorrect queryset of test users. Requested {len(user_first_names_for_test)} users, queryset includes {len(users)}: {users}" + + # fields + date_fields = [ + "antiepilepsy_medicine_start_date", + "antiepilepsy_medicine_stop_date", + ] + + toggle_buttons = [ + "antiepilepsy_medicine_risk_discussed", + "is_a_pregnancy_prevention_programme_in_place", + "has_a_valproate_annual_risk_acknowledgement_form_been_completed", + ] + + URLS = [ + "edit_antiepilepsy_medicine", + "medicine_id", + "antiepilepsy_medicine_start_date", + "antiepilepsy_medicine_add_stop_date", + "antiepilepsy_medicine_remove_stop_date", + "antiepilepsy_medicine_stop_date", + "antiepilepsy_medicine_risk_discussed", + "is_a_pregnancy_prevention_programme_in_place", + "has_a_valproate_annual_risk_acknowledgement_form_been_completed", + ] + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + for URL in URLS: + # carbamazepine + antiepilepsy_medicine = E12AntiEpilepsyMedicineFactory( + management=CASE_FROM_SAME_ORG.registration.management, + is_rescue_medicine=True, + medicine_entity=MedicineEntity.objects.get( + medicine_name="Carbamazepine" + ), + ) + + if URL in date_fields: + response = client.post( + reverse( + URL, + kwargs={ + "antiepilepsy_medicine_id": antiepilepsy_medicine.pk, + }, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + data={URL: date.today()}, + ) + elif URL in toggle_buttons: + # these are all toggle buttons + response = client.post( + reverse( + URL, + kwargs={ + "antiepilepsy_medicine_id": antiepilepsy_medicine.pk, + }, + ), + headers={"Hx-Trigger-Name": "button-true", "Hx-Request": "true"}, + ) + else: + # these are all button clicks + response = client.post( + reverse( + URL, + kwargs={"antiepilepsy_medicine_id": antiepilepsy_medicine.pk}, + ), + headers={"Hx-Trigger-Name": URL, "Hx-Request": "true"}, + data={"medicine_id": 8}, # Clobazam + ) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested to update AntiepilepsyMedicine {URL} for {CASE_FROM_SAME_ORG} in {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()} Expected {HTTPStatus.OK} response status code, received {response.status_code}" diff --git a/epilepsy12/tests/view_tests/permissions_tests/test_permissions_view.py b/epilepsy12/tests/view_tests/permissions_tests/test_permissions_view.py new file mode 100644 index 00000000..03877675 --- /dev/null +++ b/epilepsy12/tests/view_tests/permissions_tests/test_permissions_view.py @@ -0,0 +1,978 @@ +""" +Tests to ensure permissions work as expected. + +NOTE: if you wish to quickly seed test users inside the shell, see the `misc_py_shell_code.py` file. + + +## View Tests + +### E12 Users + + [x] Assert an Audit Centre Administrator can view users inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Clinician can view users inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can view users inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert Clinician who is also an RCPCH Audit Team can view users inside own Trust - response.status_code == HTTPStatus.OK + + [x] Assert RCPCH Audit Team can view users inside a different Trust - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can view users inside a different Trust - response.status_code == HTTPStatus.OK + + +### E12 Patient Records + + [x] Assert an Audit Centre Administrator can view patients inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an audit centre clinician can view patients inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can view patients inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can view patients within all Trusts - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can view patients inside a different Trust - response.status_code == HTTPStatus.OK + + [x] Assert an Audit Centre Administrator CANNOT view patients outside own Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an audit centre clinician CANNOT view patients outside own Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician CANNOT view patients outside own Trust - response.status_code == HTTPStatus.FORBIDDEN + + +## Registration + + [x] Assert an Audit Centre Administrator can view 'register' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Clinician can view 'register' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can view 'register' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can view 'register' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can view 'register' outside own Trust - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can view 'register' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can view 'register' outside own Trust - response.status_code == HTTPStatus.OK + + [x] Assert an Audit Centre Clinician cannot view 'register' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Administrator cannot view 'register' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician cannot view 'register' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + +## First Paediatric Assessment + + [x] Assert an Audit Centre Administrator can view 'first_paediatric_assessment' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Clinician can view 'first_paediatric_assessment' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can view 'first_paediatric_assessment' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can view 'first_paediatric_assessment' inside own Trust- response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can view 'first_paediatric_assessment' outside own Trust- response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can view 'first_paediatric_assessment' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can view 'first_paediatric_assessment' outside own Trust - response.status_code == HTTPStatus.OK + + [x] Assert an Audit Centre Administrator cannot view 'first_paediatric_assessment' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Clinician cannot view 'first_paediatric_assessment' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician cannot view 'first_paediatric_assessment' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + +## Epilepsy Context + + [x] Assert an Audit Centre Administrator can view 'epilepsy_context' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Clinician can view 'epilepsy_context' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can view 'epilepsy_context' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can view 'epilepsy_context' inside own Trust- response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can view 'epilepsy_context' outside own Trust- response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can view 'epilepsy_context' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can view 'epilepsy_context' outside own Trust - response.status_code == HTTPStatus.OK + + [x] Assert an Audit Centre Clinician cannot view 'epilepsy_context' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Administrator cannot view 'epilepsy_context' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician cannot view 'epilepsy_context' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + +## Multiaxial Diagnosis + + [x] Assert an Audit Centre Administrator can view 'multiaxial_diagnosis' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Clinician can view 'multiaxial_diagnosis' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can view 'multiaxial_diagnosis' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can view 'multiaxial_diagnosis' inside own Trust- response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can view 'multiaxial_diagnosis' outside own Trust- response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can view 'multiaxial_diagnosis' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can view 'multiaxial_diagnosis' outside own Trust - response.status_code == HTTPStatus.OK + + + [x] Assert an Audit Centre Clinician cannot view 'multiaxial_diagnosis' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Administrator cannot view 'multiaxial_diagnosis' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician cannot view 'multiaxial_diagnosis' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + +## Episode +for each field in fields ['edit_episode','close_episode'] + + [x] Assert an Audit Centre Administrator can view field inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Clinician can view field inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can view field inside own Trust - response.status_code = 200 + [x] Assert RCPCH Audit Team can view field inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can view field inside a different Trust - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can view field inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can view field outside own Trust - response.status_code == HTTPStatus.OK + + [x] Assert an Audit Centre Administrator cannot view field inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Clinician cannot view field inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician cannot view field inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + +## Syndrome +for each field in fields ['edit_syndrome', 'close_syndrome'] + [x] Assert an Audit Centre Administrator can view field inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Clinician can view field inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can view field inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can view field inside own and different Trust - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can view field inside own and different Trust - response.status_code == HTTPStatus.OK + + + [x] Assert an Audit Centre Administrator cannot view field inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Clinician cannot view field inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician cannot view field inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + +## Antiepilepsy Medicine +for each field in fields ['edit_antiepilepsy_medicine', 'close_antiepilepsy_medicine'] + + [x] Assert an Audit Centre Administrator can view field inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Clinician can view field inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can view field inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can view inside own and different Trust - response.status_code == HTTPStatus.OK + [x] Assert Clinical Audit Team can view inside own and different Trust - response.status_code == HTTPStatus.OK + + + [x] Assert an Audit Centre Administrator cannot view field inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Clinician cannot view field inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician cannot view field inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + +## Comorbidity +for each field in fields ['edit_comorbidity', 'close_comorbidity', 'comorbidities'] + [x] Assert an Audit Centre Administrator can view field inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Clinician can view field inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can view field inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can view field inside own and different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert Clinical Audit Team can view field inside own and different Trust - response.status_code == HTTPStatus.OK + + [x] Assert an Audit Centre Administrator cannot view field inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Clinician cannot view field inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician cannot view field inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + + +## Assessment + + [x] Assert an Audit Centre Administrator can view 'assessment' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Clinician can view 'assessment' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can view 'assessment' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can view inside own and different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert Clinical Audit Team can view field inside own and different Trust - response.status_code == HTTPStatus.OK + + [x] Assert an Audit Centre Administrator cannot view 'assessment' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Clinician cannot view 'assessment' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician cannot view 'assessment' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + +## Investigations + + [x] Assert an Audit Centre Administrator can view 'investigations' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Clinician can view 'investigations' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can view 'investigations' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can view inside own and different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert Clinical Audit Team can view field inside own and different Trust - response.status_code == HTTPStatus.OK + + [x] Assert an Audit Centre Administrator cannot view 'investigations' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Clinician cannot view 'investigations' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician cannot view 'investigations' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + +## Management + + [x] Assert an Audit Centre Administrator can view 'management' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Clinician can view 'management' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert an Audit Centre Lead Clinician can view 'management' inside own Trust - response.status_code == HTTPStatus.OK + [x] Assert RCPCH Audit Team can view inside own and different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert Clinical Audit Team can view field inside own and different Trust - response.status_code == HTTPStatus.OK + + [x] Assert an Audit Centre Administrator cannot view 'management' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Clinician cannot view 'management' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + [x] Assert an Audit Centre Lead Clinician cannot view 'management' inside a different Trust - response.status_code == HTTPStatus.FORBIDDEN + +""" + +# python imports +import pytest +from datetime import date +from http import HTTPStatus + +# 3rd party imports +from django.urls import reverse + +# E12 Imports +from epilepsy12.tests.UserDataClasses import ( + test_user_audit_centre_administrator_data, + test_user_audit_centre_clinician_data, + test_user_audit_centre_lead_clinician_data, + test_user_rcpch_audit_team_data, + test_user_clinicial_audit_team_data, +) +from epilepsy12.models import ( + Epilepsy12User, + Organisation, + Case, + Episode, + Syndrome, + Comorbidity, + ComorbidityEntity, + AntiEpilepsyMedicine, + MedicineEntity, +) + + +@pytest.mark.parametrize( + "URL", + [ + ("epilepsy12_user_list"), + ("cases"), + ], +) +@pytest.mark.django_db +def test_users_and_case_list_views_permissions_success( + client, + seed_groups_fixture, + seed_users_fixture, + seed_cases_fixture, + URL, +): + """ + Simulating different E12Users with different roles attempting to access the Users / Cases list of their own Trust. + + Additionally, tests RCPCH Audit Team can access lists of different Trust. + + + NOTE: the `seed_groups_fixture, `seed_users_fixture`, `seed_cases_fixture` fixtures are scoped to the session, they just need to be used once to seed the db across further tests. + """ + + # set up constants + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + + # ADDENBROOKE'S + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + users = Epilepsy12User.objects.all() + + for test_user in users: + # Log in Test User + client.force_login(test_user) + + # Request e12 User/Case list endpoint url of same Trust + e12_user_list_response = client.get( + reverse( + URL, + kwargs={"organisation_id": TEST_USER_ORGANISATION.id}, + ) + ) + + assert ( + e12_user_list_response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested {URL} list of {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()} Expected 200 response status code, received {e12_user_list_response.status_code}" + + # Additional test to RCPCH AUDIT TEAM / Clinical Audit Team who should be able to view nationally + if test_user.first_name in [ + test_user_rcpch_audit_team_data.role_str, + test_user_clinicial_audit_team_data.role_str, + ]: + # Request e12 user/case list endpoint url diff org + e12_user_list_response = client.get( + reverse( + URL, + kwargs={"organisation_id": DIFF_TRUST_DIFF_ORGANISATION.id}, + ) + ) + + assert ( + e12_user_list_response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested {URL} list of {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()} Expected 200 response status code, received {e12_user_list_response.status_code}" + + +@pytest.mark.parametrize( + "URL", + [ + ("epilepsy12_user_list"), + ("cases"), + ], +) +@pytest.mark.django_db +def test_users_and_cases_list_view_permissions_forbidden( + client, + URL, +): + """ + Simulating different E12Users with different roles attempting to access the Users / Cases list of different Trust. + + Assert these users CAN'T view the List of a different Trust. + """ + + # ADDENBROOKE'S - DIFFERENT TRUST + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + # RCPCH/CLINICAL AUDIT TEAM HAVE FULL ACCESS SO EXCLUDE + users = Epilepsy12User.objects.filter( + first_name__in=[ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + ] + ) + + for test_user in users: + client.force_login(test_user) + + # Request e12 user list endpoint url diff org + e12_user_list_response_different_organisation = client.get( + reverse( + URL, + kwargs={"organisation_id": DIFF_TRUST_DIFF_ORGANISATION.id}, + ) + ) + + assert ( + e12_user_list_response_different_organisation.status_code + == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested {URL} list of {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()} Expected 403 response status code, received {e12_user_list_response_different_organisation.status_code}" + + +@pytest.mark.django_db +def test_registration_view_permissions_success(client): + """ + Assert these users CAN view registration for their own Trust. + + RCPCH Audit Team have additional test to assert can view registration outside own Trust. + """ + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + CASE_FROM_SAME_ORG = Case.objects.get( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}" + ) + + users = Epilepsy12User.objects.all() + + for test_user in users: + client.force_login(test_user) + + # Get response object + response = client.get( + reverse( + "register", + kwargs={"case_id": CASE_FROM_SAME_ORG.id}, + ) + ) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested Registration page of Case in {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()} Expected 200 response status code, received {response.status_code}" + + # Additional test to RCPCH AUDIT TEAM / Clinical Audit Team who should be able to view nationally + if test_user.first_name in [ + test_user_rcpch_audit_team_data.role_str, + test_user_clinicial_audit_team_data.role_str, + ]: + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + + # Request e12 patients list endpoint url different org + response = client.get( + reverse( + "cases", + kwargs={"organisation_id": DIFF_TRUST_DIFF_ORGANISATION.id}, + ) + ) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested Registration page of Case in {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()} Expected 200 response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_registration_view_permissions_forbidden(client): + """ + Assert these users CANT view registration for different Trust. + """ + + # GOSH + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + CASE_FROM_DIFF_ORG = Case.objects.get( + first_name=f"child_{DIFF_TRUST_DIFF_ORGANISATION.OrganisationName}" + ) + + # RCPCH/CLINCAL AUDIT TEAM HAVE FULL ACCESS SO DONT INCLUDE + users = Epilepsy12User.objects.filter( + first_name__in=[ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + ] + ) + + for test_user in users: + client.force_login(test_user) + + # Get response object + response = client.get( + reverse( + "register", + kwargs={"case_id": CASE_FROM_DIFF_ORG.id}, + ) + ) + + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested registration of Case from {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()} Expected 403 response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_episode_syndrome_aem_view_permissions_success(client): + """ + Assert these users CAN view following for Case from their own Trust: + + - episode + - syndrome + - aem + + RCPCH Audit Team has additional test to assert can view outside own Trust. + """ + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + CASE_FROM_SAME_ORG = Case.objects.get( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}" + ) + + users = Epilepsy12User.objects.all() + + # Create objs to search for + episode = Episode.objects.create( + episode_definition="a", + multiaxial_diagnosis=CASE_FROM_SAME_ORG.registration.multiaxialdiagnosis, + ) + syndrome = Syndrome.objects.create( + syndrome_diagnosis_date=date( + 2023, 2, 1 + ), # arbitrary answer just to ensure at least 1 completed field so not removed inside close_syndrome view + multiaxial_diagnosis=CASE_FROM_SAME_ORG.registration.multiaxialdiagnosis, + ) + + aem = AntiEpilepsyMedicine.objects.create( + management=CASE_FROM_SAME_ORG.registration.management, + medicine_entity=MedicineEntity.objects.get(medicine_name="Sodium valproate"), + ) + + for test_user in users: + client.force_login(test_user) + + for page in ["episode", "syndrome", "antiepilepsy_medicine"]: + # Create the object to search for + if page == "episode": + OBJ_SAME_ORGANISATION = episode + elif page == "syndrome": + OBJ_SAME_ORGANISATION = syndrome + elif page == "antiepilepsy_medicine": + OBJ_SAME_ORGANISATION = aem + + for action in ["edit", "close"]: + URL = f"{action}_{page}" + + KWARGS = {f"{page}_id": OBJ_SAME_ORGANISATION.id} + + # Get response object + response = client.get( + reverse( + URL, + kwargs=KWARGS, + ) + ) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested {URL} page of Case from {CASE_FROM_SAME_ORG.organisations.all()}. Has groups: {test_user.groups.all()} Expected 200 response status code, received {response.status_code}" + + # Additional test to RCPCH AUDIT TEAM / Clinical Audit Team who should be able to view nationally + if test_user.first_name in [ + test_user_rcpch_audit_team_data.role_str, + test_user_clinicial_audit_team_data.role_str, + ]: + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + CASE_FROM_DIFF_ORG = Case.objects.get( + first_name=f"child_{DIFF_TRUST_DIFF_ORGANISATION.OrganisationName}" + ) + + # Create objs to search for + episode = Episode.objects.create( + episode_definition="a", + multiaxial_diagnosis=CASE_FROM_DIFF_ORG.registration.multiaxialdiagnosis, + ) + + syndrome = Syndrome.objects.create( + syndrome_diagnosis_date=date( + 2023, 2, 1 + ), # arbitrary answer just to ensure at least 1 completed field so not removed inside close_syndrome view + multiaxial_diagnosis=CASE_FROM_DIFF_ORG.registration.multiaxialdiagnosis, + ) + + aem = AntiEpilepsyMedicine.objects.create( + management=CASE_FROM_DIFF_ORG.registration.management, + medicine_entity=MedicineEntity.objects.get( + medicine_name="Sodium valproate" + ), + ) + + # Create the object to search for + if page == "episode": + OBJ_DIFF_ORGANISATION = episode + elif page == "syndrome": + OBJ_DIFF_ORGANISATION = syndrome + elif page == "antiepilepsy_medicine": + OBJ_DIFF_ORGANISATION = aem + + for action in ["edit", "close"]: + URL = f"{action}_{page}" + + KWARGS = {f"{page}_id": OBJ_DIFF_ORGANISATION.id} + + # Get response object + response = client.get( + reverse( + URL, + kwargs=KWARGS, + ) + ) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested {URL} page of case from {CASE_FROM_DIFF_ORG.organisations.all()}. Has groups: {test_user.groups.all()} Expected 200 response status code, received {response.status_code}" + + +@pytest.mark.parametrize("URL", [("edit_episode"), ("close_episode")]) +@pytest.mark.django_db +def test_episode_view_permissions_forbidden(client, URL): + """ + Assert these users CANT view Episode for Case from different Trust. + """ + + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + CASE_FROM_DIFF_ORG = Case.objects.get( + first_name=f"child_{DIFF_TRUST_DIFF_ORGANISATION.OrganisationName}" + ) + + EPISODE_DIFF_ORG = Episode.objects.get( + multiaxial_diagnosis=CASE_FROM_DIFF_ORG.registration.multiaxialdiagnosis + ) + + # RCPCH/CLINCAL AUDIT TEAM HAVE FULL ACCESS SO DONT INCLUDE + users = Epilepsy12User.objects.filter( + first_name__in=[ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + ] + ) + + for test_user in users: + client.force_login(test_user) + + # Get response object + response = client.get( + reverse( + URL, + kwargs={"episode_id": EPISODE_DIFF_ORG.id}, + ) + ) + + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested multiaxial_diagnosis page of case from {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()} Expected 403 response status code, received {response.status_code}" + + +@pytest.mark.parametrize("URL", [("edit_syndrome"), ("close_syndrome")]) +@pytest.mark.django_db +def test_syndrome_view_permissions_forbidden(client, URL): + """ + Assert these users CANT view syndrome for Case from different Trust. + """ + + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + CASE_FROM_DIFF_ORG = Case.objects.get( + first_name=f"child_{DIFF_TRUST_DIFF_ORGANISATION.OrganisationName}" + ) + + syndrome_DIFF_ORG = Syndrome.objects.get( + multiaxial_diagnosis=CASE_FROM_DIFF_ORG.registration.multiaxialdiagnosis + ) + + # RCPCH/CLINCAL AUDIT TEAM HAVE FULL ACCESS SO DONT INCLUDE + users = Epilepsy12User.objects.filter( + first_name__in=[ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + ] + ) + + for test_user in users: + client.force_login(test_user) + + # Get response object + response = client.get( + reverse( + URL, + kwargs={"syndrome_id": syndrome_DIFF_ORG.id}, + ) + ) + + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested syndrome page of case from {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()} Expected 403 response status code, received {response.status_code}" + + +@pytest.mark.parametrize( + "URL", [("edit_antiepilepsy_medicine"), ("close_antiepilepsy_medicine")] +) +@pytest.mark.django_db +def test_antiepilepsy_medicine_view_permissions_forbidden(client, URL): + """ + Assert these users CANT view antiepilepsy_medicine for Case from different Trust. + """ + + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + CASE_FROM_DIFF_ORG = Case.objects.get( + first_name=f"child_{DIFF_TRUST_DIFF_ORGANISATION.OrganisationName}" + ) + + antiepilepsy_medicine_DIFF_ORG = AntiEpilepsyMedicine.objects.create( + management=CASE_FROM_DIFF_ORG.registration.management, + medicine_entity=MedicineEntity.objects.get(medicine_name="Sodium valproate"), + ) + + # RCPCH/CLINCAL AUDIT TEAM HAVE FULL ACCESS SO DONT INCLUDE + users = Epilepsy12User.objects.filter( + first_name__in=[ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str, + ] + ) + + for test_user in users: + client.force_login(test_user) + + # Get response object + response = client.get( + reverse( + URL, + kwargs={"antiepilepsy_medicine_id": antiepilepsy_medicine_DIFF_ORG.id}, + ) + ) + + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested antiepilepsy_medicine page ({URL}) of case from {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()} Expected 403 response status code, received {response.status_code}" + + +@pytest.mark.parametrize( + "URL", [("edit_comorbidity"), ("close_comorbidity"), ("comorbidities")] +) +@pytest.mark.django_db +def test_comborbidity_view_permissions_success(client, URL): + """ + Assert these users CAN view comorbidities for Case from their own Trust. + + RCPCH Audit Team have additional test to assert can view comborbiditys outside own Trust. + """ + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + CASE_FROM_SAME_ORG = Case.objects.get( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}" + ) + + COMORBIDITY_SAME_ORG = Comorbidity.objects.create( + multiaxial_diagnosis=CASE_FROM_SAME_ORG.registration.multiaxialdiagnosis, + comorbidityentity=ComorbidityEntity.objects.filter( + conceptId="1148757008" + ).first(), + ) + + users = Epilepsy12User.objects.all() + + for test_user in users: + client.force_login(test_user) + + # Get response object + if URL == "comorbidities": + response = client.get( + reverse( + URL, + kwargs={ + "multiaxial_diagnosis_id": CASE_FROM_SAME_ORG.registration.multiaxialdiagnosis.id + }, + ) + ) + else: + response = client.get( + reverse( + URL, + kwargs={"comorbidity_id": COMORBIDITY_SAME_ORG.id}, + ) + ) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested comborbidity page of user from {CASE_FROM_SAME_ORG.organisations.all()}. Has groups: {test_user.groups.all()} Expected 200 response status code, received {response.status_code}" + + # Additional test to RCPCH AUDIT TEAM / Clinical Audit Team who should be able to view nationally + if test_user.first_name in [ + test_user_rcpch_audit_team_data.role_str, + test_user_clinicial_audit_team_data.role_str, + ]: + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + CASE_FROM_DIFF_ORG = Case.objects.get( + first_name=f"child_{DIFF_TRUST_DIFF_ORGANISATION.OrganisationName}" + ) + + comborbidity_DIFF_ORG = Comorbidity.objects.create( + multiaxial_diagnosis=CASE_FROM_DIFF_ORG.registration.multiaxialdiagnosis, + comorbidityentity=ComorbidityEntity.objects.filter( + conceptId="1148757008" + ).first(), + ) + + # Request e12 patients list endpoint url different org + if URL == "comorbidities": + response = client.get( + reverse( + URL, + kwargs={ + "multiaxial_diagnosis_id": CASE_FROM_DIFF_ORG.registration.multiaxialdiagnosis.id + }, + ) + ) + else: + response = client.get( + reverse( + URL, + kwargs={"comorbidity_id": comborbidity_DIFF_ORG.id}, + ) + ) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested comborbidity page of {CASE_FROM_DIFF_ORG.organisations.all()}. Has groups: {test_user.groups.all()} Expected 200 response status code, received {response.status_code}" + + +@pytest.mark.parametrize( + "URL", [("edit_comorbidity"), ("close_comorbidity"), ("comorbidities")] +) +@pytest.mark.django_db +def test_comborbidity_view_permissions_forbidden(client, URL): + """ + Assert these users CANT view comborbidity for Case from different Trust. + """ + + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + CASE_FROM_DIFF_ORG = Case.objects.get( + first_name=f"child_{DIFF_TRUST_DIFF_ORGANISATION.OrganisationName}" + ) + + COMORBIDITY_DIFF_ORG = Comorbidity.objects.create( + multiaxial_diagnosis=CASE_FROM_DIFF_ORG.registration.multiaxialdiagnosis, + comorbidityentity=ComorbidityEntity.objects.filter( + conceptId="1148757008" + ).first(), + ) + + # RCPCH/CLINCAL AUDIT TEAM HAVE FULL ACCESS SO DONT INCLUDE + users = Epilepsy12User.objects.filter( + first_name__in=[ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str + ] + ) + + for test_user in users: + client.force_login(test_user) + + # Get response object + if URL == "comorbidities": + response = client.get( + reverse( + URL, + kwargs={ + "multiaxial_diagnosis_id": CASE_FROM_DIFF_ORG.registration.multiaxialdiagnosis.id + }, + ) + ) + else: + response = client.get( + reverse( + URL, + kwargs={"comorbidity_id": COMORBIDITY_DIFF_ORG.id}, + ) + ) + + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested comorbidity page of case from {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()} Expected 403 response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_multiple_views_permissions_success(client): + """ + Assert these users CAN view the following pages for their own Trust: + + - first_paediatric_assessment + - assessment + - management + - investigations + - epilepsy_context + - multiaxial_diagnosis + + RCPCH Audit Team has additional test to assert can view assessment outside own Trust. + """ + + # GOSH + TEST_USER_ORGANISATION = Organisation.objects.get( + ODSCode="RP401", + ParentOrganisation_ODSCode="RP4", + ) + CASE_FROM_SAME_ORG = Case.objects.get( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}" + ) + + users = Epilepsy12User.objects.all() + + for test_user in users: + client.force_login(test_user) + + for url_name in [ + "assessment", + "investigations", + "management", + "first_paediatric_assessment", + "epilepsy_context", + "multiaxial_diagnosis", + ]: + # Get response object + response = client.get( + reverse( + url_name, + kwargs={"case_id": CASE_FROM_SAME_ORG.id}, + ) + ) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested {url_name} page of user from {TEST_USER_ORGANISATION}. Has groups: {test_user.groups.all()} Expected 200 response status code, received {response.status_code}" + + # Additional test to RCPCH AUDIT TEAM / Clinical Audit Team who should be able to view nationally + if test_user.first_name in [ + test_user_rcpch_audit_team_data.role_str, + test_user_clinicial_audit_team_data.role_str, + ]: + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + CASE_FROM_DIFF_ORG = Case.objects.get( + first_name=f"child_{TEST_USER_ORGANISATION.OrganisationName}" + ) + + # Request e12 patients list endpoint url different org + response = client.get( + reverse( + url_name, + kwargs={"case_id": CASE_FROM_DIFF_ORG.id}, + ) + ) + + assert ( + response.status_code == HTTPStatus.OK + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested {url_name} page of {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()} Expected 200 response status code, received {response.status_code}" + + +@pytest.mark.django_db +def test_multiple_views_permissions_forbidden(client): + """ + Assert these users CANT view these pages for different Trust. + + - first_paediatric_assessment + - assessment + - management + - investigations + - epilepsy_context + - multiaxial_diagnosis + """ + + DIFF_TRUST_DIFF_ORGANISATION = Organisation.objects.get( + ODSCode="RGT01", + ParentOrganisation_ODSCode="RGT", + ) + CASE_FROM_DIFF_ORG = Case.objects.get( + first_name=f"child_{DIFF_TRUST_DIFF_ORGANISATION.OrganisationName}" + ) + + # RCPCH/CLINCAL AUDIT TEAM HAVE FULL ACCESS SO DONT INCLUDE + users = Epilepsy12User.objects.filter( + first_name__in=[ + test_user_audit_centre_administrator_data.role_str, + test_user_audit_centre_clinician_data.role_str, + test_user_audit_centre_lead_clinician_data.role_str + ] + ) + + for test_user in users: + client.force_login(test_user) + + for url_name in [ + "assessment", + "investigations", + "management", + "first_paediatric_assessment", + "epilepsy_context", + "multiaxial_diagnosis", + ]: + # Get response object + response = client.get( + reverse( + url_name, + kwargs={"case_id": CASE_FROM_DIFF_ORG.id}, + ) + ) + + assert ( + response.status_code == HTTPStatus.FORBIDDEN + ), f"{test_user.first_name} (from {test_user.organisation_employer}) requested {url_name} page of case from {DIFF_TRUST_DIFF_ORGANISATION}. Has groups: {test_user.groups.all()} Expected 403 response status code, received {response.status_code}" diff --git a/epilepsy12/urls.py b/epilepsy12/urls.py index 6b5fe3ad..e8dfcb9d 100644 --- a/epilepsy12/urls.py +++ b/epilepsy12/urls.py @@ -35,11 +35,6 @@ path("403", views.redirect_403, name="redirect_403"), path("", views.index, name="index"), path("database", views.database, name="database"), - path( - "organisation/", - views.organisation_reports, - name="organisation_reports", - ), path("organisation//cases/", views.case_list, name="cases"), path( "organisation//case//update", @@ -67,6 +62,16 @@ views.opt_out, name="opt_out", ), + path( + "case//consent", + views.consent, + name="consent", + ), + path( + "case//consent//confirm", + views.consent_confirmation, + name="consent_confirmation", + ), path( "organisation//case//submit", views.case_submit, @@ -93,7 +98,7 @@ name="log_list", ), path( - "selected_organisation_summary", + "organisation//summary", views.selected_organisation_summary, name="selected_organisation_summary", ), @@ -673,32 +678,32 @@ ), # initial assessment endpoints path( - "registration//first_paediatric_assessment_in_acute_or_nonacute_setting", + "first_paediatric_assessment//first_paediatric_assessment_in_acute_or_nonacute_setting", views.first_paediatric_assessment_in_acute_or_nonacute_setting, name="first_paediatric_assessment_in_acute_or_nonacute_setting", ), path( - "registration//has_number_of_episodes_since_the_first_been_documented", + "first_paediatric_assessment//has_number_of_episodes_since_the_first_been_documented", views.has_number_of_episodes_since_the_first_been_documented, name="has_number_of_episodes_since_the_first_been_documented", ), path( - "registration//general_examination_performed", + "first_paediatric_assessment//general_examination_performed", views.general_examination_performed, name="general_examination_performed", ), path( - "registration//neurological_examination_performed", + "first_paediatric_assessment//neurological_examination_performed", views.neurological_examination_performed, name="neurological_examination_performed", ), path( - "registration//developmental_learning_or_schooling_problems", + "first_paediatric_assessment//developmental_learning_or_schooling_problems", views.developmental_learning_or_schooling_problems, name="developmental_learning_or_schooling_problems", ), path( - "registration//behavioural_or_emotional_problems", + "first_paediatric_assessment//behavioural_or_emotional_problems", views.behavioural_or_emotional_problems, name="behavioural_or_emotional_problems", ), diff --git a/epilepsy12/validators.py b/epilepsy12/validators.py new file mode 100644 index 00000000..0c41be15 --- /dev/null +++ b/epilepsy12/validators.py @@ -0,0 +1,89 @@ +# python / django imports +from datetime import date +from django.core.exceptions import ValidationError + +# RCPCH imports +from .general_functions.nhs_number import validate_nhs_number + + +def nhs_number_validator(number_to_validate): + """ + Validates an NHS number + """ + validation = validate_nhs_number(number_to_validate=number_to_validate) + if validation["valid"]: + return number_to_validate + else: + raise ValidationError(validation["message"]) + + +def epilepsy12_date_validator( + first_date: date = None, + second_date: date = None, + earliest_allowable_date: date = None, +) -> bool: + """ + Validator for Epilepsy12 dates + Base validation is that: + 1. At least one date must be supplied + 2. No date may be in the future + 3. No date may be before the earliest allowable date + 4. if supplied, second_date must be after first_date + """ + + if first_date and second_date: + # both dates supplied + if first_date > date.today() or second_date > date.today(): + raise ValueError("Neither date can be in the future!") + if earliest_allowable_date: + if earliest_allowable_date > first_date: + raise ValueError( + f"Date supplied ({first_date}) cannot be before {earliest_allowable_date}!" + ) + elif earliest_allowable_date > second_date: + raise ValueError( + f"Date supplied ({second_date}) cannot be before {earliest_allowable_date}!" + ) + if second_date < first_date: + raise ValueError( + f"Date supplied ({second_date}) cannot be before {first_date}!" + ) + return True + + elif first_date: + # only first_date supplied + if first_date > date.today(): + raise ValueError( + f"Date supplied ({first_date}) cannot be in the future for this measure!" + ) + if earliest_allowable_date: + if earliest_allowable_date > first_date: + raise ValueError( + f"Date supplied ({first_date}) cannot be before {earliest_allowable_date}!" + ) + return True + elif second_date: + # only second_date supplied + if second_date > date.today(): + raise ValueError( + f"Date supplied ({second_date}) cannot be in the future for this measure!" + ) + if earliest_allowable_date: + if earliest_allowable_date > second_date: + raise ValueError( + f"Date supplied ({second_date}) cannot be before {earliest_allowable_date}!" + ) + return True + else: + # no dates supplied + raise ValueError("At least one date must be supplied!") + + +def not_in_the_future_validator(value): + """ + model level validator to prevent persisting a date in the future + """ + if value <= date.today(): + return value + else: + raise ValidationError("Dates cannot be in the future.") diff --git a/epilepsy12/view_folder/assessment_views.py b/epilepsy12/view_folder/assessment_views.py index 4449d2e9..460fec15 100644 --- a/epilepsy12/view_folder/assessment_views.py +++ b/epilepsy12/view_folder/assessment_views.py @@ -1510,7 +1510,7 @@ def epilepsy_specialist_nurse_input_date(request, assessment_id): @login_required -@permission_required("epilepsy12.change_assessment", raise_exception=True) +@permission_required("epilepsy12.view_assessment", raise_exception=True) @user_may_view_this_child() def assessment(request, case_id): case = Case.objects.get(pk=case_id) diff --git a/epilepsy12/view_folder/case_views.py b/epilepsy12/view_folder/case_views.py index 324405ab..115fe52f 100644 --- a/epilepsy12/view_folder/case_views.py +++ b/epilepsy12/view_folder/case_views.py @@ -1,23 +1,24 @@ -from django.utils import timezone +# python imports from datetime import datetime + +# django imports +from django.core.exceptions import PermissionDenied +from django.utils import timezone from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.contrib.auth.decorators import login_required, permission_required from django.contrib.gis.db.models import Q from django.contrib import messages -from epilepsy12.forms import CaseForm -from epilepsy12.models import Organisation, Site, Case -from django.contrib import messages from django.core.paginator import Paginator + +# third party imports from django_htmx.http import trigger_client_event, HttpResponseClientRedirect + +# RCPCH imports +from epilepsy12.forms import CaseForm +from epilepsy12.models import Organisation, Site, Case, AuditProgress from ..constants import ( UNKNOWN_POSTCODES_NO_SPACES, - RCPCH_AUDIT_ADMINISTRATOR, - RCPCH_AUDIT_ANALYST, - RCPCH_AUDIT_LEAD, - TRUST_AUDIT_TEAM_EDIT_ACCESS, - TRUST_AUDIT_TEAM_FULL_ACCESS, - TRUST_AUDIT_TEAM_VIEW_ONLY, ) from ..decorator import user_may_view_this_organisation, user_may_view_this_child @@ -253,7 +254,7 @@ def case_list(request, organisation_id): if ( request.user.is_rcpch_audit_team_member - or request.user.is_staff + or request.user.is_rcpch_staff or request.user.is_superuser ): rcpch_choices = ( @@ -341,9 +342,19 @@ def case_submit(request, organisation_id, case_id): """ POST request callback from submit button in case_list partial. Disables further editing of case information. Case considered submitted + There is a specific permission both for locking and unlocking which is tested in this function """ case = Case.objects.get(pk=case_id) - case.locked = not case.locked + if case.locked is True and request.user.has_perm( + "epilepsy12.can_unlock_child_case_data_from_editing" + ): + case.locked = False + elif case.locked is False and request.user.has_perm( + "epilepsy12.can_lock_child_case_data_from_editing" + ): + case.locked = True + else: + raise PermissionDenied() case.save() return HttpResponseClientRedirect( @@ -432,6 +443,8 @@ def create_case(request, organisation_id): messages.error( request=request, message="It was not possible to save the case" ) + print("Invalid data") + print(form.errors) context = { "organisation_id": organisation_id, @@ -468,9 +481,12 @@ def update_case(request, organisation_id, case_id): if request.method == "POST": if ("delete") in request.POST: + if not request.user.has_perm("epilepsy12.delete_case"): + raise PermissionDenied() messages.success(request, f"You successfully deleted {case}'s details") case.delete() return redirect("cases", organisation_id=organisation_id) + form = CaseForm(request.POST, instance=case) if form.is_valid(): obj = form.save() @@ -585,3 +601,95 @@ def opt_out(request, organisation_id, case_id): return HttpResponseClientRedirect( reverse("cases", kwargs={"organisation_id": organisation_id}) ) + + +@login_required +@user_may_view_this_child() +@permission_required( + "epilepsy12.can_consent_to_audit_participation", raise_exception=True +) +def consent(request, case_id): + case = Case.objects.get(pk=case_id) + site = Site.objects.filter( + site_is_actively_involved_in_epilepsy_care=True, + site_is_primary_centre_of_epilepsy_care=True, + case=case, + ).get() + organisation_id = site.organisation.pk + + context = { + "case": case, + "case_id": case.pk, + "active_template": "consent", + "audit_progress": case.registration.audit_progress, + "organisation_id": organisation_id, + } + + template = "epilepsy12/consent.html" + + response = render(request=request, template_name=template, context=context) + + # trigger a GET request from the steps template + trigger_client_event( + response=response, name="registration_active", params={} + ) # reloads the form to show the active steps + + return response + + +@login_required +@user_may_view_this_child() +@permission_required( + "epilepsy12.can_consent_to_audit_participation", raise_exception=True +) +def consent_confirmation(request, case_id, consent_type): + """ + POST request on click of confirm button in patient_confirmation.html template + params: consent_type is one of 'consent', 'denied' + """ + case = Case.objects.get(pk=case_id) + site = Site.objects.filter( + site_is_actively_involved_in_epilepsy_care=True, + site_is_primary_centre_of_epilepsy_care=True, + case=case, + ).get() + organisation_id = site.organisation.pk + has_error = False + + if consent_type == "consent": + AuditProgress.objects.filter(pk=case.registration.audit_progress.pk).update( + consent_patient_confirmed=True + ) + case = Case.objects.get(pk=case_id) + elif consent_type == "denied": + AuditProgress.objects.filter(pk=case.registration.audit_progress.pk).update( + consent_patient_confirmed=False + ) + case = Case.objects.get(pk=case_id) + else: + # an error occurred + has_error = True + AuditProgress.objects.filter(pk=case.registration.audit_progress.pk).update( + consent_patient_confirmed=None + ) + case = Case.objects.get(pk=case_id) + + context = { + "case": case, + "case_id": case.pk, + "active_template": "consent", + "audit_progress": case.registration.audit_progress, + "organisation_id": organisation_id, + "has_error": has_error, + } + + template = "epilepsy12/forms/consent_form.html" + + response = render(request=request, template_name=template, context=context) + + # trigger a GET request from the steps template + trigger_client_event( + response=response, name="registration_active", params={} + ) # reloads the form to show the active steps + + return response diff --git a/epilepsy12/view_folder/management_views.py b/epilepsy12/view_folder/management_views.py index 9810600b..b0664310 100644 --- a/epilepsy12/view_folder/management_views.py +++ b/epilepsy12/view_folder/management_views.py @@ -5,10 +5,6 @@ from django.contrib.gis.db.models import Q from django.contrib.auth.decorators import login_required, permission_required -from epilepsy12.constants.medications import ( - ANTIEPILEPSY_MEDICINES, - BENZODIAZEPINE_TYPES, -) from epilepsy12.models import ( Management, Registration, @@ -26,6 +22,7 @@ @login_required @user_may_view_this_child() +@permission_required("epilepsy12.view_management", raise_exception=True) def management(request, case_id): # function called on form load # creates a new management object if one does not exist @@ -45,7 +42,7 @@ def management(request, case_id): antiepilepsy_medicines = AntiEpilepsyMedicine.objects.filter( management=management, is_rescue_medicine=False - ).all() + ).order_by("-antiepilepsy_medicine_start_date") site = Site.objects.filter( site_is_actively_involved_in_epilepsy_care=True, @@ -78,22 +75,13 @@ def management(request, case_id): """ -HTMX fields -There is a function/hx route for each field in the form -Each one is protected by @login_required -@user_may_view_this_child() -Each one updates the record. - - - - Fields relating to rescue medication begin here """ @login_required @user_may_view_this_child() -@permission_required("epilepsy12.change_management", raise_exception=True) +@permission_required("epilepsy12.change_antiepilepsymedicine", raise_exception=True) def has_an_aed_been_given(request, management_id): # HTMX call back from management template # POST request on toggle button click @@ -125,7 +113,7 @@ def has_an_aed_been_given(request, management_id): antiepilepsy_medicines = AntiEpilepsyMedicine.objects.filter( management=management, is_rescue_medicine=False - ) + ).order_by("-antiepilepsy_medicine_start_date") context = { "management": management, @@ -172,10 +160,24 @@ def add_antiepilepsy_medicine(request, management_id, is_rescue_medicine): medicine_entity=None, ) + # get all medicines excluding those already selected, and excluding this medicine + management = antiepilepsy_medicine.management + all_selected_antiepilepsymedicines = ( + AntiEpilepsyMedicine.objects.filter(management=management) + .exclude(pk=antiepilepsy_medicine.pk) + .values_list("medicine_entity", flat=True) + ) + + choices = ( + MedicineEntity.objects.filter( + is_rescue=antiepilepsy_medicine.is_rescue_medicine + ) + .exclude(pk__in=all_selected_antiepilepsymedicines) + .order_by("medicine_name") + ) + context = { - "choices": MedicineEntity.objects.filter(is_rescue=is_rescue).order_by( - "medicine_name" - ), + "choices": choices, "antiepilepsy_medicine": antiepilepsy_medicine, "management_id": management_id, "is_rescue_medicine": is_rescue, @@ -214,7 +216,7 @@ def remove_antiepilepsy_medicine(request, antiepilepsy_medicine_id): antiepilepsy_medicines = AntiEpilepsyMedicine.objects.filter( management=management, is_rescue_medicine=is_rescue_medicine - ).all() + ).order_by("-antiepilepsy_medicine_start_date") context = { "medicines": antiepilepsy_medicines, @@ -236,7 +238,7 @@ def remove_antiepilepsy_medicine(request, antiepilepsy_medicine_id): @login_required @user_may_view_this_child() -@permission_required("epilepsy12.change_antiepilepsymedicine", raise_exception=True) +@permission_required("epilepsy12.view_antiepilepsymedicine", raise_exception=True) def edit_antiepilepsy_medicine(request, antiepilepsy_medicine_id): """ Call back from onclick of edit button in antiepilepsy_medicine_list partial @@ -247,9 +249,21 @@ def edit_antiepilepsy_medicine(request, antiepilepsy_medicine_id): pk=antiepilepsy_medicine_id ) - choices = MedicineEntity.objects.filter( - is_rescue=antiepilepsy_medicine.is_rescue_medicine - ).order_by("medicine_name") + # get all medicines excluding those already selected, and excluding this medicine + management = antiepilepsy_medicine.management + all_selected_antiepilepsymedicines = ( + AntiEpilepsyMedicine.objects.filter(management=management) + .exclude(pk=antiepilepsy_medicine_id) + .values_list("medicine_entity", flat=True) + ) + + choices = ( + MedicineEntity.objects.filter( + is_rescue=antiepilepsy_medicine.is_rescue_medicine + ) + .exclude(pk__in=all_selected_antiepilepsymedicines) + .order_by("medicine_name") + ) if antiepilepsy_medicine.antiepilepsy_medicine_stop_date: show_end_date = True @@ -301,7 +315,7 @@ def close_antiepilepsy_medicine(request, antiepilepsy_medicine_id): antiepilepsy_medicines = AntiEpilepsyMedicine.objects.filter( management=antiepilepsy_medicine.management, is_rescue_medicine=is_rescue_medicine, - ) + ).order_by("-antiepilepsy_medicine_start_date") context = { "medicines": antiepilepsy_medicines, @@ -334,9 +348,21 @@ def medicine_id(request, antiepilepsy_medicine_id): pk=antiepilepsy_medicine_id ) - choices = MedicineEntity.objects.filter( - is_rescue=antiepilepsy_medicine.is_rescue_medicine - ).order_by("medicine_name") + # get all medicines excluding those already selected, and excluding this medicine + management = antiepilepsy_medicine.management + all_selected_antiepilepsymedicines = ( + AntiEpilepsyMedicine.objects.filter(management=management) + .exclude(pk=antiepilepsy_medicine_id) + .values_list("medicine_entity", flat=True) + ) + + choices = ( + MedicineEntity.objects.filter( + is_rescue=antiepilepsy_medicine.is_rescue_medicine + ) + .exclude(pk__in=all_selected_antiepilepsymedicines) + .order_by("medicine_name") + ) # get id of medicine entity medicine_id = request.POST.get("medicine_id") @@ -450,9 +476,21 @@ def antiepilepsy_medicine_start_date(request, antiepilepsy_medicine_id): pk=antiepilepsy_medicine_id ) - choices = MedicineEntity.objects.filter( - is_rescue=antiepilepsy_medicine.is_rescue_medicine - ).order_by("medicine_name") + # get all medicines excluding those already selected, and excluding this medicine + management = antiepilepsy_medicine.management + all_selected_antiepilepsymedicines = ( + AntiEpilepsyMedicine.objects.filter(management=management) + .exclude(pk=antiepilepsy_medicine_id) + .values_list("medicine_entity", flat=True) + ) + + choices = ( + MedicineEntity.objects.filter( + is_rescue=antiepilepsy_medicine.is_rescue_medicine + ) + .exclude(pk__in=all_selected_antiepilepsymedicines) + .order_by("medicine_name") + ) if antiepilepsy_medicine.antiepilepsy_medicine_stop_date: show_end_date = True @@ -493,9 +531,21 @@ def antiepilepsy_medicine_add_stop_date(request, antiepilepsy_medicine_id): pk=antiepilepsy_medicine_id ) - choices = MedicineEntity.objects.filter( - is_rescue=antiepilepsy_medicine.is_rescue_medicine - ).order_by("medicine_name") + # get all medicines excluding those already selected, and excluding this medicine + management = antiepilepsy_medicine.management + all_selected_antiepilepsymedicines = ( + AntiEpilepsyMedicine.objects.filter(management=management) + .exclude(pk=antiepilepsy_medicine_id) + .values_list("medicine_entity", flat=True) + ) + + choices = ( + MedicineEntity.objects.filter( + is_rescue=antiepilepsy_medicine.is_rescue_medicine + ) + .exclude(pk__in=all_selected_antiepilepsymedicines) + .order_by("medicine_name") + ) context = { "choices": choices, @@ -535,9 +585,21 @@ def antiepilepsy_medicine_remove_stop_date(request, antiepilepsy_medicine_id): antiepilepsy_medicine.antiepilepsy_medicine_stop_date = None antiepilepsy_medicine.save() - choices = MedicineEntity.objects.filter( - is_rescue=antiepilepsy_medicine.is_rescue_medicine - ).order_by("medicine_name") + # get all medicines excluding those already selected, and excluding this medicine + management = antiepilepsy_medicine.management + all_selected_antiepilepsymedicines = ( + AntiEpilepsyMedicine.objects.filter(management=management) + .exclude(pk=antiepilepsy_medicine_id) + .values_list("medicine_entity", flat=True) + ) + + choices = ( + MedicineEntity.objects.filter( + is_rescue=antiepilepsy_medicine.is_rescue_medicine + ) + .exclude(pk__in=all_selected_antiepilepsymedicines) + .order_by("medicine_name") + ) context = { "choices": choices, @@ -589,8 +651,20 @@ def antiepilepsy_medicine_stop_date(request, antiepilepsy_medicine_id): pk=antiepilepsy_medicine_id ) - choices = MedicineEntity.objects.filter( - is_rescue=antiepilepsy_medicine.is_rescue_medicine + # get all medicines excluding those already selected, and excluding this medicine + management = antiepilepsy_medicine.management + all_selected_antiepilepsymedicines = ( + AntiEpilepsyMedicine.objects.filter(management=management) + .exclude(pk=antiepilepsy_medicine_id) + .values_list("medicine_entity", flat=True) + ) + + choices = ( + MedicineEntity.objects.filter( + is_rescue=antiepilepsy_medicine.is_rescue_medicine + ) + .exclude(pk__in=all_selected_antiepilepsymedicines) + .order_by("medicine_name") ) context = { @@ -637,8 +711,20 @@ def antiepilepsy_medicine_risk_discussed(request, antiepilepsy_medicine_id): pk=antiepilepsy_medicine_id ) - choices = MedicineEntity.objects.filter( - is_rescue=antiepilepsy_medicine.is_rescue_medicine + # get all medicines excluding those already selected, and excluding this medicine + management = antiepilepsy_medicine.management + all_selected_antiepilepsymedicines = ( + AntiEpilepsyMedicine.objects.filter(management=management) + .exclude(pk=antiepilepsy_medicine_id) + .values_list("medicine_entity", flat=True) + ) + + choices = ( + MedicineEntity.objects.filter( + is_rescue=antiepilepsy_medicine.is_rescue_medicine + ) + .exclude(pk__in=all_selected_antiepilepsymedicines) + .order_by("medicine_name") ) if antiepilepsy_medicine.antiepilepsy_medicine_stop_date: @@ -690,8 +776,20 @@ def is_a_pregnancy_prevention_programme_in_place(request, antiepilepsy_medicine_ pk=antiepilepsy_medicine_id ) - choices = MedicineEntity.objects.filter( - is_rescue=antiepilepsy_medicine.is_rescue_medicine + # get all medicines excluding those already selected, and excluding this medicine + management = antiepilepsy_medicine.management + all_selected_antiepilepsymedicines = ( + AntiEpilepsyMedicine.objects.filter(management=management) + .exclude(pk=antiepilepsy_medicine_id) + .values_list("medicine_entity", flat=True) + ) + + choices = ( + MedicineEntity.objects.filter( + is_rescue=antiepilepsy_medicine.is_rescue_medicine + ) + .exclude(pk__in=all_selected_antiepilepsymedicines) + .order_by("medicine_name") ) if antiepilepsy_medicine.antiepilepsy_medicine_stop_date: @@ -745,8 +843,20 @@ def has_a_valproate_annual_risk_acknowledgement_form_been_completed( pk=antiepilepsy_medicine_id ) - choices = MedicineEntity.objects.filter( - is_rescue=antiepilepsy_medicine.is_rescue_medicine + # get all medicines excluding those already selected, and excluding this medicine + management = antiepilepsy_medicine.management + all_selected_antiepilepsymedicines = ( + AntiEpilepsyMedicine.objects.filter(management=management) + .exclude(pk=antiepilepsy_medicine_id) + .values_list("medicine_entity", flat=True) + ) + + choices = ( + MedicineEntity.objects.filter( + is_rescue=antiepilepsy_medicine.is_rescue_medicine + ) + .exclude(pk__in=all_selected_antiepilepsymedicines) + .order_by("medicine_name") ) if antiepilepsy_medicine.antiepilepsy_medicine_stop_date: @@ -843,7 +953,7 @@ def has_rescue_medication_been_prescribed(request, management_id): @login_required @user_may_view_this_child() -@permission_required("epilepsy12.change_antiepilepsymedicine", raise_exception=True) +@permission_required("epilepsy12.change_management", raise_exception=True) def individualised_care_plan_in_place(request, management_id): """ This is an HTMX callback from the individualised_care_plan partial template @@ -920,6 +1030,8 @@ def individualised_care_plan_date(request, management_id): field_name="individualised_care_plan_date", page_element="date_field", earliest_allowable_date=management.registration.registration_date, + comparison_date_field_name=None, + is_earliest_date=None, ) except ValueError as error: error_message = error diff --git a/epilepsy12/view_folder/multiaxial_diagnosis_views.py b/epilepsy12/view_folder/multiaxial_diagnosis_views.py index 5d0dad7e..371618e4 100644 --- a/epilepsy12/view_folder/multiaxial_diagnosis_views.py +++ b/epilepsy12/view_folder/multiaxial_diagnosis_views.py @@ -104,11 +104,13 @@ def multiaxial_diagnosis(request, case_id): multiaxial_diagnosis=multiaxial_diagnosis, epilepsy_or_nonepilepsy_status="E" ).exists() - syndromes = Syndrome.objects.filter(multiaxial_diagnosis=multiaxial_diagnosis).all() + syndromes = Syndrome.objects.filter( + multiaxial_diagnosis=multiaxial_diagnosis + ).order_by("-syndrome_diagnosis_date") comorbidities = Comorbidity.objects.filter( multiaxial_diagnosis=multiaxial_diagnosis - ).all() + ).order_by("-comorbidity_diagnosis_date") keyword_choices = Keyword.objects.all() @@ -246,7 +248,7 @@ def add_episode(request, multiaxial_diagnosis_id): @login_required @user_may_view_this_child() -@permission_required("epilepsy12.change_episode", raise_exception=True) +@permission_required("epilepsy12.view_episode", raise_exception=True) def edit_episode(request, episode_id): """ HTMX post request from episodes.html partial on button click to add new episode @@ -1148,11 +1150,18 @@ def add_syndrome(request, multiaxial_diagnosis_id): syndrome=None, ) - syndrome_selection = SyndromeEntity.objects.all().order_by("syndrome_name") + # create list of syndromesentities, removing already selected items, excluding current + all_selected_syndromes = ( + Syndrome.objects.filter(multiaxial_diagnosis=multiaxial_diagnosis) + .exclude(pk=syndrome.pk) + .values_list("syndrome", flat=True) + ) + syndrome_selection = SyndromeEntity.objects.exclude( + pk__in=all_selected_syndromes + ).order_by("syndrome_name") context = { "syndrome": syndrome, - # sorted(SYNDROMES, key=itemgetter(1)), "syndrome_selection": syndrome_selection, } @@ -1168,7 +1177,7 @@ def add_syndrome(request, multiaxial_diagnosis_id): @login_required @user_may_view_this_child() -@permission_required("epilepsy12.change_syndrome", raise_exception=True) +@permission_required("epilepsy12.view_syndrome", raise_exception=True) def edit_syndrome(request, syndrome_id): """ HTMX post request from episodes.html partial on button click to add new episode @@ -1177,11 +1186,19 @@ def edit_syndrome(request, syndrome_id): keywords = Keyword.objects.all() - syndrome_selection = SyndromeEntity.objects.all().order_by("syndrome_name") + # create list of syndromesentities, removing already selected items, excluding current + multiaxial_diagnosis = syndrome.multiaxial_diagnosis + all_selected_syndromes = ( + Syndrome.objects.filter(multiaxial_diagnosis=multiaxial_diagnosis) + .exclude(pk=syndrome_id) + .values_list("syndrome", flat=True) + ) + syndrome_selection = SyndromeEntity.objects.exclude( + pk__in=all_selected_syndromes + ).order_by("syndrome_name") context = { "syndrome": syndrome, - # sorted(SYNDROMES, key=itemgetter(1)), "syndrome_selection": syndrome_selection, "seizure_onset_date_confidence_selection": DATE_ACCURACY, "episode_definition_selection": EPISODE_DEFINITION, @@ -1232,7 +1249,7 @@ def remove_syndrome(request, syndrome_id): Syndrome.objects.get(pk=syndrome_id).delete() syndromes = Syndrome.objects.filter( multiaxial_diagnosis=multiaxial_diagnosis - ).order_by("syndrome_diagnosis_date") + ).order_by("-syndrome_diagnosis_date") context = {"multiaxial_diagnosis": multiaxial_diagnosis, "syndromes": syndromes} @@ -1263,7 +1280,7 @@ def close_syndrome(request, syndrome_id): syndromes = Syndrome.objects.filter( multiaxial_diagnosis=multiaxial_diagnosis - ).order_by("syndrome_diagnosis_date") + ).order_by("-syndrome_diagnosis_date") context = {"multiaxial_diagnosis": multiaxial_diagnosis, "syndromes": syndromes} @@ -1302,7 +1319,9 @@ def syndrome_present(request, multiaxial_diagnosis_id): print("Some mistake happened") # TODO need to handle this - syndromes = Syndrome.objects.filter(multiaxial_diagnosis=multiaxial_diagnosis).all() + syndromes = Syndrome.objects.filter( + multiaxial_diagnosis=multiaxial_diagnosis + ).order_by("-syndrome_diagnosis_date") context = {"multiaxial_diagnosis": multiaxial_diagnosis, "syndromes": syndromes} @@ -1433,10 +1452,9 @@ def epilepsy_cause_categories(request, multiaxial_diagnosis_id): multiaxial_diagnosis.save() else: - print( + raise ValueError( f"category is {epilepsy_cause_category}. This is an error that needs handling" ) - # TODO handle this error context = { "epilepsy_cause_selection": EPILEPSY_CAUSES, @@ -1493,7 +1511,7 @@ def relevant_impairments_behavioural_educational(request, multiaxial_diagnosis_i "multiaxial_diagnosis": multiaxial_diagnosis, "comorbidities": Comorbidity.objects.filter( multiaxial_diagnosis=multiaxial_diagnosis - ).all(), + ).order_by("-comorbidity_diagnosis_date"), } response = recalculate_form_generate_response( @@ -1523,12 +1541,21 @@ def add_comorbidity(request, multiaxial_diagnosis_id): comorbidityentity=comorbidityentity, ) - comorbidity = Comorbidity.objects.get(pk=comorbidity.pk) - comorbidity_choices = ComorbidityEntity.objects.filter( - pk__in=Subquery( - ComorbidityEntity.objects.all().distinct("conceptId").values("pk") + # get a list of comorbidityentities for select, excluding that already chosen + multiaxial_diagnosis = comorbidity.multiaxial_diagnosis + all_selected_comorbidityentities = Comorbidity.objects.filter( + multiaxial_diagnosis=multiaxial_diagnosis + ).values_list("comorbidityentity", flat=True) + + comorbidity_choices = ( + ComorbidityEntity.objects.filter( + pk__in=Subquery( + ComorbidityEntity.objects.all().distinct("conceptId").values("pk") + ) ) - ).order_by("preferredTerm") + .exclude(pk__in=all_selected_comorbidityentities) + .order_by("preferredTerm") + ) context = {"comorbidity": comorbidity, "comorbidity_choices": comorbidity_choices} @@ -1544,25 +1571,31 @@ def add_comorbidity(request, multiaxial_diagnosis_id): @login_required @user_may_view_this_child() -@permission_required("epilepsy12.change_comorbidity", raise_exception=True) +@permission_required("epilepsy12.view_comorbidity", raise_exception=True) def edit_comorbidity(request, comorbidity_id): """ POST request from comorbidities.html partial on button click to edit episode """ + # get a list of comorbidityentities for select, excluding those already chosen comorbidity = Comorbidity.objects.get(pk=comorbidity_id) - comorbidity_choices = ComorbidityEntity.objects.filter( - pk__in=Subquery( - ComorbidityEntity.objects.all().distinct("conceptId").values("pk") + multiaxial_diagnosis = comorbidity.multiaxial_diagnosis + all_selected_comorbidityentities = ( + Comorbidity.objects.filter(multiaxial_diagnosis=multiaxial_diagnosis) + .exclude(pk=comorbidity_id) + .values_list("comorbidityentity", flat=True) + ) + + comorbidity_choices = ( + ComorbidityEntity.objects.filter( + pk__in=Subquery( + ComorbidityEntity.objects.all().distinct("conceptId").values("pk") + ) ) - ).order_by("preferredTerm") - # ecl = '<< 35919005' - # comorbidity_choices = fetch_ecl(ecl) + .exclude(pk__in=all_selected_comorbidityentities) + .order_by("preferredTerm") + ) - context = { - "comorbidity": comorbidity, - "comorbidity_choices": comorbidity_choices - # 'comorbidity_choices': sorted(comorbidity_choices, key=itemgetter('preferredTerm')), - } + context = {"comorbidity": comorbidity, "comorbidity_choices": comorbidity_choices} response = recalculate_form_generate_response( model_instance=comorbidity.multiaxial_diagnosis, @@ -1582,8 +1615,10 @@ def remove_comorbidity(request, comorbidity_id): POST request from comorbidities.html partial on button click to edit episode """ comorbidity = Comorbidity.objects.get(pk=comorbidity_id) - multiaxial_diagnosis = comorbidity.multiaxial_diagnosis - Comorbidity.objects.filter(pk=comorbidity_id).delete() + multiaxial_diagnosis_id = comorbidity.multiaxial_diagnosis.pk + comorbidity.delete() + # requery multiaxial diagnosis now comorbidity removed + multiaxial_diagnosis = MultiaxialDiagnosis.objects.get(pk=multiaxial_diagnosis_id) comorbidities = Comorbidity.objects.filter( multiaxial_diagnosis=multiaxial_diagnosis @@ -1621,7 +1656,7 @@ def close_comorbidity(request, comorbidity_id): comorbidities = Comorbidity.objects.filter( multiaxial_diagnosis=multiaxial_diagnosis - ).order_by("comorbidity_diagnosis_date") + ).order_by("-comorbidity_diagnosis_date") context = { "multiaxial_diagnosis": multiaxial_diagnosis, @@ -1660,21 +1695,27 @@ def comorbidity_diagnosis_date(request, comorbidity_id): except ValueError as error: error_message = error + # get a list of all comorbidityentities for select, excluding those already chosen + # except current comorbidity selection comorbidity = Comorbidity.objects.get(pk=comorbidity_id) - comorbidity_choices = ComorbidityEntity.objects.filter( - pk__in=Subquery( - ComorbidityEntity.objects.all().distinct("conceptId").values("pk") - ) - ).order_by("preferredTerm") + multiaxial_diagnosis = comorbidity.multiaxial_diagnosis + all_selected_comorbidityentities = ( + Comorbidity.objects.filter(multiaxial_diagnosis=multiaxial_diagnosis) + .exclude(pk=comorbidity_id) + .values_list("comorbidityentity", flat=True) + ) - # ecl = '<< 35919005' - # comorbidity_choices = fetch_ecl(ecl) + comorbidity_choices = ( + ComorbidityEntity.objects.filter( + pk__in=Subquery( + ComorbidityEntity.objects.all().distinct("conceptId").values("pk") + ) + ) + .exclude(pk__in=all_selected_comorbidityentities) + .order_by("preferredTerm") + ) - context = { - "comorbidity": comorbidity, - "comorbidity_choices": comorbidity_choices - # 'comorbidity_choices': sorted(comorbidity_choices, key=itemgetter('preferredTerm')), - } + context = {"comorbidity": comorbidity, "comorbidity_choices": comorbidity_choices} response = recalculate_form_generate_response( model_instance=comorbidity.multiaxial_diagnosis, @@ -1710,20 +1751,28 @@ def comorbidity_diagnosis(request, comorbidity_id): except ValueError as error: error_message = error - comorbidity_choices = ComorbidityEntity.objects.filter( - pk__in=Subquery( - ComorbidityEntity.objects.all().distinct("conceptId").values("pk") - ) - ).order_by("preferredTerm") - - # ecl = '<< 35919005' - # comorbidity_choices = fetch_ecl(ecl) - + # get a list of all comorbidityentities for select, excluding those already chosen + # except current comorbidity selection comorbidity = Comorbidity.objects.get(pk=comorbidity_id) + multiaxial_diagnosis = comorbidity.multiaxial_diagnosis + all_selected_comorbidityentities = ( + Comorbidity.objects.filter(multiaxial_diagnosis=multiaxial_diagnosis) + .exclude(pk=comorbidity_id) + .values_list("comorbidityentity", flat=True) + ) + + comorbidity_choices = ( + ComorbidityEntity.objects.filter( + pk__in=Subquery( + ComorbidityEntity.objects.all().distinct("conceptId").values("pk") + ) + ) + .exclude(pk__in=all_selected_comorbidityentities) + .order_by("preferredTerm") + ) context = { "comorbidity_choices": comorbidity_choices, - # 'comorbidity_choices': sorted(comorbidity_choices, key=itemgetter('preferredTerm')), "comorbidity": comorbidity, } @@ -1740,7 +1789,7 @@ def comorbidity_diagnosis(request, comorbidity_id): @login_required @user_may_view_this_child() -@permission_required("epilepsy12.change_comorbidity", raise_exception=True) +@permission_required("epilepsy12.view_comorbidity", raise_exception=True) def comorbidities(request, multiaxial_diagnosis_id): """ POST request from comorbidity partial to replace it with table @@ -1748,7 +1797,7 @@ def comorbidities(request, multiaxial_diagnosis_id): multiaxial_diagnosis = MultiaxialDiagnosis.objects.get(pk=multiaxial_diagnosis_id) comorbidities = Comorbidity.objects.filter( multiaxial_diagnosis=multiaxial_diagnosis - ).all() + ).order_by("-comorbidity_diagnosis_date") context = { "multiaxial_diagnosis": multiaxial_diagnosis, diff --git a/epilepsy12/view_folder/organisation_views.py b/epilepsy12/view_folder/organisation_views.py index ad6a15a2..da83ca5a 100644 --- a/epilepsy12/view_folder/organisation_views.py +++ b/epilepsy12/view_folder/organisation_views.py @@ -1,4 +1,5 @@ # Python/Django imports + from django.contrib.auth.decorators import login_required from django.shortcuts import render from django.urls import reverse @@ -9,12 +10,8 @@ # E12 imports from ..decorator import user_may_view_this_organisation from epilepsy12.constants import INDIVIDUAL_KPI_MEASURES -from epilepsy12.models import ( - Organisation, - KPI, -) +from epilepsy12.models import Organisation, KPI, KPIAggregation from ..common_view_functions import ( - return_selected_organisation, sanction_user, trigger_client_event, cases_aggregated_by_sex, @@ -23,36 +20,51 @@ all_registered_cases_for_cohort_and_abstraction_level, aggregate_all_eligible_kpi_fields, return_all_aggregated_kpis_for_cohort_and_abstraction_level_annotated_by_sublevel, + return_tile_for_region, ) from ..general_functions import ( get_current_cohort_data, value_from_key, calculate_kpi_average, ) +from ..constants import colors + +from ..tasks import ( + aggregate_kpis_for_each_level_of_abstraction_by_organisation_asynchronously, +) @login_required -def organisation_reports(request): +@user_may_view_this_organisation() +def selected_organisation_summary(request, organisation_id): """ This function presents the organisation view - comprising the organisation contact details, a demographic summary of the hospital trust and a table summary of the key performance indicators for that organisation, its parent trust, as well as comparisons at different levels of abstraction (eg nhs region, ICB, OPENUK region and so on) - It returns the organisation.html template - It does not accept a parameter as it is the next page on from index.html, where users are not yet - logged in. It has the login_required decorator. Once the logged in user gains access, they are - presented with their own organisation's details, unless they are an RCPCH staff member not affiliated - with an organisation. If they somehow gain access, have no organisation affiliation but are not an RCPCH - member or a superuser, access is denied + If a POST request from selected_organisation_summary.html on organisation select, it returns epilepsy12/partials/selected_organisation_summary.html + Otherwise it returns the organisation.html template """ - # this function returns the users organisation or the first in list depending on affilation - # or raises a permission error - selected_organisation = return_selected_organisation(user=request.user) + nhsregion_tiles = return_tile_for_region("nhs_region") + icb_tiles = return_tile_for_region("icb") + country_tiles = return_tile_for_region("country") + + if request.POST.get("selected_organisation_summary") is not None: + selected_organisation = Organisation.objects.get( + pk=request.POST.get("selected_organisation_summary") + ) + template_name = "epilepsy12/partials/selected_organisation_summary.html" + else: + # selected_organisation = return_selected_organisation(user=request.user) + selected_organisation = Organisation.objects.get(pk=organisation_id) + template_name = "epilepsy12/organisation.html" + + lhb_tiles = None - template_name = "epilepsy12/organisation.html" + if selected_organisation.ons_region.ons_country.Country_ONS_Name == "Wales": + lhb_tiles = return_tile_for_region("lhb") - # selects the current cohort number and dates cohort_data = get_current_cohort_data() # query to return all completed E12 cases in the current cohort in this organisation @@ -64,6 +76,7 @@ def organisation_reports(request): abstraction_level="organisation", ).count() ) + # query to return all completed E12 cases in the current cohort in this organisation trust count_of_current_cohort_registered_completed_cases_in_this_trust = ( all_registered_cases_for_cohort_and_abstraction_level( @@ -73,8 +86,8 @@ def organisation_reports(request): abstraction_level="trust", ).count() ) - # query to return all cases registered in the current cohort at this organisation - count_of_current_cohort_registered_cases_in_this_organisation = ( + # query to return all cases (including incomplete) registered in the current cohort at this organisation + count_of_all_current_cohort_registered_cases_in_this_organisation = ( all_registered_cases_for_cohort_and_abstraction_level( organisation_instance=selected_organisation, cohort=cohort_data["cohort"], @@ -82,8 +95,8 @@ def organisation_reports(request): abstraction_level="organisation", ).count() ) - # query to return all cases registered in the current cohort at this organisation trust - count_of_current_cohort_registered_cases_in_this_trust = ( + # query to return all cases (including incomplete) registered in the current cohort at this organisation trust + count_of_all_current_cohort_registered_cases_in_this_trust = ( all_registered_cases_for_cohort_and_abstraction_level( organisation_instance=selected_organisation, cohort=cohort_data["cohort"], @@ -93,179 +106,59 @@ def organisation_reports(request): ) if count_of_current_cohort_registered_completed_cases_in_this_organisation > 0: - total_percent_organisation = round( + total_percent_organisation = int( ( count_of_current_cohort_registered_completed_cases_in_this_organisation - / count_of_current_cohort_registered_cases_in_this_organisation + / (count_of_all_current_cohort_registered_cases_in_this_organisation) ) + * 100 ) + else: total_percent_organisation = 0 - if count_of_current_cohort_registered_completed_cases_in_this_organisation > 0: - total_percent_trust = round( + if count_of_current_cohort_registered_completed_cases_in_this_trust > 0: + total_percent_trust = int( ( count_of_current_cohort_registered_completed_cases_in_this_trust - / count_of_current_cohort_registered_cases_in_this_trust + / (count_of_all_current_cohort_registered_cases_in_this_trust) + * 100 ) ) else: total_percent_trust = 0 - return render( - request=request, - template_name=template_name, - context={ - "user": request.user, - "selected_organisation": selected_organisation, - "organisation_list": Organisation.objects.order_by( - "OrganisationName" - ).all(), - "cases_aggregated_by_ethnicity": cases_aggregated_by_ethnicity( - selected_organisation=selected_organisation - ), - "cases_aggregated_by_sex": cases_aggregated_by_sex( - selected_organisation=selected_organisation - ), - "cases_aggregated_by_deprivation": cases_aggregated_by_deprivation_score( - selected_organisation=selected_organisation - ), - "percent_completed_organisation": total_percent_organisation, - "percent_completed_trust": total_percent_trust, - "count_of_current_cohort_registered_cases_in_this_organisation": count_of_current_cohort_registered_cases_in_this_organisation, - "count_of_current_cohort_registered_completed_cases_in_this_organisation": count_of_current_cohort_registered_completed_cases_in_this_trust, - "count_of_current_cohort_registered_cases_in_this_trust": count_of_current_cohort_registered_cases_in_this_trust, - "count_of_current_cohort_registered_completed_cases_in_this_trust": count_of_current_cohort_registered_completed_cases_in_this_trust, - "cohort_data": cohort_data, - # 'all_models': all_models, - "model_list": ( - "allregisteredcases", - "registration", - "firstpaediatricassessment", - "epilepsycontext", - "multiaxialdiagnosis", - "assessment", - "investigations", - "management", - "site", - "case", - "epilepsy12user", - "organisation", - "comorbidity", - "episode", - "syndrome", - "keyword", - ), - "individual_kpi_choices": INDIVIDUAL_KPI_MEASURES, - }, - ) - - -@login_required -def selected_organisation_summary(request): - """ - POST request from selected_organisation_summary.html on organisation select - """ - - selected_organisation = Organisation.objects.get( - pk=request.POST.get("selected_organisation_summary") - ) - - # if logged in user is from different trust and not a superuser or rcpch member, deny access - sanction_user(user=request.user) - - cohort_data = get_current_cohort_data() - - # query to return all completed E12 cases in the current cohort in this organisation - count_of_current_cohort_registered_completed_cases_in_this_organisation = ( - all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=selected_organisation, - cohort=cohort_data["cohort"], - case_complete=True, - abstraction_level="organisation", - ).count() - ) - # query to return all completed E12 cases in the current cohort in this organisation trust - count_of_current_cohort_registered_completed_cases_in_this_trust = ( - all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=selected_organisation, - cohort=cohort_data["cohort"], - case_complete=True, - abstraction_level="trust", - ).count() - ) - # query to return all cases registered in the current cohort at this organisation - count_of_current_cohort_registered_cases_in_this_organisation = ( - all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=selected_organisation, - cohort=cohort_data["cohort"], - case_complete=False, - abstraction_level="organisation", - ).count() - ) - # query to return all cases registered in the current cohort at this organisation trust - count_of_current_cohort_registered_cases_in_this_trust = ( - all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=selected_organisation, - cohort=cohort_data["cohort"], - case_complete=False, - abstraction_level="trust", - ).count() - ) - - if count_of_current_cohort_registered_completed_cases_in_this_organisation > 0: - total_percent_organisation = ( - round( - ( - count_of_current_cohort_registered_cases_in_this_organisation - / count_of_current_cohort_registered_completed_cases_in_this_organisation - ) - ) - * 10 - ) - else: - total_percent_organisation = 0 - - if count_of_current_cohort_registered_completed_cases_in_this_organisation > 0: - total_percent_trust = ( - round( - ( - count_of_current_cohort_registered_cases_in_this_organisation - / count_of_current_cohort_registered_completed_cases_in_this_organisation - ) - ) - * 10 - ) - else: - total_percent_trust = 0 + context = { + "user": request.user, + "selected_organisation": selected_organisation, + "organisation_list": Organisation.objects.order_by("OrganisationName").all(), + "cases_aggregated_by_ethnicity": cases_aggregated_by_ethnicity( + selected_organisation=selected_organisation + ), + "cases_aggregated_by_sex": cases_aggregated_by_sex( + selected_organisation=selected_organisation + ), + "cases_aggregated_by_deprivation": cases_aggregated_by_deprivation_score( + selected_organisation=selected_organisation + ), + "percent_completed_organisation": total_percent_organisation, + "percent_completed_trust": total_percent_trust, + "count_of_all_current_cohort_registered_cases_in_this_organisation": count_of_all_current_cohort_registered_cases_in_this_organisation, + "count_of_current_cohort_registered_completed_cases_in_this_organisation": count_of_current_cohort_registered_completed_cases_in_this_organisation, + "count_of_all_current_cohort_registered_cases_in_this_trust": count_of_all_current_cohort_registered_cases_in_this_trust, + "count_of_current_cohort_registered_completed_cases_in_this_trust": count_of_current_cohort_registered_completed_cases_in_this_trust, + "cohort_data": cohort_data, + "individual_kpi_choices": INDIVIDUAL_KPI_MEASURES, + "nhsregion_tiles": nhsregion_tiles, + "icb_tiles": icb_tiles, + "country_tiles": country_tiles, + "lhb_tiles": lhb_tiles, + } return render( request=request, - template_name="epilepsy12/partials/selected_organisation_summary.html", - context={ - "user": request.user, - "selected_organisation": selected_organisation, - "organisation_list": Organisation.objects.order_by( - "OrganisationName" - ).all(), - "cases_aggregated_by_ethnicity": cases_aggregated_by_ethnicity( - selected_organisation=selected_organisation - ), - "cases_aggregated_by_sex": cases_aggregated_by_sex( - selected_organisation=selected_organisation - ), - "cases_aggregated_by_deprivation": cases_aggregated_by_deprivation_score( - selected_organisation=selected_organisation - ), - "percent_completed_organisation": total_percent_organisation, - "percent_completed_trust": total_percent_trust, - "count_of_current_cohort_registered_cases_in_this_organisation": count_of_current_cohort_registered_cases_in_this_organisation, - "count_of_current_cohort_registered_completed_cases_in_this_organisation": count_of_current_cohort_registered_completed_cases_in_this_organisation, - "count_of_current_cohort_registered_cases_in_this_trust": count_of_current_cohort_registered_cases_in_this_trust, - "count_of_current_cohort_registered_completed_cases_in_this_trust": count_of_current_cohort_registered_completed_cases_in_this_trust, - "cohort_data": cohort_data, - "individual_kpi_choices": INDIVIDUAL_KPI_MEASURES, - }, + template_name=template_name, + context=context, ) @@ -282,58 +175,34 @@ def selected_trust_kpis(request, organisation_id): """ organisation = Organisation.objects.get(pk=organisation_id) - cohort_data = get_current_cohort_data() - organisation_level = all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=organisation, - cohort=cohort_data["cohort"], - case_complete=True, - abstraction_level="organisation", - ) - trust_level = all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=organisation, - cohort=cohort_data["cohort"], - case_complete=True, - abstraction_level="trust", - ) - icb_level = all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=organisation, - cohort=cohort_data["cohort"], - case_complete=True, - abstraction_level="icb", - ) - nhs_level = all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=organisation, - cohort=cohort_data["cohort"], - case_complete=True, - abstraction_level="nhs_region", - ) - open_uk_level = all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=organisation, - cohort=cohort_data["cohort"], - case_complete=True, - abstraction_level="open_uk", - ) - country_level = all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=organisation, - cohort=cohort_data["cohort"], - case_complete=True, - abstraction_level="country", - ) - national_level = all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=organisation, - cohort=cohort_data["cohort"], - case_complete=True, - abstraction_level="national", - ) - # aggregate at each level of abstraction - organisation_kpis = aggregate_all_eligible_kpi_fields(organisation_level) - trust_kpis = aggregate_all_eligible_kpi_fields(trust_level) - icb_kpis = aggregate_all_eligible_kpi_fields(icb_level) - nhs_kpis = aggregate_all_eligible_kpi_fields(nhs_level) - open_uk_kpis = aggregate_all_eligible_kpi_fields(open_uk_level) - country_kpis = aggregate_all_eligible_kpi_fields(country_level) - national_kpis = aggregate_all_eligible_kpi_fields(national_level) + # run the aggregations + aggregate_kpis_for_each_level_of_abstraction_by_organisation_asynchronously( + organisation_id=organisation.pk, open_access=False + ) + + # get aggregated KPIs for level of abstraction from KPIAggregation + organisation_kpis = KPIAggregation.objects.filter( + organisation=organisation, abstraction_level="organisation" + ).get() + trust_kpis = KPIAggregation.objects.filter( + organisation=organisation, abstraction_level="trust" + ).get() + icb_kpis = KPIAggregation.objects.filter( + organisation=organisation, abstraction_level="icb" + ).get() + nhs_kpis = KPIAggregation.objects.filter( + organisation=organisation, abstraction_level="nhs_region" + ).get() + open_uk_kpis = KPIAggregation.objects.filter( + organisation=organisation, abstraction_level="open_uk" + ).get() + country_kpis = KPIAggregation.objects.filter( + organisation=organisation, abstraction_level="country" + ).get() + national_kpis = KPIAggregation.objects.filter( + organisation=organisation, abstraction_level="national" + ).get() # create an empty instance of KPI model to access the labels - this is a bit of a hack but works and # and has very little overhead @@ -366,6 +235,7 @@ def selected_trust_kpis(request, organisation_id): trigger_client_event( response=response, name="registration_active", params={} ) # reloads the form to show the active steps + return response @@ -374,64 +244,38 @@ def selected_trust_kpis_open(request, organisation_id): Open access endpoint for KPIs table """ - # organisation = Organisation.objects.get(pk=organisation_id) - cohort_data = get_current_cohort_data() - organisation_level = all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=organisation, - cohort=cohort_data["cohort"], - case_complete=True, - abstraction_level="organisation", - ) - trust_level = all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=organisation, - cohort=cohort_data["cohort"], - case_complete=True, - abstraction_level="trust", - ) - icb_level = all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=organisation, - cohort=cohort_data["cohort"], - case_complete=True, - abstraction_level="icb", - ) - nhs_level = all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=organisation, - cohort=cohort_data["cohort"], - case_complete=True, - abstraction_level="nhs_region", - ) - open_uk_level = all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=organisation, - cohort=cohort_data["cohort"], - case_complete=True, - abstraction_level="open_uk", - ) - country_level = all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=organisation, - cohort=cohort_data["cohort"], - case_complete=True, - abstraction_level="country", - ) - national_level = all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=organisation, - cohort=cohort_data["cohort"], - case_complete=True, - abstraction_level="national", - ) - # aggregate at each level of abstraction - organisation_kpis = aggregate_all_eligible_kpi_fields(organisation_level) - trust_kpis = aggregate_all_eligible_kpi_fields(trust_level) - icb_kpis = aggregate_all_eligible_kpi_fields(icb_level) - nhs_kpis = aggregate_all_eligible_kpi_fields(nhs_level) - open_uk_kpis = aggregate_all_eligible_kpi_fields(open_uk_level) - country_kpis = aggregate_all_eligible_kpi_fields(country_level) - national_kpis = aggregate_all_eligible_kpi_fields(national_level) + # run the aggregations TODO This will need ultimately throttling to run only periodically + aggregate_kpis_for_each_level_of_abstraction_by_organisation_asynchronously( + organisation_id=organisation.pk, open_access=True + ) + + # get aggregated KPIs for level of abstraction from KPIAggregation + organisation_kpis = KPIAggregation.objects.filter( + organisation=organisation, abstraction_level="organisation" + ).get() + trust_kpis = KPIAggregation.objects.filter( + organisation=organisation, abstraction_level="trust" + ).get() + icb_kpis = KPIAggregation.objects.filter( + organisation=organisation, abstraction_level="icb" + ).get() + nhs_kpis = KPIAggregation.objects.filter( + organisation=organisation, abstraction_level="nhs_region" + ).get() + open_uk_kpis = KPIAggregation.objects.filter( + organisation=organisation, abstraction_level="open_uk" + ).get() + country_kpis = KPIAggregation.objects.filter( + organisation=organisation, abstraction_level="country" + ).get() + national_kpis = KPIAggregation.objects.filter( + organisation=organisation, abstraction_level="national" + ).get() # create an empty instance of KPI model to access the labels - this is a bit of a hack but works and # and has very little overhead - organisation = Organisation.objects.get(pk=organisation_id) kpis = KPI.objects.create( organisation=organisation, parent_trust=organisation.ParentOrganisation_OrganisationName, @@ -496,8 +340,8 @@ def view_preference(request, organisation_id, template_name): ) -@login_required -@user_may_view_this_organisation() +# @login_required +# @user_may_view_this_organisation() def selected_trust_select_kpi(request, organisation_id): """ POST request from dropdown in selected_organisation_summary.html @@ -515,65 +359,31 @@ def selected_trust_select_kpi(request, organisation_id): organisation = Organisation.objects.get(pk=organisation_id) cohort_data = get_current_cohort_data() - organisation_level = all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=organisation, - cohort=cohort_data["cohort"], - case_complete=True, - abstraction_level="organisation", + + # aggregate at each level of abstraction + organisation_kpi = KPIAggregation.objects.filter( + organisation=organisation, abstraction_level="organisation" ) - trust_level = all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=organisation, - cohort=cohort_data["cohort"], - case_complete=True, - abstraction_level="trust", + trust_kpi = KPIAggregation.objects.filter( + organisation=organisation, abstraction_level="trust" ) - icb_level = all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=organisation, - cohort=cohort_data["cohort"], - case_complete=True, - abstraction_level="icb", + icb_kpi = KPIAggregation.objects.filter( + organisation=organisation, abstraction_level="icb" ) - nhs_level = all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=organisation, - cohort=cohort_data["cohort"], - case_complete=True, - abstraction_level="nhs_region", + nhs_kpi = KPIAggregation.objects.filter( + organisation=organisation, abstraction_level="nhs_region" ) - open_uk_level = all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=organisation, - cohort=cohort_data["cohort"], - case_complete=True, - abstraction_level="open_uk", + open_uk_kpi = KPIAggregation.objects.filter( + organisation=organisation, abstraction_level="open_uk" ) - country_level = all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=organisation, - cohort=cohort_data["cohort"], - case_complete=True, - abstraction_level="country", + country_kpi = KPIAggregation.objects.filter( + organisation=organisation, abstraction_level="country" ) - national_level = all_registered_cases_for_cohort_and_abstraction_level( - organisation_instance=organisation, - cohort=cohort_data["cohort"], - case_complete=True, - abstraction_level="national", + national_kpi = KPIAggregation.objects.filter( + organisation=organisation, abstraction_level="national" ) - # aggregate at each level of abstraction - # organisation_level.aggregate(**aggregation_fields) - organisation_kpi = aggregate_all_eligible_kpi_fields(organisation_level, kpi_name) - # trust_level.aggregate(**aggregation_fields) - trust_kpi = aggregate_all_eligible_kpi_fields(trust_level, kpi_name) - # icb_level.aggregate(**aggregation_fields) - icb_kpi = aggregate_all_eligible_kpi_fields(icb_level, kpi_name) - # nhs_level.aggregate(**aggregation_fields) - nhs_kpi = aggregate_all_eligible_kpi_fields(nhs_level, kpi_name) - # open_uk_level.aggregate(**aggregation_fields) - open_uk_kpi = aggregate_all_eligible_kpi_fields(open_uk_level, kpi_name) - # country_level.aggregate(**aggregation_fields) - country_kpi = aggregate_all_eligible_kpi_fields(country_level, kpi_name) - # national_level.aggregate(**aggregation_fields) - national_kpi = aggregate_all_eligible_kpi_fields(national_level, kpi_name) - + # Get kpi totals for this measure annotated by region name all_aggregated_kpis_by_open_uk_region_in_current_cohort = return_all_aggregated_kpis_for_cohort_and_abstraction_level_annotated_by_sublevel( cohort=cohort_data["cohort"], abstraction_level="open_uk", kpi_measure=kpi_name ) @@ -616,22 +426,25 @@ def selected_trust_select_kpi(request, organisation_id): "kpi_name": kpi_name, "kpi_value": kpi_value, "selected_organisation": organisation, - "organisation_kpi": organisation_kpi[kpi_name], - "total_organisation_kpi_cases": organisation_kpi["total_number_of_cases"], - "trust_kpi": trust_kpi[kpi_name], - "total_trust_kpi_cases": trust_kpi["total_number_of_cases"], - "icb_kpi": icb_kpi[kpi_name], - "total_icb_kpi_cases": icb_kpi["total_number_of_cases"], - "nhs_kpi": nhs_kpi[kpi_name], - "total_nhs_kpi_cases": nhs_kpi["total_number_of_cases"], - "open_uk_kpi": open_uk_kpi[kpi_name], - "total_open_uk_kpi_cases": open_uk_kpi["total_number_of_cases"], - "country_kpi": country_kpi[kpi_name], - "total_country_kpi_cases": country_kpi["total_number_of_cases"], - "national_kpi": national_kpi[kpi_name], - "total_national_kpi_cases": national_kpi["total_number_of_cases"], + "organisation_kpi": getattr(organisation_kpi, kpi_name, None), + "total_organisation_kpi_cases": getattr( + organisation_kpi, "total_number_of_cases", None + ), + "trust_kpi": getattr(trust_kpi, kpi_name, None), + "total_trust_kpi_cases": getattr(trust_kpi, "total_number_of_cases", None), + "icb_kpi": getattr(icb_kpi, kpi_name, None), + "total_icb_kpi_cases": getattr(icb_kpi, "total_number_of_cases", None), + "nhs_kpi": getattr(nhs_kpi, kpi_name, None), + "total_nhs_kpi_cases": getattr(nhs_kpi, "total_number_of_cases", None), + "open_uk_kpi": getattr(open_uk_kpi, kpi_name, None), + "total_open_uk_kpi_cases": getattr(open_uk_kpi, "total_number_of_cases", None), + "country_kpi": getattr(country_kpi, kpi_name, None), + "total_country_kpi_cases": getattr(country_kpi, "total_number_of_cases", None), + "national_kpi": getattr(national_kpi, kpi_name, None), + "total_national_kpi_cases": getattr( + national_kpi, "total_number_of_cases", None + ), "open_uk": all_aggregated_kpis_by_open_uk_region_in_current_cohort, - # "open_uk_data_colors": [color for color in all_aggregated_kpis_by_open_uk_region_in_current_cohort], "open_uk_avg": open_uk_avg, "open_uk_title": f"{kpi_value} by OPEN UK Region", "open_uk_id": "open_uk_id", @@ -647,6 +460,12 @@ def selected_trust_select_kpi(request, organisation_id): "country_avg": country_avg, "country_title": f"{kpi_value} by Country", "country_id": "country_id", + # ADD COLOR PER ABSTRACTION + "icb_color": colors.RCPCH_AQUA_GREEN, + "open_uk_color": colors.RCPCH_LIGHT_BLUE, + "nhs_region_color": colors.RCPCH_STRONG_BLUE, + "country_color": colors.RCPCH_DARK_BLUE, + "individual_kpi_choices": INDIVIDUAL_KPI_MEASURES, } template_name = "epilepsy12/partials/organisation/metric.html" diff --git a/epilepsy12/view_folder/registration_views.py b/epilepsy12/view_folder/registration_views.py index cc697f4f..e1ab4773 100644 --- a/epilepsy12/view_folder/registration_views.py +++ b/epilepsy12/view_folder/registration_views.py @@ -39,7 +39,7 @@ @user_may_view_this_child() def register(request, case_id): """ - Called on registration form page load. If first time, creates new Registration objectm KPI object and + Called on registration form page load. If first time, creates new Registration object KPI object and AuditProgress object. Creates a new Site with selected organisation and associates with this case. Returns register.html template. """ @@ -269,7 +269,7 @@ def edit_lead_site(request, registration_id, site_id): "transfer": False, } - template_name = "epilepsy12/partials/registration/lead_site.html" + template_name = "epilepsy12/partials/registration/edit_or_transfer_lead_site.html" response = recalculate_form_generate_response( model_instance=registration, @@ -289,7 +289,8 @@ def edit_lead_site(request, registration_id, site_id): def transfer_lead_site(request, registration_id, site_id): """ POST request from lead_site.html on click of transfer lead centre button - Returns a lead_site partial + Does not update model + Returns a edit_or_transfer_lead_site partial """ registration = Registration.objects.get(pk=registration_id) site = Site.objects.get(pk=site_id) @@ -304,9 +305,11 @@ def transfer_lead_site(request, registration_id, site_id): "transfer": True, } + template_name = "epilepsy12/partials/registration/edit_or_transfer_lead_site.html" + response = render( request=request, - template_name="epilepsy12/partials/registration/lead_site.html", + template_name=template_name, context=context, ) @@ -360,8 +363,8 @@ def update_lead_site(request, registration_id, site_id, update): If the update parameter is 'edit' this updates updating the current site to a new centre. - Transfers trigger an email to the new lead centre lead clinician and the rcpch audit lead - Edits can only be performed by superusers or the RCPCH audit lead - no emails are sent with this option + Transfers trigger an email to the new lead centre lead clinician and the rcpch audit team + Edits can only be performed by superusers or the RCPCH audit team - no emails are sent with this option so it is reserved for editing lead centres centrally in rare situations. Redirects to the cases table @@ -371,6 +374,7 @@ def update_lead_site(request, registration_id, site_id, update): previous_lead_site = Site.objects.get(pk=site_id) if update == "edit": + # no email is sent - this just updates the lead centre new_trust_id = request.POST.get("edit_lead_site") new_organisation = Organisation.objects.get(pk=new_trust_id) Site.objects.filter(pk=site_id).update( @@ -380,31 +384,67 @@ def update_lead_site(request, registration_id, site_id, update): updated_at=timezone.now(), updated_by=request.user, ) + messages.success( + request, + f"{registration.case} has been successfully updated to {new_organisation.ParentOrganisation_OrganisationName}.", + ) elif update == "transfer": new_trust_id = request.POST.get("transfer_lead_site") new_organisation = Organisation.objects.get(pk=new_trust_id) + # update current site record to show nolonger actively involved in care Site.objects.filter(pk=site_id).update( site_is_primary_centre_of_epilepsy_care=True, site_is_actively_involved_in_epilepsy_care=False, updated_at=timezone.now(), updated_by=request.user, ) - Site.objects.create( + # create new record in Site table for child against new centre + if Site.objects.filter( organisation=new_organisation, - site_is_primary_centre_of_epilepsy_care=True, - site_is_actively_involved_in_epilepsy_care=True, - updated_at=timezone.now(), - updated_by=request.user, case=registration.case, - ) + ).exists: + # this new site already cares for this child in some capacity, either past or present + Site.objects.filter( + organisation=new_organisation, + case=registration.case, + ).update( + site_is_primary_centre_of_epilepsy_care=True, + site_is_actively_involved_in_epilepsy_care=True, + ) + else: + Site.objects.create( + organisation=new_organisation, + site_is_primary_centre_of_epilepsy_care=True, + site_is_actively_involved_in_epilepsy_care=True, + updated_at=timezone.now(), + updated_by=request.user, + case=registration.case, + ) + # find clinical lead of organisation to which child transfered + if Epilepsy12User.objects.filter( + organisation_employer=new_organisation + ).exists(): + # send to all RCPCH audit team and clinical lead of new organisation + recipients = Epilepsy12User.objects.filter( + ( + Q(organisation_employer=new_organisation) + & Q(is_active=True) + & Q(role=1) # Audit Centre Lead Clinician + ) + | (Q(is_active=True) & Q(is_rcpch_audit_team_member=True)) + ).all() + else: + # there is no allocated clinical lead. Send to all RCPCH staff + recipients = Epilepsy12User.objects.filter( + Q(is_active=True) & Q(is_rcpch_audit_team_member=True) + ).all() subject = "Epilepsy12 Lead Site Transfer" - recipients = Epilepsy12User.objects.filter(is_active=True, role=4).all() for recipient in recipients: email = construct_transfer_epilepsy12_site_email( request=request, user=recipient, - target_organisation=new_organisation.ParentName, + target_organisation=new_organisation.ParentOrganisation_OrganisationName, child=registration.case, ) try: @@ -421,7 +461,7 @@ def update_lead_site(request, registration_id, site_id, update): messages.success( request, - f"{registration.case} has been successfully transferred to {new_organisation.ParentName}.", + f"{registration.case} has been successfully transferred to {new_organisation.ParentOrganisation_OrganisationName}. Email notifications have been sent to RCPCH and the lead clinicians.", ) return HttpResponseClientRedirect( @@ -539,13 +579,17 @@ def confirm_eligible(request, registration_id): to True and replace the button with the is_eligible partial, a label confirming eligibility. The button will not be shown again. """ - context = {"has_error": False, "message": "Eligibility Criteria Confirmed."} + context = { + "has_error": False, + "message": "Eligibility Criteria Confirmed.", + "is_positive": True, + } try: Registration.objects.update_or_create( pk=registration_id, defaults={"eligibility_criteria_met": True} ) except Exception as error: - context = {"has_error": True, "message": error} + context = {"has_error": True, "message": error, "is_positive": False} registration = Registration.objects.filter(pk=registration_id).get() @@ -573,7 +617,7 @@ def confirm_eligible(request, registration_id): @login_required -# @user_may_view_this_child() +@user_may_view_this_child() @permission_required("epilepsy12.change_registration", raise_exception=True) def registration_status(request, registration_id): registration = Registration.objects.get(pk=registration_id) diff --git a/epilepsy12/view_folder/syndrome_views.py b/epilepsy12/view_folder/syndrome_views.py index 7d14f4ec..5ca181ab 100644 --- a/epilepsy12/view_folder/syndrome_views.py +++ b/epilepsy12/view_folder/syndrome_views.py @@ -2,7 +2,6 @@ from django.contrib.auth.decorators import login_required, permission_required from ..models import Syndrome, SyndromeEntity -from epilepsy12.constants.syndromes import SYNDROMES from ..common_view_functions import ( validate_and_update_model, recalculate_form_generate_response, @@ -34,10 +33,18 @@ def syndrome_diagnosis_date(request, syndrome_id): syndrome = Syndrome.objects.get(pk=syndrome_id) - syndrome_selection = SyndromeEntity.objects.all().order_by("syndrome_name") + # create list of syndromesentities, removing already selected items, excluding current + multiaxial_diagnosis = syndrome.multiaxial_diagnosis + all_selected_syndromes = ( + Syndrome.objects.filter(multiaxial_diagnosis=multiaxial_diagnosis) + .exclude(pk=syndrome_id) + .values_list("syndrome", flat=True) + ) + syndrome_selection = SyndromeEntity.objects.exclude( + pk__in=all_selected_syndromes + ).order_by("syndrome_name") context = { - # sorted(SYNDROMES, key=itemgetter(1)), "syndrome_selection": syndrome_selection, "syndrome": syndrome, } @@ -75,10 +82,18 @@ def syndrome_name(request, syndrome_id): syndrome = Syndrome.objects.get(pk=syndrome_id) - syndrome_selection = SyndromeEntity.objects.all().order_by("syndrome_name") + # create list of syndromesentities, removing already selected items, excluding current + multiaxial_diagnosis = syndrome.multiaxial_diagnosis + all_selected_syndromes = ( + Syndrome.objects.filter(multiaxial_diagnosis=multiaxial_diagnosis) + .exclude(pk=syndrome_id) + .values_list("syndrome", flat=True) + ) + syndrome_selection = SyndromeEntity.objects.exclude( + pk__in=all_selected_syndromes + ).order_by("syndrome_name") context = { - # sorted(SYNDROMES, key=itemgetter(1)), "syndrome_selection": syndrome_selection, "syndrome": syndrome, } diff --git a/epilepsy12/view_folder/user_management_views.py b/epilepsy12/view_folder/user_management_views.py index de916cba..aec21c75 100644 --- a/epilepsy12/view_folder/user_management_views.py +++ b/epilepsy12/view_folder/user_management_views.py @@ -70,7 +70,7 @@ def epilepsy12_user_list(request, organisation_id): # the basic filter filters users based on the selected organisation view # the filter_term_Q filters based on what the user has put in the search box if ( - request.user.is_staff + request.user.is_rcpch_staff or request.user.is_rcpch_audit_team_member or request.user.is_superuser ): @@ -123,7 +123,7 @@ def epilepsy12_user_list(request, organisation_id): raise Exception("No View Preference supplied") if ( - request.user.is_staff + request.user.is_rcpch_staff or request.user.is_rcpch_audit_team_member or request.user.is_superuser ): @@ -217,7 +217,7 @@ def epilepsy12_user_list(request, organisation_id): if ( request.user.is_rcpch_audit_team_member - or request.user.is_staff + or request.user.is_rcpch_staff or request.user.is_superuser ): rcpch_choices = ( @@ -254,7 +254,7 @@ def epilepsy12_user_list(request, organisation_id): @login_required @user_may_view_this_organisation() -@permission_required("epilepsy12.add_epilepsy12user") +@permission_required("epilepsy12.add_epilepsy12user", raise_exception=True) def create_epilepsy12_user(request, organisation_id, user_type): """ Creates an epilepsy12 user. It is called from epilepsy12 list of users @@ -328,7 +328,7 @@ def create_epilepsy12_user(request, organisation_id, user_type): @login_required @user_may_view_this_organisation() -@permission_required("epilepsy12.change_epilepsy12user") +@permission_required("epilepsy12.change_epilepsy12user", raise_exception=True) def edit_epilepsy12_user(request, organisation_id, epilepsy12_user_id): """ Django model form to edit/update Epilepsy12user @@ -338,7 +338,7 @@ def edit_epilepsy12_user(request, organisation_id, epilepsy12_user_id): epilepsy12_user_to_edit = get_object_or_404(Epilepsy12User, pk=epilepsy12_user_id) can_edit = False if ( - request.user.is_staff + request.user.is_rcpch_staff or request.user.organisation_employer == organisation or request.user.is_rcpch_audit_team_member ): @@ -422,7 +422,7 @@ def edit_epilepsy12_user(request, organisation_id, epilepsy12_user_id): template_name = "registration/admin_create_user.html" - if epilepsy12_user_to_edit.is_staff: + if epilepsy12_user_to_edit.is_rcpch_staff: admin_title = "Edit RCPCH Epilepsy12 staff member" user_type = "rcpch-staff" else: @@ -443,7 +443,7 @@ def edit_epilepsy12_user(request, organisation_id, epilepsy12_user_id): @login_required @user_may_view_this_organisation() -@permission_required("epilepsy12.delete_epilepsy12user") +@permission_required("epilepsy12.delete_epilepsy12user", raise_exception=True) def delete_epilepsy12_user(request, organisation_id, epilepsy12_user_id): try: Epilepsy12User.objects.get(pk=epilepsy12_user_id).delete() diff --git a/epilepsy12/views.py b/epilepsy12/views.py index 853971b6..ff02fbba 100644 --- a/epilepsy12/views.py +++ b/epilepsy12/views.py @@ -49,6 +49,13 @@ def epilepsy12_login(request): user = authenticate(request, username=email, password=password) if user is not None: + if user.organisation_employer is not None: + # select the first hospital in the list if no allocated employing hospital + selected_organisation = Organisation.objects.get( + OrganisationName=user.organisation_employer + ) + else: + selected_organisation = Organisation.objects.first() if user.email_confirmed == False: user.email_confirmed = True user.save() @@ -78,7 +85,10 @@ def epilepsy12_login(request): selected_organisation = Organisation.objects.order_by( "OrganisationName" ).first() - return redirect("organisation_reports") + return redirect( + "selected_organisation_summary", + organisation_id=selected_organisation.pk, + ) else: messages.error(request, "Invalid email or password.") else: @@ -95,9 +105,14 @@ def index(request): except the children and families page which requires an organisation id to filter against. An organisation is chosen here at random, but in future might be chosen based on the location of the visitor's ISP. """ - random_organisation = Organisation.objects.order_by("?").first() + if getattr(request.user, "organisation_employer", None) is not None: + organisation = Organisation.objects.get( + OrganisationName=request.user.organisation_employer + ) + else: + organisation = Organisation.objects.order_by("?").first() template_name = "epilepsy12/epilepsy12index.html" - context = {"organisation": random_organisation} + context = {"organisation": organisation} return render(request=request, template_name=template_name, context=context) @@ -156,6 +171,7 @@ def open_access(request, organisation_id): context = { "organisation": organisation, "organisation_list": Organisation.objects.all().order_by("OrganisationName"), + "individual_kpi_choices": INDIVIDUAL_KPI_MEASURES, } return render(request, template_name, context=context) @@ -189,24 +205,29 @@ def signup(request, *args, **kwargs): logged_in_user.is_superuser = False if logged_in_user.role == AUDIT_CENTRE_LEAD_CLINICIAN: group = Group.objects.get(name=TRUST_AUDIT_TEAM_FULL_ACCESS) - logged_in_user.is_staff = True + logged_in_user.is_staff = False + logged_in_user.is_rcpch_staff = False elif logged_in_user.role == AUDIT_CENTRE_CLINICIAN: group = Group.objects.get(name=TRUST_AUDIT_TEAM_EDIT_ACCESS) - logged_in_user.is_staff = True + logged_in_user.is_staff = False + logged_in_user.is_rcpch_staff = False elif logged_in_user.role == AUDIT_CENTRE_ADMINISTRATOR: group = Group.objects.get(name=TRUST_AUDIT_TEAM_EDIT_ACCESS) - logged_in_user.is_staff = True - elif logged_in_user.role == RCPCH_AUDIT_LEAD: + logged_in_user.is_staff = False + logged_in_user.is_rcpch_staff = False + elif logged_in_user.role == RCPCH_AUDIT_TEAM: group = Group.objects.get(name=EPILEPSY12_AUDIT_TEAM_FULL_ACCESS) - elif logged_in_user.role == RCPCH_AUDIT_ANALYST: - group = Group.objects.get(name=EPILEPSY12_AUDIT_TEAM_EDIT_ACCESS) - elif logged_in_user.role == RCPCH_AUDIT_ADMINISTRATOR: - group = Group.objects.get(name=EPILEPSY12_AUDIT_TEAM_VIEW_ONLY) + logged_in_user.is_staff = False + logged_in_user.is_rcpch_staff = True elif logged_in_user.role == RCPCH_AUDIT_PATIENT_FAMILY: group = Group.objects.get(name=PATIENT_ACCESS) + logged_in_user.is_staff = False + logged_in_user.is_rcpch_staff = False else: # no group group = Group.objects.get(name=TRUST_AUDIT_TEAM_VIEW_ONLY) + logged_in_user.is_staff = False + logged_in_user.is_rcpch_staff = False logged_in_user.save() logged_in_user.groups.add(group) @@ -521,17 +542,23 @@ def rcpch_403(request, exception): # from django-htmx middleware forces a redirect. Neat. if request.htmx: redirect = reverse_lazy("redirect_403") - return HttpResponseClientRedirect(redirect) + return HttpResponseClientRedirect(redirect, status=403) else: return render( - request, template_name="epilepsy12/error_pages/rcpch_403.html", context={} + request, + template_name="epilepsy12/error_pages/rcpch_403.html", + context={}, + status=403, ) def redirect_403(request): # return the custom 403 template. There is no context to add. return render( - request, template_name="epilepsy12/error_pages/rcpch_403.html", context={} + request, + template_name="epilepsy12/error_pages/rcpch_403.html", + context={}, + status=403, ) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..559bbb64 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,16 @@ +[pytest] + +# POINT PYTEST AT PROJECT SETTINGS +DJANGO_SETTINGS_MODULE = rcpch-audit-engine.settings + +# SET FILENAME FORMATS OF TESTS +python_files = test_*.py + +# RE USE TEST DB AS DEFAULT +addopts = + --reuse-db + -k "not examples" + +markers = + examples: mark test as workshop-type / example test + seed: mark test as 'meta test', just used for seeding \ No newline at end of file diff --git a/rcpch-audit-engine/git_context_processor.py b/rcpch-audit-engine/git_context_processor.py index 9de8db55..c2c65110 100644 --- a/rcpch-audit-engine/git_context_processor.py +++ b/rcpch-audit-engine/git_context_processor.py @@ -23,7 +23,7 @@ def get_active_branch_and_commit(request): refs_dir = Path(".") / ".git" / "refs" / "heads" / active_git_branch with refs_dir.open("r") as f: latest_git_commit = f.read().splitlines()[0] - print(latest_git_commit) + except FileNotFoundError: latest_git_commit = "[latest commit hash not found]" diff --git a/rcpch-audit-engine/settings.py b/rcpch-audit-engine/settings.py index 387408b9..ed11cd99 100644 --- a/rcpch-audit-engine/settings.py +++ b/rcpch-audit-engine/settings.py @@ -34,144 +34,139 @@ # Need to handle missing ENV var # Need to handle duplicates -ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "").split(",") + ["127.0.0.1", - "localhost", - "0.0.0.0"] +ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "").split(",") + [ + "127.0.0.1", + "localhost", + "0.0.0.0", +] # This is the token required for getting deprivation quintiles from the RCPCH Census Platform -RCPCH_CENSUS_PLATFORM_URL = os.getenv('RCPCH_CENSUS_PLATFORM_URL') -RCPCH_CENSUS_PLATFORM_TOKEN = os.getenv( - "RCPCH_CENSUS_PLATFORM_TOKEN") +RCPCH_CENSUS_PLATFORM_URL = os.getenv("RCPCH_CENSUS_PLATFORM_URL") +RCPCH_CENSUS_PLATFORM_TOKEN = os.getenv("RCPCH_CENSUS_PLATFORM_TOKEN") # this is the url for api.postcodes.io, a free service reporting postcode data off a postcode -POSTCODES_IO_API_URL = os.getenv('POSTCODES_IO_API_URL') +POSTCODES_IO_API_URL = os.getenv("POSTCODES_IO_API_URL") -NHS_ODS_API_URL = os.getenv('NHS_ODS_API_URL') -NHS_ODS_API_KEY = os.getenv('NHS_ODS_API_KEY') +NHS_ODS_API_URL = os.getenv("NHS_ODS_API_URL") +NHS_ODS_API_KEY = os.getenv("NHS_ODS_API_KEY") # SNOMED Terminology server -RCPCH_HERMES_SERVER_URL = os.getenv('RCPCH_HERMES_SERVER_URL') +RCPCH_HERMES_SERVER_URL = os.getenv("RCPCH_HERMES_SERVER_URL") # Application definition INSTALLED_APPS = [ "semantic_admin", "django.contrib.gis", - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.admindocs', - 'rest_framework', - 'whitenoise.runserver_nostatic', - 'django.contrib.staticfiles', - 'epilepsy12', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.admindocs", + "rest_framework", + "whitenoise.runserver_nostatic", + "django.contrib.staticfiles", + "epilepsy12", # third party - 'widget_tweaks', - 'django_htmx', - 'rest_framework.authtoken', - 'simple_history' + "widget_tweaks", + "django_htmx", + "rest_framework.authtoken", + "simple_history", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', + "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django_htmx.middleware.HtmxMiddleware', - 'simple_history.middleware.HistoryRequestMiddleware', - 'django_auto_logout.middleware.auto_logout', + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django_htmx.middleware.HtmxMiddleware", + "simple_history.middleware.HistoryRequestMiddleware", + "django_auto_logout.middleware.auto_logout", ] -ROOT_URLCONF = 'rcpch-audit-engine.urls' +ROOT_URLCONF = "rcpch-audit-engine.urls" # AUTO LOGOUT SESSION EXPIRATION AUTO_LOGOUT = { - 'IDLE_TIME': datetime.timedelta(minutes=30), - 'REDIRECT_TO_LOGIN_IMMEDIATELY': True, - 'MESSAGE': 'You have been automatically logged out as there was no activity for 30 minutes. Please login again to continue.', + "IDLE_TIME": datetime.timedelta(minutes=30), + "REDIRECT_TO_LOGIN_IMMEDIATELY": True, + "MESSAGE": "You have been automatically logged out as there was no activity for 30 minutes. Please login again to continue.", } LOGIN_REDIRECT_URL = "/organisation" LOGOUT_REDIRECT_URL = "/" -LOGIN_URL = '/registration/login/' +LOGIN_URL = "/registration/login/" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [Path(BASE_DIR, 'templates')], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - 'django_auto_logout.context_processors.auto_logout_client', - 'rcpch-audit-engine.git_context_processor.get_active_branch_and_commit', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [Path(BASE_DIR, "templates")], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "django_auto_logout.context_processors.auto_logout_client", + "rcpch-audit-engine.git_context_processor.get_active_branch_and_commit", ] - } + }, }, ] -WSGI_APPLICATION = 'rcpch-audit-engine.wsgi.application' +WSGI_APPLICATION = "rcpch-audit-engine.wsgi.application" # Database # https://docs.djangoproject.com/en/3.2/ref/settings/#databases - -# Database -# https://docs.djangoproject.com/en/3.2/ref/settings/#databases - - DATABASES = { "default": { - 'ENGINE': 'django.contrib.gis.db.backends.postgis', - 'NAME': os.environ.get('E12_POSTGRES_DB_NAME'), - 'USER': os.environ.get('E12_POSTGRES_DB_USER'), - 'PASSWORD': os.environ.get('E12_POSTGRES_DB_PASSWORD'), - 'HOST': os.environ.get('E12_POSTGRES_DB_HOST'), - 'PORT': os.environ.get('E12_POSTGRES_DB_PORT'), + "ENGINE": "django.contrib.gis.db.backends.postgis", + "NAME": os.environ.get("E12_POSTGRES_DB_NAME"), + "USER": os.environ.get("E12_POSTGRES_DB_USER"), + "PASSWORD": os.environ.get("E12_POSTGRES_DB_PASSWORD"), + "HOST": os.environ.get("E12_POSTGRES_DB_HOST"), + "PORT": os.environ.get("E12_POSTGRES_DB_PORT"), } } - # Password validation # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] -AUTH_USER_MODEL = 'epilepsy12.Epilepsy12User' +AUTH_USER_MODEL = "epilepsy12.Epilepsy12User" if DEBUG is True: EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" else: EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" - EMAIL_HOST = os.environ.get('EMAIL_HOST_SERVER') + EMAIL_HOST = os.environ.get("EMAIL_HOST_SERVER") EMAIL_PORT = 587 - EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER') - EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD') + EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER") + EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD") EMAIL_USE_TLS = True - DEFAULT_FROM_EMAIL = 'admin@epilepsy12.tech' + DEFAULT_FROM_EMAIL = "admin@epilepsy12.tech" PASSWORD_RESET_TIMEOUT = 259200 # Default: 259200 (3 days, in seconds) @@ -179,13 +174,14 @@ # Internationalization # https://docs.djangoproject.com/en/3.2/topics/i18n/ -LANGUAGE_CODE = 'en-gb' +LANGUAGE_CODE = "en-gb" -TIME_ZONE = 'Europe/London' +TIME_ZONE = "Europe/London" USE_I18N = True -USE_L10N = True +# The USE_L10N setting is deprecated. Starting with Django 5.0, localized formatting of data will always be enabled. For example Django will display numbers and dates using the format of the current locale. +# USE_L10N = True USE_TZ = True @@ -193,11 +189,11 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.2/howto/static-files/ -STATIC_URL = '/static/' -STATICFILES_DIRS = (str(BASE_DIR.joinpath('static')),) +STATIC_URL = "/static/" +STATICFILES_DIRS = (str(BASE_DIR.joinpath("static")),) STATIC_ROOT = str(BASE_DIR.joinpath("staticfiles")) # STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" -WHITENOISE_ROOT = os.path.join(BASE_DIR, 'static/root') +WHITENOISE_ROOT = os.path.join(BASE_DIR, "static/root") STORAGES = { "staticfiles": { @@ -208,26 +204,53 @@ # Default primary key field type # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" CSRF_TRUSTED_ORIGINS = os.getenv("DJANGO_CSRF_TRUSTED_ORIGINS", "").split(",") AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', # this is default + "django.contrib.auth.backends.ModelBackend", # this is default ) # rest framework settings REST_FRAMEWORK = { - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', - 'PAGE_SIZE': 10, - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework.authentication.SessionAuthentication', - 'rest_framework.authentication.BasicAuthentication', - 'rest_framework.authentication.TokenAuthentication', + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": 10, + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework.authentication.SessionAuthentication", + "rest_framework.authentication.BasicAuthentication", + "rest_framework.authentication.TokenAuthentication", ), - 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.IsAuthenticated', - ] + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.IsAuthenticated", + ], +} + +# Optional Logging for Debugging Purposes (esp with DEBUG=False) + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "filters": {}, + "formatters": { + "django.server": { + "()": "django.utils.log.ServerFormatter", + "format": "[{server_time}] {message}", + "style": "{", + } + }, + "handlers": { + "console": { + "level": "INFO", + "class": "logging.StreamHandler", + } + }, + "loggers": { + "epilepsy12": { + "handlers": ["console"], + "level": "INFO", + }, + }, } # Optional Logging for Debugging Purposes (esp with DEBUG=False) @@ -264,3 +287,4 @@ # }, # } # } + diff --git a/requirements/common-requirements.txt b/requirements/common-requirements.txt index ec8563b1..8c5dc010 100644 --- a/requirements/common-requirements.txt +++ b/requirements/common-requirements.txt @@ -1,18 +1,21 @@ # standard imports -requests python-dateutil +requests # third party imports +dj_database_url django +django-auto-logout +django-htmx +django-semantic-admin +django-simple-history +django-widget-tweaks +djangorestframework docutils +nhs-number psycopg2-binary +pytest-django +pytest-factoryboy rapidfuzz -dj_database_url -django-widget-tweaks -django-htmx whitenoise -django-semantic-admin -djangorestframework -django-simple-history -django-auto-logout -# RCPCH imports \ No newline at end of file +nhs-number diff --git a/requirements/development-requirements.txt b/requirements/development-requirements.txt index 40b96287..424e78ac 100644 --- a/requirements/development-requirements.txt +++ b/requirements/development-requirements.txt @@ -1,14 +1,15 @@ # load common requirements -r common-requirements.txt -# standard imports +# code linting and formatting autopep8 -coverage black -pytest -# third party imports +# testing and code analysis +coverage +pytest-django + +# versioning bump2version -# RCPCH imports diff --git a/s/docker-test b/s/docker-test new file mode 100755 index 00000000..41df84cf --- /dev/null +++ b/s/docker-test @@ -0,0 +1,10 @@ +#!/bin/bash + +# scripts may need to be made executable on some platforms before they can be run +# chmod +x is the command to do this on unixy systems + +# runs tests in the context of the django development server +# by default the --verbose flag is passed to pytest for more detailed output +# all arguments passed to this script are passed to pytest + +docker compose -f docker-compose.dev.yml exec web pytest -v $* diff --git a/static/leaflet.ajax.js b/static/leaflet.ajax.js new file mode 100644 index 00000000..8f5b5d83 --- /dev/null +++ b/static/leaflet.ajax.js @@ -0,0 +1,690 @@ +(function e(t, n, r) { + function s(o, u) { + if (!n[o]) { + if (!t[o]) { + var a = typeof require == "function" && require; + if (!u && a) return a(o, !0); + if (i) return i(o, !0); + var f = new Error("Cannot find module '" + o + "'"); + throw ((f.code = "MODULE_NOT_FOUND"), f); + } + var l = (n[o] = { exports: {} }); + t[o][0].call( + l.exports, + function (e) { + var n = t[o][1][e]; + return s(n ? n : e); + }, + l, + l.exports, + e, + t, + n, + r + ); + } + return n[o].exports; + } + var i = typeof require == "function" && require; + for (var o = 0; o < r.length; o++) s(r[o]); + return s; +})( + { + 1: [ + function (require, module, exports) { + "use strict"; + var immediate = require("immediate"); + + /* istanbul ignore next */ + function INTERNAL() {} + + var handlers = {}; + + var REJECTED = ["REJECTED"]; + var FULFILLED = ["FULFILLED"]; + var PENDING = ["PENDING"]; + + module.exports = exports = Promise; + + function Promise(resolver) { + if (typeof resolver !== "function") { + throw new TypeError("resolver must be a function"); + } + this.state = PENDING; + this.queue = []; + this.outcome = void 0; + if (resolver !== INTERNAL) { + safelyResolveThenable(this, resolver); + } + } + + Promise.prototype["catch"] = function (onRejected) { + return this.then(null, onRejected); + }; + Promise.prototype.then = function (onFulfilled, onRejected) { + if ( + (typeof onFulfilled !== "function" && this.state === FULFILLED) || + (typeof onRejected !== "function" && this.state === REJECTED) + ) { + return this; + } + var promise = new this.constructor(INTERNAL); + if (this.state !== PENDING) { + var resolver = this.state === FULFILLED ? onFulfilled : onRejected; + unwrap(promise, resolver, this.outcome); + } else { + this.queue.push(new QueueItem(promise, onFulfilled, onRejected)); + } + + return promise; + }; + function QueueItem(promise, onFulfilled, onRejected) { + this.promise = promise; + if (typeof onFulfilled === "function") { + this.onFulfilled = onFulfilled; + this.callFulfilled = this.otherCallFulfilled; + } + if (typeof onRejected === "function") { + this.onRejected = onRejected; + this.callRejected = this.otherCallRejected; + } + } + QueueItem.prototype.callFulfilled = function (value) { + handlers.resolve(this.promise, value); + }; + QueueItem.prototype.otherCallFulfilled = function (value) { + unwrap(this.promise, this.onFulfilled, value); + }; + QueueItem.prototype.callRejected = function (value) { + handlers.reject(this.promise, value); + }; + QueueItem.prototype.otherCallRejected = function (value) { + unwrap(this.promise, this.onRejected, value); + }; + + function unwrap(promise, func, value) { + immediate(function () { + var returnValue; + try { + returnValue = func(value); + } catch (e) { + return handlers.reject(promise, e); + } + if (returnValue === promise) { + handlers.reject( + promise, + new TypeError("Cannot resolve promise with itself") + ); + } else { + handlers.resolve(promise, returnValue); + } + }); + } + + handlers.resolve = function (self, value) { + var result = tryCatch(getThen, value); + if (result.status === "error") { + return handlers.reject(self, result.value); + } + var thenable = result.value; + + if (thenable) { + safelyResolveThenable(self, thenable); + } else { + self.state = FULFILLED; + self.outcome = value; + var i = -1; + var len = self.queue.length; + while (++i < len) { + self.queue[i].callFulfilled(value); + } + } + return self; + }; + handlers.reject = function (self, error) { + self.state = REJECTED; + self.outcome = error; + var i = -1; + var len = self.queue.length; + while (++i < len) { + self.queue[i].callRejected(error); + } + return self; + }; + + function getThen(obj) { + // Make sure we only access the accessor once as required by the spec + var then = obj && obj.then; + if (obj && typeof obj === "object" && typeof then === "function") { + return function appyThen() { + then.apply(obj, arguments); + }; + } + } + + function safelyResolveThenable(self, thenable) { + // Either fulfill, reject or reject with error + var called = false; + function onError(value) { + if (called) { + return; + } + called = true; + handlers.reject(self, value); + } + + function onSuccess(value) { + if (called) { + return; + } + called = true; + handlers.resolve(self, value); + } + + function tryToUnwrap() { + thenable(onSuccess, onError); + } + + var result = tryCatch(tryToUnwrap); + if (result.status === "error") { + onError(result.value); + } + } + + function tryCatch(func, value) { + var out = {}; + try { + out.value = func(value); + out.status = "success"; + } catch (e) { + out.status = "error"; + out.value = e; + } + return out; + } + + exports.resolve = resolve; + function resolve(value) { + if (value instanceof this) { + return value; + } + return handlers.resolve(new this(INTERNAL), value); + } + + exports.reject = reject; + function reject(reason) { + var promise = new this(INTERNAL); + return handlers.reject(promise, reason); + } + + exports.all = all; + function all(iterable) { + var self = this; + if (Object.prototype.toString.call(iterable) !== "[object Array]") { + return this.reject(new TypeError("must be an array")); + } + + var len = iterable.length; + var called = false; + if (!len) { + return this.resolve([]); + } + + var values = new Array(len); + var resolved = 0; + var i = -1; + var promise = new this(INTERNAL); + + while (++i < len) { + allResolver(iterable[i], i); + } + return promise; + function allResolver(value, i) { + self.resolve(value).then(resolveFromAll, function (error) { + if (!called) { + called = true; + handlers.reject(promise, error); + } + }); + function resolveFromAll(outValue) { + values[i] = outValue; + if (++resolved === len && !called) { + called = true; + handlers.resolve(promise, values); + } + } + } + } + + exports.race = race; + function race(iterable) { + var self = this; + if (Object.prototype.toString.call(iterable) !== "[object Array]") { + return this.reject(new TypeError("must be an array")); + } + + var len = iterable.length; + var called = false; + if (!len) { + return this.resolve([]); + } + + var i = -1; + var promise = new this(INTERNAL); + + while (++i < len) { + resolver(iterable[i]); + } + return promise; + function resolver(value) { + self.resolve(value).then( + function (response) { + if (!called) { + called = true; + handlers.resolve(promise, response); + } + }, + function (error) { + if (!called) { + called = true; + handlers.reject(promise, error); + } + } + ); + } + } + }, + { immediate: 2 }, + ], + 2: [ + function (require, module, exports) { + (function (global) { + "use strict"; + var Mutation = + global.MutationObserver || global.WebKitMutationObserver; + + var scheduleDrain; + + { + if (Mutation) { + var called = 0; + var observer = new Mutation(nextTick); + var element = global.document.createTextNode(""); + observer.observe(element, { + characterData: true, + }); + scheduleDrain = function () { + element.data = called = ++called % 2; + }; + } else if ( + !global.setImmediate && + typeof global.MessageChannel !== "undefined" + ) { + var channel = new global.MessageChannel(); + channel.port1.onmessage = nextTick; + scheduleDrain = function () { + channel.port2.postMessage(0); + }; + } else if ( + "document" in global && + "onreadystatechange" in global.document.createElement("script") + ) { + scheduleDrain = function () { + // Create a @@ -48,7 +53,12 @@
This site is currently under development.
- + + + {{ redirect_to_login_immediately }} + + + @@ -60,9 +70,8 @@ - - {{ redirect_to_login_immediately }} - + + - + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file diff --git a/templates/epilepsy12/audit_section.html b/templates/epilepsy12/audit_section.html index 220181d4..c5c600ab 100644 --- a/templates/epilepsy12/audit_section.html +++ b/templates/epilepsy12/audit_section.html @@ -14,7 +14,7 @@
- Audit view - {% block audit_section_title %}{% endblock %} + Record View - {% block audit_section_title %}{% endblock %}
{{ registration.case.first_name }} {{ registration.case.surname }} {{ registration.case.nhs_number}} diff --git a/templates/epilepsy12/cases/cases.html b/templates/epilepsy12/cases/cases.html index fa6027ac..e97683f5 100644 --- a/templates/epilepsy12/cases/cases.html +++ b/templates/epilepsy12/cases/cases.html @@ -5,7 +5,7 @@
- Children and young people view + Cohort View
diff --git a/templates/epilepsy12/consent.html b/templates/epilepsy12/consent.html new file mode 100644 index 00000000..1c89bc7c --- /dev/null +++ b/templates/epilepsy12/consent.html @@ -0,0 +1,10 @@ +{% extends './audit_section.html' %} +{% block audit_section_title %} +Consent declaration for {{case}} +{% endblock %} +{% block audit_section_form %} +{% include 'epilepsy12/forms/consent_form.html' with audit_progress=case.registration.audit_progress %} +{% endblock %} +{% block audit_section_instructions %} +The Epilepsy12 key consent form for {{case}}. +{% endblock %} \ No newline at end of file diff --git a/templates/epilepsy12/epilepsy12index.html b/templates/epilepsy12/epilepsy12index.html index a00b5778..98978700 100644 --- a/templates/epilepsy12/epilepsy12index.html +++ b/templates/epilepsy12/epilepsy12index.html @@ -49,7 +49,7 @@

Clinician access

Access Epilepsy12 audit data for your organisation.

- + Select @@ -89,7 +89,7 @@

-

epilepsy12@rcpch.ac.uk

+

{% include 'epilepsy12/partials/contact_email.html' %}

diff --git a/templates/epilepsy12/error_pages/rcpch_403.html b/templates/epilepsy12/error_pages/rcpch_403.html index b9321d15..c2f4de8d 100644 --- a/templates/epilepsy12/error_pages/rcpch_403.html +++ b/templates/epilepsy12/error_pages/rcpch_403.html @@ -1,18 +1,24 @@ {% extends "base.html" %} {% load static %} {% block content %} -
-
-
-
-
+
+
+
+
+
- 403 - Access Denied + 403 - Access Denied
- You are seeing this message because you are trying to access a record where you don't have the right permissions. - This must be frustrating and we are sorry. Unfortunately the only way this can be resolved is if you contact your local Epilepsy 12 administrator or RCPCH on ..... +

You are seeing this message because you are trying to access a record where you don't have + the right permissions.

+ +

This must be frustrating and we are sorry. Unfortunately the only way this can be resolved is + if you contact your local Epilepsy 12 administrator or RCPCH at {% include 'epilepsy12/partials/contact_email.html' %}.

+ + +
@@ -20,4 +26,4 @@
-{% endblock %} \ No newline at end of file + {% endblock %} \ No newline at end of file diff --git a/templates/epilepsy12/forms/case_form.html b/templates/epilepsy12/forms/case_form.html index 0e183e23..75cfb78a 100644 --- a/templates/epilepsy12/forms/case_form.html +++ b/templates/epilepsy12/forms/case_form.html @@ -80,27 +80,27 @@ {% if case and perms.epilepsy.can_opt_out_child_from_inclusion_in_audit %} -
-
- -

- Children, young people and their families can opt out of Epilepsy12 at any - time. Should {{case}} or their family choose to do so, all records - relating to {{case}}'s epilepsy care will be removed from RCPCH servers. -

- +
+
+ +

+ Children, young people and their families can opt out of Epilepsy12 at any + time. Should {{case}} or their family choose to do so, all records + relating to {{case}}'s epilepsy care will be removed from RCPCH servers. +

+ +
-
{% else %} diff --git a/templates/epilepsy12/forms/consent_form.html b/templates/epilepsy12/forms/consent_form.html new file mode 100644 index 00000000..f857885d --- /dev/null +++ b/templates/epilepsy12/forms/consent_form.html @@ -0,0 +1,55 @@ +{% load epilepsy12_template_tags %} + \ No newline at end of file diff --git a/templates/epilepsy12/forms/registration_form.html b/templates/epilepsy12/forms/registration_form.html index a8016c69..ef08fd6e 100644 --- a/templates/epilepsy12/forms/registration_form.html +++ b/templates/epilepsy12/forms/registration_form.html @@ -24,7 +24,7 @@
Exclusion

* paediatric service, or a dedicated paediatric team based in A&E. They should not have been referred or assessed by an adult service/team.

{% if registration.eligibility_criteria_met %} - {% include 'epilepsy12/partials/registration/is_eligible_label.html' with has_error=False message="Eligibility Criteria Confirmed." %} + {% include 'epilepsy12/partials/registration/is_eligible_label.html' with has_error=False message="Eligibility Criteria Confirmed." is_positive=True %} {% else %}

{% if user.is_authenticated %} - {% if not request.user|has_group:"patient_access" %} - - Clinician access - - {%endif%} - {% if request.user|has_group:"patient_access" %} Epilepsy12 audit platform - Documentation + Guidance {% if user.is_superuser %} diff --git a/templates/epilepsy12/open_access.html b/templates/epilepsy12/open_access.html index 2e01407d..c4acaaf6 100644 --- a/templates/epilepsy12/open_access.html +++ b/templates/epilepsy12/open_access.html @@ -9,16 +9,22 @@
This represents all the Key Performance Indicators for the selected hospital
- +
- +
+
+ {% include 'epilepsy12/partials/organisation/individual_metrics.html' with individual_kpi_choices=individual_kpi_choices selected_organisation=organisation %}
{% endblock %} \ No newline at end of file diff --git a/templates/epilepsy12/partials/assessment/consultant_paediatrician.html b/templates/epilepsy12/partials/assessment/consultant_paediatrician.html index 6ebe08b5..8715e312 100644 --- a/templates/epilepsy12/partials/assessment/consultant_paediatrician.html +++ b/templates/epilepsy12/partials/assessment/consultant_paediatrician.html @@ -24,7 +24,7 @@
- +
{% if active_general_paediatric_site %} diff --git a/templates/epilepsy12/partials/assessment/epilepsy_nurse.html b/templates/epilepsy12/partials/assessment/epilepsy_nurse.html index 6b43d71f..c354c30c 100644 --- a/templates/epilepsy12/partials/assessment/epilepsy_nurse.html +++ b/templates/epilepsy12/partials/assessment/epilepsy_nurse.html @@ -1,5 +1,4 @@ - - +{% load epilepsy12_template_tags %}
@@ -18,7 +17,7 @@
- +
{% if error_message %} diff --git a/templates/epilepsy12/partials/assessment/epilepsy_surgery.html b/templates/epilepsy12/partials/assessment/epilepsy_surgery.html index 39c45f27..d35c7290 100644 --- a/templates/epilepsy12/partials/assessment/epilepsy_surgery.html +++ b/templates/epilepsy12/partials/assessment/epilepsy_surgery.html @@ -38,7 +38,7 @@
- +
{% if error_message %} diff --git a/templates/epilepsy12/partials/assessment/paediatric_neurology.html b/templates/epilepsy12/partials/assessment/paediatric_neurology.html index 909d8bb4..62942c67 100644 --- a/templates/epilepsy12/partials/assessment/paediatric_neurology.html +++ b/templates/epilepsy12/partials/assessment/paediatric_neurology.html @@ -16,7 +16,7 @@
- +
{% if error_message %} diff --git a/templates/epilepsy12/partials/case_table.html b/templates/epilepsy12/partials/case_table.html index 7841c26e..5f0e764d 100644 --- a/templates/epilepsy12/partials/case_table.html +++ b/templates/epilepsy12/partials/case_table.html @@ -180,7 +180,7 @@ hx-post="{{hx_post}}" hx_trigger='click' hx-target="#case_table" - {% if case.registration.days_remaining_before_submission == 0 %} + {% if case.registration.days_remaining_before_submission == 0 or case.locked and not perms.epilepsy12.can_unlock_child_case_data_from_editing or not case.locked and not perms.epilepsy12.can_lock_child_case_data_from_editing %} disabled {% else %} {% if case.locked %} diff --git a/templates/epilepsy12/partials/cases/view_preference.html b/templates/epilepsy12/partials/cases/view_preference.html index 09192d85..5823905c 100644 --- a/templates/epilepsy12/partials/cases/view_preference.html +++ b/templates/epilepsy12/partials/cases/view_preference.html @@ -1,7 +1,7 @@ {% url 'view_preference' organisation_id=organisation.pk template_name=template_name as hx_post %} -{% if request.user.is_rcpch_audit_team_member or request.user.is_staff or request.user.is_superuser %} +{% if request.user.is_rcpch_audit_team_member or request.user.is_rcpch_staff or request.user.is_superuser %} {% include 'epilepsy12/partials/page_elements/rcpch_multiple_toggle.html' with choices=rcpch_choices hx_post=hx_post hx_target=hx_target hx_trigger="click" hx_swap="innerHTML" test_positive=test_positive label="" data_position="top left" enabled=True %} {% if request.user.view_preference == 0 %} diff --git a/templates/epilepsy12/partials/charts/deprivation_scores.html b/templates/epilepsy12/partials/charts/deprivation_scores.html index 0d8ee714..93174f7b 100644 --- a/templates/epilepsy12/partials/charts/deprivation_scores.html +++ b/templates/epilepsy12/partials/charts/deprivation_scores.html @@ -29,7 +29,7 @@ var myChart = new Chart(ctx, { type: 'pie', data: { - labels: [{% for data in cases_aggregated_by_deprivation %}'{{ data.index_of_multiple_deprivation_quintile_display }}',{% endfor %}], //loop through queryset, + labels: [{% for data in cases_aggregated_by_deprivation %}'{{ data.index_of_multiple_deprivation_quintile_display_str }}',{% endfor %}], //loop through queryset, datasets: [{ data: [{% for case in cases_aggregated_by_deprivation %}{{ case.cases_aggregated_by_deprivation }},{% endfor %}], backgroundColor: [ diff --git a/templates/epilepsy12/partials/charts/map.html b/templates/epilepsy12/partials/charts/map.html new file mode 100644 index 00000000..e76fc9b7 --- /dev/null +++ b/templates/epilepsy12/partials/charts/map.html @@ -0,0 +1,128 @@ +
+ + \ No newline at end of file diff --git a/templates/epilepsy12/partials/charts/organisations_ranked_by_kpi_measure.html b/templates/epilepsy12/partials/charts/organisations_ranked_by_kpi_measure.html index b861a52c..a7c36bcd 100644 --- a/templates/epilepsy12/partials/charts/organisations_ranked_by_kpi_measure.html +++ b/templates/epilepsy12/partials/charts/organisations_ranked_by_kpi_measure.html @@ -34,6 +34,9 @@ var rcpch_grey = '#4d4d4d'; var rcpch_dark_grey = '#808080'; + var lightColorBars = ['#00BDAA', '#11A7F2'] + var datalabelTextColor = lightColorBars.includes('{{abstraction_color}}') ? rcpch_black : 'white' + var myChart = new Chart(document.getElementById('{{data_id}}'), { type: 'bar', data: { @@ -43,19 +46,8 @@ label: '% achieving measure', data: kpi_data_mapped, borderWidth: 1, - backgroundColor: [ - rcpch_orange, - rcpch_light_orange, - rcpch_yellow, - rcpch_gold, - rcpch_vivid_green, - rcpch_aqua_green, - rcpch_purple, - rcpch_dark_blue, - rcpch_strong_green, - rcpch_dark_red, - ], - maxBarThickness: 25 + backgroundColor: ['{{ abstraction_color }}'], + maxBarThickness: 25, }, ] }, @@ -94,7 +86,7 @@ } }, datalabels: { - anchor: 'end', + anchor: 'center', clamp: true, align: 'end', formatter: function(value) { @@ -103,7 +95,7 @@ font: { weight: 'bold' }, - color: colors, + color: datalabelTextColor, display: 'auto', } }, diff --git a/templates/epilepsy12/partials/contact_email.html b/templates/epilepsy12/partials/contact_email.html new file mode 100644 index 00000000..c13ab11f --- /dev/null +++ b/templates/epilepsy12/partials/contact_email.html @@ -0,0 +1 @@ +epilepsy12@rcpch.ac.uk \ No newline at end of file diff --git a/templates/epilepsy12/partials/investigations/eeg_information.html b/templates/epilepsy12/partials/investigations/eeg_information.html index 90f51da4..2a36113a 100644 --- a/templates/epilepsy12/partials/investigations/eeg_information.html +++ b/templates/epilepsy12/partials/investigations/eeg_information.html @@ -53,7 +53,7 @@ {% if investigations.eeg_request_date and investigations.eeg_performed_date %}
- +
{% endif %} diff --git a/templates/epilepsy12/partials/investigations/mri_brain_information.html b/templates/epilepsy12/partials/investigations/mri_brain_information.html index 9850aa77..5526fe07 100644 --- a/templates/epilepsy12/partials/investigations/mri_brain_information.html +++ b/templates/epilepsy12/partials/investigations/mri_brain_information.html @@ -53,7 +53,7 @@ {% if investigations.mri_brain_requested_date and investigations.mri_brain_reported_date %}
- +
{% endif %} diff --git a/templates/epilepsy12/partials/management/antiepilepsy_medicines/antiepilepsy_medicine.html b/templates/epilepsy12/partials/management/antiepilepsy_medicines/antiepilepsy_medicine.html index 5707aed8..3fc3f67a 100644 --- a/templates/epilepsy12/partials/management/antiepilepsy_medicines/antiepilepsy_medicine.html +++ b/templates/epilepsy12/partials/management/antiepilepsy_medicines/antiepilepsy_medicine.html @@ -102,9 +102,9 @@
{% url 'medicine_id' antiepilepsy_medicine_id=antiepilepsy_medicine.pk as hx_post %} {% if is_rescue_medicine %} - {% include 'epilepsy12/partials/page_elements/select_model.html' with choices=choices hx_post=hx_post hx_target='#rescue_medicine_list' hx_trigger='change' hx_swap='innerHTML' hx_name='medicine_id' field_name='medicine_name' field_name2='preferredTerm' test_positive=antiepilepsy_medicine.medicine_entity.pk label=antiepilepsy_medicine.get_medicine_id_help_label_text reference=antiepilepsy_medicine.get_medicine_id_help_reference_text hx_default_text='Antiseizure medicine' data_position='top left' enabled=perms.epilepsy12.change_antiepilepsymedicine %} + {% include 'epilepsy12/partials/page_elements/select_model.html' with choices=choices hx_post=hx_post hx_target='#rescue_medicine_list' hx_trigger='change' hx_swap='innerHTML' hx_name='medicine_id' field_name='medicine_name' field_name2='preferredTerm' test_positive=antiepilepsy_medicine.medicine_entity.pk label=antiepilepsy_medicine.get_medicine_entity_help_label_text reference=antiepilepsy_medicine.get_medicine_entity_help_reference_text hx_default_text='Antiseizure medicine' data_position='top left' enabled=perms.epilepsy12.change_antiepilepsymedicine %} {% else %} - {% include 'epilepsy12/partials/page_elements/select_model.html' with choices=choices hx_post=hx_post hx_target='#antiepilepsy_medicine_list' hx_trigger='change' hx_swap='innerHTML' hx_name='medicine_id' field_name='medicine_name' field_name2='preferredTerm' test_positive=antiepilepsy_medicine.medicine_entity.pk label=antiepilepsy_medicine.get_medicine_id_help_label_text reference=antiepilepsy_medicine.get_medicine_id_help_reference_text hx_default_text='Antiseizure medicine' data_position='top left' enabled=perms.epilepsy12.change_antiepilepsymedicine %} + {% include 'epilepsy12/partials/page_elements/select_model.html' with choices=choices hx_post=hx_post hx_target='#antiepilepsy_medicine_list' hx_trigger='change' hx_swap='innerHTML' hx_name='medicine_id' field_name='medicine_name' field_name2='preferredTerm' test_positive=antiepilepsy_medicine.medicine_entity.pk label=antiepilepsy_medicine.get_medicine_entity_help_label_text reference=antiepilepsy_medicine.get_medicine_entity_help_reference_text hx_default_text='Antiseizure medicine' data_position='top left' enabled=perms.epilepsy12.change_antiepilepsymedicine %} {% endif %}
diff --git a/templates/epilepsy12/partials/management/individualised_care_plan.html b/templates/epilepsy12/partials/management/individualised_care_plan.html index e37af0cd..c0f3b764 100644 --- a/templates/epilepsy12/partials/management/individualised_care_plan.html +++ b/templates/epilepsy12/partials/management/individualised_care_plan.html @@ -11,7 +11,7 @@
{% url 'individualised_care_plan_date' management_id=management.pk as hx_post %} - {% include 'epilepsy12/partials/page_elements/date_field.html' with hx_post=hx_post hx_target="#individualised_care_plan" hx_swap="innerHTML" hx_trigger="change delay:1s" date_value=management.individualised_care_plan_date input_date_field_name='individualised_care_plan_date' label=management.get_individualised_care_plan_date_help_label_text reference=management.get_individualised_care_plan_date_help_reference_text data_position="top left" enabled=perms.epilepsy12.change_management error_message=error_message has_permission=perms.epilepsy12.change_management %} + {% include 'epilepsy12/partials/page_elements/date_field.html' with hx_post=hx_post hx_target="#individualised_care_plan" hx_swap="innerHTML" hx_trigger="change delay:1s" date_value=management.individualised_care_plan_date input_date_field_name='individualised_care_plan_date' label=management.get_individualised_care_plan_date_help_label_text reference=management.get_individualised_care_plan_date_help_reference_text data_position="top left" enabled=perms.epilepsy12.change_management error_message=error_message has_permission=perms.epilepsy12.change_management enabled=perms.epilepsy12.change_management %}
{% if error_message %}
@@ -85,7 +85,7 @@
Does ongoing individualised care planning include:
{% include 'epilepsy12/partials/page_elements/toggle_button.html' with test_positive=management.has_individualised_care_plan_been_updated_in_the_last_year hx_post=hx_post hx_target="#individualised_care_plan" hx_swap="innerHTML" hx_trigger="click" tooltip_id='has_individualised_care_plan_been_updated_in_the_last_year_tooltip' label=management.get_has_individualised_care_plan_been_updated_in_the_last_year_help_label_text reference=management.get_has_individualised_care_plan_been_updated_in_the_last_year_help_reference_text data_position="top left" enabled=perms.epilepsy12.change_management %}
-
+
{% endif %} diff --git a/templates/epilepsy12/partials/multiaxial_diagnosis/comorbidities/comorbidities.html b/templates/epilepsy12/partials/multiaxial_diagnosis/comorbidities/comorbidities.html index 51bedbb2..d81ab17f 100644 --- a/templates/epilepsy12/partials/multiaxial_diagnosis/comorbidities/comorbidities.html +++ b/templates/epilepsy12/partials/multiaxial_diagnosis/comorbidities/comorbidities.html @@ -39,7 +39,7 @@
{% endfor %}
diff --git a/templates/epilepsy12/partials/multiaxial_diagnosis/episodes.html b/templates/epilepsy12/partials/multiaxial_diagnosis/episodes.html index 736574e2..ab5af56a 100644 --- a/templates/epilepsy12/partials/multiaxial_diagnosis/episodes.html +++ b/templates/epilepsy12/partials/multiaxial_diagnosis/episodes.html @@ -72,7 +72,7 @@
-
+
-

Integrated Care Board

+

Integrated Care Board

@@ -63,9 +62,9 @@
No data yet!
-
+
-

NHS Region

+

NHS Region

@@ -82,10 +81,9 @@
No data yet!
- -
+
-

OPENUK Region

+

OPENUK Region

@@ -100,9 +98,10 @@
No data yet!
-
+ +
-

England and Wales

+

National

@@ -121,17 +120,17 @@
No data yet!
-
- {% include 'epilepsy12/partials/charts/organisations_ranked_by_kpi_measure.html' with data=open_uk data_title=open_uk_title data_id=open_uk_id avg_pct=open_uk_avg %} -
- {% include 'epilepsy12/partials/charts/organisations_ranked_by_kpi_measure.html' with data=icb data_title=icb_title data_id=icb_id avg_pct=icb_avg %} + {% include 'epilepsy12/partials/charts/organisations_ranked_by_kpi_measure.html' with data=icb data_title=icb_title data_id=icb_id avg_pct=icb_avg abstraction_color=icb_color %} +
+
+ {% include 'epilepsy12/partials/charts/organisations_ranked_by_kpi_measure.html' with data=open_uk data_title=open_uk_title data_id=open_uk_id avg_pct=open_uk_avg abstraction_color=open_uk_color %}
- {% include 'epilepsy12/partials/charts/organisations_ranked_by_kpi_measure.html' with data=nhs_region data_title=nhs_region_title data_id=nhs_region_id avg_pct=nhs_region_avg %} + {% include 'epilepsy12/partials/charts/organisations_ranked_by_kpi_measure.html' with data=nhs_region data_title=nhs_region_title data_id=nhs_region_id avg_pct=nhs_region_avg abstraction_color=nhs_region_color %}
- {% include 'epilepsy12/partials/charts/organisations_ranked_by_kpi_measure.html' with data=country data_title=country_title data_id=country_id avg_pct=country_avg %} + {% include 'epilepsy12/partials/charts/organisations_ranked_by_kpi_measure.html' with data=country data_title=country_title data_id=country_id avg_pct=country_avg abstraction_color=country_color %}
diff --git a/templates/epilepsy12/partials/page_elements/organisations_select.html b/templates/epilepsy12/partials/page_elements/organisations_select.html index 5e754a33..c3502d8e 100644 --- a/templates/epilepsy12/partials/page_elements/organisations_select.html +++ b/templates/epilepsy12/partials/page_elements/organisations_select.html @@ -15,77 +15,73 @@ hx_confirm: if present, the text to confirm continue {% endcomment %} -
- + +
- {% if test_positive %} - - {% else %} - - - - {% endif %} - {% if enabled %} {% if not enabled %} You do not have permission to update this field - {% endif %} \ No newline at end of file + {% endif %} diff --git a/templates/epilepsy12/partials/page_elements/single_choice_multiple_toggle_button.html b/templates/epilepsy12/partials/page_elements/single_choice_multiple_toggle_button.html index b77806b6..90dd405b 100644 --- a/templates/epilepsy12/partials/page_elements/single_choice_multiple_toggle_button.html +++ b/templates/epilepsy12/partials/page_elements/single_choice_multiple_toggle_button.html @@ -80,7 +80,7 @@
{% for choice in choices %} - {% if test_positive|slugify in choice.0|slugify %} + {% if test_positive == choice.0 %}
{% if enabled %} class="ui mini compact rcpch_dark_blue positive button" {% else %} - class="ui mini compact rcpch_dark_blue disabled button" + class="ui mini compact rcpch_dark_blue positive disabled button" {% endif %} >No
diff --git a/templates/epilepsy12/partials/registration/edit_or_transfer_lead_site.html b/templates/epilepsy12/partials/registration/edit_or_transfer_lead_site.html new file mode 100644 index 00000000..0790599d --- /dev/null +++ b/templates/epilepsy12/partials/registration/edit_or_transfer_lead_site.html @@ -0,0 +1,24 @@ +
Primary Centre for Epilepsy12
+ + + {% if edit %} +
+ + {% url 'update_lead_site' registration_id=registration.pk site_id=site.pk update="edit" as hx_post %} + {% url 'cancel_lead_site' registration_id=registration.pk site_id=site.pk as hx_cancel %} + {% include 'epilepsy12/partials/page_elements/organisations_select.html' with organisation_list=organisation_list hx_post=hx_post hx_target="#lead_site" hx_trigger="click" hx_swap="outerHTML" hx_name="edit_lead_site" test_positive=site.organisation.pk label="Update Secondary Care NHS Trust Centre" hx_default_text="Search organisations..." data_position="top left" enabled=perms.epilepsy12.can_edit_epilepsy12_lead_centre hx_cancel=hx_cancel hx_confirm="Please confirm that you wish to allocate to this organisation." %} + +
+ + + {% elif transfer %} + +
+ + {% url 'update_lead_site' registration_id=registration.pk site_id=site.pk update="transfer" as hx_post %} + {% url 'cancel_lead_site' registration_id=registration.pk site_id=site.pk as hx_cancel %} + {% include 'epilepsy12/partials/page_elements/organisations_select.html' with organisation_list=organisation_list hx_post=hx_post hx_target="#lead_site" hx_trigger="click" hx_swap="innerHTML" hx_name="transfer_lead_site" test_positive=site.organisation.pk label="Transfer Secondary Care NHS Trust Centre" hx_default_text="Search organisations..." data_position="top left" enabled=perms.epilepsy12.can_transfer_epilepsy12_lead_centre hx_confirm="Please confirm that you wish to allocate to this organisation." %} + +
+ + {% endif %} diff --git a/templates/epilepsy12/partials/registration/is_eligible_label.html b/templates/epilepsy12/partials/registration/is_eligible_label.html index 78c4b99a..dcb79eb8 100644 --- a/templates/epilepsy12/partials/registration/is_eligible_label.html +++ b/templates/epilepsy12/partials/registration/is_eligible_label.html @@ -1,5 +1,18 @@ + {% if has_error %}
{{message}}
{% else %} -
{{message}}
+ {% if is_positive %} +
{{message}}
+ {% else %} +
{{message}}
+ {% endif %} {% endif %} \ No newline at end of file diff --git a/templates/epilepsy12/partials/registration/lead_site.html b/templates/epilepsy12/partials/registration/lead_site.html index cc69508a..6a7fbbb8 100644 --- a/templates/epilepsy12/partials/registration/lead_site.html +++ b/templates/epilepsy12/partials/registration/lead_site.html @@ -56,28 +56,7 @@
Primary Centre for Epilepsy12
- {% if edit %} - -
- - {% url 'update_lead_site' registration_id=registration.pk site_id=site.pk update="edit" as hx_post %} - {% url 'cancel_lead_site' registration_id=registration.pk site_id=site.pk as hx_cancel %} - {% include 'epilepsy12/partials/page_elements/organisations_select.html' with organisation_list=organisation_list hx_post=hx_post hx_target="#lead_site" hx_trigger="click" hx_swap="innerHTML" hx_name="edit_lead_site" test_positive=site.organisation.pk label="Update Secondary Care NHS Trust Centre" hx_default_text="Search organisations..." data_position="top left" enabled=perms.epilepsy12.can_edit_epilepsy12_lead_centre hx_cancel=hx_cancel hx_confirm="Please confirm that you wish to allocate to this organisation." %} - -
- - {% elif transfer %} - -
- - {% url 'update_lead_site' registration_id=registration.pk site_id=site.pk update="transfer" as hx_post %} - {% url 'cancel_lead_site' registration_id=registration.pk site_id=site.pk as hx_cancel %} - {% include 'epilepsy12/partials/page_elements/organisations_select.html' with organisation_list=organisation_list hx_post=hx_post hx_target="#lead_site" hx_trigger="click" hx_swap="innerHTML" hx_name="transfer_lead_site" test_positive=site.organisation.pk label="Transfer Secondary Care NHS Trust Centre" hx_default_text="Search organisations..." data_position="top left" enabled=perms.epilepsy12.can_transfer_epilepsy12_lead_centre hx_confirm="Please confirm that you wish to allocate to this organisation." %} - -
- - {% endif %} {% else %} diff --git a/templates/epilepsy12/partials/selected_organisation_summary.html b/templates/epilepsy12/partials/selected_organisation_summary.html index f89b143f..f3bddbd6 100644 --- a/templates/epilepsy12/partials/selected_organisation_summary.html +++ b/templates/epilepsy12/partials/selected_organisation_summary.html @@ -8,10 +8,10 @@
Organisation View
- {% if request.user.is_rcpch_audit_team_member or request.user.is_staff or request.user.is_superuser %} + {% if request.user.is_rcpch_audit_team_member or request.user.is_rcpch_staff or request.user.is_superuser %}
- {% url 'selected_organisation_summary' as hx_post %} + {% url 'selected_organisation_summary' organisation_id=selected_organisation.pk as hx_post %} {% include 'epilepsy12/partials/page_elements/rcpch_organisations_select.html' with organisation_list=organisation_list hx_post=hx_post hx_target="#rcpch_organisation_select" hx_trigger="change" hx_swap="innerHTML" hx_name="selected_organisation_summary" test_positive=selected_organisation.pk label="Select a organisation trust to view" hx_default_text="Search general paediatric organisations..." data_position="top left" %}
@@ -132,7 +132,7 @@

Completed Patient Records

- {% include 'epilepsy12/partials/charts/progress.html' with percentage=percent_completed_organisation numerator=count_of_current_cohort_registered_completed_cases_in_this_organisation denominator=count_of_current_cohort_registered_cases_in_this_organisation title="Completed Patient Records - Organisation" pie_size='med' %} + {% include 'epilepsy12/partials/charts/progress.html' with percentage=percent_completed_organisation numerator=count_of_current_cohort_registered_completed_cases_in_this_organisation denominator=count_of_all_current_cohort_registered_cases_in_this_organisation title="Completed Patient Records - Organisation" pie_size='med' %}

{{selected_organisation.OrganisationName}}

@@ -141,7 +141,7 @@

Completed Patient Records

- {% include 'epilepsy12/partials/charts/progress.html' with percentage=percent_completed_trust numerator=count_of_current_cohort_registered_completed_cases_in_this_organisation denominator=count_of_current_cohort_registered_cases_in_this_organisation title="Completed Patient Records - Trust" pie_size='med' %} + {% include 'epilepsy12/partials/charts/progress.html' with percentage=percent_completed_trust numerator=count_of_current_cohort_registered_completed_cases_in_this_trust denominator=count_of_all_current_cohort_registered_cases_in_this_trust title="Completed Patient Records - Trust" pie_size='med' %}

{{selected_organisation.ParentOrganisation_OrganisationName}}

@@ -239,7 +239,48 @@
No data yet!
-
+
+ +
+
+ +
+
+ {% if selected_organisation.ons_region.ons_country.Country_ONS_Name == "Wales" %} +

Local Health Board

+ {% else %} +

Integrated Care Board

+ {% endif %} +
+
+ {% if selected_organisation.ons_region.ons_country.Country_ONS_Name == "Wales" %} + {% include 'epilepsy12/partials/charts/map.html' with abstraction_level='lhb' abstraction_level_border_colour='#ff8000' abstraction_level_data=lhb_tiles selected_organisation=selected_organisation hospital_region_name=selected_organisation.nhs_region.NHS_Region %} + {% else %} + {% include 'epilepsy12/partials/charts/map.html' with abstraction_level='icb' abstraction_level_border_colour='#ff8000' abstraction_level_data=icb_tiles selected_organisation=selected_organisation hospital_region_name=selected_organisation.integrated_care_board.ICB_Name %} + {% endif %} +
+
+ +
+
+

NHS Region

+
+
+ {% include 'epilepsy12/partials/charts/map.html' with abstraction_level='nhs_region' abstraction_level_border_colour='#ffd200' abstraction_level_data=nhsregion_tiles selected_organisation=selected_organisation hospital_region_name=selected_organisation.nhs_region.NHS_Region %} +
+
+ +
+
+

Country

+
+
+ {% include 'epilepsy12/partials/charts/map.html' with abstraction_level='country' abstraction_level_border_colour='#66cc33' abstraction_level_data=country_tiles selected_organisation=selected_organisation hospital_region_name=selected_organisation.ons_region.ons_country.Country_ONS_Name %} +
+
+ +
+
diff --git a/templates/epilepsy12/steps.html b/templates/epilepsy12/steps.html index b9c83ecd..375bec1a 100644 --- a/templates/epilepsy12/steps.html +++ b/templates/epilepsy12/steps.html @@ -47,12 +47,22 @@ {% url 'management' case_id as active_url %} {% include 'epilepsy12/step.html' with active_url=active_url title="Treatment and care planning" description="Medication, other treatment,
teams and care plans" active_template=active_template template_name='management' complete_instance=audit_progress.management_complete registration_status=audit_progress.registration_complete %} - -
-
Performance summary
-
Key performance indicators
-
-
+ {% if perms.epilepsy12.can_consent_to_audit_participation %} + {% url 'consent' case_id as active_url %} + {% if audit_progress.consent_patient_confirmed is not None %} + {% include 'epilepsy12/step.html' with active_url=active_url title="Consent" description="Declaration" active_template=active_template template_name='consent' complete_instance=True registration_status=audit_progress.registration_complete %} + {% else %} + {% include 'epilepsy12/step.html' with active_url=active_url title="Consent" description="Declaration" active_template=active_template template_name='consent' complete_instance=False registration_status=audit_progress.registration_complete %} + {% endif %} + {% endif %} + + +
+
Performance summary
+
Key performance indicators
+
+
+ {% if active_template == "register" %} {% include 'epilepsy12/progress_wheel.html' with total_expected_fields=audit_progress.registration_total_expected_fields total_completed_fields=audit_progress.registration_total_completed_fields title="Verification and registration" %} @@ -83,4 +93,4 @@ {% endif %}
- Back to child and young person view \ No newline at end of file + Back to Cohort View \ No newline at end of file diff --git a/templates/registration/user_management/epilepsy12_user_list.html b/templates/registration/user_management/epilepsy12_user_list.html index ead6c76c..087d494c 100644 --- a/templates/registration/user_management/epilepsy12_user_list.html +++ b/templates/registration/user_management/epilepsy12_user_list.html @@ -24,11 +24,11 @@
- {% if user.is_staff or user.is_rcpch_team_member or user.is_superuser %} + {% if user.is_rcpch_staff or user.is_rcpch_team_member or user.is_superuser %} {% endif %} - +
diff --git a/templates/registration/user_management/epilepsy12_user_table.html b/templates/registration/user_management/epilepsy12_user_table.html index 11cf6df7..85a60a0b 100644 --- a/templates/registration/user_management/epilepsy12_user_table.html +++ b/templates/registration/user_management/epilepsy12_user_table.html @@ -192,7 +192,7 @@ _="init js $('#{{epilepsy12_user.id}}_rcpch_audit_team_member_icon').popup(); end" > {% endif %} - {% if epilepsy12_user.is_staff %} + {% if epilepsy12_user.is_rcpch_staff %}