From a0b87f4e594382b087908aaeab6ce8e6402b118e Mon Sep 17 00:00:00 2001 From: Kennedy Kori Date: Tue, 24 Aug 2021 11:17:18 +0300 Subject: [PATCH] chore: change optionable input fields to choice fields --- .gitignore | 30 +-- .idea/misc.xml | 6 - .idea/modules.xml | 8 - .idea/pepfar_mle.iml | 45 ---- .../merge_production_dotenvs_in_dotenv.xml | 21 -- .idea/runConfigurations/migrate.xml | 32 --- .idea/runConfigurations/pytest___.xml | 25 --- .idea/runConfigurations/pytest__users.xml | 25 --- .idea/runConfigurations/runserver.xml | 33 --- .idea/runConfigurations/runserver_plus.xml | 33 --- .idea/vcs.xml | 6 - .idea/webResources.xml | 14 -- .vscode/settings.json | 121 ----------- fahari/common/constants.py | 177 +++++++++++----- .../0018_alter_facility_keph_level.py | 183 +++++++++++++++++ fahari/common/models.py | 149 +++++++++++++- .../tests/test_administrative_unit_utils.py | 125 ++++++++++++ fahari/common/tests/test_models.py | 65 +++++- fahari/common/utils/__init__.py | 25 +++ .../common/utils/administrative_unit_utils.py | 192 ++++++++++++++++++ scripts/mark_fahari_facilities.py | 0 21 files changed, 858 insertions(+), 457 deletions(-) delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/pepfar_mle.iml delete mode 100644 .idea/runConfigurations/merge_production_dotenvs_in_dotenv.xml delete mode 100644 .idea/runConfigurations/migrate.xml delete mode 100644 .idea/runConfigurations/pytest___.xml delete mode 100644 .idea/runConfigurations/pytest__users.xml delete mode 100644 .idea/runConfigurations/runserver.xml delete mode 100644 .idea/runConfigurations/runserver_plus.xml delete mode 100644 .idea/vcs.xml delete mode 100644 .idea/webResources.xml delete mode 100644 .vscode/settings.json create mode 100644 fahari/common/migrations/0018_alter_facility_keph_level.py create mode 100644 fahari/common/tests/test_administrative_unit_utils.py create mode 100644 fahari/common/utils/__init__.py create mode 100644 fahari/common/utils/administrative_unit_utils.py mode change 100644 => 100755 scripts/mark_fahari_facilities.py diff --git a/.gitignore b/.gitignore index 67a73c3d..8658e84f 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ coverage.xml *.cover .hypothesis/ junitxml_report/* +/**/junitxml_report/* # Translations *.mo @@ -151,11 +152,6 @@ typings/ ### VisualStudioCode template .vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace # Local History for Visual Studio Code .history/ @@ -169,29 +165,10 @@ typings/ # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 -# User-specific stuff: -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/dictionaries - -# Sensitive or high-churn files: -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.xml -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml - -# Gradle: -.idea/**/gradle.xml -.idea/**/libraries # CMake cmake-build-debug/ -# Mongo Explorer plugin: -.idea/**/mongoSettings.xml ## File-based project format: *.iws @@ -199,6 +176,7 @@ cmake-build-debug/ ## Plugin-specific files: # IntelliJ +.idea/* out/ # mpeltonen/sbt-idea plugin @@ -207,9 +185,6 @@ out/ # JIRA plugin atlassian-ide-plugin.xml -# Cursive Clojure plugin -.idea/replstate.xml - # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties @@ -317,6 +292,7 @@ Session.vim # Temporary .netrwhist +.~lock.* # Auto-generated tag files tags diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 10af178f..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 0653f384..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/.idea/pepfar_mle.iml b/.idea/pepfar_mle.iml deleted file mode 100644 index 0c164939..00000000 --- a/.idea/pepfar_mle.iml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.idea/runConfigurations/merge_production_dotenvs_in_dotenv.xml b/.idea/runConfigurations/merge_production_dotenvs_in_dotenv.xml deleted file mode 100644 index fcc355cc..00000000 --- a/.idea/runConfigurations/merge_production_dotenvs_in_dotenv.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - diff --git a/.idea/runConfigurations/migrate.xml b/.idea/runConfigurations/migrate.xml deleted file mode 100644 index 812ad795..00000000 --- a/.idea/runConfigurations/migrate.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - diff --git a/.idea/runConfigurations/pytest___.xml b/.idea/runConfigurations/pytest___.xml deleted file mode 100644 index 5d3ef5e6..00000000 --- a/.idea/runConfigurations/pytest___.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - diff --git a/.idea/runConfigurations/pytest__users.xml b/.idea/runConfigurations/pytest__users.xml deleted file mode 100644 index 829d8c5a..00000000 --- a/.idea/runConfigurations/pytest__users.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - diff --git a/.idea/runConfigurations/runserver.xml b/.idea/runConfigurations/runserver.xml deleted file mode 100644 index 101c5b21..00000000 --- a/.idea/runConfigurations/runserver.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - diff --git a/.idea/runConfigurations/runserver_plus.xml b/.idea/runConfigurations/runserver_plus.xml deleted file mode 100644 index 9b8eca7c..00000000 --- a/.idea/runConfigurations/runserver_plus.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 5ace414d..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/.idea/webResources.xml b/.idea/webResources.xml deleted file mode 100644 index 15b4f865..00000000 --- a/.idea/webResources.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 28db038f..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "cSpell.words": [ - "action", - "analyze", - "baringo", - "bomet", - "bouvet", - "buildkit", - "bungoma", - "busia", - "cabo", - "check", - "codeql", - "cunha", - "cura", - "d'ivoire", - "darussalam", - "dcop", - "docker", - "e", - "elgeyo", - "embu", - "eustatius", - "ex", - "fahari", - "faso", - "forwarded", - "gaierror", - "garissa", - "gcloud", - "gethostbyname", - "gishu", - "github", - "homa", - "http", - "ipt", - "isiolo", - "isready", - "itertools", - "jamii", - "jpetrucciani", - "jsmin", - "kajiado", - "kericho", - "kiambu", - "kilifi", - "kisii", - "kitts", - "kitui", - "kwale", - "laikipia", - "lamu", - "lemy", - "leste", - "maarten", - "machakos", - "makueni", - "mandera", - "marakwet", - "marino", - "marsabit", - "master", - "mayen", - "meru", - "migori", - "mle", - "murang", - "murang'a", - "muranga", - "mypy", - "nakuru", - "nandi", - "narok", - "new", - "nithi", - "nocover", - "noinput", - "noqa", - "nostatic", - "nyamira", - "nyandarua", - "nyeri", - "nzoia", - "organisation", - "organisations", - "paeds", - "pg", - "phne", - "plurinational", - "pokot", - "postgres", - "proto", - "pytest", - "pytestmark", - "reprs", - "rica", - "runserver", - "samburu", - "savannahghi", - "siaya", - "sint", - "spreadsheetml", - "string", - "taita", - "taveta", - "test", - "tokelau", - "u", - "uasin", - "v", - "viet", - "vihiga", - "wajir", - "wordprocessingml", - "x", - "zfill" - ], - "python.linting.pylintEnabled": true, - "python.formatting.provider": "black", - "python.linting.enabled": true -} diff --git a/fahari/common/constants.py b/fahari/common/constants.py index 118d4362..e9cbbe06 100644 --- a/fahari/common/constants.py +++ b/fahari/common/constants.py @@ -2,55 +2,134 @@ "Kajiado", "Nairobi", ] -COUNTIES = ( - ("47_NAIROBI_CITY", "Nairobi City"), - ("46_NYAMIRA", "Nyamira"), - ("45_KISII", "Kisii"), - ("44_MIGORI", "Migori"), - ("43_HOMA_BAY", "Homa Bay"), - ("42_KISUMU", "Kisumu"), - ("41_SIAYA", "Siaya"), - ("40_BUSIA", "Busia"), - ("39_BUNGOMA", "Bungoma"), - ("38_VIHIGA", "Vihiga"), - ("37_KAKAMEGA", "Kakamega"), - ("36_BOMET", "Bomet"), - ("35_KERICHO", "Kericho"), - ("34_KAJIADO", "Kajiado"), - ("33_NAROK", "Narok"), - ("32_NAKURU", "Nakuru"), - ("31_LAIKIPIA", "Laikipia"), - ("30_BARINGO", "Baringo"), - ("29_NANDI", "Nandi"), - ("28_ELGEYO_MARAKWET", "Elgeyo/Marakwet"), - ("27_UASIN_GISHU", "Uasin Gishu"), - ("26_TRANS_NZOIA", "Trans Nzoia"), - ("25_SAMBURU", "Samburu"), - ("24_WEST_POKOT", "West Pokot"), - ("23_TURKANA", "Turkana"), - ("22_KIAMBU", "Kiambu"), - ("21_MURANGA", "Murang'a"), - ("20_KIRINYAGA", "Kirinyaga"), - ("19_NYERI", "Nyeri"), - ("18_NYANDARUA", "Nyandarua"), - ("17_MAKUENI", "Makueni"), - ("16_MACHAKOS", "Machakos"), - ("15_KITUI", "Kitui"), - ("14_EMBU", "Embu"), - ("13_THARAKA_NITHI", "Tharaka-Nithi"), - ("12_MERU", "Meru"), - ("11_ISIOLO", "Isiolo"), - ("10_MARSABIT", "Marsabit"), - ("09_MANDERA", "Mandera"), - ("08_WAJIR", "Wajir"), - ("07_GARISSA", "Garissa"), - ("06_TAITA_TAVETA", "Taita/Taveta"), - ("05_LAMU", "Lamu"), - ("04_TANA_RIVER", "Tana River"), - ("03_KILIFI", "Kilifi"), - ("02_KWALE", "Kwale"), - ("01_MOMBASA", "Mombasa"), -) + + +_CONSTITUENCIES = "Constituencies" +_SUB_COUNTIES = "Sub Counties" +ADMINISTRATIVE_UNITS = { + "Nairobi": { + _CONSTITUENCIES: ( + "Dagoretti North", + "Dagoretti South", + "Embakasi Central", + "Embakasi East", + "Embakasi North", + "Embakasi South", + "Embakasi West", + "Kamukunji", + "Kasarani", + "Kibra", + "Langata", + "Makadara", + "Mathare", + "Roysambu", + "Ruaraka", + "Starehe", + "Westlands", + ), + _SUB_COUNTIES: { + "Dagoretti North": ("Gatini", "Kabiro", "Kawangware", "Kileleshwa", "Kilimani"), + "Dagoretti South": ("Mutu-Ini", "Ngando", "Riruta", "Uthiru/Ruthimitu", "Waithaka"), + "Embakasi Central": ( + "Kayole Central", + "Kayole North", + "Kayole South", + "Komarock", + "Matopeni/Spring Valley", + ), + "Embakasi East": ( + "Embakasi", + "Lower Savannah", + "Mihango", + "Upper Savannah", + "Utawala", + ), + "Embakasi North": ( + "Dandora Area I", + "Dandora Area II", + "Dandora Area III", + "Dandora Area Iv", + "Karioboangi North", + ), + "Embakasi South": ("Imara Daima", "Kwa Njenga", "Kwa Rueben", "Kware", "Pipeline"), + "Embakasi West": ( + "Kariobangi South", + "Maringo/Hamza", + "Mowlem", + "Umoja I", + "Umoja II", + ), + "Kamukunji": ( + "Airbase", + "California", + "Eastleigh North", + "Eastleigh South", + "Pumwani", + ), + "Kasarani": ("Clay City", "Kasarani", "Mwiki", "Njiru", "Ruai"), + "Kibra": ( + "Laini Saba", + "Lindi", + "Makina" "Sarang'ombe", + "Woodley/Kenyatta Golf Course", + ), + "Langata": ("Karen", "Mugumo-ini", "Nairobi West", "Nyayo Highrise", "South C"), + "Makadara": ("Harambee", "Makongeni", "Maringo/Hamza", "Viwandani"), + "Mathare": ("Hospital", "Huruma", "Kiamaiko", "Mabatini", "Ngei"), + "Roysambu": ("Githurai", "Kahawa", "Kahawa West", "Roysambu", "Zimmerman"), + "Ruaraka": ("Babandogo", "Korogocho", "Lucky Summer", "Mathare North", "Utalii"), + "Starehe": ( + "Landimawe", + "Nairobi Central", + "Nairobi South", + "Ngara", + "Pangani", + "Ziwani/Kariokor", + ), + "Westlands": ("Kangemi", "Karura", "Kitisuru", "Mountain View", "Parklands/Highridge"), + }, + }, + "Kajiado": { + _CONSTITUENCIES: ( + "Kajiado Central", + "Kajiado East", + "Kajiado North", + "Kajiado West", + "Magadi", + ), + _SUB_COUNTIES: { + "Kajiado Central": ( + "Dalalekutuk", + "Ildamat", + "Matapato North", + "Matapato South", + "Purko", + ), + "Kajiado East": ( + "Imaroro", + "Kaputiei North", + "Kenyawa-poka", + "Kitengela", + "Oloosirkon/Sholinke", + ), + "Kajiado North": ("Ngong", "Nkaimurunya", "Olkeri", "Oloolua", "Ongata Rongai"), + "Kajiado West": ( + "Ewuaso Oo Nkidong'i", + "Iloodokilani", + "Keekonyokie", + "Magadi", + "Mosiro", + ), + "Loitokitok": ( + "Entonet/Lenkism", + "Imbrikani/Eselelnkei", + "Kimana", + "Kuku", + "Rombo", + ), + }, + }, +} COUNTRY_CODES = ( ("ABW", "Aruba"), diff --git a/fahari/common/migrations/0018_alter_facility_keph_level.py b/fahari/common/migrations/0018_alter_facility_keph_level.py new file mode 100644 index 00000000..38d17acf --- /dev/null +++ b/fahari/common/migrations/0018_alter_facility_keph_level.py @@ -0,0 +1,183 @@ +# Generated by Django 3.2.6 on 2021-08-24 08:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0017_auto_20210727_1306'), + ] + + operations = [ + migrations.AlterField( + model_name='facility', + name='keph_level', + field=models.CharField(blank=True, choices=[('Level 1', 'Level 1'), ('Level 2', 'Level 2'), ('Level 3', 'Level 3'), ('Level 4', 'Level 4'), ('Level 5', 'Level 5'), ('Level 6', 'Level 6')], max_length=12, null=True), + ), + migrations.AlterField( + model_name='facility', + name='owner_type', + field=models.CharField(blank=True, choices=[('Private Practice', 'Private Practice'), ('Ministry of Health', 'Ministry of Health'), ('Faith Based Organization', 'Faith Based Organization'), ('Non-Governmental Organizations', 'Non-Governmental Organizations')], max_length=64, null=True), + ), + migrations.AlterField( + model_name='facility', + name='constituency', + field=models.CharField(blank=True, choices=[('Dagoretti North', 'Dagoretti North'), + ('Dagoretti South', 'Dagoretti South'), + ('Embakasi Central', 'Embakasi Central'), + ('Embakasi East', 'Embakasi East'), + ('Embakasi North', 'Embakasi North'), + ('Embakasi South', 'Embakasi South'), + ('Embakasi West', 'Embakasi West'), + ('Kajiado Central', 'Kajiado Central'), + ('Kajiado East', 'Kajiado East'), + ('Kajiado North', 'Kajiado North'), + ('Kajiado West', 'Kajiado West'), ('Kamukunji', 'Kamukunji'), + ('Kasarani', 'Kasarani'), ('Kibra', 'Kibra'), + ('Langata', 'Langata'), ('Magadi', 'Magadi'), + ('Makadara', 'Makadara'), ('Mathare', 'Mathare'), + ('Roysambu', 'Roysambu'), ('Ruaraka', 'Ruaraka'), + ('Starehe', 'Starehe'), ('Westlands', 'Westlands')], + max_length=64, null=True), + ), + migrations.AlterField( + model_name='facility', + name='county', + field=models.CharField(choices=[('Nairobi', 'Nairobi'), ('Kajiado', 'Kajiado')], max_length=64), + ), + migrations.AlterField( + model_name='facility', + name='facility_type', + field=models.CharField(blank=True, choices=[('Basic Health Centre', 'Basic Health Centre'), + ('Comprehensive Teaching & Tertiary Referral Hospital', 'Comprehensive Teaching & Tertiary Referral Hospital'), + ('Comprehensive health Centre', 'Comprehensive health Centre'), + ('Dental Clinic', 'Dental Clinic'), + ('Dialysis Center', 'Dialysis Center'), + ('Dispensaries and clinic-out patient only', 'Dispensaries and clinic-out patient only'), + ('Dispensary', 'Dispensary'), + ('Farewell Home', 'Farewell Home'), + ('Health Centre', 'Health Centre'), + ('Laboratory', 'Laboratory'), + ('Medical Center', 'Medical Center'), + ('Medical Clinic', 'Medical Clinic'), + ('Nursing Homes', 'Nursing Home'), + ('Nursing and Maternity Home', 'Nursing and Maternity Home'), + ('Ophthalmology', 'Ophthalmology'), ('Pharmacy', 'Pharmacy'), + ('Primary care hospitals', 'Primary care hospital'), + ('Radiology Clinic', 'Radiology Clinic'), + ('Rehab. Center - Drug and Substance abuse', 'Rehab. Center - Drug and Substance abuse'), + ('Secondary care hospitals', 'Secondary care hospital'), + ('Specialized & Tertiary Referral hospitals', 'Specialized & Tertiary Referral hospital'), + ('VCT', 'VCT')], + max_length=64, null=True), + ), + migrations.AlterField( + model_name='facility', + name='facility_type_category', + field=models.CharField(blank=True, + choices=[('DISPENSARY', 'Dispensary'), ('HEALTH CENTRE', 'Health Centre'), + ('HOSPITALS', 'Hospital'), ('MEDICAL CENTER', 'Medical Center'), + ('MEDICAL CLINIC', 'Medical Clinic'), ('NURSING HOME', 'Nursing Home'), + ('Primary health care services', 'Primary Health Care Service'), + ('STAND ALONE', 'Stand Alone')], max_length=64, null=True), + ), + migrations.AlterField( + model_name='facility', + name='sub_county', + field=models.CharField(blank=True, choices=[('Dagoretti North', 'Dagoretti North'), + ('Dagoretti South', 'Dagoretti South'), + ('Embakasi Central', 'Embakasi Central'), + ('Embakasi East', 'Embakasi East'), + ('Embakasi North', 'Embakasi North'), + ('Embakasi South', 'Embakasi South'), + ('Embakasi West', 'Embakasi West'), + ('Kajiado Central', 'Kajiado Central'), + ('Kajiado East', 'Kajiado East'), + ('Kajiado North', 'Kajiado North'), + ('Kajiado West', 'Kajiado West'), ('Kamukunji', 'Kamukunji'), + ('Kasarani', 'Kasarani'), ('Kibra', 'Kibra'), + ('Langata', 'Langata'), ('Loitokitok', 'Loitokitok'), + ('Makadara', 'Makadara'), ('Mathare', 'Mathare'), + ('Roysambu', 'Roysambu'), ('Ruaraka', 'Ruaraka'), + ('Starehe', 'Starehe'), ('Westlands', 'Westlands')], + max_length=64, null=True), + ), + migrations.AlterField( + model_name='facility', + name='ward', + field=models.CharField(blank=True, choices=[('Airbase', 'Airbase'), ('Babandogo', 'Babandogo'), + ('California', 'California'), ('Clay City', 'Clay City'), + ('Dalalekutuk', 'Dalalekutuk'), + ('Dandora Area I', 'Dandora Area I'), + ('Dandora Area II', 'Dandora Area II'), + ('Dandora Area III', 'Dandora Area III'), + ('Dandora Area Iv', 'Dandora Area Iv'), + ('Eastleigh North', 'Eastleigh North'), + ('Eastleigh South', 'Eastleigh South'), + ('Embakasi', 'Embakasi'), + ('Entonet/Lenkism', 'Entonet/Lenkism'), + ("Ewuaso Oo Nkidong'i", "Ewuaso Oo Nkidong'i"), + ('Gatini', 'Gatini'), ('Githurai', 'Githurai'), + ('Harambee', 'Harambee'), ('Hospital', 'Hospital'), + ('Huruma', 'Huruma'), ('Ildamat', 'Ildamat'), + ('Iloodokilani', 'Iloodokilani'), + ('Imara Daima', 'Imara Daima'), ('Imaroro', 'Imaroro'), + ('Imbrikani/Eselelnkei', 'Imbrikani/Eselelnkei'), + ('Kabiro', 'Kabiro'), ('Kahawa', 'Kahawa'), + ('Kahawa West', 'Kahawa West'), ('Kangemi', 'Kangemi'), + ('Kaputiei North', 'Kaputiei North'), ('Karen', 'Karen'), + ('Kariobangi South', 'Kariobangi South'), + ('Karioboangi North', 'Karioboangi North'), + ('Karura', 'Karura'), ('Kasarani', 'Kasarani'), + ('Kawangware', 'Kawangware'), + ('Kayole Central', 'Kayole Central'), + ('Kayole North', 'Kayole North'), + ('Kayole South', 'Kayole South'), + ('Keekonyokie', 'Keekonyokie'), + ('Kenyawa-poka', 'Kenyawa-poka'), ('Kiamaiko', 'Kiamaiko'), + ('Kileleshwa', 'Kileleshwa'), ('Kilimani', 'Kilimani'), + ('Kimana', 'Kimana'), ('Kitengela', 'Kitengela'), + ('Kitisuru', 'Kitisuru'), ('Komarock', 'Komarock'), + ('Korogocho', 'Korogocho'), ('Kuku', 'Kuku'), + ('Kwa Njenga', 'Kwa Njenga'), ('Kwa Rueben', 'Kwa Rueben'), + ('Kware', 'Kware'), ('Laini Saba', 'Laini Saba'), + ('Landimawe', 'Landimawe'), ('Lindi', 'Lindi'), + ('Lower Savannah', 'Lower Savannah'), + ('Lucky Summer', 'Lucky Summer'), ('Mabatini', 'Mabatini'), + ('Magadi', 'Magadi'), + ("MakinaSarang'ombe", "MakinaSarang'ombe"), + ('Makongeni', 'Makongeni'), ('Maringo/Hamza', 'Maringo/Hamza'), + ('Maringo/Hamza', 'Maringo/Hamza'), + ('Matapato North', 'Matapato North'), + ('Matapato South', 'Matapato South'), + ('Mathare North', 'Mathare North'), + ('Matopeni/Spring Valley', 'Matopeni/Spring Valley'), + ('Mihango', 'Mihango'), ('Mosiro', 'Mosiro'), + ('Mountain View', 'Mountain View'), ('Mowlem', 'Mowlem'), + ('Mugumo-ini', 'Mugumo-ini'), ('Mutu-Ini', 'Mutu-Ini'), + ('Mwiki', 'Mwiki'), ('Nairobi Central', 'Nairobi Central'), + ('Nairobi South', 'Nairobi South'), + ('Nairobi West', 'Nairobi West'), ('Ngando', 'Ngando'), + ('Ngara', 'Ngara'), ('Ngei', 'Ngei'), ('Ngong', 'Ngong'), + ('Njiru', 'Njiru'), ('Nkaimurunya', 'Nkaimurunya'), + ('Nyayo Highrise', 'Nyayo Highrise'), ('Olkeri', 'Olkeri'), + ('Oloolua', 'Oloolua'), + ('Oloosirkon/Sholinke', 'Oloosirkon/Sholinke'), + ('Ongata Rongai', 'Ongata Rongai'), ('Pangani', 'Pangani'), + ('Parklands/Highridge', 'Parklands/Highridge'), + ('Pipeline', 'Pipeline'), ('Pumwani', 'Pumwani'), + ('Purko', 'Purko'), ('Riruta', 'Riruta'), ('Rombo', 'Rombo'), + ('Roysambu', 'Roysambu'), ('Ruai', 'Ruai'), + ('South C', 'South C'), ('Umoja I', 'Umoja I'), + ('Umoja II', 'Umoja II'), ('Upper Savannah', 'Upper Savannah'), + ('Utalii', 'Utalii'), ('Utawala', 'Utawala'), + ('Uthiru/Ruthimitu', 'Uthiru/Ruthimitu'), + ('Viwandani', 'Viwandani'), ('Waithaka', 'Waithaka'), ( + 'Woodley/Kenyatta Golf Course', 'Woodley/Kenyatta Golf Course'), + ('Zimmerman', 'Zimmerman'), + ('Ziwani/Kariokor', 'Ziwani/Kariokor')], + max_length=64, + null=True), + ), + ] diff --git a/fahari/common/models.py b/fahari/common/models.py index ddab63bc..bc8e6d0c 100644 --- a/fahari/common/models.py +++ b/fahari/common/models.py @@ -17,6 +17,15 @@ from PIL import Image from .constants import CONTENT_TYPES, COUNTRY_CODES, IMAGE_TYPES +from .utils import ( + get_constituencies, + get_counties, + get_sub_counties, + get_wards, + has_constituency, + has_sub_county, + has_ward, +) LOGGER = logging.getLogger(__file__) User = get_user_model() @@ -397,19 +406,102 @@ class Facility(AbstractBase): The data is fetched - and updated - from the Kenya Master Health Facilities List. """ + class KEPHLevels(models.TextChoices): + """The different Kenya Package for Health (KEPH) levels. + + This are the different tiers of health care delivery systems as + defined by the Ministry of Health. + """ + + LEVEL_1 = "Level 1", "Level 1" + LEVEL_2 = "Level 2", "Level 2" + LEVEL_3 = "Level 3", "Level 3" + LEVEL_4 = "Level 4", "Level 4" + LEVEL_5 = "Level 5", "Level 5" + LEVEL_6 = "Level 6", "Level 6" + + class FacilityOwnerType(models.TextChoices): + """The different types of medical facility ownerships.""" + + PRIVATE_PRACTICE = "Private Practice", "Private Practice" + MINISTRY_OF_HEALTH = "Ministry of Health", "Ministry of Health" + FAITH_BASED_ORG = "Faith Based Organization", "Faith Based Organization" + NON_GOVERNMENT_ORG = "Non-Governmental Organizations", "Non-Governmental Organizations" + + class FacilityType(models.TextChoices): + """The different types of facility types.""" + + BASIC_HEALTH_CENTER = "Basic Health Centre", "Basic Health Centre" + CTTRH = ( + "Comprehensive Teaching & Tertiary Referral Hospital", + "Comprehensive Teaching & Tertiary Referral Hospital", + ) + COMPREHENSIVE_HEALTH_CENTRE = "Comprehensive health Centre", "Comprehensive health Centre" + DENTAL_CLINIC = "Dental Clinic", "Dental Clinic" + DIALYSIS_CENTER = "Dialysis Center", "Dialysis Center" + DCPO = ( + "Dispensaries and clinic-out patient only", + "Dispensaries and clinic-out patient only", + ) + DISPENSARY = "Dispensary", "Dispensary" + FAREWELL_HOME = "Farewell Home", "Farewell Home" + HEALTH_CENTRE = "Health Centre", "Health Centre" + LABORATORY = "Laboratory", "Laboratory" + MEDICAL_CENTER = "Medical Center", "Medical Center" + MEDICAL_CLINIC = "Medical Clinic", "Medical Clinic" + NURSING_HOMES = "Nursing Homes", "Nursing Home" + NURSING_AND_MATERNITY_HOME = "Nursing and Maternity Home", "Nursing and Maternity Home" + OPHTHALMOLOGY = "Ophthalmology", "Ophthalmology" + PHARMACY = "Pharmacy", "Pharmacy" + PRIMARY_CARE_HOSPITAL = "Primary care hospitals", "Primary care hospital" + RADIOLOGY_CLINIC = "Radiology Clinic", "Radiology Clinic" + RCDASA = ( + "Rehab. Center - Drug and Substance abuse", + "Rehab. Center - Drug and Substance abuse", + ) + SECONDARY_CARE_HOSPITAL = "Secondary care hospitals", "Secondary care hospital" + STRH = ( + "Specialized & Tertiary Referral hospitals", + "Specialized & Tertiary Referral hospital", + ) + VCT = "VCT", "VCT" + + class FacilityTypeCategory(models.TextChoices): + """The different types of facility type categories.""" + + DISPENSARY = "DISPENSARY", "Dispensary" + HEALTH_CENTRE = "HEALTH CENTRE", "Health Centre" + HOSPITAL = "HOSPITALS", "Hospital" + MEDICAL_CENTER = "MEDICAL CENTER", "Medical Center" + MEDICAL_CLINIC = "MEDICAL CLINIC", "Medical Clinic" + NURSING_HOME = "NURSING HOME", "Nursing Home" + PRIMARY_HEALTH_CARE_SERVICES = ( + "Primary health care services", + "Primary Health Care Service", + ) + STAND_ALONE = "STAND ALONE", "Stand Alone" + name = models.TextField(unique=True) mfl_code = models.IntegerField(unique=True, help_text="MFL Code") - county = models.CharField(max_length=64) - sub_county = models.CharField(max_length=64, null=True, blank=True) - constituency = models.CharField(max_length=64, null=True, blank=True) - ward = models.CharField(max_length=64, null=True, blank=True) + county = models.CharField(max_length=64, choices=get_counties()) + sub_county = models.CharField(max_length=64, null=True, blank=True, choices=get_sub_counties()) + constituency = models.CharField( + max_length=64, null=True, blank=True, choices=get_constituencies() + ) + ward = models.CharField(max_length=64, null=True, blank=True, choices=get_wards()) operation_status = models.CharField(max_length=24, default="Operational") registration_number = models.CharField(max_length=64, null=True, blank=True) - keph_level = models.CharField(max_length=12, null=True, blank=True) - facility_type = models.CharField(max_length=64, null=True, blank=True) - facility_type_category = models.CharField(max_length=64, null=True, blank=True) + keph_level = models.CharField(max_length=12, choices=KEPHLevels.choices, null=True, blank=True) + facility_type = models.CharField( + max_length=64, null=True, blank=True, choices=FacilityType.choices + ) + facility_type_category = models.CharField( + max_length=64, null=True, blank=True, choices=FacilityTypeCategory.choices + ) facility_owner = models.CharField(max_length=64, null=True, blank=True) - owner_type = models.CharField(max_length=64, null=True, blank=True) + owner_type = models.CharField( + max_length=64, null=True, choices=FacilityOwnerType.choices, blank=True + ) regulatory_body = models.CharField(max_length=64, null=True, blank=True) beds = models.IntegerField(default=0) cots = models.IntegerField(default=0) @@ -426,6 +518,9 @@ class Facility(AbstractBase): model_validators = [ "facility_name_longer_than_three_characters", + "constituency_belongs_to_selected_county", + "sub_county_belongs_to_selected_county", + "ward_belongs_to_selected_sub_county", ] def get_absolute_url(self): @@ -436,6 +531,44 @@ def facility_name_longer_than_three_characters(self): if len(self.name) < 3: raise ValidationError("the facility name should exceed 3 characters") + def constituency_belongs_to_selected_county(self): + if self.constituency and not has_constituency(self.county, self.constituency): + raise ValidationError( + { + "constituency": '"{}" constituency does not belong to "{}" county'.format( + self.constituency, self.county + ) + } + ) + + def sub_county_belongs_to_selected_county(self): + if self.sub_county and not has_sub_county(self.county, self.sub_county): + raise ValidationError( + { + "sub_county": '"{}" sub county does not belong to "{}" county'.format( + self.sub_county, self.county + ) + } + ) + + def ward_belongs_to_selected_sub_county(self): + if self.ward and not self.sub_county: + raise ValidationError( + { + "ward": 'the sub county in which "{}" ward belongs to must be provided'.format( + self.ward + ) + } + ) + elif self.ward and self.sub_county and not has_ward(self.sub_county, self.ward): + raise ValidationError( + { + "ward": '"{}" ward does not belong to "{}" sub county'.format( + self.ward, self.sub_county + ) + } + ) + def __str__(self): return f"{self.name} - {self.mfl_code} ({self.county})" diff --git a/fahari/common/tests/test_administrative_unit_utils.py b/fahari/common/tests/test_administrative_unit_utils.py new file mode 100644 index 00000000..53d41d2e --- /dev/null +++ b/fahari/common/tests/test_administrative_unit_utils.py @@ -0,0 +1,125 @@ +import pytest + +from fahari.common.utils import ( + get_constituencies, + get_constituencies_for_county, + get_counties, + get_sub_counties, + get_sub_counties_for_county, + get_wards, + get_wards_for_sub_county, + has_constituency, + has_sub_county, + has_ward, +) + + +def test_get_counties(): + counties = get_counties() + assert ("Nairobi", "Nairobi") in counties + assert ("Kajiado", "Kajiado") in counties + + +def test_get_constituencies(): + constituencies = get_constituencies() + assert ("Magadi", "Magadi") in constituencies + assert ("Kibra", "Kibra") in constituencies + assert ("Dagoretti North", "Dagoretti North") in constituencies + assert ("Kajiado East", "Kajiado East") in constituencies + assert ("Starehe", "Starehe") in constituencies + assert len(constituencies) == 22 + + +def test_get_constituencies_for_county(): + constituencies = get_constituencies_for_county("Kajiado") + assert ("Magadi", "Magadi") in constituencies + assert ("Kajiado Central", "Kajiado Central") in constituencies + assert ("Kajiado East", "Kajiado East") in constituencies + assert len(constituencies) == 5 + + +def test_get_constituencies_for_county_with_missing_county(): + """Assert correct output when a non existing county is given as input.""" + constituencies = get_constituencies_for_county("Marsabit") + assert len(constituencies) == 0 + + +def test_get_sub_counties(): + sub_counties = get_sub_counties() + assert ("Loitokitok", "Loitokitok") in sub_counties + assert ("Roysambu", "Roysambu") in sub_counties + assert ("Kibra", "Kibra") in sub_counties + assert ("Dagoretti South", "Dagoretti South") in sub_counties + assert len(sub_counties) == 22 + + +def test_get_sub_counties_for_county(): + sub_counties = get_sub_counties_for_county("Nairobi") + assert ("Westlands", "Westlands") in sub_counties + assert len(sub_counties) == 17 + + +def test_get_sub_counties_for_county_with_missing_county(): + """Assert correct output when a non existing county is given as input.""" + sub_counties = get_sub_counties_for_county("Mombasa") + assert len(sub_counties) == 0 + + +def test_get_wards_for_sub_county(): + wards = get_wards_for_sub_county("Dagoretti North") + assert ("Kilimani", "Kilimani") in wards + assert len(wards) == 5 + + +def test_get_wards(): + wards = get_wards() + assert ("Ongata Rongai", "Ongata Rongai") in wards + assert ("Ewuaso Oo Nkidong'i", "Ewuaso Oo Nkidong'i") in wards + assert ("Iloodokilani", "Iloodokilani") in wards + assert ("Keekonyokie", "Keekonyokie") in wards + assert ("Magadi", "Magadi") in wards + assert ("Mosiro", "Mosiro") in wards + assert ("Ziwani/Kariokor", "Ziwani/Kariokor") in wards + assert ("Landimawe", "Landimawe") in wards + assert ("Nairobi South", "Nairobi South") in wards + assert len(wards) == 109 + + +def test_get_wards_for_sub_county_with_missing_sub_county(): + """Assert correct output when a non-existing sub-county is given as input.""" + wards = get_wards_for_sub_county("Naivasha") + assert len(wards) == 0 + + +def test_has_constituency(): + assert has_constituency("Kajiado", "Magadi") + assert has_constituency("Nairobi", "Starehe") + assert not has_constituency("Nairobi", "Kajiado East") + + +def test_has_constituency_with_missing_county(): + with pytest.raises(ValueError): + has_constituency("Mombasa", "Lamu") + + +def test_has_sub_county(): + assert has_sub_county("Nairobi", "Dagoretti North") + assert not has_sub_county("Kajiado", "Westlands") + + +def test_has_sub_county_with_missing_county(): + with pytest.raises(ValueError): + has_sub_county("Nakuru", "Naivasha") + + +def test_has_wards(): + assert has_ward("Ruaraka", "Korogocho") + assert has_ward("Mathare", "Huruma") + assert has_ward("Kajiado Central", "Ildamat") + assert has_ward("Kajiado North", "Ngong") + assert not has_ward("Kajiado East", "Ngei") + + +def test_has_ward_with_missing_sub_county(): + with pytest.raises(ValueError): + assert has_ward("Kitui", "Kitui") diff --git a/fahari/common/tests/test_models.py b/fahari/common/tests/test_models.py index 192fc0be..efe09a3a 100644 --- a/fahari/common/tests/test_models.py +++ b/fahari/common/tests/test_models.py @@ -56,7 +56,7 @@ def test_facility_string_representation(): """Test common behavior of the abstract base model.""" facility_name = fake.name() mfl_code = randint(1, 999_999) - county = "47_NAIROBI_CITY" + county = "Nairobi" created_by = uuid.uuid4() updated_by = created_by organisation = baker.make("common.Organisation") @@ -70,7 +70,7 @@ def test_facility_string_representation(): updated_by=updated_by, ) facility.save() - assert str(facility) == f"{facility_name} - {mfl_code} (47_NAIROBI_CITY)" + assert str(facility) == f"{facility_name} - {mfl_code} (Nairobi)" def test_google_application_credentials(): @@ -216,7 +216,7 @@ def test_facility_error_saving(): """Test common behavior of the abstract base model.""" facility_name = "a" # too short, will trigger validator mfl_code = randint(1, 999_999) - county = "47_NAIROBI_CITY" + county = "Nairobi" created_by = uuid.uuid4() updated_by = created_by organisation = baker.make("common.Organisation") @@ -232,7 +232,64 @@ def test_facility_error_saving(): with pytest.raises(ValidationError) as e: facility.save() - assert ("the facility name should exceed 3 characters") in e.value.messages + assert "the facility name should exceed 3 characters" in e.value.messages + + +def test_facility_invalid_constituency_selection(): + facility = Facility( + name="ABC Hospital", + mfl_code=123456, + county="Kajiado", + organisation=baker.make("common.Organisation"), + constituency="Westlands", # doesn't belong in Kajiado county + ) + with pytest.raises(ValidationError) as e: + facility.save() + + assert '"Westlands" constituency does not belong to "Kajiado" county' in e.value.messages + + +def test_facility_invalid_sub_county_selection(): + facility = Facility( + name="XYZ Medical Center", + mfl_code=123456, + county="Nairobi", + organisation=baker.make("common.Organisation"), + sub_county="Kajiado East", # doesn't belong in Nairobi county + ) + with pytest.raises(ValidationError) as e: + facility.save() + + assert '"Kajiado East" sub county does not belong to "Nairobi" county' in e.value.messages + + +def test_facility_invalid_ward_selection(): + facility = Facility( + name="ABC Hospital", + mfl_code=123456, + county="Kajiado", + organisation=baker.make("common.Organisation"), + sub_county="Kajiado Central", + ward="Ngando", + ) + with pytest.raises(ValidationError) as e: + facility.save() + + assert '"Ngando" ward does not belong to "Kajiado Central" sub county' in e.value.messages + + +def test_facility_ward_selection_without_sub_county_selection(): + facility = Facility( + name="XYZ VCT Clinic", + mfl_code=123456, + county="Nairobi", + organisation=baker.make("common.Organisation"), + ward="Ngara", # missing sub county selection + ) + with pytest.raises(ValidationError) as e: + facility.save() + + assert 'the sub county in which "Ngara" ward belongs to must be provided' in e.value.messages def test_organisation_string_representation(): diff --git a/fahari/common/utils/__init__.py b/fahari/common/utils/__init__.py new file mode 100644 index 00000000..0c3ec8b2 --- /dev/null +++ b/fahari/common/utils/__init__.py @@ -0,0 +1,25 @@ +from .administrative_unit_utils import ( + get_constituencies, + get_constituencies_for_county, + get_counties, + get_sub_counties, + get_sub_counties_for_county, + get_wards, + get_wards_for_sub_county, + has_constituency, + has_sub_county, + has_ward, +) + +__all__ = [ + "get_constituencies", + "get_constituencies_for_county", + "get_counties", + "get_sub_counties", + "get_sub_counties_for_county", + "get_wards", + "get_wards_for_sub_county", + "has_constituency", + "has_sub_county", + "has_ward", +] diff --git a/fahari/common/utils/administrative_unit_utils.py b/fahari/common/utils/administrative_unit_utils.py new file mode 100644 index 00000000..a45e75f8 --- /dev/null +++ b/fahari/common/utils/administrative_unit_utils.py @@ -0,0 +1,192 @@ +from functools import lru_cache +from itertools import chain +from typing import Collection, Dict, Iterable, Optional, Tuple, cast + +from ..constants import ADMINISTRATIVE_UNITS + +FieldChoice = Tuple[str, str] + +_CONSTITUENCIES = "Constituencies" +_SUB_COUNTIES = "Sub Counties" + + +def _sorted_field_choices(choices: Iterable[FieldChoice]) -> Collection[FieldChoice]: + # Sort by the display value rather than the storage value. This way, the choices + # appear sorted on the select DOM component of the user interface. + sorted_choices = sorted(choices, key=lambda choice: choice[1]) + return tuple(sorted_choices) + + +@lru_cache(maxsize=None) +def get_counties() -> Collection[FieldChoice]: + """Return a `Collection` of choices the counties involved in the Fahari ya Jamii Project. + + :return: A Collection of choices of the counties involved in the Fahari ya + Jamii Project. + """ + return tuple((county, county) for county in ADMINISTRATIVE_UNITS) + + +@lru_cache(maxsize=None) +def get_constituencies() -> Collection[FieldChoice]: + """Return a `Collection` of choices of the constituencies involved in the Fahari ya Jamii Project. + + :return: A Collection of choices of the constituencies involved in the + Fahari ya Jamii Project. + """ + constituencies = (get_constituencies_for_county(county[0]) for county in get_counties()) + return _sorted_field_choices(chain.from_iterable(constituencies)) + + +@lru_cache(maxsize=None) +def get_constituencies_for_county(county: str) -> Collection[FieldChoice]: + """Return a `Collection` of choices of the constituencies of the given county. + + If the given county doesn't exist, an empty `Collection` will be returned + instead. + + :param county: The county whose constituencies to return. + + :return: A Collection of choices of the constituencies that belong to the + given county or an empty Collection if the provided county doesn't + exist. + """ + county_admin_units = ADMINISTRATIVE_UNITS.get(county, dict()) + constituencies = county_admin_units.get(_CONSTITUENCIES, tuple()) + return _sorted_field_choices((constituency, constituency) for constituency in constituencies) + + +@lru_cache(maxsize=None) +def get_sub_counties() -> Collection[FieldChoice]: + """Return a `Collection` of choices of all the sub-counties in the Fahari ya Jamii Project. + + :return: A Collection of choices of all the sub-counties involved in the + Fahari ya Jamii Project. + """ + sub_counties = (get_sub_counties_for_county(county[0]) for county in get_counties()) + return _sorted_field_choices(chain.from_iterable(sub_counties)) + + +@lru_cache(maxsize=None) +def get_sub_counties_for_county(county: str) -> Collection[FieldChoice]: + """Return a `Collection` of choices of the sub-counties of the given county. + + If the given county doesn't exist, an empty `Collection` will be returned + instead. + + :param county: The county whose sub-counties to return. + + :return: An Collection of choices of the sub-counties that belong to the + given county or an empty Collection if the provided county doesn't + exist. + """ + county_admin_units = ADMINISTRATIVE_UNITS.get(county, dict()) + sub_counties = county_admin_units.get(_SUB_COUNTIES, dict()) + return _sorted_field_choices((sub_county, sub_county) for sub_county in sub_counties) + + +@lru_cache(maxsize=None) +def get_wards() -> Collection[FieldChoice]: + """Return a `Collection` of choices of the wards involved in the Fahari ya Jamii Project. + + :return: A Collection of choices of the wards involved in the Fahari ya + Jamii Project. + """ + wards = (get_wards_for_sub_county(sub_county[0]) for sub_county in get_sub_counties()) + return _sorted_field_choices(chain.from_iterable(wards)) + + +@lru_cache(maxsize=None) +def get_wards_for_sub_county(sub_county: str) -> Collection[FieldChoice]: + """Return a `Collection` of choices of the wards of the given sub-county. + + If the given sub-county doesn't exist, an empty `Collection` will be + returned. + + :param sub_county: The sub-county whose wards to return. + + :return: A Collection of choices of the wards that belong to the given + sub-county or an empty Collection if the provided sub-county + doesn't exist. + """ + owning_county: Optional[str] = None + for county in get_counties(): + if (sub_county, sub_county) in get_sub_counties_for_county(county[0]): + owning_county = county[0] + break + + if owning_county is None: + return tuple() + + sub_counties = cast( + Dict[str, Collection[str]], ADMINISTRATIVE_UNITS[owning_county][_SUB_COUNTIES] + ) + wards = sub_counties[sub_county] + return _sorted_field_choices((ward, ward) for ward in wards) + + +def has_constituency(county: str, constituency: str) -> bool: + """Check if the given constituency exists in the given county. + + The provided county MUST be part of the Fahari ya Jamii program, otherwise, + a `ValueError` will be raised. + + Return `true` if the given constituency exists and belongs to the given + county or `false` otherwise. + + :param county: A county in the FYJ program. + :param constituency: The constituency whose ownership we want to check. + + :return: true if the given constituency belongs to the given county or + false otherwise. + + :raise ValueError: If the given county is not part of the FYJ program. + """ + if county not in ADMINISTRATIVE_UNITS: + raise ValueError('county "{}" does not exist'.format(county)) + return (constituency, constituency) in get_constituencies_for_county(county) + + +def has_sub_county(county: str, sub_county: str) -> bool: + """Check if the given sub-county exists in the given county. + + The provided county MUST be part of the Fahari ya Jamii program, otherwise, + a `ValueError` will be raised. + + Return `true` if the given sub-county exists and belongs to the given + county or `false` otherwise. + + :param county: A county in the FYJ program. + :param sub_county: The sub-county whose ownership we want to check. + + :return: true if the given sub-county belongs to the given county or false + otherwise. + + :raise ValueError: If the given county is not part of the FYJ program. + """ + if county not in ADMINISTRATIVE_UNITS: + raise ValueError('county "{}" does not exist'.format(county)) + return (sub_county, sub_county) in get_sub_counties_for_county(county) + + +def has_ward(sub_county: str, ward: str) -> bool: + """Check if the given ward exists inn the given sub-county. + + The provided sub-county MUST belong to a sub-county in thr Fahari ya Jamii + program, otherwise, a `ValueError` will be raised. + + Return `true` if the given ward exists and belongs to the given sub-county + or `false` otherwise. + + :param sub_county: A sub-county belonging to a county in the FYJ program. + :param ward: The ward whose ownership we want to check. + + :return: true if the given ward belongs to the given sub-county or false + otherwise. + + :raise ValueError: If the given sub-county doesn't belong to a county in + the FYJ program. + """ + if (sub_county, sub_county) not in get_sub_counties(): + raise ValueError('sub county "{}" does not exist'.format(sub_county)) + return (ward, ward) in get_wards_for_sub_county(sub_county) diff --git a/scripts/mark_fahari_facilities.py b/scripts/mark_fahari_facilities.py old mode 100644 new mode 100755