diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ed7e5ce --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,81 @@ +on: + release: + types: + - published + - unpublished + - created + - edited + - deleted + - prereleased + - released +# Trigger the action manually from the UI +# workflow_dispatch: +# inputs: +# branch: +# description: 'Branch' +# required: true +# default: 'develop' +# type: choice +# options: +# - develop +# - staging +# - master +# tag: +# description: 'Version Tag' +# required: true +# default: 'warning' +jobs: + build: + name: Build Docker Images + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Docker meta + id: meta + uses: docker/metadata-action@v5.5.1 + with: + images: "unicef/hope-country-workspace" + tags: | + type=ref,event=branch + type=ref,event=pr + type=ref,event=tag + type=semver,pattern={{version}} + type=semver,pattern={{raw}} + env: + DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index + - run: | + echo "Log level: $TAG" + echo "Environment: $ENVIRONMENT" + echo "${{ toJSON(github.event) }}" + env: + LEVEL: ${{ inputs.tag }} + ENVIRONMENT: ${{ inputs.environment }} + - if: github.event_name == 'release' && github.event.action == 'created' + run: echo "version=${{ steps.meta.outputs.version }}dev" >> "$GITHUB_OUTPUT" + - if: github.event_name == 'release' && github.event.action == 'prereleased' + run: echo "version=${{ steps.meta.outputs.version }}rc" >> "$GITHUB_OUTPUT" + - if: github.event_name == 'release' && github.event.action == 'published' + run: echo "version=${{ steps.meta.outputs.version }}" >> "$GITHUB_OUTPUT" + - run: echo "Build ... unicef/hope-country-workspace:${{ steps.meta.outputs.version }}-${{ env.version }}" + +# if: github.event_name == 'release' && github.event.action == 'published' +# + +# Build ... "unicef/hope-country-workspace:${{ steps.meta.outputs.version }}" +# +# docker build \ +# --target dist \ +# -t "unicef/hope-country-workspace:${{ steps.meta.outputs.version }}" \ +# --cache-from "type=gha" \ +# --cache-to "type=gha,mode=max" \ +# -f docker/Dockerfile . +# docker push "unicef/hope-country-workspace:${{ steps.meta.outputs.version }}" +# docker inspect --format='{{index .Id}}' "unicef/hope-country-workspace:${{ steps.meta.outputs.version }}" +# +# - name: Generate artifact attestation +# uses: actions/attest-build-provenance@v1 +# with: +# subject-name: unicef/hope-country-workspace +# subject-digest: ${{ steps.push.outputs.digest }} +# push-to-registry: true diff --git a/src/country_workspace/workspaces/models.py b/src/country_workspace/workspaces/models.py index afd6e2a..115aaef 100644 --- a/src/country_workspace/workspaces/models.py +++ b/src/country_workspace/workspaces/models.py @@ -7,7 +7,7 @@ from country_workspace.models import Batch, Household, Individual, Office, Program -__all__ = ["CountryProgram", "CountryHousehold", "CountryIndividual"] +__all__ = ["CountryProgram", "CountryHousehold", "CountryIndividual", "CountryBatch"] class CountryBatch(Batch): diff --git a/tests/workspace/test_ws_batch.py b/tests/workspace/test_ws_batch.py new file mode 100644 index 0000000..3dc12f1 --- /dev/null +++ b/tests/workspace/test_ws_batch.py @@ -0,0 +1,125 @@ +from typing import TYPE_CHECKING + +from django.urls import reverse + +import pytest +from responses import RequestsMock +from testutils.utils import select_office + +from country_workspace.constants import HOUSEHOLD_CHECKER_NAME, INDIVIDUAL_CHECKER_NAME +from country_workspace.state import state + +if TYPE_CHECKING: + from django_webtest.pytest_plugin import MixinWithInstanceVariables + from testutils.types import CWTestApp + + from country_workspace.workspaces.models import CountryBatch + +pytestmark = [pytest.mark.admin, pytest.mark.smoke, pytest.mark.django_db] + + +@pytest.fixture() +def office(): + from testutils.factories import OfficeFactory + + co = OfficeFactory() + state.tenant = co + yield co + + +@pytest.fixture() +def program(office): + from testutils.factories import CountryProgramFactory, DataCheckerFactory + + return CountryProgramFactory( + household_checker=DataCheckerFactory(name=HOUSEHOLD_CHECKER_NAME), + individual_checker=DataCheckerFactory(name=INDIVIDUAL_CHECKER_NAME), + household_columns="name\nid\nxx", + individual_columns="name\nid\nxx", + ) + + +@pytest.fixture() +def batch(program): + from testutils.factories import CountryHouseholdFactory + + hh = CountryHouseholdFactory(batch__program=program, batch__country_office=program.country_office) + return hh.batch + + +@pytest.fixture() +def app(django_app_factory: "MixinWithInstanceVariables", mocked_responses: "RequestsMock") -> "CWTestApp": + from testutils.factories import SuperUserFactory + + django_app = django_app_factory(csrf_checks=False) + admin_user = SuperUserFactory(username="superuser") + django_app.set_user(admin_user) + django_app._user = admin_user + yield django_app + + +def test_batch_changelist(app: "CWTestApp", batch: "CountryBatch") -> None: + url = reverse("workspace:workspaces_countrybatch_changelist") + with select_office(app, batch.program.country_office): + # res = app.get(url).follow() + # res.forms["select-tenant"]["tenant"] = household.country_office.pk + # res.forms["select-tenant"].submit() + res = app.get(url) + assert res.status_code == 200, res.location + assert f"Add {batch._meta.verbose_name}" not in res.text + # filter by program + res = app.get(f"{url}?program__exact={batch.program.pk}") + assert res.status_code == 200, res.location + assert res.status_code == 200, res.location + + +# +# def test_hh_change(app: "CWTestApp", household: "CountryHousehold") -> None: +# url = reverse("workspace:workspaces_countryhousehold_change", args=[household.pk]) +# res = app.get(url).follow() +# res.forms["select-tenant"]["tenant"] = household.country_office.pk +# res.forms["select-tenant"].submit() +# +# res = app.get(f"{url}?batch__program__exact={household.program.pk}") +# assert res.status_code == 200, res.location +# assert f"Change {household._meta.verbose_name}" in res.text +# res = res.forms["countryhousehold_form"].submit() +# assert res.status_code == 302, res.location +# +# +# def test_hh_delete(app: "CWTestApp", household: "CountryHousehold") -> None: +# url = reverse("workspace:workspaces_countryhousehold_change", args=[household.pk]) +# res = app.get(url).follow() +# res.forms["select-tenant"]["tenant"] = household.country_office.pk +# res.forms["select-tenant"].submit() +# res = app.get(f"{url}?batch__program__exact={household.program.pk}") +# assert res.status_code == 200, res.location +# res = res.click("Delete") +# res = res.forms[1].submit().follow() +# assert res.status_code == 200 +# with pytest.raises(ObjectDoesNotExist): +# household.refresh_from_db() +# +# +# def test_hh_validate_single(app: "CWTestApp", household: "CountryHousehold") -> None: +# res = app.get("/").follow() +# res.forms["select-tenant"]["tenant"] = household.country_office.pk +# res.forms["select-tenant"].submit() +# with user_grant_permissions(app._user, ["workspaces.change_countryhousehold"], household.program): +# url = reverse("workspace:workspaces_countryhousehold_change", args=[household.pk]) +# res = app.get(f"{url}?batch__program__exact={household.program.pk}") +# res = res.click("Validate") +# res = res.follow() +# assert res.status_code == 200 +# +# +# def test_hh_validate_program(app: "CWTestApp", household: "CountryHousehold") -> None: +# res = app.get("/").follow() +# res.forms["select-tenant"]["tenant"] = household.country_office.pk +# res.forms["select-tenant"].submit() +# with user_grant_permissions(app._user, ["workspaces.change_countryhousehold"], household.program): +# url = reverse("workspace:workspaces_countryhousehold_changelist") +# res = app.get(f"{url}?batch__program__exact={household.program.pk}") +# res.click("Validate Programme").follow() +# household.refresh_from_db() +# assert household.last_checked diff --git a/tests/workspace/test_ws_smoke.py b/tests/workspace/test_ws_smoke.py deleted file mode 100644 index 68dc3d7..0000000 --- a/tests/workspace/test_ws_smoke.py +++ /dev/null @@ -1,200 +0,0 @@ -from typing import TYPE_CHECKING, Any, Iterable, Mapping, Optional - -from django.db.models import Model -from django.urls import reverse -from django.utils.safestring import mark_safe - -import pytest -from admin_extra_buttons.handlers import ButtonHandler -from admin_extra_buttons.mixins import ExtraButtonsMixin -from django_regex.utils import RegexList as _RegexList -from pytest_django.fixtures import SettingsWrapper -from responses import RequestsMock - -from country_workspace.state import state -from country_workspace.workspaces.sites import workspace -from country_workspace.workspaces.templatetags.workspace_urls import workspace_urlname - -if TYPE_CHECKING: - from django.contrib.admin import ModelAdmin - - from django_webtest import DjangoTestApp, DjangoWebtestResponse - from django_webtest.pytest_plugin import MixinWithInstanceVariables - from pytest import FixtureRequest, Metafunc - -pytestmark = [pytest.mark.admin, pytest.mark.smoke, pytest.mark.django_db] - - -class RegexList(_RegexList): # type: ignore[misc] - def extend(self, __iterable: "Iterable[Any]") -> None: - for e in __iterable: - self.append(e) - - -GLOBAL_EXCLUDED_MODELS = RegexList( - [ - "countryhouseold", - "countryindividual", - ] -) - -GLOBAL_EXCLUDED_BUTTONS = RegexList( - [ - r"hope_flex_fields.FieldsetAdmin:detect_changes", - r"country_workspace.CountryHouseholdAdmin:import_file", - ] -) - -KWARGS: Mapping[str, Any] = {} -# - - -def reverse_model_admin(model_admin: "ModelAdmin[Model]", op: str, args: Optional[list[Any]] = None) -> str: - if args: - return reverse(workspace_urlname(model_admin.model._meta, mark_safe(op)), args=args) - else: - return reverse(workspace_urlname(model_admin.model._meta, mark_safe(op))) - - -def log_submit_error(res: "DjangoWebtestResponse") -> str: - try: - return f"Submit failed with: {repr(res.context['form'].errors)}" - except KeyError: - return "Submit failed" - - -def pytest_generate_tests(metafunc: "Metafunc") -> None: # noqa - import django - - ids: list[str] - - markers = metafunc.definition.own_markers - excluded_models = RegexList(GLOBAL_EXCLUDED_MODELS) - excluded_buttons = RegexList(GLOBAL_EXCLUDED_BUTTONS) - if "skip_models" in [m.name for m in markers]: - skip_rule = list(filter(lambda m: m.name == "skip_models", markers))[0] - excluded_models.extend(skip_rule.args) - if "skip_buttons" in [m.name for m in markers]: - skip_rule = list(filter(lambda m: m.name == "skip_buttons", markers))[0] - excluded_buttons.extend(skip_rule.args) - django.setup() - if "button_handler" in metafunc.fixturenames: - m1: list[tuple[ModelAdmin[ExtraButtonsMixin], ButtonHandler]] = [] - ids = [] - for model, admin in workspace._registry.items(): - if hasattr(admin, "extra_button_handlers"): - name = model._meta.object_name - assert admin.urls # we need to force this call - # admin.get_urls() # we need to force this call - buttons = admin.extra_button_handlers.values() - full_name = f"{model._meta.app_label}.{name}" - admin_name = f"{model._meta.app_label}.{admin.__class__.__name__}" - if not (full_name in excluded_models): - for btn in buttons: - tid = f"{admin_name}:{btn.name}" - if tid not in excluded_buttons: - m1.append((admin, btn)) - ids.append(tid) - metafunc.parametrize("model_admin,button_handler", m1, ids=ids) - elif "app_label" in metafunc.fixturenames: - m: dict[str, int] = {} - for model, admin in workspace._registry.items(): - m[model._meta.app_label] = 1 - metafunc.parametrize("app_label", m.keys(), ids=m.keys()) - elif "model_admin" in metafunc.fixturenames: - m2: list[ModelAdmin[Model]] = [] - ids = [] - for model, admin in workspace._registry.items(): - name = model._meta.object_name - full_name = f"{model._meta.app_label}.{name}" - if not (full_name in excluded_models): - m2.append(admin) - ids.append(f"{admin.__class__.__name__}:{full_name}") - metafunc.parametrize("model_admin", m2, ids=ids) - - -@pytest.fixture() -def office(): - from testutils.factories import OfficeFactory - - co = OfficeFactory() - state.tenant = co - yield co - - -@pytest.fixture() -def program(office): - from testutils.factories import ProgramFactory - - return ProgramFactory() - - -@pytest.fixture() -def record(db: Any, program, request: "FixtureRequest") -> Model: - from testutils.factories import AutoRegisterModelFactory, get_factory_for_model - - model_admin = request.getfixturevalue("model_admin") - instance: Model = model_admin.model.objects.first() - if not instance: - factory: type[AutoRegisterModelFactory[Any]] = get_factory_for_model(model_admin.model) - try: - kwargs: dict[str, Any] = {} - if "program" in model_admin.model._meta.fields: - kwargs["program"] = program - kwargs["country_office"] = program.country_office - elif "country_office" in model_admin.model._meta.fields: - kwargs["country_office"] = program.country_office - instance = factory(**kwargs) - # if hasattr(instance, "batch"): - state.tenant = instance.country_office - except Exception as e: - raise Exception(f"Error creating fixture for {factory} using {KWARGS}") from e - return instance - - -@pytest.fixture() -def app( - django_app_factory: "MixinWithInstanceVariables", - mocked_responses: "RequestsMock", - settings: SettingsWrapper, -) -> "DjangoTestApp": - from testutils.factories import SuperUserFactory - - django_app = django_app_factory(csrf_checks=False) - admin_user = SuperUserFactory(username="superuser") - django_app.set_user(admin_user) - django_app._user = admin_user - yield django_app - - -def test_ws_app_list(app: "DjangoTestApp", app_label: str) -> None: - url = reverse("workspace:app_list", args=[app_label]) - res = app.get(url).follow() - assert res.status_code == 200 - - -def test_ws_changelist(app: "DjangoTestApp", model_admin: "ModelAdmin[Model]", record: Model) -> None: - url = reverse_model_admin(model_admin, "changelist") - res = app.get(url).follow() - res.forms["select-tenant"]["tenant"] = record.country_office.pk - res.forms["select-tenant"].submit() - res = app.get(url) - assert res.status_code == 200, res.location - assert f"Add {record._meta.verbose_name}" not in res.text - - -def test_ws_change(app: "DjangoTestApp", model_admin: "ModelAdmin[Model]", record: Model) -> None: - url = reverse_model_admin(model_admin, "change", args=[record.pk]) - res = app.get(url).follow() - res.forms["select-tenant"]["tenant"] = record.country_office.pk - res.forms["select-tenant"].submit() - res = app.get(url) - assert res.status_code == 200, res.location - assert f"Change {record._meta.verbose_name}" in res.text - - -def show_error(res: Any) -> tuple[str]: - errors = [] - for k, v in dict(res.context["adminform"].form.errors).items(): - errors.append(f'{k}: {"".join(v)}') - return (f"Form submitting failed: {res.status_code}: {errors}",)