Skip to content

Commit 67f7bf2

Browse files
add ! beneficiary_group
1 parent 22ef82c commit 67f7bf2

File tree

15 files changed

+1274
-933
lines changed

15 files changed

+1274
-933
lines changed

src/country_workspace/admin/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from ..cache.smart_panel import panel_cache
88
from .batch import BatchAdmin # noqa
9+
from .beneficiary_group import BeneficiaryGroupAdmin # noqa
910
from .constance import ConstanceAdmin # noqa
1011
from .household import HouseholdAdmin # noqa
1112
from .individual import IndividualAdmin # noqa
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from django.contrib import admin
2+
from django.http import HttpRequest
3+
4+
from country_workspace.models import BeneficiaryGroup
5+
from .base import BaseModelAdmin
6+
7+
8+
@admin.register(BeneficiaryGroup)
9+
class BeneficiaryGroupAdmin(BaseModelAdmin):
10+
list_display = ("name", "group_label", "group_label_plural", "member_label", "member_label_plural", "master_detail")
11+
search_fields = (
12+
"name",
13+
"group_label",
14+
"group_label_plural",
15+
"member_label",
16+
"member_label_plural",
17+
)
18+
ordering = ("name",)
19+
20+
def has_add_permission(self, request: HttpRequest) -> bool:
21+
return False
22+
23+
def has_delete_permission(self, request: HttpRequest, obj: BeneficiaryGroup = None) -> bool:
24+
return False
25+
26+
def has_change_permission(self, request: HttpRequest, obj: BeneficiaryGroup = None) -> bool:
27+
return False

src/country_workspace/admin/program.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class ProgramAdmin(BaseModelAdmin):
2020
list_display = (
2121
"name",
2222
"sector",
23+
"beneficiary_group",
2324
"status",
2425
"active",
2526
"beneficiary_validator",
@@ -32,6 +33,7 @@ class ProgramAdmin(BaseModelAdmin):
3233
"status",
3334
"active",
3435
"sector",
36+
"beneficiary_group",
3537
"beneficiary_validator",
3638
"household_checker",
3739
"individual_checker",

src/country_workspace/contrib/hope/sync/office.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from django.core.cache import cache
44
from hope_flex_fields.models import DataChecker
55

6-
from country_workspace.models import Office, Program, SyncLog
6+
from country_workspace.models import BeneficiaryGroup, Office, Program, SyncLog
77

88
from .. import constants
99
from ..client import HopeClient
@@ -30,10 +30,11 @@ def sync_offices(stdout: TextIOBase | None = None) -> dict[str, int]:
3030
)
3131
totals["add" if created else "upd"] += 1
3232
SyncLog.objects.register_sync(Office)
33-
return totals
33+
return totals
3434

3535

