Skip to content

Commit 8687aa5

Browse files
add ! sync countries through admin panel (#87)
1 parent 659f910 commit 8687aa5

File tree

11 files changed

+197
-79
lines changed

11 files changed

+197
-79
lines changed

src/country_workspace/admin/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from .office import OfficeAdmin
1616
from .program import ProgramAdmin
1717
from .role import UserRoleAdmin
18-
from .sync import SyncLog
18+
from .sync_log import SyncLog
1919
from .user import UserAdmin
2020

2121
site.register(ContentType, admin_class=ContentTypeAdmin)
Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,7 @@
1-
from typing import TypedDict
21
from admin_extra_buttons.mixins import ExtraButtonsMixin
32
from adminfilters.mixin import AdminAutoCompleteSearchMixin, AdminFiltersMixin
4-
from django.contrib import admin, messages
5-
from django.db.models import Model
6-
from django.http import HttpRequest
7-
from admin_extra_buttons.api import button
8-
9-
from country_workspace.contrib.hope.sync.context_programs import SyncStep, sync_context_programs
3+
from django.contrib import admin
104

115

126
class BaseModelAdmin(ExtraButtonsMixin, AdminAutoCompleteSearchMixin, AdminFiltersMixin, admin.ModelAdmin):
137
pass
14-
15-
16-
class SyncConfig(TypedDict):
17-
model: type[Model]
18-
step: SyncStep
19-
20-
21-
class SyncAdminMixin:
22-
sync_config: SyncConfig
23-
24-
@button()
25-
def sync(self, request: HttpRequest) -> None:
26-
totals = sync_context_programs(step=self.sync_config["step"])
27-
if errors := totals.get("errors"):
28-
self.message_user(request, "; ".join(errors), level=messages.ERROR)
29-
else:
30-
info = totals[self.sync_config["model"]._meta.model_name]
31-
self.message_user(request, f"{info['add']} created - {info['upd']} updated", level=messages.SUCCESS)

src/country_workspace/admin/locations.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
from django.db.models import Field
1010
from django.forms import FileField, FileInput, Form
1111

12-
from country_workspace.models.locations import Area, AreaType, Country
12+
from ..models.locations import Area, AreaType, Country
13+
from ..contrib.hope.sync.context_geo import SyncStep
14+
from .base import BaseModelAdmin
15+
from .sync import SyncAdminMixin, SyncConfig, ContextGeoSyncHandler
16+
1317

1418
if TYPE_CHECKING:
1519
from django.http import HttpRequest
@@ -22,7 +26,7 @@ class ImportCSVForm(Form):
2226

2327

2428
@admin.register(Country)
25-
class CountryAdmin(AdminFiltersMixin, admin.ModelAdmin):
29+
class CountryAdmin(SyncAdminMixin, BaseModelAdmin):
2630
list_display = (
2731
"name",
2832
"iso_code2",
@@ -31,6 +35,7 @@ class CountryAdmin(AdminFiltersMixin, admin.ModelAdmin):
3135
"name",
3236
"iso_code2",
3337
)
38+
sync_config = SyncConfig(model=Country, step=SyncStep.COUNTRIES, sync_handler=ContextGeoSyncHandler())
3439

3540

3641
@admin.register(AreaType)

src/country_workspace/admin/office.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
from admin_extra_buttons.decorators import link
33
from django.contrib import admin
44
from django.urls import reverse
5-
from country_workspace.contrib.hope.sync.context_programs import SyncStep
65

6+
from ..contrib.hope.sync.context_programs import SyncStep
77
from ..models import Office
8-
from .base import BaseModelAdmin, SyncAdminMixin, SyncConfig
8+
from .base import BaseModelAdmin
9+
from .sync import SyncAdminMixin, SyncConfig, ContextProgramsSyncHandler
910

1011

1112
@admin.register(Office)
@@ -15,7 +16,7 @@ class OfficeAdmin(SyncAdminMixin, BaseModelAdmin):
1516
list_filter = ("active",)
1617
readonly_fields = ("hope_id", "slug")
1718
ordering = ("name",)
18-
sync_config = SyncConfig(model=Office, step=SyncStep.OFFICES)
19+
sync_config = SyncConfig(model=Office, step=SyncStep.OFFICES, sync_handler=ContextProgramsSyncHandler())
1920

2021
@link(change_list=False)
2122
def programmes(self, btn: LinkButton) -> None:

src/country_workspace/admin/program.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
from ..cache.manager import cache_manager
1010
from ..compat.admin_extra_buttons import confirm_action
1111
from ..models import Program
12-
from .base import BaseModelAdmin, SyncAdminMixin, SyncConfig
13-
from country_workspace.contrib.hope.sync.context_programs import SyncStep
12+
from ..contrib.hope.sync.context_programs import SyncStep
13+
from .base import BaseModelAdmin
14+
from .sync import SyncAdminMixin, SyncConfig, ContextProgramsSyncHandler
15+
1416

1517
if TYPE_CHECKING:
1618
from admin_extra_buttons.buttons import LinkButton
@@ -39,7 +41,7 @@ class ProgramAdmin(SyncAdminMixin, BaseModelAdmin):
3941
)
4042
ordering = ("name",)
4143
autocomplete_fields = ("country_office",)
42-
sync_config = SyncConfig(model=Program, step=SyncStep.PROGRAMS)
44+
sync_config = SyncConfig(model=Program, step=SyncStep.PROGRAMS, sync_handler=ContextProgramsSyncHandler())
4345

