diff --git a/src/country_workspace/config/fragments/constance.py b/src/country_workspace/config/fragments/constance.py index 0ddac09..8b0c45d 100644 --- a/src/country_workspace/config/fragments/constance.py +++ b/src/country_workspace/config/fragments/constance.py @@ -3,22 +3,36 @@ CONSTANCE_BACKEND = "constance.backends.database.DatabaseBackend" -# CONSTANCE_CONFIG_FIELDSETS = { -# "User settings": { -# "fields": ("NEW_USER_IS_STAFF", "NEW_USER_DEFAULT_GROUP"), -# "collapse": False, -# } -# } - CONSTANCE_ADDITIONAL_FIELDS = { "email": [ "django.forms.EmailField", {}, ], "group_select": [ - "country_workspace.utils.constance.GroupSelect", + "country_workspace.utils.constance.GroupChoiceField", {"initial": NEW_USER_DEFAULT_GROUP}, ], + "read_only_text": [ + "django.forms.fields.CharField", + { + "required": False, + "widget": "country_workspace.utils.constance.ObfuscatedInput", + }, + ], + "write_only_text": [ + "django.forms.fields.CharField", + { + "required": False, + "widget": "country_workspace.utils.constance.WriteOnlyTextarea", + }, + ], + "write_only_input": [ + "django.forms.fields.CharField", + { + "required": False, + "widget": "country_workspace.utils.constance.WriteOnlyInput", + }, + ], } CONSTANCE_CONFIG = { @@ -28,5 +42,10 @@ "Group to assign to any new user", "group_select", ), - "HOPE_API_URL": ("https://hope.unicef.org/api/rest/", "", str), + "HOPE_API_URL": ("https://hope.unicef.org/api/rest/", "HOPE API Server address", str), + "HOPE_API_TOKEN": ("", "HOPE API Access Token", "write_only_input"), + "AURORA_API_URL": ("https://register.unicef.org/api/", "Aurora API Server address", str), + "AURORA_API_TOKEN": ("", "Aurora API Access Token", "write_only_input"), + "KOBO_API_URL": ("", "Kobo API Server address", str), + "KOBO_API_TOKEN": ("", "Kobo API Access Token", "write_only_input"), } diff --git a/src/country_workspace/utils/constance.py b/src/country_workspace/utils/constance.py index 996c504..68de33d 100644 --- a/src/country_workspace/utils/constance.py +++ b/src/country_workspace/utils/constance.py @@ -1,12 +1,47 @@ import logging from typing import Any -from django.forms import ChoiceField +from django.forms import ChoiceField, HiddenInput, Textarea, TextInput +from django.template import Context, Template +from django.utils.safestring import mark_safe + +from constance import config logger = logging.getLogger(__name__) -class GroupSelect(ChoiceField): +class ObfuscatedInput(HiddenInput): + + def render(self, name, value, attrs=None, renderer=None): + context = self.get_context(name, value, attrs) + context["value"] = str(value) + context["label"] = "Set" if value else "Not Set" + + tpl = Template('{{ label }}') + return mark_safe(tpl.render(Context(context))) # nosec B308 B703 + + +class WriteOnlyWidget: + def format_value(self, value): + value = "***" + return super().format_value(value) + + def value_from_datadict(self, data, files, name): + value = data.get(name) + if value == "***": + return getattr(config, name) + return value + + +class WriteOnlyTextarea(WriteOnlyWidget, Textarea): + pass + + +class WriteOnlyInput(WriteOnlyWidget, TextInput): + pass + + +class GroupChoiceField(ChoiceField): def __init__(self, **kwargs: Any) -> None: from django.contrib.auth.models import Group diff --git a/src/country_workspace/web/templates/workspace/includes/program_title.html b/src/country_workspace/web/templates/workspace/includes/program_title.html index 33df093..519db17 100644 --- a/src/country_workspace/web/templates/workspace/includes/program_title.html +++ b/src/country_workspace/web/templates/workspace/includes/program_title.html @@ -21,5 +21,7 @@ {% endif %} {% endif %} + {% else %} +

 

