diff --git a/location/gql_mutations.py b/location/gql_mutations.py index 9098e19..6fe5e2f 100644 --- a/location/gql_mutations.py +++ b/location/gql_mutations.py @@ -1,6 +1,6 @@ import graphene from .apps import LocationConfig -from core import assert_string_length, filter_validity +from core import assert_string_length from core.schema import OpenIMISMutation from .models import Location, HealthFacility, UserDistrict from django.contrib.auth.models import AnonymousUser @@ -133,7 +133,7 @@ def async_mutate(cls, user, **data): if np_uuid: new_parent = Location.objects.get(uuid=np_uuid) Location.objects.filter(parent=location).filter( - *filter_validity() + *Location.filter_validity() ).update(parent=new_parent) else: tree_delete((location,), now) @@ -171,7 +171,7 @@ def tree_reset_types(parent, location, new_level): location.type = LocationConfig.location_types[-1] return location.type = LocationConfig.location_types[new_level] - for child in location.children.filter(*filter_validity()).all(): + for child in location.children.filter(*Location.filter_validity()).all(): child.save_history() tree_reset_types(location, child, new_level + 1) child.save() diff --git a/location/migrations/0005_20191212_1415.py b/location/migrations/0005_20191212_1415.py index 25b1394..704ee57 100644 --- a/location/migrations/0005_20191212_1415.py +++ b/location/migrations/0005_20191212_1415.py @@ -12,7 +12,10 @@ class Migration(migrations.Migration): ("location", "0004_locationmutation"), ] replaces = [ - ('location', "0005_healthfacilitycatchment_healthfacilitylegalform_healthfacilitymutation_healthfacilitysublevel") + ( + 'location', + "0005_healthfacilitycatchment_healthfacilitylegalform_healthfacilitymutation_healthfacilitysublevel" + ) ] operations = [ diff --git a/location/migrations/0019_alter_location_code.py b/location/migrations/0019_alter_location_code.py index a3fbf29..12d4f15 100644 --- a/location/migrations/0019_alter_location_code.py +++ b/location/migrations/0019_alter_location_code.py @@ -61,6 +61,7 @@ def _extract_mssql_views(): else: raise RuntimeError(f'{view} not found using query {query}') + def _drop_mssql_views(): with connection.cursor() as cursor: for view in DEPENDENT_VIEWS: diff --git a/location/models.py b/location/models.py index 8a4aa2f..042bfee 100644 --- a/location/models.py +++ b/location/models.py @@ -1,7 +1,6 @@ from django.core.cache import caches from django_redis.cache import RedisCache import uuid -from core import filter_validity from core.models import CachedManager from django.conf import settings from django.db import models, connection @@ -245,9 +244,9 @@ def get_allowed_ids(self, user, strict=True): allowed = list( OfficerVillage.objects.filter( officer=core_models.Officer.objects.filter( - code=user.login_name, *filter_validity() + code=user.login_name, *core_models.Officer.filter_validity() ).first(), - *filter_validity(), + *OfficerVillage.filter_validity(), ).values_list("location_id", flat=True) ) else: @@ -270,7 +269,7 @@ def cache_location_graph(location_id=None): """Cache the location graph as a dictionary of edges.""" locations = Location.objects.filter( Q(parent__isnull=False) | Q(type='R'), - *filter_validity(), + *Location.filter_validity(), ) graph = {} location_types = {} @@ -639,8 +638,8 @@ def get_user_districts(cls, user): user=user, location__type="D", location__parent__isnull=False, - *filter_validity(), - *filter_validity(prefix="location__"), + *UserDistrict.filter_validity(), + *Location.filter_validity(prefix="location__"), ) .order_by("location__parent__code") .order_by("location__code") @@ -679,7 +678,7 @@ def get_user_locations(cls, user): if not core_models.InteractiveUser.is_interactive_user(user): return Location.objects.none() return ( - Location.objects.filter(*filter_validity()) + Location.objects.filter(*Location.filter_validity()) .filter(parent__parent__userdistrict__user=user.i_user) .order_by("code") ) diff --git a/location/schema.py b/location/schema.py index c4dd027..34d2224 100644 --- a/location/schema.py +++ b/location/schema.py @@ -32,7 +32,6 @@ from location.apps import LocationConfig import graphene from django.db.models import Q -from core.utils import filter_validity from core import models as core_models from django.conf import settings @@ -123,14 +122,14 @@ def resolve_locations(self, info, **kwargs): def resolve_locations_all(self, info, **kwargs): if info.context.user.is_anonymous: raise PermissionDenied(_("unauthorized")) - return Location.objects.filter(*filter_validity()).all() + return Location.objects.filter(*Location.filter_validity()).all() def resolve_locations_str(self, info, **kwargs): if info.context.user.is_anonymous: raise PermissionDenied(_("unauthorized")) queryset = Location.get_queryset(None, info.context.user) - filters = [*filter_validity(**kwargs)] + filters = [*Location.filter_validity(**kwargs)] str = kwargs.get("str") if str is not None: @@ -141,7 +140,7 @@ def resolve_locations_str(self, info, **kwargs): def resolve_health_facilities_str(self, info, **kwargs): if not info.context.user.is_authenticated: raise PermissionDenied(_("unauthorized")) - filters = [*filter_validity(**kwargs)] + filters = [*HealthFacility.filter_validity(**kwargs)] search = kwargs.get("str") district_uuid = kwargs.get("district_uuid") district_uuids = kwargs.get("districts_uuids") diff --git a/location/test_helpers.py b/location/test_helpers.py index 63d4b07..ba48feb 100644 --- a/location/test_helpers.py +++ b/location/test_helpers.py @@ -5,6 +5,7 @@ UserDistrict, HealthFacility, HealthFacilityLegalForm, + HealthFacilitySubLevel, HealthFacilityCatchment, ) @@ -16,7 +17,7 @@ def generate_random_string(length=6): def assign_user_districts(user, district_codes): for dc in district_codes: - dc_location = Location.objects.get(code=dc, validity_to__isnull=True) + dc_location = Location.objects.get(code=dc, *Location.filter_validity()) UserDistrict.objects.get_or_create( user=user.i_user, validity_to=None, @@ -92,6 +93,43 @@ def create_test_village(custom_props=None): return test_village +def create_test_basic_health_facility_legal_form(): + create_test_health_facility_legal_form(code="C", legal_form="Charity", sort_order=1) + create_test_health_facility_legal_form(code="D", legal_form="District organization", sort_order=2) + create_test_health_facility_legal_form(code="P", legal_form="Private organization", sort_order=3) + create_test_health_facility_legal_form(code="G", legal_form="Government", sort_order=4) + + +def create_test_health_facility_legal_form(code="C", legal_form="Company", sort_order=1): + """Create a test health facility legal form if it doesn't exist.""" + obj, created = HealthFacilityLegalForm.objects.get_or_create( + code=code, + defaults={ + "legal_form": legal_form, + "sort_order": sort_order, + } + ) + return obj + + +def create_test_basic_health_facility_sub_level(): + create_test_health_facility_sub_level(code="D", health_facility_sub_level="Dispensary", sort_order=1) + create_test_health_facility_sub_level(code="H", health_facility_sub_level="Hospital", sort_order=2) + create_test_health_facility_sub_level(code="C", health_facility_sub_level="Health Centre", sort_order=3) + + +def create_test_health_facility_sub_level(code="S", health_facility_sub_level="Standard", sort_order=1): + """Create a test health facility sub-level if it doesn't exist.""" + obj, created = HealthFacilitySubLevel.objects.get_or_create( + code=code, + defaults={ + "health_facility_sub_level": health_facility_sub_level, + "sort_order": sort_order, + } + ) + return obj + + def create_test_health_facility( code=None, location_id=None, valid=True, custom_props=None ): @@ -114,6 +152,16 @@ def create_test_health_facility( location = Location.objects.filter(type="D", validity_to__isnull=True).first() custom_props["location"] = location or create_test_location("D") + # Ensure required reference data exists + if not custom_props.get("legal_form"): + legal_form = create_test_health_facility_legal_form() + custom_props["legal_form"] = legal_form + + if not custom_props.get("sub_level") and "sub_level" not in custom_props: + # Only set sub_level if not explicitly set to None + sub_level = create_test_health_facility_sub_level() + custom_props["sub_level"] = sub_level + obj = HealthFacility.objects.filter(code=code, validity_to__isnull=valid).first() if obj is not None: if custom_props: @@ -124,7 +172,6 @@ def create_test_health_facility( **{ "code": code, "level": "H", - "legal_form": HealthFacilityLegalForm.objects.filter(code="C").first(), "name": "Test location " + code, "care_type": "B", "validity_from": "2019-01-01", @@ -158,3 +205,288 @@ def create_test_health_catchment(hf, location, custom_props=None): ) return obj + + +def create_basic_test_locations(): + """ + Create basic test location hierarchy for testing purposes. + Creates locations based on the provided SQL data structure. + """ + from django.utils import timezone + import uuid + + # Basic location data for testing + location_data = [ + { + "code": "R2", + "name": "Tahida", + "type": "R", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "68753566-9d2e-4cec-936e-4c6bf1968c0d", + }, + { + "code": "R2D1", + "name": "Rajo", + "type": "D", + "parent_code": "R2", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "2ee8ea9c-aef7-400b-9b36-f391f956f73e", + }, + { + "code": "R2D2", + "name": "Vida", + "type": "D", + "parent_code": "R2", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "176d0c41-13dc-4faf-9c1e-95109f086059", + }, + { + "code": "R2D1M1", + "name": "Jaber", + "type": "W", + "parent_code": "R2D1", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "bf590058-be5c-494e-9e05-c7f2695c645e", + }, + { + "code": "R2D1M1V1", + "name": "Utha", + "type": "V", + "parent_code": "R2D1M1", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "b2e5b0c1-3d57-408c-b7de-11511ce1cbcf", + }, + { + "code": "R2D2M1", + "name": "Majhi", + "type": "W", + "parent_code": "R2D2", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "225789ce-4d14-4098-8ae2-3c90e96fae8f", + }, + { + "code": "R2D2M1V1", + "name": "Radho", + "type": "V", + "parent_code": "R2D2M1", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "9ea9f849-2c7c-4454-810d-cf60bde6bdc7", + }, + { + "code": "R1", + "name": "Ultha", + "type": "R", + "validity_from": timezone.datetime(2016, 1, 1), + "uuid": "40c4010d-8c9d-4be3-8653-e647b21b19a9", + }, + { + "code": "R1D1", + "name": "Rapta", + "type": "D", + "parent_code": "R1", + "validity_from": timezone.datetime(2016, 1, 1), + "uuid": "35043da3-1e04-46f9-a67e-00b9973b588f", + }, + { + "code": "R1D1M1", + "name": "Jamu", + "type": "W", + "parent_code": "R1D1", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "820e1a1f-4195-445b-a14c-4f762fad6780", + }, + { + "code": "R1D1M2", + "name": "Adhi", + "type": "W", + "parent_code": "R1D1", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "522c4c5e-10f1-4f6a-98ef-1ae75a259eb5", + }, + { + "code": "R1D1M3", + "name": "Jobber", + "type": "W", + "parent_code": "R1D1", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "2641ec90-7879-469e-9d8b-f180c720a079", + }, + { + "code": "R1D1M4", + "name": "Radler", + "type": "W", + "parent_code": "R1D1", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "6c3b52cb-7926-4345-8048-77ac99ae80c1", + }, + { + "code": "R1D1M5", + "name": "Radler", + "type": "W", + "parent_code": "R1D1", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "6c3b52cb-7926-4345-8048-77ac99ae80c2", # slight change for uniqueness + }, + { + "code": "R1D1M1V1", + "name": "Rachla", + "type": "V", + "parent_code": "R1D1M1", + "validity_from": timezone.datetime(2016, 1, 1), + "uuid": "4842af48-fa6a-46fa-b5bb-08001bb58f5f", + }, + { + "code": "R1D1M1V2", + "name": "Darbu", + "type": "V", + "parent_code": "R1D1M1", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "108a16ea-5d7d-4534-a7e5-ab82c474fa7f", + }, + { + "code": "R1D1M1V3", + "name": "Agdo", + "type": "V", + "parent_code": "R1D1M1", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "eba563de-13cb-4cea-9bdd-ecab9a4344c5", + }, + { + "code": "R1D1M2V1", + "name": "Jamula", + "type": "V", + "parent_code": "R1D1M2", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "40485985-e4ab-43f9-9700-bf80e342a1ee", + }, + { + "code": "R1D1M3V1", + "name": "Rathula", + "type": "V", + "parent_code": "R1D1M3", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "f28d1e17-92ea-4db4-b26b-88ca429731b5", + }, + { + "code": "R1D1M4V1", + "name": "Jobla", + "type": "V", + "parent_code": "R1D1M4", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "712451a5-6074-441c-9a57-5229d33a1a6c", + }, + { + "code": "R1D1M5V1", + "name": "Rolo", + "type": "V", + "parent_code": "R1D1M5", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "e4a522fc-fa81-4954-9f11-1fee3853dbc0", + }, + { + "code": "R1D2", + "name": "Jambero", + "type": "D", + "parent_code": "R1", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "6ca4b45b-ac17-4ff4-954c-dc1294bc66d1", + }, + { + "code": "R1D3", + "name": "Uptol", + "type": "D", + "parent_code": "R1", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "e04c7312-46b0-4526-94d5-1717e4ec978f", + }, + { + "code": "R1D2M1", + "name": "Actoloby", + "type": "W", + "parent_code": "R1D2", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "4cf9a26d-6cb9-48cc-b42b-55ef61a9d0f2", + }, + { + "code": "R1D2M2", + "name": "Remorlogy", + "type": "W", + "parent_code": "R1D2", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "cac524c0-1bac-4c96-9376-9d0ee35eb0aa", + }, + { + "code": "R1D2M1V1", + "name": "Holobo", + "type": "V", + "parent_code": "R1D2M1", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "ca5ec00f-eaa3-4af8-ac11-7bc5abb3341b", + }, + { + "code": "R1D2M1V2", + "name": "Octo", + "type": "V", + "parent_code": "R1D2M1", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "d7c17b9f-c508-4b92-b308-4c3727a5ada0", + }, + { + "code": "R1D2M1V3", + "name": "Raberjab", + "type": "V", + "parent_code": "R1D2M1", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "d862b77c-0e83-484b-b337-3c7adb06c034", + }, + { + "code": "R1D2M2V1", + "name": "Agilo", + "type": "V", + "parent_code": "R1D2M2", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "b29ccc93-779d-459c-942c-df0b98b22ebb", + }, + { + "code": "R1D3M1", + "name": "Uminal", + "type": "W", + "parent_code": "R1D3", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "7e89aae5-3627-49e9-aa94-bf387c510939", + }, + { + "code": "R1D3M1V1", + "name": "Uminalum", + "type": "V", + "parent_code": "R1D3M1", + "validity_from": timezone.datetime(2017, 1, 1), + "uuid": "f30342eb-94bf-4155-92d2-77eaf6559cd6", + }, + ] + + created_locations = {} + for data in location_data: + parent = None + if "parent_code" in data: + parent = created_locations.get(data["parent_code"]) + if not parent: + # Try to find existing parent + parent = Location.objects.filter(code=data["parent_code"], validity_to__isnull=True).first() + + location, created = Location.objects.get_or_create( + code=data["code"], + validity_to=None, + defaults={ + "name": data["name"], + "type": data["type"], + "parent": parent, + "validity_from": data["validity_from"], + "validity_to": None, + "audit_user_id": -1, + "uuid": data.get("uuid", str(uuid.uuid4())), + } + ) + created_locations[data["code"]] = location + + return created_locations diff --git a/location/tests/test.py b/location/tests/test.py index ac25ded..b1fd96d 100644 --- a/location/tests/test.py +++ b/location/tests/test.py @@ -5,12 +5,20 @@ create_test_location, assign_user_districts, ) -from core.test_helpers import create_test_officer, create_test_interactive_user, create_test_claim_admin +from core.test_helpers import ( + create_test_officer, + create_test_interactive_user, + create_test_claim_admin, + create_manager_role, + create_imis_admin_role, + create_enrolment_officer_role, + create_claim_admin_role, + create_admin_role +) from django.core.cache import caches from location.models import LocationManager, UserDistrict, Location, cache, cache_location_if_not_cached -from core.utils import filter_validity -from core.models.user import Role + _TEST_USER_NAME = "test_batch_run" _TEST_USER_PASSWORD = "test_batch_run" @@ -21,7 +29,7 @@ "other_names": _TEST_USER_NAME, "user_types": "INTERACTIVE", "language": "en", - "roles": [1, 5, 9], + "roles": [create_admin_role().id], } @@ -39,10 +47,10 @@ class LocationTest(TestCase): def setUpTestData(cls): cls.test_village = create_test_village() - super_user_role = Role.objects.filter(is_system=64, *filter_validity()).first() - ca_role = Role.objects.filter(is_system=16, *filter_validity()).first() - eo_role = Role.objects.filter(is_system=1, *filter_validity()).first() - xx_role = Role.objects.filter(is_system=2, *filter_validity()).first() + super_user_role = create_imis_admin_role() + ca_role = create_claim_admin_role() + eo_role = create_enrolment_officer_role() + xx_role = create_manager_role() cls.test_user = create_test_interactive_user( username="loctest", roles=[xx_role.id] ) @@ -117,7 +125,7 @@ def test_allowed(self): self.test_user._u.id, loc_types=["R", "D", "W"] ) self.assertEqual(len(allowed), 2) - allowed_ids_non_qs = list(l.id for l in allowed) + allowed_ids_non_qs = list(loc.id for loc in allowed) self.assertEqual(sorted(allowed_ids_non_qs), sorted(allowed_ids)) # Not strict should include parent, but not sibling @@ -264,7 +272,7 @@ def test_get_user_districts_does_not_rely_on_cache_for_correctness(self): all_valid_districts = Location.objects.filter( type="D", parent__isnull=False, - *filter_validity(), + *Location.filter_validity(), ) self.assertTrue(all_valid_districts.exists()) diff --git a/location/tests/test_graphql.py b/location/tests/test_graphql.py index 6400b0e..8268729 100644 --- a/location/tests/test_graphql.py +++ b/location/tests/test_graphql.py @@ -2,8 +2,11 @@ import json import uuid -from core import filter_validity -from core.test_helpers import create_test_interactive_user +from core.test_helpers import ( + create_test_interactive_user, + create_imis_admin_role, + create_enrolment_officer_role, +) from django.conf import settings from django.core import exceptions from graphene_django.utils.testing import GraphQLTestCase @@ -14,6 +17,7 @@ create_test_location, assign_user_districts, create_test_village, + create_basic_test_locations, ) from rest_framework import status @@ -42,12 +46,18 @@ def setUpTestData(cls): cls.test_ward = cls.test_village.parent cls.test_region = cls.test_village.parent.parent.parent cls.test_district = cls.test_village.parent.parent + cls.test_region.name = "LocTestRegion" + cls.test_region.code = "LTR" + cls.test_region.save() + create_basic_test_locations() + admin_role = create_imis_admin_role() cls.admin_user = create_test_interactive_user( - username="testLocationAdmin", roles=[7] + username="testLocationAdmin", roles=[admin_role.id] ) cls.admin_token = get_token(cls.admin_user, DummyContext(user=cls.admin_user)) + manager_role = create_enrolment_officer_role() cls.noright_user = create_test_interactive_user( - username="testLocationNoRight", roles=[1] + username="testLocationNoRight", roles=[manager_role.id] ) cls.noright_token = get_token( cls.noright_user, DummyContext(user=cls.noright_user) @@ -125,6 +135,7 @@ def _test_arg_locations_query(self, arg, token=None): node { id name + code } } } @@ -156,7 +167,7 @@ def _test_arg_locations_query(self, arg, token=None): def test_code_locations_query(self): self._test_arg_locations_query('code:"%s"' % self.test_region.code) self._test_arg_locations_query('name:"%s"' % self.test_region.name) - self._test_arg_locations_query('name_Icontains:"Test ", type:"R"') + self._test_arg_locations_query('name_Icontains:"LocTest ", type:"R"') def test_code_locations_district_limited_query(self): """ @@ -231,11 +242,11 @@ def test_locations_str_query(self): invalid_villages_qs = Location.objects.filter(type='V', code=invalid_village.code, name=invalid_village.name) self.assertEquals(invalid_villages_qs.count(), 1) - self.assertEquals(invalid_villages_qs.filter(*filter_validity()).count(), 0) + self.assertEquals(invalid_villages_qs.filter(*Location.filter_validity()).count(), 0) valid_villages_qs = Location.objects.filter(type='V', code=self.test_village.code, name=self.test_village.name) self.assertEquals(valid_villages_qs.count(), 1) - self.assertEquals(valid_villages_qs.filter(*filter_validity()).count(), 1) + self.assertEquals(valid_villages_qs.filter(*Location.filter_validity()).count(), 1) query_str = """{ locationsStr( @@ -290,7 +301,8 @@ def test_locations_str_query(self): ) # Check that the same works for non-admin users - non_admin_user = create_test_interactive_user(username="imnotadmin", roles=[1]) + manager_role = create_enrolment_officer_role() + non_admin_user = create_test_interactive_user(username="imnotadmin", roles=[manager_role.id]) self.assertFalse(non_admin_user.is_superuser) non_admin_user_token = get_token(non_admin_user, DummyContext(user=non_admin_user)) district_code = self.test_village.parent.parent.code @@ -429,15 +441,18 @@ class HealthFacilityGQLTestCase(GraphQLTestCase): def setUpClass(cls): super().setUpClass() - cls.admin_user = create_test_interactive_user(username="testHFAdmin") + admin_role = create_imis_admin_role() + cls.admin_user = create_test_interactive_user(username="testHFAdmin", roles=[admin_role.id]) cls.admin_token = get_token(cls.admin_user, DummyContext(user=cls.admin_user)) + manager_role = create_enrolment_officer_role() cls.noright_user = create_test_interactive_user( - username="testHFNoRight", roles=[1] + username="testHFNoRight", roles=[manager_role.id] ) cls.noright_token = get_token( cls.noright_user, DummyContext(user=cls.noright_user) ) cls.admin_dist_user = create_test_interactive_user(username="testHFDist") + create_basic_test_locations() assign_user_districts(cls.noright_user, ["R1D1", "R2D1", "R2D2"]) cls.admin_dist_token = get_token( cls.admin_dist_user, DummyContext(user=cls.admin_dist_user)