4446
@button()
4547
def invalidate_cache(self, request: HttpRequest, pk: str) -> None:
Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,46 @@
1-
from admin_extra_buttons.decorators import button
2-
from django.contrib import admin
3-
from django.http import HttpRequest, HttpResponse
1+
from typing import TypedDict, Protocol, TypeVar, Generic
2+
from django.contrib import messages
3+
from django.db.models import Model
4+
from django.http import HttpRequest
5+
from admin_extra_buttons.api import button
46

5-
from ..models import SyncLog
6-
from .base import BaseModelAdmin
7+
from ..contrib.hope.sync.context_programs import sync_context_programs, SyncStep as ContextProgramsSyncStep
8+
from ..contrib.hope.sync.context_geo import sync_context_geo, SyncStep as ContextGeoSyncStep
79

810

9-
@admin.register(SyncLog)
10-
class SyncLogAdmin(BaseModelAdmin):
11-
list_display = ("content_type", "content_object", "last_update_date", "last_id")
11+
T_SyncStep = TypeVar("T_SyncStep", bound=ContextProgramsSyncStep | ContextGeoSyncStep)
12+
type SyncHandlerResp = dict[str, list[str] | dict[str, int]]
13+
14+
15+
class SyncHandler(Protocol, Generic[T_SyncStep]):
16+
def sync(self, step: T_SyncStep) -> SyncHandlerResp:
17+
pass
18+
19+
20+
class ContextProgramsSyncHandler:
21+
def sync(self, step: ContextProgramsSyncStep) -> SyncHandlerResp:
22+
return sync_context_programs(step)
23+
24+
25+
class ContextGeoSyncHandler:
26+
def sync(self, step: ContextGeoSyncStep) -> SyncHandlerResp:
27+
return sync_context_geo(step)
28+
29+
30+
class SyncConfig(TypedDict):
31+
model: type[Model]
32+
step: T_SyncStep
33+
sync_handler: SyncHandler
34+
35+
36+
class SyncAdminMixin:
37+
sync_config: SyncConfig
1238

