Skip to content

Commit b649b67

Browse files
sudip-khanalsusilnem
authored andcommitted
feat(local-unit): Add local unit status field and migration script (#2528)
* feat(model): add local unit status field and migration script * fix(migration): replace migration script with migration operation
1 parent ab7f9d6 commit b649b67

File tree

9 files changed

+90
-32
lines changed

9 files changed

+90
-32
lines changed

local_units/admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class LocalUnitAdmin(CompareVersionAdmin, admin.OSMGeoAdmin):
5555
)
5656
readonly_fields = (
5757
"validated",
58+
"status",
5859
"is_locked",
5960
"is_new_local_unit",
6061
)

local_units/enums.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
"deprecate_reason": models.LocalUnit.DeprecateReason,
55
"validation_status": models.LocalUnitChangeRequest.Status,
66
"validators": models.Validator,
7+
"status": models.LocalUnit.Status,
78
}

local_units/filterset.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44

55

66
class LocalUnitFilters(filters.FilterSet):
7+
status = filters.ChoiceFilter(
8+
choices=LocalUnit.Status.choices,
9+
label="Status",
10+
)
11+
712
class Meta:
813
model = LocalUnit
914
fields = (
@@ -14,6 +19,7 @@ class Meta:
1419
"draft",
1520
"validated",
1621
"is_locked",
22+
"status",
1723
)
1824

1925

local_units/management/commands/notify_validators.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from local_units.models import LocalUnit, Validator
99
from local_units.utils import (
1010
get_email_context,
11-
get_global_validators_by_type,
12-
get_region_validators_by_type,
11+
get_local_unit_global_validators,
12+
get_local_unit_region_validators,
1313
)
1414
from main.sentry import SentryMonitor
1515
from notifications.notification import send_notification
@@ -24,15 +24,15 @@ def handle(self, *args, **options):
2424

2525
# Regional Validators: 14 days
2626
queryset_for_regional_validators = LocalUnit.objects.filter(
27-
validated=False,
27+
status__in=[LocalUnit.Status.UNVALIDATED, LocalUnit.Status.PENDING_EDIT_VALIDATION],
2828
is_deprecated=False,
2929
last_sent_validator_type=Validator.LOCAL,
3030
created_at__lte=timezone.now() - timedelta(days=7),
3131
)
3232

