Skip to content

Commit ab7f9d6

Browse files
feat(command): add script to generate permissions by local unit type (#2517)
* feat(command): add script to generate permissions by local unit type * fix(api): update email notification sending logic on local unit creation * fix(api): update permission on local unit update and validate api * fix(permission): update permission creation logic * chore(filter): add filter by country id on externally managed api * chore(local-units): Update local unit permission script - Add validation check for unique managed local units * chore(script): add script to delete old global validator permission of local unit * fix(serializer): add local unit permission in user me serializer * fix(api): add is new local unit field and update userme serializer * fix(models): make is new local unit field default false --------- Co-authored-by: Sushil Tiwari <[email protected]>
1 parent aadb9e5 commit ab7f9d6

16 files changed

+395
-170
lines changed

api/serializers.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
from collections import defaultdict
23
from typing import List, Union
34

45
from django.conf import settings
@@ -11,6 +12,7 @@
1112

1213
# from api.utils import pdf_exporter
1314
from api.tasks import generate_url
15+
from api.utils import CountryValidator, RegionValidator
1416
from deployments.models import EmergencyProject, Personnel, PersonnelDeployment
1517
from dref.models import Dref, DrefFinalReport, DrefOperationalUpdate
1618
from lang.models import String
@@ -1771,6 +1773,10 @@ class UserMeSerializer(UserSerializer):
17711773
user_countries_regions = serializers.SerializerMethodField()
17721774
limit_access_to_guest = serializers.BooleanField(read_only=True, source="profile.limit_access_to_guest")
17731775

1776+
local_unit_country_validators = serializers.SerializerMethodField()
1777+
local_unit_region_validators = serializers.SerializerMethodField()
1778+
local_unit_global_validators = serializers.SerializerMethodField()
1779+
17741780
class Meta:
17751781
model = User
17761782
fields = UserSerializer.Meta.fields + (
@@ -1782,6 +1788,9 @@ class Meta:
17821788
"is_per_admin_for_countries",
17831789
"user_countries_regions",
17841790
"limit_access_to_guest",
1791+
"local_unit_country_validators",
1792+
"local_unit_region_validators",
1793+
"local_unit_global_validators",
17851794
)
17861795

17871796
@staticmethod
@@ -1845,6 +1854,49 @@ def get_user_countries_regions(user):
18451854
qs = UserCountry.objects.filter(user=user).distinct("country")
18461855
return UserCountrySerializer(qs, many=True).data
18471856

1857+
@staticmethod
1858+
def get_local_unit_global_validators(user) -> List[int]:
1859+
permission_codenames = Permission.objects.filter(
1860+
codename__startswith="local_unit_global_validator", group__user=user
1861+
).values_list("codename", flat=True)
1862+
global_validators = {int(code.split("_")[-1]) for code in permission_codenames}
1863+
return list(global_validators)
1864+
1865+
@staticmethod
1866+
def get_local_unit_region_validators(user) -> list[RegionValidator]:
1867+
permission_codenames = Permission.objects.filter(
1868+
codename__startswith="local_unit_region_validator",
1869+
group__user=user,
1870+
).values_list("codename", flat=True)
1871+
region_validators = defaultdict(set)
1872+
for code in permission_codenames:
1873+
parts = code.split("_")[-2:]
1874+
if len(parts) == 2 and all(p.isdigit() for p in parts):
1875+
local_unit_type_id = int(parts[0])
1876+
region_id = int(parts[1])
1877+
region_validators[region_id].add(local_unit_type_id)
1878+
return [
1879+
{"region": region, "local_unit_types": list(local_unit_types)}
1880+
for region, local_unit_types in region_validators.items()
1881+
]
1882+
1883+
@staticmethod
1884+
def get_local_unit_country_validators(user) -> list[CountryValidator]:
1885+
permission_codenames = Permission.objects.filter(
1886+
codename__startswith="local_unit_country_validator", group__user=user
1887+
).values_list("codename", flat=True)
1888+
country_validator = defaultdict(set)
1889+
for code in permission_codenames:
1890+
parts = code.split("_")[-2:]
1891+
if len(parts) == 2 and all(p.isdigit() for p in parts):
1892+
local_unit_type_id = int(parts[0])
1893+
country_id = int(parts[1])
1894+
country_validator[country_id].add(local_unit_type_id)
1895+
return [
1896+
{"country": country, "local_unit_types": list(local_unit_types)}
1897+
for country, local_unit_types in country_validator.items()
1898+
]
1899+
18481900

18491901
class ActionSerializer(ModelSerializer):
18501902
class Meta:

api/utils.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import base64
22
import typing
3-
from typing import Optional
3+
from typing import Optional, TypedDict
44

55
from django.core.exceptions import ValidationError
66
from django.db import models
@@ -146,3 +146,13 @@ def generate_field_report_title(
146146
else:
147147
summary = f"{country.iso3}: {dtype.name} - {start_date} - {title} {suffix}"
148148
return summary
149+
150+
151+
class RegionValidator(TypedDict):
152+
region: int
153+
local_unit_types: list[int]
154+
155+
156+
class CountryValidator(TypedDict):
157+
country: int
158+
local_unit_types: list[int]

local_units/admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class LocalUnitAdmin(CompareVersionAdmin, admin.OSMGeoAdmin):
5656
readonly_fields = (
5757
"validated",
5858
"is_locked",
59+
"is_new_local_unit",
5960
)
6061
list_filter = (
6162
AutocompleteFilterFactory("Country", "country"),

local_units/filterset.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,5 @@ class Meta:
3535
"country__name": ["exact", "in"],
3636
"country__iso3": ["exact", "in"],
3737
"country__iso": ["exact", "in"],
38+
"country__id": ["exact", "in"],
3839
}

local_units/management/commands/make_global_validator_permission.py

Lines changed: 0 additions & 29 deletions
This file was deleted.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from django.contrib.auth.models import Group, Permission
2+
from django.contrib.contenttypes.models import ContentType
3+
from django.core.management.base import BaseCommand
4+
from django.db import transaction
5+
6+
from api.models import Country, CountryType, Region
7+
from local_units.models import LocalUnit, LocalUnitType
8+
9+
10+
class Command(BaseCommand):
11+
help = "Create local unit validator permissions and groups for each local unit type"
12+
13+
@transaction.atomic
14+
def handle(self, *args, **options):
15+
content_type = ContentType.objects.get_for_model(LocalUnit)
16+
local_unit_types = LocalUnitType.objects.all()
17+
18+
# Global level Permissions and Groups
19+
self.stdout.write(self.style.NOTICE("\n--- Creating/Updating GLOBAL validator permissions and groups"))
20+
for local_unit_type in local_unit_types.iterator():
21+
codename = f"local_unit_global_validator_{local_unit_type.id}"
22+
name = f"Global validator for {local_unit_type.name}"
23+
permission, _ = Permission.objects.get_or_create(
24+
codename=codename,
25+
content_type=content_type,
26+
defaults=dict(name=name),
27+
)
28+
group_name = f"Local unit global validator for {local_unit_type.name}"
29+
group, _ = Group.objects.get_or_create(name=group_name)
30+
group.permissions.add(permission)
31+
self.stdout.write(self.style.SUCCESS(f"\t--> {local_unit_type.name}"))
32+
33+
# Region level Permissions and Groups
34+
self.stdout.write(self.style.NOTICE("\n--- Creating/Updating REGION-LEVEL validator permissions and groups"))
35+
regions = Region.objects.all()
36+
for region in regions.iterator():
37+
region_name = region.get_name_display()
38+
self.stdout.write(self.style.NOTICE(f"--> Region: {region_name}"))
39+
for local_unit_type in local_unit_types.iterator():
40+
codename = f"local_unit_region_validator_{local_unit_type.id}_{region.id}"
41+
name = f"Local unit validator for {local_unit_type.name} {region_name}"
42+
permission, _ = Permission.objects.get_or_create(
43+
codename=codename,
44+
content_type=content_type,
45+
defaults=dict(name=name),
46+
)
47+
group_name = f"Local unit validator for {local_unit_type.name} {region_name}"
48+
group, _ = Group.objects.get_or_create(name=group_name)
49+
group.permissions.add(permission)
50+
self.stdout.write(self.style.SUCCESS(f"\t--> {local_unit_type.name}"))
51+
52+
# Country level Permissions and Groups
53+
self.stdout.write(self.style.NOTICE("\n--- Creating/Updating COUNTRY-LEVEL validator permissions and groups"))
54+
countries = Country.objects.filter(
55+
is_deprecated=False, independent=True, iso3__isnull=False, record_type=CountryType.COUNTRY
56+
)
57+
for country in countries.iterator():
58+
self.stdout.write(self.style.NOTICE(f"--> Country: {country.name}"))
59+
for local_unit_type in local_unit_types.iterator():
60+
codename = f"local_unit_country_validator_{local_unit_type.id}_{country.id}"
61+
name = f"Local unit validator for {local_unit_type.name} {country.name}"
62+
permission, _ = Permission.objects.get_or_create(
63+
codename=codename,
64+
content_type=content_type,
65+
defaults=dict(name=name),
66+
)
67+
group_name = f"Local unit validator for {local_unit_type.name} {country.name}"
68+
group, _ = Group.objects.get_or_create(name=group_name)
69+
group.permissions.add(permission)
70+
self.stdout.write(self.style.SUCCESS(f"\t--> {local_unit_type.name}"))
71+
72+
self.stdout.write(self.style.SUCCESS("\n All permissions and groups created/updated successfully\n"))

local_units/management/commands/notify_validators.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@
88
from local_units.models import LocalUnit, Validator
99
from local_units.utils import (
1010
get_email_context,
11-
get_global_validators,
12-
get_region_admins,
11+
get_global_validators_by_type,
12+
get_region_validators_by_type,
1313
)
1414
from main.sentry import SentryMonitor
15-
16-
# from notifications.notification import send_notification
15+
from notifications.notification import send_notification
1716

1817

1918
class Command(BaseCommand):
@@ -28,15 +27,15 @@ def handle(self, *args, **options):
2827
validated=False,
2928
is_deprecated=False,
3029
last_sent_validator_type=Validator.LOCAL,
31-
created_at__lte=timezone.now() - timedelta(days=14),
30+
created_at__lte=timezone.now() - timedelta(days=7),
3231
)
3332

3433
# Global Validators: 28 days
3534
queryset_for_global_validators = LocalUnit.objects.filter(
3635
validated=False,
3736
is_deprecated=False,
3837
last_sent_validator_type=Validator.REGIONAL,
39-
created_at__lte=timezone.now() - timedelta(days=28),
38+
created_at__lte=timezone.now() - timedelta(days=14),
4039
)
4140

4241
for local_unit in queryset_for_regional_validators:
@@ -46,11 +45,11 @@ def handle(self, *args, **options):
4645
email_subject = "Action Required: Local Unit Pending Validation"
4746
email_type = "Local Unit"
4847

49-
for region_admin_validator in get_region_admins(local_unit):
48+
for region_admin_validator in get_region_validators_by_type(local_unit):
5049
try:
5150
email_context["full_name"] = region_admin_validator.get_full_name()
5251
email_body = render_to_string("email/local_units/local_unit.html", email_context)
53-
print(email_subject, region_admin_validator.email, email_body, email_type) # send_notification disabling
52+
send_notification(email_subject, region_admin_validator.email, email_body, email_type)
5453
local_unit.last_sent_validator_type = Validator.REGIONAL
5554
local_unit.save(update_fields=["last_sent_validator_type"])
5655
except Exception as e:
@@ -68,11 +67,11 @@ def handle(self, *args, **options):
6867
email_subject = "Action Required: Local Unit Pending Validation"
6968
email_type = "Local Unit"
7069

71-
for global_validator in get_global_validators():
70+
for global_validator in get_global_validators_by_type(local_unit):
7271
try:
7372
email_context["full_name"] = global_validator.get_full_name()
7473
email_body = render_to_string("email/local_units/local_unit.html", email_context)
75-
print(email_subject, global_validator.email, email_body, email_type) # send_notification disabling
74+
send_notification(email_subject, global_validator.email, email_body, email_type)
7675
local_unit.last_sent_validator_type = Validator.GLOBAL
7776
local_unit.save(update_fields=["last_sent_validator_type"])
7877
except Exception as e:
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from django.contrib.auth.models import Group, Permission
2+
from django.contrib.contenttypes.models import ContentType
3+
from django.core.management.base import BaseCommand
4+
from django.db import transaction
5+
6+
from local_units.models import LocalUnit
7+
8+
9+
class Command(BaseCommand):
10+
help = "Delete old permission and group for Local Unit Global Validator"
11+
12+
@transaction.atomic
13+
def handle(self, *args, **options):
14+
self.stdout.write(self.style.NOTICE(" Deleting old permission and group for local unit global validator"))
15+
codename = "local_unit_global_validator"
16+
group_name = "Local Unit Global Validators"
17+
content_type = ContentType.objects.get_for_model(LocalUnit)
18+
19+
permission = Permission.objects.filter(codename=codename, content_type=content_type)
20+
if permission.exists():
21+
permission.delete()
22+
self.stdout.write(self.style.SUCCESS(f"\t--> Deleted Permission {codename}"))
23+
else:
24+
self.stdout.write(self.style.WARNING(f"\t--> Permission {codename} not found"))
25+
group = Group.objects.filter(name=group_name)
26+
if group.exists():
27+
group.delete()
28+
self.stdout.write(self.style.SUCCESS(f"\t--> Deleted Group {group_name}"))
29+
else:
30+
self.stdout.write(self.style.WARNING(f"\t--> Group {group_name} not found"))
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 4.2.19 on 2025-07-25 11:24
2+
3+
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("local_units", "0021_localunit_update_reason_overview_and_more"),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name="localunit",
16+
name="is_new_local_unit",
17+
field=models.BooleanField(default=False, verbose_name="Is New Local Unit?"),
18+
),
19+
]

local_units/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,8 @@ class DeprecateReason(models.IntegerChoices):
371371
default=Validator.LOCAL,
372372
)
373373

374+
is_new_local_unit = models.BooleanField(default=False, verbose_name=("Is New Local Unit?"))
375+
374376
def __str__(self):
375377
branch_name = self.local_branch_name or self.english_branch_name
376378
return f"{branch_name} ({self.country.name})"

0 commit comments

Comments
 (0)