1339
@button()
14-
def sync_all(self, request: HttpRequest) -> "HttpResponse":
15-
SyncLog.objects.refresh()
40+
def sync(self, request: HttpRequest) -> None:
41+
totals = self.sync_config["sync_handler"].sync(step=self.sync_config["step"])
42+
if errors := totals.get("errors"):
43+
self.message_user(request, "; ".join(errors), level=messages.ERROR)
44+
else:
45+
info = totals[self.sync_config["model"]._meta.model_name]
46+
self.message_user(request, f"{info['add']} created - {info['upd']} updated", level=messages.SUCCESS)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from admin_extra_buttons.decorators import button
2+
from django.contrib import admin
3+
from django.http import HttpRequest, HttpResponse
4+
5+
from ..models import SyncLog
6+
from .base import BaseModelAdmin
7+
8+
9+
@admin.register(SyncLog)
10+
class SyncLogAdmin(BaseModelAdmin):
11+
list_display = ("content_type", "content_object", "last_update_date", "last_id")
12+
13+
@button()
14+
def sync_all(self, request: HttpRequest) -> "HttpResponse":
15+
SyncLog.objects.refresh()
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from io import TextIOBase
2+
from typing import Any, Final
3+
from dataclasses import dataclass
4+
from enum import auto
5+
from django.db.models import Model
6+
7+
from ....models import Country
8+
from .base import BaseSync, SyncConfig, BaseSyncStep, sync_context
9+
10+
11+
MODELS: Final[tuple[type[Model], ...]] = (Country,)
12+
"""List of models to synchronize."""
13+
14+
15+
class SyncStep(BaseSyncStep):
16+
"""Synchronization steps for geo-related models."""
17+
18+
COUNTRIES = (auto(), lambda self: self.sync_countries)
19+
20+
21+
@dataclass
22+
class SyncContextGeo(BaseSync):
23+
"""Context for synchronizing geo-related models."""
24+
25+
SyncStep = SyncStep
26+
27+
def sync_countries(self) -> None:
28+
"""Fetch and process Country records from the remote API."""
29+
self.sync_entity(
30+
SyncConfig(
31+
model=Country,
32+
path="lookups/country",
33+
prepare_defaults=lambda r: {f: r.get(f) for f in ("name", "iso_code2")},
34+
),
35+
)
36+
37+
38+
def sync_context_geo(
39+
step: SyncStep | None = None,
40+
stdout: TextIOBase | None = None,
41+
) -> dict[str, Any]:
42+
"""Run synchronization for geo-related models.
43+
44+
Args:
45+
step (SyncStep | None): Specific step to execute (e.g., SyncStep.COUNTRIES). If None, all steps are run.
46+
stdout (TextIOBase | None): Optional output stream for logging.
47+
48+
Returns:
49+
dict[str, Any]: Synchronization results, including counts and errors.
50+
51+
"""
52+
return sync_context(
53+
SyncContextGeo,
54+
step=step,
55+
stdout=stdout,
56+
)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from django.db import migrations, models
2+
from django.db.migrations.state import StateApps
3+
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
4+
from uuid import uuid4
5+
6+
7+
def fill_hope_id(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None:
8+
Country = apps.get_model("country_workspace", "Country")
9+
for country in Country.objects.all():
10+
country.hope_id = f"TEMP_COUNTRY_{uuid4()}"
11+
country.save()
12+
13+
14+
class Migration(migrations.Migration):
15+
dependencies = [
16+
("country_workspace", "0011_remove_household_updates_update_and_more"),
17+
]
18+
19+
operations = [
20+
migrations.AddField(
21+
model_name="country",
22+
name="hope_id",
23+
field=models.CharField(editable=False, max_length=200, null=True, unique=True),
24+
),
25+
migrations.RunPython(fill_hope_id, reverse_code=migrations.RunPython.noop),
26+
migrations.AlterField(
27+
model_name="country",
28+
name="hope_id",
29+
field=models.CharField(editable=False, max_length=200, unique=True),
30+
),
31+
]

src/country_workspace/models/locations.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
class Country(BaseModel):
1111
name = models.CharField(max_length=255, db_index=True)
1212
iso_code2 = models.CharField(max_length=2, unique=True)
13+
hope_id = models.CharField(max_length=200, unique=True, editable=False)
1314

1415
class Meta:
1516
verbose_name_plural = "Countries"

0 commit comments

Comments
 (0)