3333
# Global Validators: 28 days
3434
queryset_for_global_validators = LocalUnit.objects.filter(
35-
validated=False,
35+
status__in=[LocalUnit.Status.UNVALIDATED, LocalUnit.Status.PENDING_EDIT_VALIDATION],
3636
is_deprecated=False,
3737
last_sent_validator_type=Validator.REGIONAL,
3838
created_at__lte=timezone.now() - timedelta(days=14),
@@ -45,7 +45,7 @@ def handle(self, *args, **options):
4545
email_subject = "Action Required: Local Unit Pending Validation"
4646
email_type = "Local Unit"
4747

48-
for region_admin_validator in get_region_validators_by_type(local_unit):
48+
for region_admin_validator in get_local_unit_region_validators(local_unit):
4949
try:
5050
email_context["full_name"] = region_admin_validator.get_full_name()
5151
email_body = render_to_string("email/local_units/local_unit.html", email_context)
@@ -67,7 +67,7 @@ def handle(self, *args, **options):
6767
email_subject = "Action Required: Local Unit Pending Validation"
6868
email_type = "Local Unit"
6969

70-
for global_validator in get_global_validators_by_type(local_unit):
70+
for global_validator in get_local_unit_global_validators(local_unit):
7171
try:
7272
email_context["full_name"] = global_validator.get_full_name()
7373
email_body = render_to_string("email/local_units/local_unit.html", email_context)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Generated by Django 4.2.19 on 2025-08-07 06:23
2+
3+
from django.db import migrations, models
4+
5+
6+
def migrate_validated_to_status(apps, schema_editor):
7+
"""
8+
Update all validated field values to corresponding status values
9+
"""
10+
LocalUnit = apps.get_model("local_units", "LocalUnit")
11+
LocalUnit.objects.filter(validated=True).update(status=1)
12+
LocalUnit.objects.filter(validated=False).update(status=2)
13+
14+
15+
class Migration(migrations.Migration):
16+
17+
dependencies = [
18+
("local_units", "0022_localunit_is_new_local_unit"),
19+
]
20+
21+
operations = [
22+
migrations.AddField(
23+
model_name="localunit",
24+
name="status",
25+
field=models.IntegerField(
26+
choices=[(1, "Validated"), (2, "Unvalidated"), (3, "Pending Edit Validation")],
27+
default=2,
28+
verbose_name="Validation Status",
29+
),
30+
),
31+
migrations.RunPython(
32+
migrate_validated_to_status,
33+
reverse_code=migrations.RunPython.noop,
34+
),
35+
]

local_units/models.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,12 @@ class DeprecateReason(models.IntegerChoices):
291291
SECURITY_CONCERNS = 3, _("Security concerns")
292292
OTHER = 4, _("Other")
293293

294+
class Status(models.IntegerChoices):
295+
# NOTE: Tracks the status of LocalUnit entries to manage validation workflows.
296+
VALIDATED = 1, "Validated"
297+
UNVALIDATED = 2, "Unvalidated"
298+
PENDING_EDIT_VALIDATION = 3, "Pending Edit Validation"
299+
294300
# added to track health local unit data (Table B)
295301
health = models.ForeignKey(
296302
HealthData, on_delete=models.SET_NULL, verbose_name=_("Health Data"), related_name="health_data", null=True, blank=True
@@ -329,7 +335,10 @@ class DeprecateReason(models.IntegerChoices):
329335
auto_now=False,
330336
)
331337
draft = models.BooleanField(default=False, verbose_name=_("Draft"))
332-
validated = models.BooleanField(default=False, verbose_name=_("Validated"))
338+
validated = models.BooleanField(default=False, verbose_name=_("Validated")) # NOTE: This field might be deprecated soon.
339+
status = models.IntegerField(
340+
choices=Status.choices, default=Status.UNVALIDATED, verbose_name=_("Validation Status")
341+
) # NOTE: Replacement of validated field for better status tracking
333342
visibility = models.IntegerField(choices=VisibilityChoices.choices, verbose_name=_("visibility"), default=2) # 2:IFRC
334343
source_en = models.CharField(max_length=500, blank=True, null=True, verbose_name=_("Source in Local Language"))
335344
source_loc = models.CharField(max_length=500, blank=True, null=True, verbose_name=_("Source in English"))

local_units/serializers.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ class LocalUnitDetailSerializer(serializers.ModelSerializer):
199199
health = HealthDataSerializer(required=False, allow_null=True)
200200
location_geojson = serializers.SerializerMethodField()
201201
visibility_display = serializers.CharField(source="get_visibility_display", read_only=True)
202-
validated = serializers.BooleanField(read_only=True)
202+
status_details = serializers.CharField(source="get_status_display", read_only=True)
203203

204204
class Meta:
205205
model = LocalUnit
@@ -211,7 +211,8 @@ class Meta:
211211
"created_at",
212212
"modified_at",
213213
"draft",
214-
"validated",
214+
"status",
215+
"status_details",
215216
"postcode",
216217
"address_loc",
217218
"address_en",
@@ -250,7 +251,7 @@ class PrivateLocalUnitDetailSerializer(NestedCreateMixin, NestedUpdateMixin):
250251
# NOTE: location_geojson contains the geojson of the location
251252
location_geojson = serializers.SerializerMethodField(read_only=True)
252253
visibility_display = serializers.CharField(source="get_visibility_display", read_only=True)
253-
validated = serializers.BooleanField(read_only=True)
254+
status_details = serializers.CharField(source="get_status_display", read_only=True)
254255
location_json = LocationSerializer(required=True)
255256
location = serializers.CharField(required=False)
256257
modified_by_details = LocalUnitMiniUserSerializer(source="modified_by", read_only=True)
@@ -271,7 +272,8 @@ class Meta:
271272
"modified_at",
272273
"modified_by",
273274
"draft",
274-
"validated",
275+
"status",
276+
"status_details",
275277
"postcode",
276278
"address_loc",
277279
"address_en",
@@ -393,7 +395,7 @@ class LocalUnitSerializer(serializers.ModelSerializer):
393395
country_details = LocalUnitCountrySerializer(source="country", read_only=True)
394396
type_details = LocalUnitTypeSerializer(source="type", read_only=True)
395397
health_details = MiniHealthDataSerializer(read_only=True, source="health")
396-
validated = serializers.BooleanField(read_only=True)
398+
status_details = serializers.CharField(source="get_status_display", read_only=True)
397399

398400
class Meta:
399401
model = LocalUnit
@@ -404,7 +406,8 @@ class Meta:
404406
"english_branch_name",
405407
"location_geojson",
406408
"type",
407-
"validated",
409+
"status",
410+
"status_details",
408411
"address_loc",
409412
"address_en",
410413
"country_details",
@@ -423,7 +426,7 @@ class PrivateLocalUnitSerializer(serializers.ModelSerializer):
423426
country_details = LocalUnitCountrySerializer(source="country", read_only=True)
424427
type_details = LocalUnitTypeSerializer(source="type", read_only=True)
425428
health_details = MiniHealthDataSerializer(read_only=True, source="health")
426-
validated = serializers.BooleanField(read_only=True)
429+
status_details = serializers.CharField(source="get_status_display", read_only=True)
427430
modified_by_details = LocalUnitMiniUserSerializer(source="modified_by", read_only=True)
428431
is_locked = serializers.BooleanField(read_only=True)
429432

@@ -436,7 +439,8 @@ class Meta:
436439
"english_branch_name",
437440
"location_geojson",
438441
"type",
439-
"validated",
442+
"status",
443+
"status_details",
440444
"address_loc",
441445
"address_en",
442446
"country_details",

local_units/test_views.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,6 @@ def test_create_local_unit_administrative(self):
385385
"type": type.id,
386386
"country": country.id,
387387
"draft": False,
388-
"validated": True,
389388
"postcode": "4407",
390389
"address_loc": "4407",
391390
"address_en": "",
@@ -416,6 +415,7 @@ def test_create_local_unit_administrative(self):
416415
data["english_branch_name"] = "Test branch name"
417416
response = self.client.post("/api/v2/local-units/", data=data, format="json")
418417
self.assertEqual(response.status_code, 201)
418+
self.assertEqual(response.data["status"], LocalUnit.Status.UNVALIDATED)
419419

420420
# Checking the request changes for the local unit is created or not
421421
request_change = LocalUnitChangeRequest.objects.all()
@@ -438,7 +438,6 @@ def test_create_update_local_unit_health(self):
438438
"created_at": "2024-05-13T06:53:14.978083Z",
439439
"modified_at": "2024-05-13T06:53:14.978099Z",
440440
"draft": False,
441-
"validated": True,
442441
"postcode": "",
443442
"address_loc": "Silele Clinic is is in Hosea Inkhundla under the Shiselweni, Sigombeni is in Nkom'iyahlaba Inkhundla under the Manzini region and Mahwalala is in the Mbabane West Inkhundla under the Hhohho region.", # noqa: E501
444443
"address_en": "",
@@ -506,6 +505,7 @@ def test_create_update_local_unit_health(self):
506505
self.assertEqual(response.status_code, 201)
507506
self.assertEqual(LocalUnitChangeRequest.objects.count(), 1)
508507
self.assertEqual(response.data["is_locked"], True)
508+
self.assertEqual(response.data["status"], LocalUnit.Status.UNVALIDATED)
509509
# Locked local unit should not be updated, until it is unlocked
510510
local_unit_id = response.data["id"]
511511
response = self.client.put(f"/api/v2/local-units/{local_unit_id}/", data=data, format="json")
@@ -529,7 +529,6 @@ def test_revert_local_unit(self):
529529
"created_at": "2024-05-13T06:53:14.978083Z",
530530
"modified_at": "2024-05-13T06:53:14.978099Z",
531531
"draft": False,
532-
"validated": True,
533532
"postcode": "",
534533
"address_loc": "Silele Clinic is is in Hosea Inkhundla under the Shiselweni, Sigombeni is in Nkom'iyahlaba Inkhundla under the Manzini region and Mahwalala is in the Mbabane West Inkhundla under the Hhohho region.", # noqa: E501
535534
"address_en": "",
@@ -579,8 +578,8 @@ def test_revert_local_unit(self):
579578
response = self.client.post(f"/api/v2/local-units/{local_unit_id}/validate/")
580579
self.assert_200(response)
581580
self.assertEqual(response.data["is_locked"], False)
582-
self.assertEqual(response.data["validated"], True)
583-
581+
# self.assertEqual(response.data["validated"], True)
582+
self.assertEqual(response.data["status"], LocalUnit.Status.VALIDATED)
584583
# saving the previous data
585584
previous_data = response.data
586585

@@ -591,7 +590,6 @@ def test_revert_local_unit(self):
591590
response = self.client.put(f"/api/v2/local-units/{local_unit_id}/", data=data, format="json")
592591
self.assert_200(response)
593592
self.assertEqual(response.data["local_branch_name"], data["local_branch_name"])
594-
595593
# Reverting the local unit
596594
revert_data = {
597595
"reason": "Reverting the local unit test",
@@ -609,7 +607,7 @@ def test_revert_local_unit(self):
609607
self.assertEqual(local_unit_change_request.rejected_reason, revert_data["reason"])
610608
# Checking if the local unit is unlocked
611609
self.assertEqual(local_unit.is_locked, False)
612-
self.assertEqual(local_unit.validated, True)
610+
self.assertEqual(local_unit.status, LocalUnit.Status.VALIDATED)
613611

614612
def test_latest_changes(self):
615613
region = Region.objects.create(name=2)
@@ -627,7 +625,7 @@ def test_latest_changes(self):
627625
"country": country.id,
628626
"created_at": "2024-05-13T06:53:14.978083Z",
629627
"modified_at": "2024-05-13T06:53:14.978099Z",
630-
"validated": True,
628+
"status": LocalUnit.Status.VALIDATED,
631629
"date_of_data": "2024-05-13",
632630
"level": level.id,
633631
"address_loc": "Silele Clinic is is in Hosea Inkhundla under the Shiselweni, Sigombeni is in Nkom'iyahlaba Inkhundla under the Manzini region and Mahwalala is in the Mbabane West Inkhundla under the Hhohho region.", # noqa: E501
@@ -687,7 +685,7 @@ def test_validate_local_unit(self):
687685
self.authenticate()
688686
response = self.client.post("/api/v2/local-units/", data=data, format="json")
689687
self.assert_201(response)
690-
688+
self.assertEqual(response.data["status"], LocalUnit.Status.UNVALIDATED)
691689
local_unit_id = response.data["id"]
692690
self.authenticate(self.global_validator_user)
693691
# validating the local unit by the Global validator
@@ -697,6 +695,7 @@ def test_validate_local_unit(self):
697695
local_unit=local_unit_id, status=LocalUnitChangeRequest.Status.APPROVED
698696
).last()
699697
self.assertEqual(local_unit_request.current_validator, Validator.GLOBAL)
698+
self.assertEqual(response.data["status"], LocalUnit.Status.VALIDATED)
700699

701700
# Testing For the local unit admin/country validator
702701
self.authenticate(self.country_validator_user)
@@ -709,6 +708,7 @@ def test_validate_local_unit(self):
709708
local_unit=local_unit_id, status=LocalUnitChangeRequest.Status.APPROVED
710709
).last()
711710
self.assertEqual(local_unit_request.current_validator, Validator.LOCAL)
711+
self.assertEqual(response.data["status"], LocalUnit.Status.VALIDATED)
712712

