diff --git a/documentation/docs/admin-users/download.md b/documentation/docs/admin-users/download.md index 0f0ef74f9..388f834bd 100644 --- a/documentation/docs/admin-users/download.md +++ b/documentation/docs/admin-users/download.md @@ -7,4 +7,4 @@ author: Dr Marcus Baw You can download the entire Epilepsy12 documentation manual as a PDF for offline reading. Click the button below to download. -[:fontawesome-solid-file-pdf: Download documentation manual in PDF format](../../pdf/rcpch-epilepsy12-full-documentation.pdf){ .md-button .md-button--primary } \ No newline at end of file +[:fontawesome-solid-file-pdf: Download documentation manual in PDF format](../pdf/rcpch-epilepsy12-full-documentation.pdf){ .md-button .md-button--primary } \ No newline at end of file diff --git a/documentation/docs/clinician-users/entering-patient-data.md b/documentation/docs/clinician-users/entering-patient-data.md index 7fe54178d..4137c0801 100644 --- a/documentation/docs/clinician-users/entering-patient-data.md +++ b/documentation/docs/clinician-users/entering-patient-data.md @@ -26,7 +26,7 @@ The NHS number is checked against the NHS number [checksum](https://www.datadict ### Transferring a patient between centres -Only the [**Lead Clinician**](../user-group-permissions/#lead-clinician) has permissions to transfer children to another centre. +Only the [**Lead Clinician**](user-group-permissions.md#lead-clinician) has permissions to transfer children to another centre. The steps to do this for the lead clinician in Organisation A diff --git a/documentation/docs/development/docker-setup.md b/documentation/docs/development/docker-setup.md index 7d642ae2a..350cff90d 100644 --- a/documentation/docs/development/docker-setup.md +++ b/documentation/docs/development/docker-setup.md @@ -26,7 +26,7 @@ cd rcpch-audit-engine ``` !!! warning "Windows Setup" - **If you are on Windows**, after installing Docker and cloning the repository, please now skip to the [(Windows) Setup for development using Docker Compose](./docker-setup.md#windows-setup-for-development-using-docker-compose) section. + **If you are on Windows**, after installing Docker and cloning the repository, please now skip to the [(Windows) Setup for development using Docker Compose](./docker-setup-windows.md) section. ### Ensure you are on the default `development` branch @@ -48,7 +48,6 @@ cp envs/env-template envs/.env !!! warning "Mac Users" If using Mac and Safari, to access the Epilepsy 12 engine in your development, you must change the `SITE_DOMAIN` name in .env to 'localhost', and type this into your browser once you have executed `s/up` in the next step. This will load the E12 engine in your Safari browser. - However, for simplicity, we recommend using a different browser, such as Chrome, and leaving the .env file unaltered. ### Start the development environment for the first time using our startup script @@ -107,7 +106,6 @@ This Docker setup is quite new so please do open an issue if there is anything t !!! warning "Terminal is now occupied" If you have successfully run the Docker Compose deployment, your terminal will be showing the combined and colour-coded logging output for all the containers and will no longer show an interactive prompt, which is means you can not run any more commands in that terminal. To resolve this, simply **open another Terminal window** in the same working directory, in which you can run commands. - If opening another terminal is impractical or impossible, then in most Shell environments you can press `Ctrl`+`Z` to suspend the current process, and then `bg` to resume it in the background. This will return you to an interactive prompt. Once you've executed your further commands, you can then use `fg` to bring the console logging output back to the foreground again. ### Creating a superuser @@ -117,6 +115,7 @@ You can use our convenience script to create a superuser in the context of the ` ```console s/create-superuser ``` + The script will prompt you for required user attributes: ```console @@ -208,7 +207,7 @@ For testing of the UI it is often useful to have some dummy data in the database s/seed ``` -See the [Seeding the Database](../manual-setup/#seeding-the-database) section for more details on the usage of this script, for example setting a non-default Cohort Number. +See the [Seeding the Database](manual-setup.md#seeding-the-database) section for more details on the usage of this script, for example setting a non-default Cohort Number. ## Tips and Tricks, Gotchas and Caveats diff --git a/documentation/docs/development/getting-started.md b/documentation/docs/development/getting-started.md index 33cc61cee..7dbe63b39 100644 --- a/documentation/docs/development/getting-started.md +++ b/documentation/docs/development/getting-started.md @@ -39,4 +39,4 @@ Security is a very important aspect of managing projects such as these, and we u ## Legal -Details of Information Governance, Clinical Safety and Medical Device registration can be viewed in the [Legal](../legal/legal.md) section. +Details of Information Governance, Clinical Safety and Medical Device registration can be viewed in the [Legal](../legal/intellectual-property.md) section. diff --git a/documentation/docs/development/imd.md b/documentation/docs/development/imd.md new file mode 100644 index 000000000..43bbf6789 --- /dev/null +++ b/documentation/docs/development/imd.md @@ -0,0 +1,18 @@ +--- +title: Indices of Multiple Deprivation +reviewers: Dr Simon Chapman +--- + +## Calculation + +This is discussed more extensively in the RCPCH Census Platform which returns index of multiple deprivation quantiles against a UK postcode across the devolved nations. Currently Jersey is not supported. + +### Methodology + +Index of multiple deprivation is dependent on geography, and the underlying assumptions inherent in the index are underpinned with responses in the national censuses. The index is a composite of multiple domains from employment to housing to access to green space. Different countries even within the UK have different domains (though they are similar) and this means that findings in one country cannot be compared with another. There is a discussion of this in more detail in the [repository](https://github.com/rcpch/rcpch-census-platform) + +The country is first broken into areas of similar population size, known as low layer super output areas (LSOAs) and the findings for each measure summarized at this level. The LSOAs are then ranked in order by raw score, with the lower raw scores representing the least deprived. These are then broken into quantiles, depending on the size of the population / reporting priorities. It is typical to report as deciles or quintiles. + +The last English data was published in 2019, with Wales the same year. + +Jersey has recently been added to Epilepsy12 and is currently not supported by the RCPCH Census Platform. Because the population is small (~100,000), IMD is reported in vingtaines, rather than quintiles or deciles. The actual data is not published and is being requested for inclusion. In Epilepsy12 currently IMD quantiles are therefore not reported. diff --git a/documentation/docs/development/organisations.md b/documentation/docs/development/organisations.md index 4f0fe029a..8b577faeb 100644 --- a/documentation/docs/development/organisations.md +++ b/documentation/docs/development/organisations.md @@ -9,7 +9,7 @@ The organisational structure of health care in England and Wales influences repo ### Organisations and Trusts -This is the lowest level of abstraction and represents either an acute or a community hospital/organisation responsible for epilepsy care of children and young people. There are often several organisations in a Trust. Each organisation, like each Trust, has its own ODS code, and from year to year there is movement between trusts as organisations change their allegiances between trusts when mergers are carried out. The organisation model therefore has more than once instance for some organisations, as their parent status or other details such as name change. +This is the lowest level of abstraction and represents either an acute or a community hospital/organisation responsible for epilepsy care of children and young people. There are often several organisations in a Trust. Each organisation, like each Trust, has its own ODS code, and from year to year there is movement between trusts as organisations change their allegiances between trusts when mergers are carried out. The organisation model therefore has more than once instance for some organisations, as their parent status or other details such as name change. On a monthly basis the NHS ODS API is polled with any changes to organisational structure, and the local database record for that organisation is updated to reflect the latest changes. @@ -32,3 +32,21 @@ There are 7 of these in England and their model is taken from NHS Digital. Each ### Local Authorities Local authority codes for each organisation are not stored except for those organisations in London. Local authorities are administrative regions not related to health or the NHS. In London local authorities are usually referred to as London Boroughs. There is a boundary model for London Boroughs taken from NHS Digital and this is used only for mapping. As above, although versioned, there is no process in place to check for updates nationally and update the database record. + +### Jersey and the Channel Islands + +Jersey joined Epilepsy12 in 2024. Jersey is in the Channel Islands and part of the UK but does not participate in the NHS. There are reciprocal agreements about some hospital treatment, but inpatient care is free only to people who have been resident for 6 months and have a Health Card. + +***Organisational structure*** +The E12 structure is that organisations have a Trust or Local Health Board as their parent. There is a hierarchy above this which varies between England and Wales. Organisations and Trusts in England might have the same name, but will always have separate ODS codes, which are often similar. Jersey General Hospital in St Helier is a Trust which provides medical care directly. To work around this, Jersey General Hospital has been created both as an Organisation and a Trust, each with the same ODS code, so that it is its own parent. + +***Levels of Abstraction*** +Jersey has been added as a separate country, so that it can report at the level of organisation, Open UK Network, trust and country, though the numbers for these 3 hierarchies will be the same. + +## Maps + +Django GIS and the additional Postgres support for geoJSON are both reasons why these tools were used for this project. The Organisation View presents a dashboard that includes a scatterplot of patients specific to that organisation, with mean, median, minimum and maximum distances for patients to travel to clinic. There are also maps with boundaries demarcating health geographies such as NHS England regions and Integrated Care Boards. These are provided by `.shp` files which contain the coordinates for plotting the shapes on top of the maps. There is documentation on adding shape files [here](shape-files.md) + +The basic maps are provided by [MapBox](https://www.mapbox.com/) using their free tier, with the API key stored in credentials. An API key is required. + +E12 currently looks up postcodes against an API which returns longitude and latitude, and these are stored in the model (using SRID 27700) and this is used to plot them on the scatter plots. This currently does not function for Jersey as longitude and latitude for post codes are currently not stored. diff --git a/documentation/docs/development/postcodes.md b/documentation/docs/development/postcodes.md new file mode 100644 index 000000000..e8392e163 --- /dev/null +++ b/documentation/docs/development/postcodes.md @@ -0,0 +1,14 @@ +--- +title: Postcodes +reviewers: Dr Simon Chapman +--- + +This should be read in conjunction with sections on mapping and index of multiple deprivation. + +Postcodes for patients are stored in RCPCH-Audit-Engine securely as part of the national agreement for the RCPCH Epilepsy12 clinical audit. Opt out can be requested by patients and all data with the exception of the platform unique identifier (not the NHS number or Unique Reference Number) which is retained to generate an accurate denominator. + +Postcodes are used primarily to calculate indices of multiple deprivation, but are also used to provide a scatterplot for clinicians of patients in a given organisation to report maximum/minimum/mean and median distances patients have to travel for care. + +Postcodes are passed to findthatpostcode.uk. This reports information against postcode which include LSOA (see Indices of Multiple Deprivation) as well as longitude and latitude. These latter data points are used for scatter plots. + +Jersey is currently not supported as there is no open source solution for mapping currently though this is tracked in a [github issue](https://github.com/rcpch/rcpch-audit-engine/issues/1107) \ No newline at end of file diff --git a/documentation/docs/development/shape-files.md b/documentation/docs/development/shape-files.md new file mode 100644 index 000000000..5d0e56845 --- /dev/null +++ b/documentation/docs/development/shape-files.md @@ -0,0 +1,186 @@ +--- +title: Level of Abstractions and their boundaries, using GIS +reviewers: Dr Simon Chapman +--- + +## Shape Files + +GeoDjango and GIS is a package that sits ontop of Django and is accompanied by an additional pack for Postgres. It has additional fields and methods for geographical data, such as longitude, latitude and conversions for mapping different coordinate systems (eg northings and eastings vs longitude and latitude). + +Shape files contain the coordinates to map boundaries of regions. Epilepsy12 has boundary files for the countries of the United Kingdom from the UK [Office of National Statistics](https://geoportal.statistics.gov.uk/). + +In November 2024 support was added for Jersey. The shape files for this were taken from [GADM](https://gadm.org/index.html), a well-known resource of geographical data. This is a walk through of this as an example but note this is not how it is implemented actually. It is an example to show how to add a new model and boundary data to it. In reality, no new model was created for Jersey but instead the `.shp` file was mapped to existing fields in the `Country` model and then a new migration created to add the Organisation in Jersey with all its relationships. + +### Adding a new .shp file + +1. add the files to the `shape_files` directory. There are different file extensions but the key one is the `.shp` file for Epilepsy12 +2. Use `ogrinfo` on the command line to inspect the `.shp` file field structure. For example: + + `ogrinfo -al -so epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_0.shp` returns + + ```console + Layer name: gadm41_JEY_0 + Metadata: + DBF_DATE_LAST_UPDATE=2022-07-18 + Geometry: Polygon + Feature Count: 1 + Extent: (-2.255138, 49.147083) - (-1.924584, 49.292915) + Layer SRS WKT: + GEOGCRS["WGS 84", + DATUM["World Geodetic System 1984", + ELLIPSOID["WGS 84",6378137,298.257223563, + LENGTHUNIT["metre",1]]], + PRIMEM["Greenwich",0, + ANGLEUNIT["degree",0.0174532925199433]], + CS[ellipsoidal,2], + AXIS["latitude",north, + ORDER[1], + ANGLEUNIT["degree",0.0174532925199433]], + AXIS["longitude",east, + ORDER[2], + ANGLEUNIT["degree",0.0174532925199433]], + ID["EPSG",4326]] + Data axis to CRS axis mapping: 2,1 + GID_0: String (10.0) + COUNTRY: String (10.0) + ``` + + This is not an essential step - it just allows you to inspect the file and see the fields. + +3. Use `ogrinspect` to convert the `.shp` file to a `LayerMap` instance + `python manage.py ogrinspect epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_0.shp JerseyBoundary --srid=4326 --mapping --multi` + + This generates the code for the Model (change the name to the Model name you want). Note the flags - `srid` relates to the spatial reference identifier (SRID), a unique identifier associated with a specific coordinate system and resolution. The other flags create the boundary mapping object. This returns: + + ```console + # This is an auto-generated Django model module created by ogrinspect. + from django.contrib.gis.db import models + + + class JerseyBoundary(models.Model): + gid_0 = models.CharField(max_length=10) + country = models.CharField(max_length=10) + geom = models.MultiPolygonField(srid=4326) + + + # Auto-generated `LayerMapping` dictionary for JerseyBoundary model + jerseyboundary_mapping = { + 'gid_0': 'GID_0', + 'country': 'COUNTRY', + 'geom': 'MULTIPOLYGON', + } + ``` + +4. Add the new model to the `models_folder`. Generally the model with the boundaries in Epilepsy12 is an abstract model and inherited by the parent model which contains more information and the relationships: + + ```python + from django.contrib.gis.db import models + + class JerseyBoundary(models.Model): + gid_0 = models.CharField(max_length=10) + country = models.CharField(max_length=10) + geom = models.MultiPolygonField(srid=4326) + + class Meta: + abstract = True + + + class Jersey(JerseyBoundary): + class Meta: + indexes = [models.Index(fields=["gid_0"])] + verbose_name = "Jersey" + verbose_name_plural = "Jersey" + ordering = ("country",) + + def __str__(self) -> str: + return self.country + ``` + +5. Create a migration for the new models: + + `python manage.py makemigrations` + +6. Create a new custom migration for the shape mapping / data import process: + + `python manage.py makemigrations epilepsy12 --name jersey_shape_file_mapping --empty` + +7. Create a function within the migration that creates a new model, creates a new layer map and prescribes the path to the `.shp` file to import the data into the database. It would look like this: + + ```python + # Generated by Django 5.1.2 on 2024-11-23 13:16 + + # python imports + import os + + # django imports + from django.db import migrations + + # from django.apps import apps as django_apps + from django.apps import apps as django_apps + from django.contrib.gis.utils import LayerMapping + + Jersey = django_apps.get_model("epilepsy12", "Jersey") + + # Auto-generated `LayerMapping` dictionary for JerseyBoundary model + jerseyboundary_mapping = { + "gid_0": "GID_0", + "country": "COUNTRY", + "geom": "MULTIPOLYGON", + } + + # Get the path to the shape file + app_config = django_apps.get_app_config("epilepsy12") + app_path = app_config.path + jersey_shp_file_path = os.path.join( + app_path, "shape_files", "gadm41_JEY_shp", "gadm41_JEY_0.shp" + ) + + + def load_jersey_shape_file_mapping(apps, schema_editor): + """ + Load the Jersey shape file mapping into the database + """ + + # Load the Jersey shape file mapping into the database + lm = LayerMapping( + Jersey, + jersey_shp_file_path, + jerseyboundary_mapping, + transform=False, + encoding="utf-8", + ) + + lm.save(strict=True, verbose=True) + + + class Migration(migrations.Migration): + + dependencies = [ + ("epilepsy12", "0043_jersey"), + ] + + operations = [ + migrations.RunPython(load_jersey_shape_file_mapping), + ] + + ``` + +8. Run the migration + + `python manage.py migrate` + +```console +root@600d309878ba:/app# python manage.py makemigrations +Migrations for 'epilepsy12': + epilepsy12/migrations/0043_jersey.py + + Create model Jersey +root@600d309878ba:/app# python manage.py makemigrations epilepsy12 --name jersey_shape_file_mapping --empty +Migrations for 'epilepsy12': + epilepsy12/migrations/0044_jersey_shape_file_mapping.py +root@600d309878ba:/app# python manage.py migrate +Operations to perform: + Apply all migrations: admin, auth, authtoken, captcha, contenttypes, epilepsy12, otp_email, otp_static, otp_totp, phonenumber, sessions +Running migrations: + Applying epilepsy12.0043_jersey... OK + Applying epilepsy12.0044_jersey_shape_file_mapping...Saved: Jersey +``` diff --git a/documentation/docs/development/user-groups.md b/documentation/docs/development/user-groups.md index 2cb8310fa..9fdc31b6a 100644 --- a/documentation/docs/development/user-groups.md +++ b/documentation/docs/development/user-groups.md @@ -3,7 +3,7 @@ title: User groups and permissions reviewers: Dr Simon Chapman --- -The user groups are summarised [here](../clinician-users/clinician-user-guide.md###Permission) +The user groups are summarised [here](../clinician-users/clinician-user-guide.md) Django allows permission-based and group based access. The user groups defined above are containers for permissions to all the models. Generic django permissions allow prescription of view, change, create and delete to each model (found in ```epilepsy12/constants/user_types.py```). diff --git a/documentation/docs/legal/privacy-impact-assessment.md b/documentation/docs/legal/privacy-impact-assessment.md index 210d9cc58..b2a3e8773 100644 --- a/documentation/docs/legal/privacy-impact-assessment.md +++ b/documentation/docs/legal/privacy-impact-assessment.md @@ -1,7 +1,7 @@ --- title: Data Protection Impact Assessment reviewers: Dr Marcus Baw -dpia_path: ../../reference-documents/2023-12-PIA-2018-02-epilepsy12-pia-v2-1_Final.pdf +dpia_path: ../reference-documents/2023-12-PIA-2018-02-epilepsy12-pia-v2-1_Final.pdf hide: - toc --- diff --git a/documentation/docs/legal/privacy-notice.md b/documentation/docs/legal/privacy-notice.md index f456bcfae..93a96c4f1 100644 --- a/documentation/docs/legal/privacy-notice.md +++ b/documentation/docs/legal/privacy-notice.md @@ -1,8 +1,8 @@ --- title: Privacy Notice reviewers: Dr Marcus Baw -privacy_notice_path: ../../reference-documents/20230118 E12 Privacy Notice.pdf -privacy_notice_welsh_path: ../../reference-documents/20230118 E12 Privacy Notice Welsh.pdf +privacy_notice_path: ../reference-documents/20230118 E12 Privacy Notice.pdf +privacy_notice_welsh_path: ../reference-documents/20230118 E12 Privacy Notice Welsh.pdf hide: - toc --- diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index c9c9ffa3a..4fe20c453 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -147,6 +147,7 @@ nav: - 'admin-users/admin-user-guide.md' - 'admin-users/download.md' - 'admin-users/how-to-edit-docs.md' + - 'admin-users/how-to-add-banners.md' - Developer Guide: - Getting Started: 'development/getting-started.md' - Architecture Overview: 'development/architecture-overview.md' @@ -174,9 +175,12 @@ nav: - Audit Forms: 'development/audit-forms.md' - Form Scoring: 'development/form-scoring.md' - Date Validations: 'development/date-validations.md' + - Indices of Multiple Deprivation: 'development/imd.md' - KPIs: 'development/key-performance-indicators.md' - Organisations, Trusts and Regions: 'development/organisations.md' + - Postcodes: 'development/postcodes.md' - Reporting: 'development/reporting.md' + - Shape Files and Maps: 'development/shape-files.md' - User Groups: 'development/user-groups.md' - Users: 'development/user-workflow.md' - Views: 'development/views.md' diff --git a/epilepsy12/admin.py b/epilepsy12/admin.py index aedfbce1c..26176f579 100644 --- a/epilepsy12/admin.py +++ b/epilepsy12/admin.py @@ -115,29 +115,45 @@ def get_form(self, request, obj=None, **kwargs): class CaseAdmin(SimpleHistoryAdmin): - search_fields = ["first_name", "surname", "nhs_number", "date_of_birth"] + search_fields = [ + "first_name", + "surname", + "nhs_number", + "unique_reference_number", + "date_of_birth", + ] class OrganisationalAuditSubmissionAdmin(SimpleHistoryAdmin): - search_fields = ["trust__name", "local_health_board__name", "trust__ods_code", "local_health_board__ods_code"] + search_fields = [ + "trust__name", + "local_health_board__name", + "trust__ods_code", + "local_health_board__ods_code", + ] list_filter = ["submission_period"] + class OrganisationalAuditSubmissionPeriodAdmin(SimpleHistoryAdmin): actions = ["download"] @admin.action(description="Download submissions as CSV") def download(self, request, queryset): if queryset.count() > 1: - self.message_user(request, "Please select only one submission period to download", messages.ERROR) + self.message_user( + request, + "Please select only one submission period to download", + messages.ERROR, + ) else: submission_period = queryset.first() - + filename = f"e12-org-audit-{submission_period.year}.csv" - + data = export_submission_period_as_csv(submission_period) response = HttpResponse(data, content_type="text/csv") - response['Content-Disposition'] = f"attachment; filename={filename}" + response["Content-Disposition"] = f"attachment; filename={filename}" return response @@ -149,7 +165,13 @@ def download(self, request, queryset): admin.site.register(Comorbidity, SimpleHistoryAdmin) admin.site.register(EpilepsyContext, SimpleHistoryAdmin) admin.site.register(Investigations, SimpleHistoryAdmin) -admin.site.register(Organisation, SimpleHistoryAdmin) + + +class OrganisationAdmin(SimpleHistoryAdmin): + search_fields = ["name", "ods_code"] + + +admin.site.register(Organisation, OrganisationAdmin) admin.site.register(FirstPaediatricAssessment, SimpleHistoryAdmin) admin.site.register(Management, SimpleHistoryAdmin) admin.site.register(Registration, SimpleHistoryAdmin) @@ -181,12 +203,20 @@ def download(self, request, queryset): admin.site.register(LondonBorough) admin.site.register(IntegratedCareBoard) admin.site.register(NHSEnglandRegion) -admin.site.register(Trust) + + +class TrustAdmin(SimpleHistoryAdmin): + search_fields = ["name", "ods_code"] + + +admin.site.register(Trust, TrustAdmin) admin.site.register(LocalHealthBoard) admin.site.register(OPENUKNetwork) admin.site.register(OrganisationalAuditSubmission, OrganisationalAuditSubmissionAdmin) -admin.site.register(OrganisationalAuditSubmissionPeriod, OrganisationalAuditSubmissionPeriodAdmin) +admin.site.register( + OrganisationalAuditSubmissionPeriod, OrganisationalAuditSubmissionPeriodAdmin +) admin.site.register(Banner) admin.site.site_header = "Epilepsy12 admin" diff --git a/epilepsy12/common_view_functions/aggregate_by.py b/epilepsy12/common_view_functions/aggregate_by.py index 035a61444..1bf2d382e 100644 --- a/epilepsy12/common_view_functions/aggregate_by.py +++ b/epilepsy12/common_view_functions/aggregate_by.py @@ -634,6 +634,7 @@ def get_all_kpi_aggregation_data_for_view( updates the relevant AbstractionModel and returns the KPI model as a dict. """ + ALL_DATA = {} for enum_abstraction_level in EnumAbstractionLevel: # For the given abstraction, get the {ABSTRACTION}KPIAggregation model diff --git a/epilepsy12/common_view_functions/map_from_shape_file.py b/epilepsy12/common_view_functions/map_from_shape_file.py index 8725a5043..5040cd172 100644 --- a/epilepsy12/common_view_functions/map_from_shape_file.py +++ b/epilepsy12/common_view_functions/map_from_shape_file.py @@ -1,11 +1,11 @@ # python imports from datetime import date import json +import logging # django imports from django.apps import apps from django.conf import settings -from django.db.models import Count, Q # third party imports import pandas as pd @@ -25,6 +25,8 @@ ) from .tiles_for_region import return_tile_for_region +logger = logging.getLogger(__name__) + def generate_case_count_choropleth_map( properties, organisation, abstraction_level, cohort @@ -35,7 +37,9 @@ def generate_case_count_choropleth_map( """ px.set_mapbox_access_token(settings.MAPBOX_API_KEY) - region_tile = region_tile_for_abstraction_level(abstraction_level=abstraction_level) + region_tile = region_tile_for_abstraction_level( + abstraction_level=abstraction_level, organisation=organisation + ) geojson_data = json.loads(region_tile) features = geojson_data["features"] @@ -111,9 +115,10 @@ def generate_case_count_choropleth_map( else: identifier = None - # Highlight the region of the organisation by colouring the region boudary in a pink colour + # Highlight the region of the organisation by colouring the region boundary in a pink colour organisation_region = getattr(organisation, identifier) organisation_region_identifier = getattr(organisation_region, properties) + highlighted_region = dataframe[ dataframe["identifier"] == organisation_region_identifier ] @@ -157,6 +162,7 @@ def generate_case_counts_for_each_region_in_each_abstraction_level( """ Case = apps.get_model("epilepsy12", "Case") + Organisation = apps.get_model("epilepsy12", "Organisation") # Create a new DataFrame to store the results df = pd.DataFrame(columns=["identifier", "name", "cases"]) @@ -276,7 +282,9 @@ def all_organisations_within_a_level_of_abstraction( return level_abstraction_organisations -def region_tile_for_abstraction_level(abstraction_level: EnumAbstractionLevel): +def region_tile_for_abstraction_level( + abstraction_level: EnumAbstractionLevel, organisation +): """ Returns the geojson tile for a given level of abstraction """ @@ -290,7 +298,7 @@ def region_tile_for_abstraction_level(abstraction_level: EnumAbstractionLevel): elif abstraction_level == EnumAbstractionLevel.NHS_ENGLAND_REGION: region_tile = return_tile_for_region("nhs_england_region") elif abstraction_level == EnumAbstractionLevel.COUNTRY: - region_tile = return_tile_for_region("country") + region_tile = return_tile_for_region("country", organisation) else: # pragma: no cover raise ValueError("Invalid abstraction level") diff --git a/epilepsy12/common_view_functions/tiles_for_region.py b/epilepsy12/common_view_functions/tiles_for_region.py index c2ac6dd84..81409dc65 100644 --- a/epilepsy12/common_view_functions/tiles_for_region.py +++ b/epilepsy12/common_view_functions/tiles_for_region.py @@ -12,7 +12,8 @@ def return_tile_for_region( abstraction_level: Literal[ "icb", "nhs_england_region", "london_borough", "lhb", "country" - ] + ], + organisation=None, ): """ Returns geojson data for a given region. @@ -23,18 +24,25 @@ def return_tile_for_region( LocalHealthBoard = apps.get_model("epilepsy12", "LocalHealthBoard") LondonBorough = apps.get_model("epilepsy12", "LondonBorough") - model = IntegratedCareBoard + model = IntegratedCareBoard.objects.all() if abstraction_level == "nhs_england_region": - model = NHSEnglandRegion + model = NHSEnglandRegion.objects.all() elif abstraction_level == "country": model = CountryBoundaries + if organisation: + model = CountryBoundaries.objects.filter( + boundary_identifier=organisation.country.boundary_identifier + ).all() + else: + model = CountryBoundaries.objects.all() elif abstraction_level == "lhb": - model = LocalHealthBoard + model = LocalHealthBoard.objects.all() elif abstraction_level == "london_borough": - model = LondonBorough + model = LondonBorough.objects.all() + + unedited_tile = serialize("geojson", model) - unedited_tile = serialize("geojson", model.objects.all()) geojson_dict = json.loads(unedited_tile) geojson_dict.pop("crs", None) diff --git a/epilepsy12/constants/rcpch_organisations.py b/epilepsy12/constants/rcpch_organisations.py index 301a92464..94c14e37e 100644 --- a/epilepsy12/constants/rcpch_organisations.py +++ b/epilepsy12/constants/rcpch_organisations.py @@ -8529,6 +8529,32 @@ }, # 01/04/2013 Region: Y60 MIDLANDS COMMISSIONING REGION, ICB: QNX NHS SUSSEX INTEGRATED CARE BOARD, OPENUK: South East Thames Epilepsy Group ] +JERSEY_ORGANISATION = { + "OrganisationID": "", + "OrganisationCode": "RGT1W", + "OrganisationType": "", + "SubType": "", + "Sector": "", + "OrganisationStatus": "", + "IsPimsManaged": "", + "OrganisationName": "JERSEY GENERAL HOSPITAL", + "Address1": "THE PARADE", + "Address2": "", + "Address3": "", + "City": "ST HELIER", + "County": "JERSEY", + "Postcode": "JE1 3Q", + "Latitude": "49.18841258908002", + "Longitude": "-2.1122213730166157", + "ParentODSCode": "RGT1W", # This is a special case as it is both an organisation and a trust so the parent trust ODS code is the same as the organisation code and exists in both models. + "ParentName": "JERSEY GENERAL HOSPITAL", + "Phone": "01534 442000", + "Email": "", + "Website": "", + "Fax": "", + "LocalAuthority": "", +} # 01/04/2015 - Jersey General Hospital, Jersey, Channel Islands is a special case as it is both an organisation and a trust. It is member of the South West Interest Group Paediatric Epilepsy. It has no other organisational hierarchies + """ Steps to update organisation in the console once created in the admin from django.contrib.gis.geos import Point diff --git a/epilepsy12/constants/trust.py b/epilepsy12/constants/trust.py index 4196d6280..7c29636f2 100644 --- a/epilepsy12/constants/trust.py +++ b/epilepsy12/constants/trust.py @@ -5,6 +5,7 @@ Does not include Wales - these are in the Local Health Boards file Some ambulance trusts are here but commented out. """ + TRUSTS = [ { "ods_code": "RVV", @@ -2185,3 +2186,13 @@ "country": "ENGLAND", }, ] + +JERSEY_NHS_TRUST = { + "ods_code": "RGT1W", + "trust_name": "JERSEY GENERAL HOSPITAL", + "address_line_1": "THE PARADE", + "address_line_2": "", + "town": "ST HELIER", + "postcode": "JE1 3Q", + "country": "JERSEY", +} # 01/04/2015 Jersey is a special case as it is both an organisation and a Trust. The ODS code is the same for both. diff --git a/epilepsy12/forms_folder/case_form.py b/epilepsy12/forms_folder/case_form.py index d53d53f51..04aa5e5ed 100644 --- a/epilepsy12/forms_folder/case_form.py +++ b/epilepsy12/forms_folder/case_form.py @@ -1,20 +1,18 @@ from datetime import date -from random import randint +from random import randint from django import forms from django.conf import settings from django.forms import ValidationError -import nhs_number +import nhs_number as nhs_number_package -from ..models import Case +from ..models import Case, Organisation from ..constants import * from ..general_functions import is_valid_postcode, return_random_postcode class CaseForm(forms.ModelForm): - - - + unknown_postcode = forms.CharField(required=False) first_name = forms.CharField( @@ -56,7 +54,14 @@ class CaseForm(forms.ModelForm): "data-mask": "000 000 0000", } ), - required=True, + required=False, + ) + unique_reference_number = forms.CharField( + help_text="Enter the Unique Reference Number (URN).", + widget=forms.TextInput( + attrs={"class": "form-control", "placeholder": "URN", "type": "text"} + ), + required=False, ) postcode = forms.CharField( @@ -79,19 +84,67 @@ class CaseForm(forms.ModelForm): locked_by = forms.CharField(help_text="User who locked the record", required=False) def __init__(self, *args, **kwargs) -> None: - + self.organisation_id = kwargs.pop( + "organisation_id", None + ) # This is the organisation_id + + # set a flag to check if this is Jersey + self.is_jersey = ( + Organisation.objects.get( + id=self.organisation_id + ).country.boundary_identifier + == "JEY" + ) super(CaseForm, self).__init__(*args, **kwargs) + self.existing_nhs_number = self.instance.nhs_number self.fields["ethnicity"].widget.attrs.update({"class": "ui rcpch dropdown"}) + if self.is_jersey: + # this is Jersey - hide the NHS number field + self.fields["nhs_number"].widget = forms.HiddenInput() + self.fields["nhs_number"].required = False + self.fields["unique_reference_number"].required = True + self.fields["unique_reference_number"].initial = ( + self.instance.unique_reference_number + ) + else: + # this is England or Wales - hide the URN field + self.fields["unique_reference_number"].widget = forms.HiddenInput() + self.fields["unique_reference_number"].required = False + self.fields["nhs_number"].required = True + self.fields["nhs_number"].initial = self.instance.nhs_number - self.existing_nhs_number = self.instance.nhs_number - # Check if DEBUG is True and set the initial value conditionally if settings.DEBUG: - self.fields['first_name'].initial = 'Bob' - self.fields['surname'].initial = 'Dylan' - self.fields['date_of_birth'].initial = date(randint(2005, 2021), randint(1, 12), randint(1, 28)) - self.fields['postcode'].initial = return_random_postcode(country_boundary_identifier='E01000001') - self.fields['nhs_number'].initial = nhs_number.generate()[0] + self.fields["first_name"].initial = "Bob" + self.fields["surname"].initial = "Dylan" + self.fields["date_of_birth"].initial = date( + randint(2005, 2021), randint(1, 12), randint(1, 28) + ) + # set a random postcode if DEBUG is True: E01000001 is the boundary identifier for England but if is_jersey is True + # then a Jersey postcode will be returned instead of an English/Welsh postcode + self.fields["postcode"].initial = return_random_postcode( + country_boundary_identifier=Organisation.objects.get( + id=self.organisation_id + ).country.boundary_identifier, + is_jersey=self.is_jersey, + ) + if self.is_jersey: + # this is Jersey + if self.instance.unique_reference_number is None: + # this is a new form - create a new URN that is unique (not in the database) + # we are trying to avoid a generator function here to keep the code simple + # so there is a small chance that the URN generated is not unique but + # this is acceptable for the purposes of this example since it is only for testing + urn = f"{randint(100000, 999999)}" # 6 digit URN + if not Case.objects.filter(unique_reference_number=urn).exists(): + self.fields["unique_reference_number"].initial = urn + self.fields["nhs_number"].initial = None + else: + # this is England or Wales + self.fields["unique_reference_number"].initial = None + if self.instance.nhs_number is None: + # this is a new form + self.fields["nhs_number"].initial = nhs_number_package.generate()[0] class Meta: model = Case @@ -101,6 +154,7 @@ class Meta: "date_of_birth", "sex", "nhs_number", + "unique_reference_number", "postcode", "ethnicity", "unknown_postcode", @@ -108,8 +162,7 @@ class Meta: def clean_postcode(self): postcode = self.cleaned_data["postcode"] - - if is_valid_postcode(postcode=postcode): + if is_valid_postcode(postcode=postcode, is_jersey=self.is_jersey): return postcode raise ValidationError("Invalid postcode") @@ -124,6 +177,11 @@ def clean_date_of_birth(self): def clean_nhs_number(self): # remove spaces + organisation = Organisation.objects.get(id=self.organisation_id) + if organisation.ods_code == "RGT1W": + nhs_number = None + return nhs_number + formatted_nhs_number = ( str(self.cleaned_data["nhs_number"]).replace(" ", "").zfill(10) ) @@ -141,7 +199,7 @@ def clean_nhs_number(self): raise ValidationError("NHS Number already taken!") # check NHS number is valid - validity = nhs_number.is_valid(formatted_nhs_number) + validity = nhs_number_package.is_valid(formatted_nhs_number) if validity: return formatted_nhs_number else: diff --git a/epilepsy12/general_functions/postcode.py b/epilepsy12/general_functions/postcode.py index 7a23c9396..3f1a8c09d 100644 --- a/epilepsy12/general_functions/postcode.py +++ b/epilepsy12/general_functions/postcode.py @@ -1,5 +1,7 @@ import requests import logging +from random import randint +import re from django.conf import settings from ..constants import UNKNOWN_POSTCODES_NO_SPACES @@ -8,9 +10,11 @@ logger = logging.getLogger(__name__) -def is_valid_postcode(postcode: str) -> bool: +def is_valid_postcode(postcode: str, is_jersey=False) -> bool: """ - Returns True if postcode valid. + Returns True if postcode valid. False otherwise. + If is_jersey is True, the postcode is validated against the Jersey postcode format - this is less + rigorous than the API validation but is necessary since the API does not support Jersey postcodes. """ # convert to upper case and remove spaces @@ -19,6 +23,9 @@ def is_valid_postcode(postcode: str) -> bool: if formatted in UNKNOWN_POSTCODES_NO_SPACES: return True + if is_jersey: + return validate_jersey_postcode(value=postcode) + # check against API url = f"{settings.POSTCODE_API_BASE_URL}/postcodes/{postcode}" @@ -43,9 +50,22 @@ def is_valid_postcode(postcode: str) -> bool: return True +def validate_jersey_postcode(value): + """ + Validates if the given value matches the Jersey postcode format (JE# #AA or JE###AA without spaces). + """ + value = value.upper().replace(" ", "") # Convert to uppercase and remove all spaces + pattern = ( + r"^JE\d{1,2}\d[ABD-HJLNP-UW-Z]{2}$" # Regex for Jersey postcodes without spaces + ) + if not re.match(pattern, value): + return False + return True + + def coordinates_for_postcode(postcode: str) -> bool: """ - Returns longitude and latitude for a valide postcode. + Returns longitude and latitude for a valid postcode. """ # convert to upper case and remove spaces @@ -71,8 +91,71 @@ def coordinates_for_postcode(postcode: str) -> bool: return None -def return_random_postcode(country_boundary_identifier: str): - """Returns random postcode (str) inside country_boundary_identifier or `None` if invalid.""" +def return_random_postcode( + country_boundary_identifier: str, is_jersey: bool = False +) -> str: + """ + Returns random postcode (str) inside country_boundary_identifier or `None` if invalid. + + Also accepts a boolean `is_jersey` to determine if the country is Jersey. In these circumstances the postcode + will be randomly chosen from a predefined list of Jersey postcodes since the API does not support Jersey postcodes. + """ + JERSEY_POSTCODES = [ + "JE1 1AA", + "JE1 1AB", + "JE1 2BA", + "JE2 3CD", + "JE2 4EF", + "JE3 5GH", + "JE3 5IJ", + "JE3 6KL", + "JE3 7MN", + "JE4 8OP", + "JE4 8QR", + "JE4 9ST", + "JE1 3UV", + "JE2 1WX", + "JE2 2YZ", + "JE3 3AA", + "JE3 4BB", + "JE3 5CC", + "JE3 6DD", + "JE4 7EE", + "JE4 8FF", + "JE1 1GG", + "JE1 2HH", + "JE1 3JJ", + "JE2 4KK", + "JE2 5LL", + "JE2 6MM", + "JE3 7NN", + "JE3 8OO", + "JE4 9PP", + "JE1 4QQ", + "JE1 5RR", + "JE1 6SS", + "JE2 7TT", + "JE2 8UU", + "JE3 9VV", + "JE4 1WW", + "JE4 2XX", + "JE4 3YY", + "JE4 4ZZ", + "JE1 7AA", + "JE1 8BB", + "JE2 9CC", + "JE2 1DD", + "JE3 2EE", + "JE3 3FF", + "JE3 4GG", + "JE4 5HH", + "JE4 6JJ", + "JE4 7KK", + ] + + if is_jersey: + return JERSEY_POSTCODES[randint(0, len(JERSEY_POSTCODES) - 1)] + url = f"{settings.POSTCODE_API_BASE_URL}/areas/{country_boundary_identifier}" response = requests.get(url=url) @@ -84,3 +167,6 @@ def return_random_postcode(country_boundary_identifier: str): return response.json()["data"]["relationships"]["example_postcodes"]["data"][0][ "id" ].replace(" ", "") + + +0 diff --git a/epilepsy12/management/commands/old_pt_data_scripts.py b/epilepsy12/management/commands/old_pt_data_scripts.py index d2b36cdf3..343bec911 100644 --- a/epilepsy12/management/commands/old_pt_data_scripts.py +++ b/epilepsy12/management/commands/old_pt_data_scripts.py @@ -1,6 +1,7 @@ """ These scripts clean old patient data csv and convert into records which can be seeded into E12 db. """ + import pprint import nhs_number @@ -188,7 +189,7 @@ def insert_old_pt_data(csv_path: str = "data.csv"): } continue - if not is_valid_postcode(record["postcode"]): + if not is_valid_postcode(record["postcode"], is_jersey=False): reason = "Invalid postcode" print( f'Record: {record["nhs_number"]} - { reason } - Skipping insertion...' diff --git a/epilepsy12/management/commands/seed.py b/epilepsy12/management/commands/seed.py index db3581660..cfbe3ee37 100644 --- a/epilepsy12/management/commands/seed.py +++ b/epilepsy12/management/commands/seed.py @@ -40,6 +40,7 @@ "7A2AJ", # Bronglais "7A6BJ", # Chepstow Community "7A6AV", # Ysbyty Ystrad Fawr + "RGT1W", # Jersey General Hospital ] @@ -168,6 +169,9 @@ def run_dummy_cases_seed(cases, organisations, noskip, verbose=True): num_cases_to_seed_in_org = int(cases / len(organisations)) print(f"Creating {num_cases_to_seed_in_org} Cases in {org}") + # Check if the organisation is in Jersey - this is important for generating postcodes and URNs rather than NHS Numbers + is_jersey = org.country.boundary_identifier == "JEY" + # Create random attributes random_date = date(randint(2005, 2021), randint(1, 12), randint(1, 28)) date_of_birth = random_date @@ -177,7 +181,8 @@ def run_dummy_cases_seed(cases, organisations, noskip, verbose=True): random_ethnicity = randint(0, len(choice(ETHNICITIES))) ethnicity = ETHNICITIES[random_ethnicity][0] postcode = return_random_postcode( - country_boundary_identifier=org.country.boundary_identifier + country_boundary_identifier=org.country.boundary_identifier, + is_jersey=is_jersey, ) index_of_multiple_deprivation_quintile = randint(1, 5) @@ -194,6 +199,7 @@ def run_dummy_cases_seed(cases, organisations, noskip, verbose=True): "seed_male": seed_male, "seed_female": seed_female, }, + is_jersey=is_jersey, ) diff --git a/epilepsy12/migrations/0001_initial.py b/epilepsy12/migrations/0001_initial.py index 0707d4ef6..fe4b75b52 100644 --- a/epilepsy12/migrations/0001_initial.py +++ b/epilepsy12/migrations/0001_initial.py @@ -1462,7 +1462,10 @@ class Migration(migrations.Migration): ), ("created_at", models.DateTimeField(auto_now_add=True)), ("updated_at", models.DateTimeField(auto_now=True)), - ("conceptId", models.CharField(blank=True, default=None, null=True, unique=True)), + ( + "conceptId", + models.CharField(blank=True, default=None, null=True, unique=True), + ), ("term", models.CharField(blank=True, default=None, null=True)), ( "preferredTerm", @@ -1519,7 +1522,10 @@ class Migration(migrations.Migration): ), ("created_at", models.DateTimeField(auto_now_add=True)), ("updated_at", models.DateTimeField(auto_now=True)), - ("conceptId", models.CharField(blank=True, default=None, null=True, unique=True)), + ( + "conceptId", + models.CharField(blank=True, default=None, null=True, unique=True), + ), ("term", models.CharField(blank=True, default=None, null=True)), ( "preferredTerm", @@ -2685,7 +2691,10 @@ class Migration(migrations.Migration): ), ("created_at", models.DateTimeField(blank=True, editable=False)), ("updated_at", models.DateTimeField(blank=True, editable=False)), - ("conceptId", models.CharField(blank=True, default=None, null=True, unique=True)), + ( + "conceptId", + models.CharField(blank=True, default=None, null=True, unique=True), + ), ("term", models.CharField(blank=True, default=None, null=True)), ( "preferredTerm", @@ -2844,7 +2853,10 @@ class Migration(migrations.Migration): ), ("created_at", models.DateTimeField(blank=True, editable=False)), ("updated_at", models.DateTimeField(blank=True, editable=False)), - ("conceptId", models.CharField(blank=True, default=None, null=True, unique=True)), + ( + "conceptId", + models.CharField(blank=True, default=None, null=True, unique=True), + ), ("term", models.CharField(blank=True, default=None, null=True)), ( "preferredTerm", @@ -3961,7 +3973,10 @@ class Migration(migrations.Migration): null=True, ), ), - ("conceptId", models.CharField(blank=True, default=None, null=True, unique=True)), + ( + "conceptId", + models.CharField(blank=True, default=None, null=True, unique=True), + ), ("term", models.CharField(blank=True, default=None, null=True)), ( "preferredTerm", @@ -6207,7 +6222,10 @@ class Migration(migrations.Migration): null=True, ), ), - ("conceptId", models.CharField(blank=True, default=None, null=True, unique=True)), + ( + "conceptId", + models.CharField(blank=True, default=None, null=True, unique=True), + ), ("term", models.CharField(blank=True, default=None, null=True)), ( "preferredTerm", diff --git a/epilepsy12/migrations/0005_seed_organisations.py b/epilepsy12/migrations/0005_seed_organisations.py index 2d48d87c6..2ac2a051e 100644 --- a/epilepsy12/migrations/0005_seed_organisations.py +++ b/epilepsy12/migrations/0005_seed_organisations.py @@ -80,7 +80,7 @@ def seed_organisations(apps, schema_editor): geocode_coordinates=new_point, telephone=rcpch_organisation["Phone"], ) - # add trust or local health board + # add trust or local health board and country if ( LocalHealthBoard.objects.filter( ods_code=rcpch_organisation["ParentODSCode"] diff --git a/epilepsy12/migrations/0046_alter_country_bng_e_alter_country_bng_n_and_more.py b/epilepsy12/migrations/0046_alter_country_bng_e_alter_country_bng_n_and_more.py new file mode 100644 index 000000000..a3412d43c --- /dev/null +++ b/epilepsy12/migrations/0046_alter_country_bng_e_alter_country_bng_n_and_more.py @@ -0,0 +1,48 @@ +# Generated by Django 5.1.2 on 2024-11-23 17:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("epilepsy12", "0045_alter_kpi_assessment_of_mental_health_issues_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="country", + name="bng_e", + field=models.BigIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name="country", + name="bng_n", + field=models.BigIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name="country", + name="globalid", + field=models.CharField(blank=True, max_length=38, null=True), + ), + migrations.AlterField( + model_name="country", + name="lat", + field=models.FloatField(blank=True, null=True), + ), + migrations.AlterField( + model_name="country", + name="long", + field=models.FloatField(blank=True, null=True), + ), + migrations.AlterField( + model_name="country", + name="welsh_name", + field=models.CharField(blank=True, max_length=17, null=True), + ), + migrations.AlterField( + model_name="trust", + name="ods_code", + field=models.CharField(max_length=10, unique=True), + ), + ] diff --git a/epilepsy12/migrations/0047_add_jersey_boundaries_to_country_remap_identifier.py b/epilepsy12/migrations/0047_add_jersey_boundaries_to_country_remap_identifier.py new file mode 100644 index 000000000..1052e6a0e --- /dev/null +++ b/epilepsy12/migrations/0047_add_jersey_boundaries_to_country_remap_identifier.py @@ -0,0 +1,57 @@ +# python imports +import os + +# django imports +from django.db import migrations + +# from django.apps import apps as django_apps +from django.apps import apps as django_apps +from django.contrib.gis.utils import LayerMapping + +Country = django_apps.get_model("epilepsy12", "Country") + +# Auto-generated `LayerMapping` dictionary for JerseyBoundary model +jerseyboundary_mapping = { + "boundary_identifier": "GID_0", + "name": "COUNTRY", + "geom": "MULTIPOLYGON", +} + +# Get the path to the shape file +app_config = django_apps.get_app_config("epilepsy12") +app_path = app_config.path +jersey_shp_file_path = os.path.join( + app_path, "shape_files", "gadm41_JEY_shp", "gadm41_JEY_0.shp" +) + + +def load_jersey_shape_file_mapping(apps, schema_editor): + """ + Load the Jersey shape file mapping into the database + """ + + # Load the Jersey shape file mapping into the database + lm = LayerMapping( + Country, + jersey_shp_file_path, + jerseyboundary_mapping, + transform=True, + source_srs=4326, + encoding="utf-8", + ) + # Note that the target srs is 27700 so that the boundaries are in the same projection as the rest of the boundaries + # in the database. By setting the SRID here of the source_srs to 4326, the LayerMapping will automatically transform + # the boundaries to the target srs of 27700. + + lm.save(strict=True, verbose=True) + + +class Migration(migrations.Migration): + + dependencies = [ + ("epilepsy12", "0046_alter_country_bng_e_alter_country_bng_n_and_more"), + ] + + operations = [ + migrations.RunPython(load_jersey_shape_file_mapping), + ] diff --git a/epilepsy12/migrations/0048_add_jersey_general_hospital_and_relationships.py b/epilepsy12/migrations/0048_add_jersey_general_hospital_and_relationships.py new file mode 100644 index 000000000..bdd3d0755 --- /dev/null +++ b/epilepsy12/migrations/0048_add_jersey_general_hospital_and_relationships.py @@ -0,0 +1,90 @@ +# Generated by Django 5.1.2 on 2024-11-23 17:41 + +# python imports +from datetime import date +import logging + +# Django imports +from django.db import migrations +from django.contrib.gis.geos import Point + +# Epilepsy12 imports +from epilepsy12.constants import JERSEY_ORGANISATION, JERSEY_NHS_TRUST + +logger = logging.getLogger(__name__) + + +def create_jersey_general_hospital(apps, schema_editor): + """ + Create the Jersey General Hospital + """ + Organisation = apps.get_model("epilepsy12", "Organisation") + Trust = apps.get_model("epilepsy12", "Trust") + Country = apps.get_model("epilepsy12", "Country") + OPENUKNetwork = apps.get_model("epilepsy12", "OPENUKNetwork") + + jersey = Country.objects.get(boundary_identifier="JEY") + swipe = OPENUKNetwork.objects.get(boundary_identifier="SWIPE") + + if Organisation.objects.filter( + ods_code=JERSEY_ORGANISATION["OrganisationCode"] + ).exists(): + logger.info("Jersey General Hospital already exists. Skipping creation.") + return + + # Create the Jersey General Hospital trust and assign it to Jersey, the country + jersey_trust = Trust.objects.create( + ods_code=JERSEY_NHS_TRUST["ods_code"], + name=JERSEY_NHS_TRUST["trust_name"], + address_line_1=JERSEY_NHS_TRUST["address_line_1"], + address_line_2=JERSEY_NHS_TRUST["address_line_2"], + town=JERSEY_NHS_TRUST["town"], + postcode=JERSEY_NHS_TRUST["postcode"], + country=jersey.name, + telephone=None, + website=None, + active=True, + published_at=date(2015, 4, 1), + ) + + # Create the Jersey General Hospital organisation and assign it to Jersey, the country and the Jersey General Hospital trust and the OPENUK Network + Organisation.objects.create( + ods_code=JERSEY_ORGANISATION["OrganisationCode"], + name=JERSEY_ORGANISATION["OrganisationName"], + website=JERSEY_ORGANISATION["Website"], + address1=JERSEY_ORGANISATION["Address1"], + address2=JERSEY_ORGANISATION["Address2"], + address3=JERSEY_ORGANISATION["Address3"], + city=JERSEY_ORGANISATION["City"], + county=JERSEY_ORGANISATION["County"], + latitude=float(JERSEY_ORGANISATION["Latitude"]), + longitude=float(JERSEY_ORGANISATION["Longitude"]), + postcode=JERSEY_ORGANISATION["Postcode"], + geocode_coordinates=Point( + x=float(JERSEY_ORGANISATION["Longitude"]), + y=float(JERSEY_ORGANISATION["Latitude"]), + ), + telephone=JERSEY_ORGANISATION["Phone"], + active=True, + published_at=date(2015, 4, 1), + country=jersey, + trust=jersey_trust, + local_health_board=None, + openuk_network=swipe, + nhs_england_region=None, + integrated_care_board=None, + london_borough=None, + ) + + logger.info("Jersey General Hospital created and all relationships added....") + + +class Migration(migrations.Migration): + + dependencies = [ + ("epilepsy12", "0047_add_jersey_boundaries_to_country_remap_identifier"), + ] + + operations = [ + migrations.RunPython(create_jersey_general_hospital), + ] diff --git a/epilepsy12/migrations/0049_case_unique_reference_number_and_more.py b/epilepsy12/migrations/0049_case_unique_reference_number_and_more.py new file mode 100644 index 000000000..fd10788da --- /dev/null +++ b/epilepsy12/migrations/0049_case_unique_reference_number_and_more.py @@ -0,0 +1,61 @@ +# Generated by Django 5.1.2 on 2024-11-23 22:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("epilepsy12", "0048_add_jersey_general_hospital_and_relationships"), + ] + + operations = [ + migrations.AddField( + model_name="case", + name="unique_reference_number", + field=models.CharField( + blank=True, + help_text="This is a unique reference number for Jersey patients. It is used to identify the patient in the audit.", + max_length=20, + null=True, + unique=True, + verbose_name="Unique Reference Number (URN)", + ), + ), + migrations.AddField( + model_name="historicalcase", + name="unique_reference_number", + field=models.CharField( + blank=True, + db_index=True, + help_text="This is a unique reference number for Jersey patients. It is used to identify the patient in the audit.", + max_length=20, + null=True, + verbose_name="Unique Reference Number (URN)", + ), + ), + migrations.AlterField( + model_name="case", + name="nhs_number", + field=models.CharField( + blank=True, + help_text="This is the NHS number for England and Wales. It is used to identify the patient in the audit.", + max_length=10, + null=True, + unique=True, + verbose_name="NHS Number", + ), + ), + migrations.AlterField( + model_name="historicalcase", + name="nhs_number", + field=models.CharField( + blank=True, + db_index=True, + help_text="This is the NHS number for England and Wales. It is used to identify the patient in the audit.", + max_length=10, + null=True, + verbose_name="NHS Number", + ), + ), + ] diff --git a/epilepsy12/migrations/0050_alter_case_unique_together.py b/epilepsy12/migrations/0050_alter_case_unique_together.py new file mode 100644 index 000000000..747642e6b --- /dev/null +++ b/epilepsy12/migrations/0050_alter_case_unique_together.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2024-12-03 21:44 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("epilepsy12", "0049_case_unique_reference_number_and_more"), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="case", + unique_together={("nhs_number", "unique_reference_number")}, + ), + ] diff --git a/epilepsy12/models_folder/__init__.py b/epilepsy12/models_folder/__init__.py index edfda034e..7fbdd9b52 100644 --- a/epilepsy12/models_folder/__init__.py +++ b/epilepsy12/models_folder/__init__.py @@ -64,7 +64,6 @@ from .entities.country import Country from .entities.london_borough import LondonBorough - from .entities.open_uk_network import OPENUKNetwork from .entities.integrated_care_board import IntegratedCareBoard from .entities.nhs_england_region import NHSEnglandRegion @@ -72,5 +71,8 @@ from .entities.trust import Trust from .entities.local_health_board import LocalHealthBoard -from .organisational_audit import OrganisationalAuditSubmissionPeriod, OrganisationalAuditSubmission -from .banner import Banner \ No newline at end of file +from .organisational_audit import ( + OrganisationalAuditSubmissionPeriod, + OrganisationalAuditSubmission, +) +from .banner import Banner diff --git a/epilepsy12/models_folder/case.py b/epilepsy12/models_folder/case.py index 2cc66acae..d326e4372 100644 --- a/epilepsy12/models_folder/case.py +++ b/epilepsy12/models_folder/case.py @@ -64,7 +64,20 @@ class Case(TimeStampAbstractBaseClass, UserStampAbstractBaseClass, HelpTextMixin blank=True, ) nhs_number = models.CharField( # the NHS number for England and Wales - "NHS Number", unique=True, blank=True, null=True, max_length=10 + "NHS Number", + unique=True, + blank=True, + null=True, + max_length=10, + help_text="This is the NHS number for England and Wales. It is used to identify the patient in the audit.", + ) + unique_reference_number = models.CharField( + "Unique Reference Number (URN)", + unique=True, + blank=True, + null=True, + max_length=20, + help_text="This is a unique reference number for Jersey patients. It is used to identify the patient in the audit.", ) first_name = CharField( "First name", @@ -201,6 +214,12 @@ def save(self, *args, **kwargs) -> None: Both are included here and stored in the model, as the shape files for the UK health boundaries are produced as BNG, rather than WGS84. """ try: + # If the postcode begins with JE, it is a Jersey postcode. Skip the coordinates lookup. + if self.postcode.lower().startswith("je"): + self.location_wgs84 = None + self.location_bng = None + return super().save(*args, **kwargs) + # Fetch the coordinates (WGS 84) lon, lat = coordinates_for_postcode(postcode=self.postcode) @@ -221,6 +240,7 @@ def save(self, *args, **kwargs) -> None: logger.exception( f"Cannot get longitude and latitude for {self.postcode}: {error}" ) + pass else: # if the IMD quintile has previously been added and postcode now unknown, set # index_of_multiple_deprivation_quintile back to None @@ -241,6 +261,7 @@ def delete(self, *args, **kwargs): class Meta: verbose_name = "Patient" verbose_name_plural = "Patients" + unique_together = ["nhs_number", "unique_reference_number"] # custom permissions for Case class permissions = [ CAN_LOCK_CHILD_CASE_DATA_FROM_EDITING, diff --git a/epilepsy12/models_folder/entities/country.py b/epilepsy12/models_folder/entities/country.py index 917b7379f..b192391e4 100644 --- a/epilepsy12/models_folder/entities/country.py +++ b/epilepsy12/models_folder/entities/country.py @@ -19,12 +19,12 @@ class CountryBoundaries(models.Model): boundary_identifier = models.CharField(max_length=9) name = models.CharField(max_length=16) - welsh_name = 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) + welsh_name = models.CharField(max_length=17, null=True, blank=True) + bng_e = models.BigIntegerField(null=True, blank=True) + bng_n = models.BigIntegerField(null=True, blank=True) + long = models.FloatField(null=True, blank=True) + lat = models.FloatField(null=True, blank=True) + globalid = models.CharField(max_length=38, null=True, blank=True) geom = models.MultiPolygonField(srid=27700) class Meta: diff --git a/epilepsy12/models_folder/entities/trust.py b/epilepsy12/models_folder/entities/trust.py index e6c0620f6..0d0204ec5 100644 --- a/epilepsy12/models_folder/entities/trust.py +++ b/epilepsy12/models_folder/entities/trust.py @@ -3,7 +3,7 @@ class Trust(TimeStampAbstractBaseClass): - ods_code = models.CharField(max_length=3, unique=True) + ods_code = models.CharField(max_length=10, unique=True) name = models.CharField(max_length=100) address_line_1 = models.CharField( max_length=100, null=True, blank=True, default=None diff --git a/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_0.cpg b/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_0.cpg new file mode 100644 index 000000000..3ad133c04 --- /dev/null +++ b/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_0.cpg @@ -0,0 +1 @@ +UTF-8 \ No newline at end of file diff --git a/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_0.dbf b/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_0.dbf new file mode 100644 index 000000000..67c62f334 Binary files /dev/null and b/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_0.dbf differ diff --git a/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_0.prj b/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_0.prj new file mode 100644 index 000000000..f45cbadf0 --- /dev/null +++ b/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_0.prj @@ -0,0 +1 @@ +GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]] \ No newline at end of file diff --git a/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_0.shp b/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_0.shp new file mode 100644 index 000000000..329845e77 Binary files /dev/null and b/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_0.shp differ diff --git a/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_0.shx b/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_0.shx new file mode 100644 index 000000000..fdf50b32c Binary files /dev/null and b/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_0.shx differ diff --git a/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_1.cpg b/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_1.cpg new file mode 100644 index 000000000..3ad133c04 --- /dev/null +++ b/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_1.cpg @@ -0,0 +1 @@ +UTF-8 \ No newline at end of file diff --git a/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_1.dbf b/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_1.dbf new file mode 100644 index 000000000..4cf277f4c Binary files /dev/null and b/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_1.dbf differ diff --git a/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_1.prj b/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_1.prj new file mode 100644 index 000000000..f45cbadf0 --- /dev/null +++ b/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_1.prj @@ -0,0 +1 @@ +GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]] \ No newline at end of file diff --git a/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_1.shp b/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_1.shp new file mode 100644 index 000000000..3d81604ed Binary files /dev/null and b/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_1.shp differ diff --git a/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_1.shx b/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_1.shx new file mode 100644 index 000000000..7271e0476 Binary files /dev/null and b/epilepsy12/shape_files/gadm41_JEY_shp/gadm41_JEY_1.shx differ 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 index e0f390af3..ab0a871e4 100644 --- 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 @@ -42,8 +42,6 @@ def test_measure_10_school_individual_healthcare_plan( FIRST_PAEDIATRIC_ASSESSMENT_DATE = date(2023, 1, 1) DATE_OF_BIRTH = FIRST_PAEDIATRIC_ASSESSMENT_DATE - age - print(f"age: {age}") - # create case case = e12_case_factory( date_of_birth=DATE_OF_BIRTH, diff --git a/epilepsy12/tests/factories/E12CaseFactory.py b/epilepsy12/tests/factories/E12CaseFactory.py index c7cf8a690..592f0422f 100644 --- a/epilepsy12/tests/factories/E12CaseFactory.py +++ b/epilepsy12/tests/factories/E12CaseFactory.py @@ -11,7 +11,7 @@ from .E12SiteFactory import E12SiteFactory from .E12RegistrationFactory import E12RegistrationFactory from epilepsy12.constants import SEX_TYPE, DEPRIVATION_QUINTILES -import nhs_number +import nhs_number as nhs_number_package class E12CaseFactory(factory.django.DjangoModelFactory): @@ -68,13 +68,30 @@ class Params: registration=None, ) + is_jersey = factory.Trait( + nhs_number=None, + unique_reference_number=factory.Sequence(lambda n: f"JERSEY-{n}"), + ) + + @factory.lazy_attribute + def unique_reference_number(self): + """Returns a unique reference number which has not been used in the db yet.""" + not_found_unique_ref_num = True + + while not_found_unique_ref_num: + candidate_num = f"JEY-{nhs_number_package.generate()[0]}" + + if not Case.objects.filter(unique_reference_number=candidate_num).exists(): + not_found_unique_ref_num = False + return candidate_num + @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] + candidate_num = nhs_number_package.generate()[0] if not Case.objects.filter(nhs_number=candidate_num).exists(): not_found_unique_nhs_num = False diff --git a/epilepsy12/tests/model_tests/test_investigations.py b/epilepsy12/tests/model_tests/test_investigations.py index 92f609027..843d3b6a9 100644 --- a/epilepsy12/tests/model_tests/test_investigations.py +++ b/epilepsy12/tests/model_tests/test_investigations.py @@ -147,8 +147,6 @@ def test_validation_request_or_performed_in_future( # 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( diff --git a/epilepsy12/tests/model_tests/test_registration.py b/epilepsy12/tests/model_tests/test_registration.py index 969ad4a54..57605be75 100644 --- a/epilepsy12/tests/model_tests/test_registration.py +++ b/epilepsy12/tests/model_tests/test_registration.py @@ -541,10 +541,6 @@ def test_accept_registration_transfer_response_previously_involved( assert kch_site.case == case, "KCH is still historically associated with the case" # GOSH is now the primary site of care - for filter_site in Site.objects.filter(organisation=GOSH, case=case): - print( - f"primary: {filter_site.site_is_primary_centre_of_epilepsy_care}, general: {filter_site.site_is_general_paediatric_centre}, active: {filter_site.site_is_actively_involved_in_epilepsy_care}, primary: {filter_site.site_is_primary_centre_of_epilepsy_care}" - ) new_site = Site.objects.get( organisation=GOSH, case=case, @@ -732,10 +728,6 @@ def test_accept_registration_transfer_response_transfer_centre_still_involved( assert kch_site.case == case, "KCH is still a general paediatric centre" # GOSH is now the primary site of care - for filter_site in Site.objects.filter(organisation=GOSH, case=case): - print( - f"primary: {filter_site.site_is_primary_centre_of_epilepsy_care}, general: {filter_site.site_is_general_paediatric_centre}, active: {filter_site.site_is_actively_involved_in_epilepsy_care}, primary: {filter_site.site_is_primary_centre_of_epilepsy_care}" - ) new_site = Site.objects.get( organisation=GOSH, case=case, @@ -1181,11 +1173,6 @@ def test_reject_registration_transfer_response_transfer_centre_still_involved( assert response.status_code == 200 - for mysite in Site.objects.all(): - print( - f"name: {mysite} active transfer {mysite.active_transfer}, general paediatrics {mysite.site_is_general_paediatric_centre}, primary epilepsy {mysite.site_is_primary_centre_of_epilepsy_care}, actively involved {mysite.site_is_actively_involved_in_epilepsy_care}" - ) - # Verify the Site instance kch_sites = Site.objects.filter( organisation=KCH, diff --git a/epilepsy12/tests/view_tests/db_actions/test_update_actions.py b/epilepsy12/tests/view_tests/db_actions/test_update_actions.py index 004ae8959..4f94ca113 100644 --- a/epilepsy12/tests/view_tests/db_actions/test_update_actions.py +++ b/epilepsy12/tests/view_tests/db_actions/test_update_actions.py @@ -366,6 +366,7 @@ [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 @@ -385,7 +386,9 @@ from epilepsy12.tests.factories import ( E12CaseFactory, ) -from epilepsy12.tests.view_tests.permissions_tests.perm_tests_utils import twofactor_signin +from epilepsy12.tests.view_tests.permissions_tests.perm_tests_utils import ( + twofactor_signin, +) # E12 imports from epilepsy12.models import ( @@ -864,7 +867,6 @@ def test_user_updates_toggles_true_success(client): 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") ) @@ -1239,7 +1241,7 @@ def test_age_at_registration_cannot_be_gt_24yo(client, GOSH): ) client.force_login(test_user) - + # 2fa enable twofactor_signin(client, test_user) diff --git a/epilepsy12/tests/view_tests/form_calculations/test_completed_fields.py b/epilepsy12/tests/view_tests/form_calculations/test_completed_fields.py index 0b280019a..3e2606997 100644 --- a/epilepsy12/tests/view_tests/form_calculations/test_completed_fields.py +++ b/epilepsy12/tests/view_tests/form_calculations/test_completed_fields.py @@ -519,7 +519,6 @@ def test_completed_fields_assessment_random_fields(e12_case_factory, GOSH): 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 diff --git a/epilepsy12/tests/view_tests/permissions_tests/test_perms_download_e12users_list.py b/epilepsy12/tests/view_tests/permissions_tests/test_perms_download_e12users_list.py index 6a10b553e..9e9b346b1 100644 --- a/epilepsy12/tests/view_tests/permissions_tests/test_perms_download_e12users_list.py +++ b/epilepsy12/tests/view_tests/permissions_tests/test_perms_download_e12users_list.py @@ -18,7 +18,9 @@ Epilepsy12User, Organisation, ) -from epilepsy12.tests.view_tests.permissions_tests.perm_tests_utils import twofactor_signin +from epilepsy12.tests.view_tests.permissions_tests.perm_tests_utils import ( + twofactor_signin, +) @pytest.mark.django_db @@ -52,8 +54,6 @@ def test_download_button_access( ) ) if test_user.first_name.endswith(test_user_rcpch_audit_team_data.role_str): - - print(response) assert ( response["Content-Disposition"] == 'attachment; filename="epilepsy12users.csv"' diff --git a/epilepsy12/views/case_views.py b/epilepsy12/views/case_views.py index 47408f892..121ef91f0 100644 --- a/epilepsy12/views/case_views.py +++ b/epilepsy12/views/case_views.py @@ -85,6 +85,7 @@ def case_list(request, organisation_id): Q(first_name__icontains=filter_term) | Q(surname__icontains=filter_term) | Q(nhs_number__icontains=filter_term) + | Q(unique_reference_number__icontains=filter_term) | Q(id__icontains=filter_term) ) ) @@ -111,6 +112,7 @@ def case_list(request, organisation_id): Q(first_name__icontains=filter_term) | Q(surname__icontains=filter_term) | Q(nhs_number__icontains=filter_term) + | Q(unique_reference_number__icontains=filter_term) | Q(id__icontains=filter_term) ) ) @@ -127,6 +129,7 @@ def case_list(request, organisation_id): Q(first_name__icontains=filter_term) | Q(surname__icontains=filter_term) | Q(nhs_number__icontains=filter_term) + | Q(unique_reference_number__icontains=filter_term) | Q(id__icontains=filter_term) ) ) @@ -141,6 +144,8 @@ def case_list(request, organisation_id): Only RCPCH audit staff have this final option. """ + jersey_flag = organisation.country.boundary_identifier == "JEY" + if request.user.view_preference == 2: # this is an RCPCH audit team member requesting National level filtered_cases = Case.objects.all() @@ -174,12 +179,16 @@ def case_list(request, organisation_id): or request.GET.get("sort_flag") == "sort_by_nhs_number_up" ): all_cases = filtered_cases.order_by("nhs_number").all() + if jersey_flag: + all_cases = filtered_cases.order_by("unique_reference_number").all() sort_flag = "sort_by_nhs_number_up" elif ( request.htmx.trigger_name == "sort_by_nhs_number_down" or request.GET.get("sort_flag") == "sort_by_nhs_number_down" ): all_cases = filtered_cases.order_by("-nhs_number").all() + if jersey_flag: + all_cases = filtered_cases.order_by("-unique_reference_number").all() sort_flag = "sort_by_nhs_number_down" elif ( request.htmx.trigger_name == "sort_by_ethnicity_up" @@ -618,7 +627,7 @@ def create_case(request, organisation_id): country_choice, ("ZZ993VZ", "No fixed abode"), ) - form = CaseForm(request.POST or None) + form = CaseForm(request.POST or None, organisation_id=organisation_id) template_name = "epilepsy12/cases/case.html" @@ -636,13 +645,24 @@ def create_case(request, organisation_id): organisation=organisation, case=obj, ) - messages.success(request, "You successfully created the case") + messages.success( + request, + f"You successfully add {obj} to Epilepsy12. They can now be registered in to the current cohort.", + ) return redirect("cases", organisation_id=organisation_id) else: + error_messages = [] + for field, errors in form.errors.items(): + for error in errors: + if field == "__all__": + error_messages.append(f"{error}") + else: + error_messages.append(f"{field}: {error}") + error_messages = ", ".join(error_messages) messages.error( - request=request, message="It was not possible to save the case" + request=request, + message=f"It was not possible to save the case: {error_messages}", ) - logger.info(f"Invalid data provided to case form: {form.errors}") context = { "organisation_id": organisation_id, @@ -662,13 +682,15 @@ def update_case(request, organisation_id, case_id): """ Django function based view. Receives POST request to update view or delete """ - case = get_object_or_404(Case, pk=case_id) - form = CaseForm(instance=case) - organisation = Organisation.objects.filter(pk=organisation_id).get() + case = get_object_or_404(Case, pk=case_id) + form = CaseForm(instance=case, organisation_id=organisation_id) # set select boxes for situations when postcode unknown - country_choice = ("ZZ993CZ", "Address unspecified - England") + country_choice = ( + "ZZ993CZ", + "Address unspecified - England", + ) # TODO we are using the same value for England and Jersey. is that correct? if organisation.country.boundary_identifier == "W92000004": country_choice = ("ZZ993GZ", "Address unspecified - Wales") @@ -694,7 +716,7 @@ def update_case(request, organisation_id, case_id): return HttpResponseClientRedirect(redirect_to=url, status=200) if request.method == "POST": - form = CaseForm(request.POST, instance=case) + form = CaseForm(request.POST, instance=case, organisation_id=organisation_id) if form.is_valid(): obj = form.save() if case.locked != obj.locked: @@ -782,6 +804,10 @@ def opt_out(request, organisation_id, case_id): f"All data on {case} has been permanently removed from Epilepsy12. The Epilepsy12 unique identifier has been preserved to contribute to annual totals.", ) case.nhs_number = None + case.unique_reference_number = None + case.location_wgs84 = None + case.location_bng = None + case.location_wgs = None case.first_name = None case.surname = None case.sex = None diff --git a/epilepsy12/views/multiaxial_diagnosis_views.py b/epilepsy12/views/multiaxial_diagnosis_views.py index 09bb8f9b3..5409fcfc4 100644 --- a/epilepsy12/views/multiaxial_diagnosis_views.py +++ b/epilepsy12/views/multiaxial_diagnosis_views.py @@ -1622,8 +1622,6 @@ def close_comorbidity(request, comorbidity_id): comorbidity = Comorbidity.objects.get(pk=comorbidity_id) multiaxial_diagnosis = comorbidity.multiaxial_diagnosis - print() - # if all the fields are none this was not completed - delete the record if ( completed_fields(comorbidity) == 0 diff --git a/epilepsy12/views/organisation_views.py b/epilepsy12/views/organisation_views.py index fc6a55d5c..f62907493 100644 --- a/epilepsy12/views/organisation_views.py +++ b/epilepsy12/views/organisation_views.py @@ -105,6 +105,7 @@ def selected_organisation_summary(request, organisation_id): ) ) + # differentiate between England and Wales if selected_organisation.country.boundary_identifier == "W92000004": # Wales abstraction_level = "local_health_board" # generate choropleth map of case counts for each level of abstraction @@ -116,19 +117,23 @@ def selected_organisation_summary(request, organisation_id): ) else: # generate choropleth map of case counts for each level of abstraction - abstraction_level = "trust" - icb_heatmap = generate_case_count_choropleth_map( - properties="ods_code", - abstraction_level=EnumAbstractionLevel.ICB, - organisation=selected_organisation, - cohort=cohort_number, - ) - nhsregion_heatmap = generate_case_count_choropleth_map( - properties="region_code", - abstraction_level=EnumAbstractionLevel.NHS_ENGLAND_REGION, - organisation=selected_organisation, - cohort=cohort_number, - ) + if selected_organisation.ods_code == "RGT1W": + # Jersey is a special case and although is mapped to England, is in the Channel Islands and has no ICB, NHS Region or LHB + abstraction_level = "trust" + else: + abstraction_level = "trust" + icb_heatmap = generate_case_count_choropleth_map( + properties="ods_code", + abstraction_level=EnumAbstractionLevel.ICB, + organisation=selected_organisation, + cohort=cohort_number, + ) + nhsregion_heatmap = generate_case_count_choropleth_map( + properties="region_code", + abstraction_level=EnumAbstractionLevel.NHS_ENGLAND_REGION, + organisation=selected_organisation, + cohort=cohort_number, + ) country_heatmap = generate_case_count_choropleth_map( properties="boundary_identifier", @@ -146,22 +151,23 @@ def selected_organisation_summary(request, organisation_id): 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 = ( + # 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_number, - case_complete=True, - abstraction_level=abstraction_level, + case_complete=False, + abstraction_level="organisation", ).count() ) - # 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 = ( + + # 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_number, - case_complete=False, - abstraction_level="organisation", + case_complete=True, + abstraction_level=abstraction_level, ).count() ) # query to return all cases (including incomplete) registered in the current cohort at this organisation trust @@ -260,9 +266,15 @@ def selected_organisation_summary(request, organisation_id): context["icb_heatmap"] = None context["nhsregion_heatmap"] = None else: - context["lhb_heatmap"] = None - context["icb_heatmap"] = icb_heatmap - context["nhsregion_heatmap"] = nhsregion_heatmap + if selected_organisation.ods_code == "RGT1W": + # Jersey is a special case as it is in the Channel Islands and has no ICB, NHS Region or LHB + context["trust_heatmap"] = None + context["icb_heatmap"] = None + context["nhsregion_heatmap"] = None + else: + context["lhb_heatmap"] = None + context["icb_heatmap"] = icb_heatmap + context["nhsregion_heatmap"] = nhsregion_heatmap return render( request=request, diff --git a/templates/epilepsy12/audit_section.html b/templates/epilepsy12/audit_section.html index 90b32524f..ffcb2cd15 100644 --- a/templates/epilepsy12/audit_section.html +++ b/templates/epilepsy12/audit_section.html @@ -18,7 +18,7 @@
{% if registration %} - {{ registration.case.first_name }} {{ registration.case.surname }} {{ registration.case.nhs_number}} + {{ registration.case.first_name }} {{ registration.case.surname }} {% if registration.case.unique_reference_number is not None %}{{ registration.case.unique_reference_number }}{% else %}{{ registration.case.nhs_number }}{% endif %} {% elif case %} {{ case.first_name }} {{ case.surname }} {{ case.nhs_number }} {% endif %} diff --git a/templates/epilepsy12/forms/case_form.html b/templates/epilepsy12/forms/case_form.html index d1ab13789..43b9cd6c9 100644 --- a/templates/epilepsy12/forms/case_form.html +++ b/templates/epilepsy12/forms/case_form.html @@ -65,12 +65,21 @@ -
- - {{form.nhs_number}} {% if form.nhs_number.errors %} -
{{form.nhs_number.errors}}
- {% endif %} -
+ {% if organisation.country.boundary_identifier == "JEY" %} +
+ + {{form.unique_reference_number}} {% if form.unique_reference_number.errors %} +
{{form.unique_reference_number.errors}}
+ {% endif %} +
+ {% else %} +
+ + {{form.nhs_number}} {% if form.nhs_number.errors %} +
{{form.nhs_number.errors}}
+ {% endif %} +
+ {% endif %}
diff --git a/templates/epilepsy12/partials/case_table.html b/templates/epilepsy12/partials/case_table.html index 73b07bcf6..9b29e5481 100644 --- a/templates/epilepsy12/partials/case_table.html +++ b/templates/epilepsy12/partials/case_table.html @@ -42,7 +42,7 @@
-

NHS Number

+ {% if organisation.country.boundary_identifier == 'JEY' %}

Unique Reference Number (URN)

{% else %}NHS Number{% endif %} {% if sort_flag and sort_flag == "sort_by_nhs_number_down" %} @@ -126,7 +126,7 @@ {% none_masked case.first_name %} {% none_masked case.surname %} {% none_masked case.get_sex_display %} - {% none_masked case.nhs_number %} + {% if organisation.country.boundary_identifier == 'JEY' %}{{ case.unique_reference_number }}{% else %}{% none_masked case.nhs_number %}{% endif %} {% none_masked case.registration.audit_submission_date|date:'D j M Y' %} {% if case.registration.cohort is not None %}{{ case.registration.cohort }}{% endif %} {% if case.registration.days_remaining_before_submission is not None %}{{ case.registration.days_remaining_before_submission }}{% endif %} diff --git a/templates/epilepsy12/partials/kpis/kpi_table_head.html b/templates/epilepsy12/partials/kpis/kpi_table_head.html index 8e4d73ed6..72b3f72f9 100644 --- a/templates/epilepsy12/partials/kpis/kpi_table_head.html +++ b/templates/epilepsy12/partials/kpis/kpi_table_head.html @@ -29,6 +29,7 @@ _="init js $('#totals_trust_{{kpi_head_id}}').popup(); end" >({{all_data.TRUST_KPIS.total_cases_registered}})

+ {% if country.boundary_identifier != 'JEY' %}

Integrated Care Board

({{all_data.NHS_ENGLAND_REGION_KPIS.total_cases_registered}})

+ {% endif %} {% else %}

Local Health Board

diff --git a/templates/epilepsy12/partials/kpis/kpis.html b/templates/epilepsy12/partials/kpis/kpis.html index 32b07ca01..1b8417e9f 100644 --- a/templates/epilepsy12/partials/kpis/kpis.html +++ b/templates/epilepsy12/partials/kpis/kpis.html @@ -50,7 +50,7 @@

Clinical Team

- {% include './kpi_table_head.html' with kpi_head_id=1 %} + {% include './kpi_table_head.html' with kpi_head_id=1 country=organisation.country %} {% comment %} [ @@ -232,7 +232,7 @@

Clinical Team

Investigations

{% comment %} [ECG, MRI,] {% endcomment %}
- {% include './kpi_table_head.html' with kpi_head_id=2 %} + {% include './kpi_table_head.html' with kpi_head_id=2 country=organisation.country %} {% for kpi_name in kpi_names_list|slice:"4:6" %} @@ -294,7 +294,7 @@

Investigations

Mental Health

{% comment %} ['assessment_of_mental_health_issues', 'mental_health_support'] {% endcomment %}
- {% include './kpi_table_head.html' with kpi_head_id=3 %} + {% include './kpi_table_head.html' with country=organisation.country kpi_head_id=3 %} {% for kpi_name in kpi_names_list|slice:"6:8" %} @@ -356,7 +356,7 @@

Mental Health

Medication

{% comment %} [Sodium valproate] {% endcomment %}
- {% include './kpi_table_head.html' with kpi_head_id=4 %} + {% include './kpi_table_head.html' with country=organisation.country kpi_head_id=4 %} {% comment %} {% endcomment %} {% for kpi_name in kpi_names_list|slice:"8:9" %} @@ -418,7 +418,7 @@

Medication

Care Planning

{% comment %} ['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', 'sudep', 'service_contact_details', 'school_individual_healthcare_plan'] {% endcomment %}
- {% include './kpi_table_head.html' with kpi_head_id=5 %} + {% include './kpi_table_head.html' with country=organisation.country kpi_head_id=5 %} {% comment %} comprehensive_care_planning_agreement {% endcomment %} diff --git a/templates/epilepsy12/partials/selected_organisation_summary.html b/templates/epilepsy12/partials/selected_organisation_summary.html index 2646f95c8..e2f1e4817 100644 --- a/templates/epilepsy12/partials/selected_organisation_summary.html +++ b/templates/epilepsy12/partials/selected_organisation_summary.html @@ -376,6 +376,9 @@

{{selected_organisation}}
+ {% if selected_organisation.country.name != "Jersey" %} + +
@@ -393,17 +396,17 @@

- {% else %} + {% elif selected_organisation.local_health_board %}

Local Health Board

{% endif %}
- {% if selected_organisation.trust %} + {% if selected_organisation.trust %}
- {% else %} + {% elif selected_organisation.local_health_board %}
Local Health Board

+ {% if selected_organisation.trust %}
@@ -440,6 +444,8 @@

{% endif %} + {% endif %} +