3636
def sync_programs(limit_to_office: "Office | None" = None, stdout: TextIOBase | None = None) -> dict[str, int]:
37+
sync_beneficiary_groups(stdout=stdout)
3738
if stdout:
3839
stdout.write("Fetching Programs data from HOPE...")
3940
client = HopeClient()
@@ -50,6 +51,8 @@ def sync_programs(limit_to_office: "Office | None" = None, stdout: TextIOBase |
5051
office = Office.objects.get(code=record["business_area_code"])
5152
if record["status"] not in [Program.ACTIVE, Program.DRAFT]:
5253
continue
54+
# TODO: beneficiary group - choose from the REST API when it will be available
55+
beneficiary_group = BeneficiaryGroup.objects.first()
5356
p, created = Program.objects.get_or_create(
5457
hope_id=record["id"],
5558
defaults={
@@ -58,6 +61,7 @@ def sync_programs(limit_to_office: "Office | None" = None, stdout: TextIOBase |
5861
"status": record["status"],
5962
"sector": record["sector"],
6063
"country_office": office,
64+
"beneficiary_group": beneficiary_group,
6165
},
6266
)
6367
if created:
@@ -73,6 +77,29 @@ def sync_programs(limit_to_office: "Office | None" = None, stdout: TextIOBase |
7377
return totals
7478

7579

80+
def sync_beneficiary_groups(stdout: TextIOBase | None = None) -> bool:
81+
totals = {"add": 0, "upd": 0}
82+
client = HopeClient()
83+
if stdout:
84+
stdout.write("Fetching Beneficiary Groups data from HOPE...")
85+
with cache.lock("sync-beneficiary-groups"):
86+
for record in client.get("beneficiary-groups"):
87+
__, created = BeneficiaryGroup.objects.get_or_create(
88+
hope_id=record["id"],
89+
defaults={
90+
"name": record["name"],
91+
"group_label": record["group_label"],
92+
"group_label_plural": record["group_label_plural"],
93+
"member_label": record["member_label"],
94+
"member_label_plural": record["member_label_plural"],
95+
"master_detail": record["master_detail"],
96+
},
97+
)
98+
totals["add" if created else "upd"] += 1
99+
SyncLog.objects.register_sync(BeneficiaryGroup)
100+
return totals
101+
102+
76103
def sync_all(stdout: TextIOBase | None = None) -> bool:
77104
sync_offices(stdout=stdout)
78105
sync_programs(stdout=stdout)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Generated by Django 5.1.7 on 2025-03-24 12:31
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
dependencies = [
9+
("country_workspace", "0008_alter_individual_options"),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name="BeneficiaryGroup",
15+
fields=[
16+
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
17+
(
18+
"hope_id",
19+
models.CharField(
20+
blank=True, help_text="Unique ID from HOPE", max_length=100, null=True, unique=True
21+
),
22+
),
23+
("name", models.CharField(help_text="Name of the beneficiary group", max_length=255, unique=True)),
24+
("group_label", models.CharField(help_text="Label for the group", max_length=255)),
25+
("group_label_plural", models.CharField(help_text="Plural label for the group", max_length=255)),
26+
("member_label", models.CharField(help_text="Label for the member", max_length=255)),
27+
("member_label_plural", models.CharField(help_text="Plural label for the member", max_length=255)),
28+
(
29+
"master_detail",
30+
models.BooleanField(default=True, help_text="Indicates if this is a master-detail group"),
31+
),
32+
],
33+
options={
34+
"verbose_name": "Beneficiary Group",
35+
"verbose_name_plural": "Beneficiary Groups",
36+
"ordering": ("name",),
37+
},
38+
),
39+
migrations.AlterField(
40+
model_name="office",
41+
name="kobo_country_code",
42+
field=models.CharField(blank=True, max_length=3, null=True),
43+
),
44+
migrations.AddField(
45+
model_name="program",
46+
name="beneficiary_group",
47+
field=models.ForeignKey(
48+
blank=True,
49+
help_text="Beneficiary group to which this program belongs",
50+
null=True,
51+
on_delete=django.db.models.deletion.PROTECT,
52+
related_name="programs",
53+
to="country_workspace.beneficiarygroup",
54+
),
55+
),
56+
]

src/country_workspace/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .batch import Batch # noqa
2+
from .beneficiary_group import BeneficiaryGroup # noqa
23
from .household import Household # noqa
34
from .individual import Individual # noqa
45
from .jobs import AsyncJob # noqa
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from django.db import models
2+
3+
4+
class BeneficiaryGroup(models.Model):
5+
hope_id = models.CharField(max_length=100, blank=True, null=True, unique=True)
6+
name = models.CharField(max_length=255, unique=True)
7+
group_label = models.CharField(max_length=255)
8+
group_label_plural = models.CharField(max_length=255)
9+
member_label = models.CharField(max_length=255)
10+
member_label_plural = models.CharField(max_length=255)
11+
master_detail = models.BooleanField(default=True)
12+
13+
class Meta:
14+
verbose_name = "Beneficiary Group"
15+
verbose_name_plural = "Beneficiary Groups"
16+
ordering = ("name",)
17+
18+
def __str__(self) -> str:
19+
return self.name

src/country_workspace/models/program.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from strategy_field.utils import fqn
88

99
from country_workspace.models.office import Office
10+
from country_workspace.models.beneficiary_group import BeneficiaryGroup
1011

1112
from ..validators.registry import NoopValidator, beneficiary_validator_registry
1213
from .base import BaseModel, Validable
@@ -44,6 +45,14 @@ class Program(BaseModel):
4445
(WASH, _("WASH")),
4546
)
4647
hope_id = models.CharField(max_length=200, unique=True, editable=False)
48+
beneficiary_group = models.ForeignKey(
49+
BeneficiaryGroup,
50+
on_delete=models.PROTECT,
51+
related_name="programs",
52+
null=True,
53+
blank=True,
54+
help_text="Beneficiary group to which this program belongs",
55+
)
4756
country_office = models.ForeignKey(Office, on_delete=models.CASCADE, related_name="programs")
4857
name = models.CharField(max_length=255)
4958
code = models.CharField(max_length=255, blank=True, null=True)

src/country_workspace/workspaces/sites.py

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from django.template.response import TemplateResponse
1313
from django.urls import NoReverseMatch, URLPattern, URLResolver, reverse
1414
from django.utils.text import capfirst
15-
from django.utils.translation import gettext_lazy
15+
from django.utils.translation import gettext_lazy as _
1616
from django.views import View
1717
from smart_admin.autocomplete import SmartAutocompleteJsonView
1818

@@ -119,9 +119,9 @@ class TenantAdminSite(admin.AdminSite):
119119
password_change_template = None
120120
password_change_done_template = None
121121

122-
site_title = gettext_lazy("HOPE Country Workspace site admin")
122+
site_title = _("HOPE Country Workspace site admin")
123123
site_header = "Country Workspace"
124-
index_title = gettext_lazy("")
124+
index_title = _("")
125125
login_form = TenantAuthenticationForm
126126

127127
namespace = "workspace"
@@ -183,6 +183,99 @@ def _build_app_dict(self, request: HttpRequest, label: str | None = None) -> dic
183183

184184
return app_dict
185185

186+
def get_menu_items(self, request: "HttpRequest") -> list[dict[str, Any]]:
187+
"""Return a simplified list of menu items based on the current program."""
188+
items = [
189+
{
190+
"name": _("Home"),
191+
"url": reverse("workspace:index"),
192+
"icon": "icon-home",
193+
"selected": not hasattr(self, "modeladmin_name"),
194+
},
195+
]
196+
197+
program = get_selected_program()
198+
if program:
199+
bg = program.beneficiary_group
200+
items.extend(
201+
[
202+
{
203+
"name": _("Programme"),
204+
"url": reverse("workspace:workspaces_countryprogram_change", args=[program.pk]),
205+
"icon": "icon-equalizer",
206+
"selected": getattr(self, "modeladmin_name", None) == "CountryProgramAdmin",
207+
},
208+
]
209+
)
210+
211+
if bg and bg.master_detail:
212+
items.append(
213+
{
214+
"name": bg.group_label_plural,
215+
"url": reverse("workspace:workspaces_countryhousehold_changelist"),
216+
"icon": "icon-members",
217+
"selected": getattr(self, "modeladmin_name", None) == "CountryHouseholdAdmin",
218+
}
219+
)
220+
items.append(
221+
{
222+
"name": bg.member_label_plural,
223+
"url": reverse("workspace:workspaces_countryindividual_changelist"),
224+
"icon": "icon-user",
225+
"selected": getattr(self, "modeladmin_name", None) == "CountryIndividualAdmin",
226+
"indent": True,
227+
}
228+
)
229+
elif bg:
230+
items.append(
231+
{
232+
"name": bg.member_label_plural,
233+
"url": reverse("workspace:workspaces_countryindividual_changelist"),
234+
"icon": "icon-user",
235+
"selected": getattr(self, "modeladmin_name", None) == "CountryIndividualAdmin",
236+
}
237+
)
238+
239+
items.extend(
240+
[
241+
{
242+
"name": _("Batches"),
243+
"url": reverse("workspace:workspaces_countrybatch_changelist"),
244+
"icon": "icon-sign",
245+
"selected": getattr(self, "modeladmin_name", None) == "CountryBatchAdmin",
246+
},
247+
{
248+
"name": _("Jobs"),
249+
"url": reverse("workspace:workspaces_countryasyncjob_changelist"),
250+
"icon": "icon-globe",
251+
"selected": getattr(self, "modeladmin_name", None) == "CountryJobAdmin",
252+
},
253+
]
254+
)
255+
256+
items.append(
257+
{
258+
"name": _("Logout"),
259+
"url": reverse("admin:logout"),
260+
"icon": "icon-logout",
261+
"selected": False,
262+
"is_form": True,
263+
}
264+
)
265+
266+
if request.user.is_staff:
267+
items.append(
268+
{
269+
"name": _("Admin"),
270+
"url": reverse("admin:index"),
271+
"icon": "icon-shield1",
272+
"selected": False,
273+
"target": "_admin",
274+
}
275+
)
276+
277+
return items
278+
186279
def each_context(self, request: "HttpRequest") -> "dict[str, Any]":
187280
ret = super().each_context(request)
188281
selected_tenant = get_selected_tenant()
@@ -192,6 +285,7 @@ def each_context(self, request: "HttpRequest") -> "dict[str, Any]":
192285
ret["active_tenant"] = selected_tenant
193286
ret["active_program"] = selected_program
194287
ret["namespace"] = self.namespace
288+
ret["menu_items"] = self.get_menu_items(request)
195289
return ret # type: ignore
196290

197291
def autocomplete_view(self, request: "HttpRequest") -> HttpResponse:

0 commit comments

Comments
 (0)