713713
# Testing For the regional validator
714714
self.authenticate(self.region_validator_user)
@@ -720,6 +720,7 @@ def test_validate_local_unit(self):
720720
local_unit=local_unit_id, status=LocalUnitChangeRequest.Status.APPROVED
721721
).last()
722722
self.assertEqual(local_unit_request.current_validator, Validator.REGIONAL)
723+
self.assertEqual(response.data["status"], LocalUnit.Status.VALIDATED)
723724

724725
# Testing for Root User/Global validator
725726
self.authenticate(self.root_user)
@@ -731,6 +732,7 @@ def test_validate_local_unit(self):
731732
local_unit=local_unit_id, status=LocalUnitChangeRequest.Status.APPROVED
732733
).last()
733734
self.assertEqual(local_unit_request.current_validator, Validator.GLOBAL)
735+
self.assertEqual(response.data["status"], LocalUnit.Status.VALIDATED)
734736

735737

736738
class TestExternallyManagedLocalUnit(APITestCase):

local_units/views.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class PrivateLocalUnitViewSet(viewsets.ModelViewSet):
6666
"level",
6767
)
6868
.exclude(is_deprecated=True)
69-
.order_by("validated")
69+
.order_by("status")
7070
)
7171
filterset_class = LocalUnitFilters
7272
search_fields = (
@@ -109,13 +109,13 @@ def update(self, request, *args, **kwargs):
109109
# NOTE: Locking the local unit after the change request is created
110110
previous_data = PrivateLocalUnitDetailSerializer(local_unit, context={"request": request}).data
111111
local_unit.is_locked = True
112-
local_unit.validated = False
112+
local_unit.status = LocalUnit.Status.PENDING_EDIT_VALIDATION
113113
local_unit.update_reason_overview = update_reason
114114
local_unit.is_new_local_unit = False
115115
local_unit.save(
116116
update_fields=[
117117
"is_locked",
118-
"validated",
118+
"status",
119119
"update_reason_overview",
120120
"is_new_local_unit",
121121
]
@@ -164,12 +164,12 @@ def get_validate(self, request, pk=None, version=None):
164164
change_request_instance.save(update_fields=["status", "updated_by", "updated_at", "current_validator"])
165165

166166
# Validate the local unit
167-
local_unit.validated = True
167+
local_unit.status = LocalUnit.Status.VALIDATED
168168
local_unit.is_locked = False
169169
local_unit.is_new_local_unit = False
170170
local_unit.save(
171171
update_fields=[
172-
"validated",
172+
"status",
173173
"is_locked",
174174
"is_new_local_unit",
175175
]
@@ -189,7 +189,7 @@ def get_validate(self, request, pk=None, version=None):
189189
def get_revert(self, request, pk=None, version=None):
190190
local_unit = self.get_object()
191191

192-
if local_unit.validated:
192+
if local_unit.status == LocalUnit.Status.VALIDATED:
193193
return bad_request("Local unit is already validated and cannot be reverted")
194194

195195
rejected_data = PrivateLocalUnitDetailSerializer(local_unit, context={"request": request}).data
@@ -233,9 +233,9 @@ def get_revert(self, request, pk=None, version=None):
233233

234234
# NOTE: Unlocking the reverted local unit
235235
local_unit.is_locked = False
236-
local_unit.validated = True
236+
local_unit.status = LocalUnit.Status.VALIDATED
237237
local_unit.is_new_local_unit = False
238-
local_unit.save(update_fields=["is_locked", "validated", "is_new_local_unit"])
238+
local_unit.save(update_fields=["is_locked", "status", "is_new_local_unit"])
239239

240240
# reverting the previous data of change request to local unit by passing through serializer
241241
serializer = PrivateLocalUnitDetailSerializer(

0 commit comments

Comments
 (0)