diff --git a/src/country_workspace/config/settings.py b/src/country_workspace/config/settings.py index d47c897..489cf54 100644 --- a/src/country_workspace/config/settings.py +++ b/src/country_workspace/config/settings.py @@ -44,7 +44,7 @@ "country_workspace.security", "country_workspace.apps.Config", "country_workspace.workspaces.apps.Config", - "country_workspace.versions", + "country_workspace.versioning", ) MIDDLEWARE = ( diff --git a/src/country_workspace/migrations/0001_initial.py b/src/country_workspace/migrations/0001_initial.py index 94a5c26..4ad1848 100644 --- a/src/country_workspace/migrations/0001_initial.py +++ b/src/country_workspace/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.1 on 2024-10-14 10:37 +# Generated by Django 5.1.1 on 2024-10-15 16:46 import django.contrib.auth.models import django.contrib.auth.validators diff --git a/src/country_workspace/sync/office.py b/src/country_workspace/sync/office.py index 3a29ba5..ec1c691 100644 --- a/src/country_workspace/sync/office.py +++ b/src/country_workspace/sync/office.py @@ -62,21 +62,29 @@ def sync_programs(limit_to_office: "Optional[Office]" = None) -> int: def sync_lookup(sl: SyncLog): fd = sl.content_object + if not fd: + return client = HopeClient() record = client.get_lookup(sl.data["remote_url"]) choices = [] for k, v in record.items(): choices.append((k, v)) - + if not fd.attrs: + fd.attrs = {} fd.attrs["choices"] = choices fd.save() sl.last_update_date = timezone.now() sl.save() +def sync_lookups() -> bool: + for sl in SyncLog.objects.filter(object_id__gt=0).exclude(content_type__isnull=True): + sync_lookup(sl) + return True + + def sync_all() -> bool: sync_offices() sync_programs() - for sl in SyncLog.objects.filter(object_id__gt=0): - sync_lookup(sl) + sync_lookups() return True diff --git a/src/country_workspace/versions/__init__.py b/src/country_workspace/versioning/__init__.py similarity index 100% rename from src/country_workspace/versions/__init__.py rename to src/country_workspace/versioning/__init__.py diff --git a/src/country_workspace/versioning/api.py b/src/country_workspace/versioning/api.py new file mode 100644 index 0000000..6a88a25 --- /dev/null +++ b/src/country_workspace/versioning/api.py @@ -0,0 +1,6 @@ +from country_workspace.versioning.management.manager import Manager + + +def run_scripts(): + m = Manager() + m.forward(m.max_version) diff --git a/src/country_workspace/versions/utils.py b/src/country_workspace/versioning/checkers.py similarity index 79% rename from src/country_workspace/versions/utils.py rename to src/country_workspace/versioning/checkers.py index 89ee95b..ec3e038 100644 --- a/src/country_workspace/versions/utils.py +++ b/src/country_workspace/versioning/checkers.py @@ -1,28 +1,23 @@ -from typing import TYPE_CHECKING - from django import forms from django.conf import settings from django.utils.text import slugify -from country_workspace.constants import HOUSEHOLD_CHECKER_NAME, INDIVIDUAL_CHECKER_NAME - -if TYPE_CHECKING: - from hope_flex_fields.models import DataChecker, FieldDefinition, Fieldset +from hope_flex_fields.models import DataChecker, FieldDefinition, Fieldset +from country_workspace.constants import HOUSEHOLD_CHECKER_NAME, INDIVIDUAL_CHECKER_NAME -def create_hope_field_definitions(apps, schema_editor): - fd: "FieldDefinition" = apps.get_model("hope_flex_fields", "FieldDefinition") +def create_hope_field_definitions(): for m in settings.LOOKUPS: n = f"HOPE HH {m}" - fd.objects.get_or_create(name=n, slug=slugify(n), field_type=forms.ChoiceField) - fd.objects.get_or_create( + FieldDefinition.objects.get_or_create(name=n, slug=slugify(n), field_type=forms.ChoiceField) + FieldDefinition.objects.get_or_create( name="HOPE IND Gender", slug=slugify("HOPE IND Gender"), attrs={"choices": [["FEMALE", "FEMALE"], ["MALE", "MALE"], ["UNKNOWN", "UNKNOWN"]]}, field_type=forms.ChoiceField, ) - fd.objects.get_or_create( + FieldDefinition.objects.get_or_create( name="HOPE IND Disability", slug=slugify("HOPE IND Disability"), field_type=forms.ChoiceField, @@ -30,22 +25,18 @@ def create_hope_field_definitions(apps, schema_editor): ) -def create_hope_core_fieldset(apps, schema_editor): - dc: "DataChecker" = apps.get_model("hope_flex_fields", "DataChecker") - fs: "Fieldset" = apps.get_model("hope_flex_fields", "Fieldset") - fd: "FieldDefinition" = apps.get_model("hope_flex_fields", "FieldDefinition") - - _char = fd.objects.get(field_type=forms.CharField) - _date = fd.objects.get(field_type=forms.DateField) - _bool = fd.objects.get(field_type=forms.BooleanField) - _int = fd.objects.get(field_type=forms.IntegerField) +def create_hope_core_fieldset(): + _char = FieldDefinition.objects.get(field_type=forms.CharField) + _date = FieldDefinition.objects.get(field_type=forms.DateField) + _bool = FieldDefinition.objects.get(field_type=forms.BooleanField) + _int = FieldDefinition.objects.get(field_type=forms.IntegerField) - _h_relationship = fd.objects.get(slug="hope-hh-relationship") - _h_residence = fd.objects.get(slug="hope-hh-residencestatus") - _i_gender = fd.objects.get(slug="hope-ind-gender") - _i_disability = fd.objects.get(slug="hope-ind-disability") + _h_relationship = FieldDefinition.objects.get(slug="hope-hh-relationship") + _h_residence = FieldDefinition.objects.get(slug="hope-hh-residencestatus") + _i_gender = FieldDefinition.objects.get(slug="hope-ind-gender") + _i_disability = FieldDefinition.objects.get(slug="hope-ind-disability") - hh_fs, __ = fs.objects.get_or_create(name=HOUSEHOLD_CHECKER_NAME) + hh_fs, __ = Fieldset.objects.get_or_create(name=HOUSEHOLD_CHECKER_NAME) hh_fs.fields.get_or_create( name="address", attrs={"label": "Household ID", "required": True}, @@ -144,7 +135,7 @@ def create_hope_core_fieldset(apps, schema_editor): # hh_fs.fields.get_or_create(field=_bf, name="hh_latrine_h_f", attrs={"label": "Latrine"}) # hh_fs.fields.get_or_create(field=_bf, name="hh_electricity_h_f") - ind_fs, __ = fs.objects.get_or_create(name="HOPE individual core") + ind_fs, __ = Fieldset.objects.get_or_create(name="HOPE individual core") ind_fs.fields.get_or_create( name="address", attrs={"label": "Household ID", "required": True}, @@ -228,7 +219,7 @@ def create_hope_core_fieldset(apps, schema_editor): field=_h_relationship, ) - hh_dc, __ = dc.objects.get_or_create(name=HOUSEHOLD_CHECKER_NAME) + hh_dc, __ = DataChecker.objects.get_or_create(name=HOUSEHOLD_CHECKER_NAME) hh_dc.fieldsets.add(hh_fs) - ind_dc, __ = dc.objects.get_or_create(name=INDIVIDUAL_CHECKER_NAME) + ind_dc, __ = DataChecker.objects.get_or_create(name=INDIVIDUAL_CHECKER_NAME) ind_dc.fieldsets.add(ind_fs) diff --git a/src/country_workspace/versions/migrations/__init__.py b/src/country_workspace/versioning/checks.py similarity index 100% rename from src/country_workspace/versions/migrations/__init__.py rename to src/country_workspace/versioning/checks.py diff --git a/src/country_workspace/versioning/management/__init__.py b/src/country_workspace/versioning/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/country_workspace/versioning/management/commands/__init__.py b/src/country_workspace/versioning/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/country_workspace/versioning/management/commands/applyversion.py b/src/country_workspace/versioning/management/commands/applyversion.py new file mode 100644 index 0000000..6f0a2f7 --- /dev/null +++ b/src/country_workspace/versioning/management/commands/applyversion.py @@ -0,0 +1,26 @@ +from django.core.management.base import BaseCommand, no_translations + +from country_workspace.versioning.management.manager import Manager + + +class Command(BaseCommand): + help = "Creates new version for apps." + + def add_arguments(self, parser): + parser.add_argument("num", nargs="?", help="Specify the version label") + + @no_translations + def handle(self, num, **options): + m = Manager() + if not num: + num = m.max_version + print(f"Available update {m.max_version}") + print(f"Applied update {m.max_applied_version}") + if num == "zero": + m.zero() + else: + num = int(num) + if not num: + num = m.max_applied_version + if num >= m.max_applied_version: + m.forward(num) diff --git a/src/country_workspace/versioning/management/commands/makeversion.py b/src/country_workspace/versioning/management/commands/makeversion.py new file mode 100644 index 0000000..f08573a --- /dev/null +++ b/src/country_workspace/versioning/management/commands/makeversion.py @@ -0,0 +1,48 @@ +import re +from pathlib import Path + +from django.core.management.base import BaseCommand, no_translations +from django.utils.timezone import now + +import country_workspace + +VERSION_TEMPLATE = """# Generated by HCW %(version)s on %(timestamp)s + +class Version: + operations = [] +""" + +regex = re.compile(r"(\d+).*") + + +def get_version(filename): + if m := regex.match(filename): + return int(m.group(1)) + return None + + +ts = now().strftime("%Y_%m_%d_%H%M%S") + + +class Command(BaseCommand): + help = "Creates new version" + + def add_arguments(self, parser): + parser.add_argument( + "label", + nargs="?", + help="Specify the version label", + ) + + @no_translations + def handle(self, label, **options): + folder = Path(__file__).parent.parent.parent / "versions" + last_ver = 0 + for filename in folder.iterdir(): + if ver := get_version(filename.name): + last_ver = max(last_ver, ver) + new_ver = last_ver + 1 + dest_file = folder / "{:>04}_{}.py".format(new_ver, label or ts) + with dest_file.open("w") as f: + f.write(VERSION_TEMPLATE % {"timestamp": ts, "version": country_workspace.VERSION}) + print(f"Created version {dest_file.name}") diff --git a/src/country_workspace/versioning/management/commands/showversion.py b/src/country_workspace/versioning/management/commands/showversion.py new file mode 100644 index 0000000..3819934 --- /dev/null +++ b/src/country_workspace/versioning/management/commands/showversion.py @@ -0,0 +1,39 @@ +import re +from pathlib import Path + +from django.core.management.base import BaseCommand, no_translations + +from country_workspace.versioning.models import Version + +regex = re.compile(r"(\d+).*") + + +def get_version(filename): + if m := regex.match(filename): + return int(m.group(1)) + return None + + +class Command(BaseCommand): + help = "Creates new version for apps." + + def add_arguments(self, parser): + parser.add_argument( + "num", + nargs="?", + help="Specify the version label", + ) + + @no_translations + def handle(self, *app_labels, **options): + folder = Path(__file__).parent.parent.parent / "versions" + existing = {} + applied = list(Version.objects.order_by("name").values_list("name", flat=True)) + for filename in sorted(folder.iterdir()): + if ver := get_version(filename.name): + existing[ver] = filename.name + for filename in existing.values(): + if filename in applied: + print(f"[x] {filename}") + else: + print(f"[ ] {filename}") diff --git a/src/country_workspace/versioning/management/manager.py b/src/country_workspace/versioning/management/manager.py new file mode 100644 index 0000000..6249e80 --- /dev/null +++ b/src/country_workspace/versioning/management/manager.py @@ -0,0 +1,89 @@ +import importlib.util +import re +from pathlib import Path +from typing import Callable + +from country_workspace import VERSION +from country_workspace.versioning.models import Version + +regex = re.compile(r"(\d+).*") +default_folder = Path(__file__).parent.parent / "scripts" + + +def get_version(filename): + if m := regex.match(filename): + return int(m.group(1)) + return None + + +def get_funcs(filename: Path, direction: str = "forward"): + if not filename.exists(): # pragma: no cover + raise FileNotFoundError(filename) + spec = importlib.util.spec_from_file_location("version", filename.absolute()) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + funcs = [] + for op in module.Version.operations: + if isinstance(op, (list, tuple)): + if direction == "forward": + funcs.append(op[0]) + else: + funcs.append(op[1]) + else: + if direction == "forward": + funcs.append(op) + else: + funcs.append(lambda: True) + + return funcs + + +class Manager: + def __init__(self, folder: Path = default_folder): + self.folder = folder + self.existing = [] + self.applied = list(Version.objects.order_by("name").values_list("name", flat=True)) + self.max_version = 0 + self.max_applied_version = 0 + for applied in self.applied: + self.max_applied_version = max(get_version(applied), self.max_applied_version) + + for filename in sorted(self.folder.iterdir()): + if v := get_version(filename.name): + self.existing.append(filename) + self.max_version = max(self.max_version, v) + + def zero(self): + self.backward(0) + + def forward(self, to_num) -> list[tuple[Path, list[Callable[[None], None]]]]: + print("Upgrading...") + processed = [] + for entry in self.existing: + if get_version(entry.stem) > to_num: + break + if entry.name not in self.applied: + funcs = get_funcs(entry, direction="forward") + print(f" Applying {entry.stem}") + for func in funcs: + func() + Version.objects.create(name=entry.name, version=VERSION) + processed.append((entry, funcs)) + self.applied = list(Version.objects.order_by("name").values_list("name", flat=True)) + return processed + + def backward(self, to_num) -> list[tuple[Path, list[Callable[[None], None]]]]: + print("Downgrading...") + processed = [] + for entry in reversed(self.applied): + if get_version(entry) <= to_num: + break + file_path = Path(self.folder) / entry + funcs = get_funcs(file_path, direction="backward") + print(f" Discharging {file_path.stem}") + for func in funcs: + func() + Version.objects.get(name=file_path.name).delete() + processed.append((entry, funcs)) + self.applied = list(Version.objects.order_by("name").values_list("name", flat=True)) + return processed diff --git a/src/country_workspace/versioning/migrations/0001_initial.py b/src/country_workspace/versioning/migrations/0001_initial.py new file mode 100644 index 0000000..ff88d7c --- /dev/null +++ b/src/country_workspace/versioning/migrations/0001_initial.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.1 on 2024-10-15 16:45 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Version", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=255, unique=True)), + ("version", models.CharField(max_length=255)), + ("applied", models.DateTimeField(default=django.utils.timezone.now)), + ], + ), + ] diff --git a/src/country_workspace/versioning/migrations/__init__.py b/src/country_workspace/versioning/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/country_workspace/versioning/models.py b/src/country_workspace/versioning/models.py new file mode 100644 index 0000000..fc63746 --- /dev/null +++ b/src/country_workspace/versioning/models.py @@ -0,0 +1,8 @@ +from django.db import models +from django.utils.timezone import now + + +class Version(models.Model): + name = models.CharField(max_length=255, unique=True) + version = models.CharField(max_length=255) + applied = models.DateTimeField(default=now) diff --git a/src/country_workspace/versioning/scripts/0001_initial.py b/src/country_workspace/versioning/scripts/0001_initial.py new file mode 100644 index 0000000..7255fe6 --- /dev/null +++ b/src/country_workspace/versioning/scripts/0001_initial.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.1 on 2024-10-09 06:19 +from hope_flex_fields.models import DataChecker, FieldDefinition, Fieldset + +from country_workspace.constants import HOUSEHOLD_CHECKER_NAME, INDIVIDUAL_CHECKER_NAME +from country_workspace.versioning.checkers import create_hope_core_fieldset, create_hope_field_definitions + + +def removes_hope_field_definitions(): + FieldDefinition.objects.filter(name__startswith="HOPE ").delete() + + +def removes_hope_core_fieldset(): + Fieldset.objects.filter(name=INDIVIDUAL_CHECKER_NAME).delete() + DataChecker.objects.filter(name=INDIVIDUAL_CHECKER_NAME).delete() + + Fieldset.objects.filter(name=HOUSEHOLD_CHECKER_NAME).delete() + DataChecker.objects.filter(name=HOUSEHOLD_CHECKER_NAME).delete() + + +class Version: + operations = [ + (create_hope_field_definitions, removes_hope_field_definitions), + (create_hope_core_fieldset, removes_hope_core_fieldset), + ] diff --git a/src/country_workspace/versioning/scripts/0002_synclog.py b/src/country_workspace/versioning/scripts/0002_synclog.py new file mode 100644 index 0000000..88eedf0 --- /dev/null +++ b/src/country_workspace/versioning/scripts/0002_synclog.py @@ -0,0 +1,5 @@ +from country_workspace.versioning.synclog import create_default_synclog, removes_default_synclog + + +class Version: + operations = [(create_default_synclog, removes_default_synclog)] diff --git a/src/country_workspace/versioning/scripts/__init__.py b/src/country_workspace/versioning/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/country_workspace/versioning/synclog.py b/src/country_workspace/versioning/synclog.py new file mode 100644 index 0000000..9030156 --- /dev/null +++ b/src/country_workspace/versioning/synclog.py @@ -0,0 +1,17 @@ +from django.conf import settings +from django.contrib.contenttypes.models import ContentType + +from hope_flex_fields.models import FieldDefinition + +from country_workspace.models import SyncLog + + +def create_default_synclog(): + for m in settings.LOOKUPS: + fd = FieldDefinition.objects.get(name="HOPE HH {m}".format(m=m)) + ct = ContentType.objects.get_for_model(SyncLog) + SyncLog.objects.get_or_create(content_type=ct, object_id=fd.id, data={"remote_url": "lookups/%s" % m.lower()}) + + +def removes_default_synclog(): + pass diff --git a/src/country_workspace/versions/migrations/0001_initial.py b/src/country_workspace/versions/migrations/0001_initial.py deleted file mode 100644 index 5590066..0000000 --- a/src/country_workspace/versions/migrations/0001_initial.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 5.1.1 on 2024-10-09 06:19 -from django.db import migrations -from typing import TYPE_CHECKING -from country_workspace.constants import HOUSEHOLD_CHECKER_NAME, INDIVIDUAL_CHECKER_NAME -from country_workspace.versions.utils import create_hope_field_definitions, create_hope_core_fieldset - -if TYPE_CHECKING: - from hope_flex_fields.models import FieldDefinition, Fieldset, DataChecker - - -def removes_hope_field_definitions(apps, schema_editor): - fd: "FieldDefinition" = apps.get_model("hope_flex_fields", "FieldDefinition") - fd.objects.filter(name__startswith="HOPE ").delete() - - -def removes_hope_core_fieldset(apps, schema_editor): - fs: "Fieldset" = apps.get_model("hope_flex_fields", "Fieldset") - dc: "DataChecker" = apps.get_model("hope_flex_fields", "DataChecker") - - fs.objects.filter(name=INDIVIDUAL_CHECKER_NAME).delete() - dc.objects.filter(name=INDIVIDUAL_CHECKER_NAME).delete() - dc.objects.filter(name=HOUSEHOLD_CHECKER_NAME).delete() - fs.objects.filter(name=HOUSEHOLD_CHECKER_NAME).delete() - - -class Migration(migrations.Migration): - dependencies = [("hope_flex_fields", "0007_create_default_fields"), - ] - - operations = [ - migrations.RunPython(create_hope_field_definitions, removes_hope_field_definitions), - migrations.RunPython(create_hope_core_fieldset, removes_hope_core_fieldset) - - ] diff --git a/src/country_workspace/workspaces/admin/actions/mass_update.py b/src/country_workspace/workspaces/admin/actions/mass_update.py index b25a3a9..a40f56d 100644 --- a/src/country_workspace/workspaces/admin/actions/mass_update.py +++ b/src/country_workspace/workspaces/admin/actions/mass_update.py @@ -24,14 +24,7 @@ class OperationManager: - COMMON = [ - ("set", None), - ("set null", lambda old_value: None), - ] - - def __init__( - self, - ): + def __init__(self): self._dict: dict[str, "Operation"] = dict() self._cache = {} @@ -54,7 +47,10 @@ def get_choices_for_target(self, target): operations = OperationManager() operations.register(forms.Field, "set", lambda old_value, new_value: new_value) -operations.register(forms.Field, "set null", lambda old_value, new_value: None) +# operations.register(forms.Field, "set null", lambda old_value, new_value: None) +operations.register(forms.CharField, "upper", lambda old_value, new_value: old_value.upper()) +operations.register(forms.CharField, "lower", lambda old_value, new_value: old_value.lower()) +operations.register(forms.BooleanField, "toggle", lambda old_value, new_value: not old_value) class MassUpdateWidget(widgets.MultiWidget): @@ -63,8 +59,7 @@ class MassUpdateWidget(widgets.MultiWidget): def __init__(self, field: FlexFormMixin, attrs=None): _widgets = ( - widgets.CheckboxInput(), - widgets.Select(choices=operations.get_choices_for_target(field.flex_field.field.field_type)), + widgets.Select(choices=[("", "-")] + operations.get_choices_for_target(field.flex_field.field.field_type)), field.widget, ) super().__init__(_widgets, attrs) @@ -80,7 +75,7 @@ class MassUpdateField(MultiValueField): def __init__(self, *, field, **kwargs): field.required = False - fields = (forms.BooleanField(required=False), forms.CharField(required=False), field) + fields = (forms.CharField(required=False), field) self.widget = MassUpdateWidget(field) super().__init__(fields, require_all_fields=False, required=False, **kwargs) @@ -97,13 +92,13 @@ def __init__(self, *args, **kwargs): checker: "DataChecker" = kwargs.pop("checker") super().__init__(*args, **kwargs) for name, fld in checker.get_form()().fields.items(): - self.fields[f"flex_fields__{name}"] = MassUpdateField(field=fld) + self.fields[f"flex_fields__{name}"] = MassUpdateField(label=fld.label, field=fld) def get_selected(self) -> "FormOperations": ret = {} for k, v in self.cleaned_data.items(): - if k.startswith("flex_fields__") and v[0]: - ret[k.replace("flex_fields__", "")] = v[1:] + if k.startswith("flex_fields__") and v and v[0] != "": + ret[k.replace("flex_fields__", "")] = v[0:] return ret diff --git a/src/country_workspace/workspaces/migrations/0001_initial.py b/src/country_workspace/workspaces/migrations/0001_initial.py index 6aa3c7b..d3044fe 100644 --- a/src/country_workspace/workspaces/migrations/0001_initial.py +++ b/src/country_workspace/workspaces/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.1 on 2024-10-14 10:37 +# Generated by Django 5.1.1 on 2024-10-15 16:46 import django.db.models.deletion from django.db import migrations, models diff --git a/tests/conftest.py b/tests/conftest.py index 533d9e4..19afe88 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -128,10 +128,14 @@ def force_migrated_records(request, active_marks): from hope_flex_fields.apps import sync_content_types from hope_flex_fields.utils import create_default_fields - from country_workspace.versions.utils import create_hope_core_fieldset, create_hope_field_definitions + from country_workspace.versioning.api import run_scripts + from country_workspace.versioning.checkers import create_hope_core_fieldset, create_hope_field_definitions + from country_workspace.versioning.synclog import create_default_synclog # we need to recreate these records because with selenium they are not available create_default_fields(apps, None) sync_content_types(None) - create_hope_field_definitions(apps, None) - create_hope_core_fieldset(apps, None) + create_hope_field_definitions() + create_hope_core_fieldset() + create_default_synclog() + run_scripts() diff --git a/tests/extras/testutils/factories/__init__.py b/tests/extras/testutils/factories/__init__.py index 7a10533..b9ab84a 100644 --- a/tests/extras/testutils/factories/__init__.py +++ b/tests/extras/testutils/factories/__init__.py @@ -18,6 +18,7 @@ from .sync import SyncLogFactory # noqa from .user import GroupFactory, SuperUserFactory, User, UserFactory # noqa from .userrole import UserRole, UserRoleFactory # noqa +from .version import VersionFactory # noqa from .workspaces import CountryChecker # noqa for _, name, _ in pkgutil.iter_modules([str(Path(__file__).parent)]): diff --git a/tests/extras/testutils/factories/version.py b/tests/extras/testutils/factories/version.py new file mode 100644 index 0000000..b94adf9 --- /dev/null +++ b/tests/extras/testutils/factories/version.py @@ -0,0 +1,12 @@ +import factory +from testutils.factories import AutoRegisterModelFactory + +from country_workspace.versioning.models import Version + + +class VersionFactory(AutoRegisterModelFactory): + name = factory.Sequence(lambda n: f"Version {n}") + version = factory.Sequence(lambda n: "{n}") + + class Meta: + model = Version diff --git a/tests/test_sync.py b/tests/test_sync.py index cafa1a8..9ce6c21 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -8,7 +8,7 @@ import vcr from vcr.record_mode import RecordMode -from country_workspace.sync.office import sync_all, sync_offices, sync_programs +from country_workspace.sync.office import sync_all, sync_lookups, sync_offices, sync_programs def scrub_string(response): @@ -49,3 +49,8 @@ def test_sync_programs(): with my_vcr.use_cassette(Path(__file__).parent / "sync_programs.yaml"): assert sync_offices() assert sync_programs() + + +def test_sync_lookup(force_migrated_records): + with my_vcr.use_cassette(Path(__file__).parent / "sync_lookups.yaml"): + assert sync_lookups() diff --git a/tests/versioning/scripts/0001_test.py b/tests/versioning/scripts/0001_test.py new file mode 100644 index 0000000..ed003ff --- /dev/null +++ b/tests/versioning/scripts/0001_test.py @@ -0,0 +1,12 @@ +def forward(): + pass + + +def back(): + pass + + +class Version: + operations = [ + (forward, back), + ] diff --git a/tests/versioning/scripts/0002_test.py b/tests/versioning/scripts/0002_test.py new file mode 100644 index 0000000..ed003ff --- /dev/null +++ b/tests/versioning/scripts/0002_test.py @@ -0,0 +1,12 @@ +def forward(): + pass + + +def back(): + pass + + +class Version: + operations = [ + (forward, back), + ] diff --git a/tests/versioning/scripts/0003_test.py b/tests/versioning/scripts/0003_test.py new file mode 100644 index 0000000..8df2b4e --- /dev/null +++ b/tests/versioning/scripts/0003_test.py @@ -0,0 +1,12 @@ +def forward(): + pass + + +def back(): + pass + + +class Version: + operations = [ + forward, + ] diff --git a/tests/versioning/scripts/__init__.py b/tests/versioning/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/versioning/test_ver.py b/tests/versioning/test_ver.py new file mode 100644 index 0000000..2bda4b9 --- /dev/null +++ b/tests/versioning/test_ver.py @@ -0,0 +1,91 @@ +from pathlib import Path + +import pytest + +from country_workspace import VERSION +from country_workspace.versioning.management.manager import Manager + +sample_folder = Path(__file__).parent / "scripts" +FILES = [ + (sample_folder / "0001_test.py"), + (sample_folder / "0002_test.py"), + (sample_folder / "0003_test.py"), +] + + +@pytest.fixture() +def version1(): + from testutils.factories import VersionFactory + + f = sample_folder / "0001_test.py" + return VersionFactory(name=f.name, version=VERSION) + + +@pytest.fixture() +def version2(): + from testutils.factories import VersionFactory + + f = sample_folder / "0002_test.py" + return VersionFactory(name=f.name, version=VERSION) + + +@pytest.fixture() +def version3(): + from testutils.factories import VersionFactory + + f = sample_folder / "0003_test.py" + return VersionFactory(name=f.name, version=VERSION) + + +@pytest.fixture() +def manager(): + return Manager(sample_folder) + + +def test_manager_1(manager: Manager): + assert manager.max_version == 3 + assert manager.max_applied_version == 0 + assert manager.applied == [] + assert manager.existing == FILES + + +def test_manager_2(version1, manager: Manager): + assert manager.max_version == 3 + assert manager.max_applied_version == 1 + assert manager.applied == [version1.name] + assert manager.existing == FILES + + +def test_manager_forward(manager: Manager): + from country_workspace.versioning.models import Version + + assert manager.applied == [] + manager.forward(1) + assert manager.applied == [FILES[0].name] + + assert Version.objects.filter(name=FILES[0].name, version=VERSION).exists() + assert not Version.objects.filter(name=FILES[1].name, version=VERSION).exists() + manager.forward(2) + assert Version.objects.filter(name=FILES[0].name, version=VERSION).exists() + assert Version.objects.filter(name=FILES[1].name, version=VERSION).exists() + manager.forward(manager.max_version) + + +def test_manager_backward(version1, version2, version3, manager: Manager): + from country_workspace.versioning.models import Version + + assert manager.applied == [version1.name, version2.name, version3.name] + ret = manager.backward(2) + assert [r[0] for r in ret] == [FILES[2].name] + + assert manager.applied == [FILES[0].name, FILES[1].name] + assert list(Version.objects.values_list("name", flat=True)) == [FILES[0].name, FILES[1].name] + + +def test_manager_zero(version1, manager: Manager): + from country_workspace.versioning.models import Version + + manager.zero() + assert manager.applied == [] + assert not Version.objects.filter(name=FILES[0].name, version=VERSION).exists() + assert not Version.objects.filter(name=FILES[1].name, version=VERSION).exists() diff --git a/tests/workspace/actions/test_ws_mass_update.py b/tests/workspace/actions/test_ws_mass_update.py index 0a5d88e..8f6fe59 100644 --- a/tests/workspace/actions/test_ws_mass_update.py +++ b/tests/workspace/actions/test_ws_mass_update.py @@ -85,9 +85,8 @@ def test_mass_update(app: "DjangoTestApp", household: "CountryHousehold") -> Non res = form.submit() form = res.forms["mass-update-form"] - form["flex_fields__address_0"].checked = True - form["flex_fields__address_1"].select(text="set") - form["flex_fields__address_2"] = "__NEW VALUE__" + form["flex_fields__address_0"].select(text="set") + form["flex_fields__address_1"] = "__NEW VALUE__" res = form.submit("_apply") household.refresh_from_db()