{% endif %} diff --git a/src/country_workspace/workspaces/admin/__init__.py b/src/country_workspace/workspaces/admin/__init__.py index 9cc9ecb..2ebd84f 100644 --- a/src/country_workspace/workspaces/admin/__init__.py +++ b/src/country_workspace/workspaces/admin/__init__.py @@ -1,4 +1,5 @@ from .batch import CountryBatchAdmin # noqa from .household import CountryHouseholdAdmin # noqa from .individual import CountryIndividualAdmin # noqa +from .job import CountryJobAdmin # noqa from .program import CountryProgramAdmin # noqa diff --git a/src/country_workspace/workspaces/admin/batch.py b/src/country_workspace/workspaces/admin/batch.py index 996e97a..a07e03d 100644 --- a/src/country_workspace/workspaces/admin/batch.py +++ b/src/country_workspace/workspaces/admin/batch.py @@ -1,5 +1,6 @@ from typing import TYPE_CHECKING +from django.contrib.admin import register from django.db.models import QuerySet from django.http import HttpRequest from django.urls import reverse @@ -11,6 +12,7 @@ from ..filters import CWLinkedAutoCompleteFilter from ..models import CountryBatch from ..options import WorkspaceModelAdmin +from ..sites import workspace from .hh_ind import SelectedProgramMixin if TYPE_CHECKING: @@ -26,6 +28,7 @@ def queryset(self, request: HttpRequest, queryset: QuerySet) -> QuerySet: return queryset +@register(CountryBatch, site=workspace) class CountryBatchAdmin(SelectedProgramMixin, WorkspaceModelAdmin): list_display = ["name", "program", "country_office"] search_fields = ("label",) diff --git a/src/country_workspace/workspaces/admin/household.py b/src/country_workspace/workspaces/admin/household.py index 02bfc1a..29c9437 100644 --- a/src/country_workspace/workspaces/admin/household.py +++ b/src/country_workspace/workspaces/admin/household.py @@ -11,7 +11,13 @@ if TYPE_CHECKING: from ..models import CountryProgram +from django.contrib.admin import register +from ..models import CountryHousehold +from ..sites import workspace + + +@register(CountryHousehold, site=workspace) class CountryHouseholdAdmin(BeneficiaryBaseAdmin): list_display = ["name", "batch"] search_fields = ("name",) diff --git a/src/country_workspace/workspaces/admin/individual.py b/src/country_workspace/workspaces/admin/individual.py index 0bc8077..39f4ea5 100644 --- a/src/country_workspace/workspaces/admin/individual.py +++ b/src/country_workspace/workspaces/admin/individual.py @@ -1,15 +1,17 @@ from typing import Any, Optional -from django.contrib.admin import AdminSite +from django.contrib.admin import AdminSite, register from django.db.models import Model from django.http import HttpRequest from ...state import state from ..filters import HouseholdFilter, ProgramFilter from ..models import CountryHousehold, CountryIndividual, CountryProgram +from ..sites import workspace from .hh_ind import BeneficiaryBaseAdmin +@register(CountryIndividual, site=workspace) class CountryIndividualAdmin(BeneficiaryBaseAdmin): list_display = [ "name", @@ -30,7 +32,7 @@ class CountryIndividualAdmin(BeneficiaryBaseAdmin): change_form_template = "workspace/individual/change_form.html" ordering = ("name",) - def __init__(self, model: Model, admin_site: AdminSite): + def __init__(self, model: Model, admin_site: "AdminSite"): self._selected_household = None super().__init__(model, admin_site) diff --git a/src/country_workspace/workspaces/admin/job.py b/src/country_workspace/workspaces/admin/job.py new file mode 100644 index 0000000..7a2898e --- /dev/null +++ b/src/country_workspace/workspaces/admin/job.py @@ -0,0 +1,21 @@ +from django.contrib.admin import register + +from ..models import CountryJob +from ..options import WorkspaceModelAdmin +from ..sites import workspace + + +@register(CountryJob, site=workspace) +class CountryJobAdmin(WorkspaceModelAdmin): + list_display = ( + "name", + "sector", + "status", + "active", + ) + + def has_add_permission(self, request): + return False + + +# workspace.register(CountryJob, CountryJobAdmin) diff --git a/src/country_workspace/workspaces/admin/program.py b/src/country_workspace/workspaces/admin/program.py index 7ddac45..082b26d 100644 --- a/src/country_workspace/workspaces/admin/program.py +++ b/src/country_workspace/workspaces/admin/program.py @@ -1,6 +1,7 @@ from typing import Any from django import forms +from django.contrib.admin import register from django.db.models import QuerySet from django.db.transaction import atomic from django.http import HttpRequest, HttpResponse, HttpResponseRedirect @@ -21,6 +22,7 @@ from ...sync.office import sync_programs from ..models import CountryProgram from ..options import WorkspaceModelAdmin +from ..sites import workspace from .forms import ImportFileForm @@ -55,6 +57,7 @@ def clean_field_name(v): return v.replace("_h_c", "").replace("_h_f", "").replace("_i_c", "").replace("_i_f", "").lower() +@register(CountryProgram, site=workspace) class CountryProgramAdmin(WorkspaceModelAdmin): list_display = ( "name", diff --git a/src/country_workspace/workspaces/apps.py b/src/country_workspace/workspaces/apps.py index 71625bb..d43fefb 100644 --- a/src/country_workspace/workspaces/apps.py +++ b/src/country_workspace/workspaces/apps.py @@ -8,13 +8,8 @@ class Config(AppConfig): name = __name__.rpartition(".")[0] - def ready(self) -> None: - from country_workspace.workspaces import models - - from . import admin - from .sites import workspace - - workspace.register(models.CountryBatch, admin.CountryBatchAdmin) - workspace.register(models.CountryHousehold, admin.CountryHouseholdAdmin) - workspace.register(models.CountryIndividual, admin.CountryIndividualAdmin) - workspace.register(models.CountryProgram, admin.CountryProgramAdmin) + # def ready(self) -> None: + # from country_workspace.workspaces import models + # + # from . import admin + # from .sites import workspace diff --git a/src/country_workspace/workspaces/models.py b/src/country_workspace/workspaces/models.py index 115aaef..a0633ac 100644 --- a/src/country_workspace/workspaces/models.py +++ b/src/country_workspace/workspaces/models.py @@ -5,7 +5,7 @@ from hope_flex_fields.models import DataChecker -from country_workspace.models import Batch, Household, Individual, Office, Program +from country_workspace.models import AsyncJob, Batch, Household, Individual, Office, Program __all__ = ["CountryProgram", "CountryHousehold", "CountryIndividual", "CountryBatch"] @@ -45,5 +45,10 @@ class Meta: class CountryChecker(DataChecker): country_office = models.ForeignKey(Office, on_delete=models.CASCADE) + +class CountryJob(AsyncJob): + class Meta: + proxy = True + # class Meta: # app_label = "workspaces" diff --git a/tests/utils/test_utils_constance.py b/tests/utils/test_utils_constance.py new file mode 100644 index 0000000..a9f1fbf --- /dev/null +++ b/tests/utils/test_utils_constance.py @@ -0,0 +1,31 @@ +from constance.test import override_config + +from country_workspace.utils.constance import GroupChoiceField, ObfuscatedInput, WriteOnlyInput, WriteOnlyTextarea + + +def test_utils_groupchoicefield(): + field = GroupChoiceField() + assert field + + +# LdapDNField + + +# ObfuscatedInput +def test_obfuscatedinput(): + field = ObfuscatedInput() + assert field.render("name", "value") == 'Set' + + +# WriteOnlyTextarea +def test_writeonlytextarea(): + field = WriteOnlyTextarea() + assert field.render("name", "value") == '' + + +@override_config(HOPE_API_TOKEN="abc") +def test_writeonlyinput(): + field = WriteOnlyInput() + assert field.render("name", "value") + assert field.value_from_datadict({"HOPE_API_TOKEN": "***"}, {}, "HOPE_API_TOKEN") == "abc" + assert field.value_from_datadict({"HOPE_API_TOKEN": "123"}, {}, "HOPE_API_TOKEN") == "123" diff --git a/tests/test_flags.py b/tests/utils/test_utils_flags.py similarity index 100% rename from tests/test_flags.py rename to tests/utils/test_utils_flags.py diff --git a/tests/test_utils_http.py b/tests/utils/test_utils_http.py similarity index 100% rename from tests/test_utils_http.py rename to tests/utils/test_utils_http.py diff --git a/tests/workspace/test_ws_autocmplete.py b/tests/workspace/test_ws_autocmplete.py new file mode 100644 index 0000000..8691357 --- /dev/null +++ b/tests/workspace/test_ws_autocmplete.py @@ -0,0 +1,35 @@ +from django.urls import reverse + +import pytest +from django_webtest import DjangoTestApp +from django_webtest.pytest_plugin import MixinWithInstanceVariables +from responses import RequestsMock +from testutils.utils import select_office + + +@pytest.fixture() +def program(): + from testutils.factories import CountryProgramFactory + + return CountryProgramFactory() + + +@pytest.fixture() +def app(django_app_factory: "MixinWithInstanceVariables", mocked_responses: "RequestsMock") -> "DjangoTestApp": + from testutils.factories.user 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 + return django_app + + +def test_project_autocomplete(app: DjangoTestApp, program) -> None: + url = reverse("workspace:autocomplete") + with select_office(app, program.country_office): + res = app.get(url, expect_errors=True) + assert res.status_code == 403 + + res = app.get(f"{url}?app_label=country_workspace&model_name=batch&field_name=program") + assert res.status_code == 200