From 867af0ed2d91bcba696a6983f3e670ed95d10cdf Mon Sep 17 00:00:00 2001 From: Colin Copeland Date: Tue, 11 Feb 2025 10:52:55 -0500 Subject: [PATCH] publish assignments --- apps/odk_publish/consumers.py | 7 +-- apps/odk_publish/etl/load.py | 24 +++++++-- apps/odk_publish/models.py | 10 ++++ tests/odk_publish/etl/load/test_publish.py | 57 ++++++++++++++++------ 4 files changed, 75 insertions(+), 23 deletions(-) diff --git a/apps/odk_publish/consumers.py b/apps/odk_publish/consumers.py index 4100895..62c9d54 100644 --- a/apps/odk_publish/consumers.py +++ b/apps/odk_publish/consumers.py @@ -37,12 +37,7 @@ def receive(self, text_data): self.publish_form_template(event_data=event_data) except Exception as e: logger.exception("Error publishing form") - tbe = traceback.TracebackException.from_exception( - exc=e, - capture_locals=True, - compact=True, - limit=1, - ) + tbe = traceback.TracebackException.from_exception(exc=e, compact=True) message = "".join(tbe.format()) # If the error is from ODK Central, format the error message for easier reading if len(e.args) >= 2 and isinstance(e.args[1], Response): diff --git a/apps/odk_publish/etl/load.py b/apps/odk_publish/etl/load.py index 4bacb3c..c6f89c2 100644 --- a/apps/odk_publish/etl/load.py +++ b/apps/odk_publish/etl/load.py @@ -64,22 +64,40 @@ def publish_form_template(event: PublishTemplateEvent, user: User, send_message: ) send_message(f"Downloaded template: {file}") with transaction.atomic(): - # Create the next version + # Create the next version locally template_version = FormTemplateVersion.objects.create( form_template=form_template, user=user, file=file, version=version ) - # Create a version for each app user + # Create a version for each app user locally app_users = form_template.project.app_users.filter(name__in=event.app_users) app_user_versions = template_version.create_app_user_versions( app_users=app_users, send_message=send_message ) - # Publish each app user version to ODK Central + # Get or create app users in ODK Central + central_app_user_assignments = client.odk_publish.get_or_create_app_users( + display_names=[app_user.name for app_user in app_users] + ) + send_message(f"Synced user(s): {', '.join(central_app_user_assignments.keys())}") + # Assign this form to the app users + for app_user_version in app_user_versions: + central_app_user_assignments[app_user_version.app_user.name].xml_form_ids.append( + app_user_version.xml_form_id + ) + # Publish each app user form version to ODK Central for app_user_version in app_user_versions: form = client.odk_publish.create_or_update_form( xml_form_id=app_user_version.app_user_form_template.xml_form_id, definition=app_user_version.file.read(), ) send_message(f"Published form: {form.xmlFormId}") + # Create or update the form assignments on the server + for assignment in central_app_user_assignments.values(): + client.odk_publish.assign_app_users_forms(app_users=[assignment]) + send_message(f"Assigned user {assignment.displayName} to {assignment.xml_form_ids[0]}") + # Update AppUsers with null central_id + update_app_users_central_id( + project=form_template.project, app_users=central_app_user_assignments + ) send_message(f"Successfully published {version}", complete=True) diff --git a/apps/odk_publish/models.py b/apps/odk_publish/models.py index 1323948..6c77853 100644 --- a/apps/odk_publish/models.py +++ b/apps/odk_publish/models.py @@ -263,3 +263,13 @@ class Meta: def __str__(self): return f"{self.app_user_form_template} - {self.form_template_version}" + + @property + def xml_form_id(self) -> str: + """The ODK Central xmlFormId for this version's AppUserFormTemplate.""" + return self.app_user_form_template.xml_form_id + + @property + def app_user(self) -> AppUser: + """The app user for this version.""" + return self.app_user_form_template.app_user diff --git a/tests/odk_publish/etl/load/test_publish.py b/tests/odk_publish/etl/load/test_publish.py index 1df3e6e..441620f 100644 --- a/tests/odk_publish/etl/load/test_publish.py +++ b/tests/odk_publish/etl/load/test_publish.py @@ -1,8 +1,11 @@ +import datetime as dt + import pytest from django.core.files.uploadedfile import SimpleUploadedFile from gspread.utils import ExportFormat from apps.odk_publish.etl.load import PublishTemplateEvent, publish_form_template +from apps.odk_publish.etl.odk.publish import ProjectAppUserAssignment from tests.odk_publish.factories import ( AppUserFormTemplateFactory, FormTemplateFactory, @@ -19,13 +22,13 @@ class TestPublishFormTemplate: def send_message(self, message: str, complete: bool = False): pass - def test_publish(self, mocker): + def test_publish(self, mocker, requests_mock): project = ProjectFactory(central_server__base_url="https://central", central_id=2) form_template = FormTemplateFactory(project=project, form_id_base="staff_registration") # Create 2 app users - user_form1 = AppUserFormTemplateFactory(form_template=form_template) - user_form2 = AppUserFormTemplateFactory(form_template=form_template) - event = PublishTemplateEvent(form_template=form_template.id, app_users=["user1", "user2"]) + user_form1 = AppUserFormTemplateFactory(form_template=form_template, app_user__name="user1") + user_form2 = AppUserFormTemplateFactory(form_template=form_template, app_user__name="user2") + event = PublishTemplateEvent(form_template=form_template.id, app_users=["user1"]) # Mock Gspread download mock_gspread_client = mocker.patch("apps.odk_publish.etl.google.gspread_client") mock_gspread_client.return_value.open_by_url.return_value.export.return_value = ( @@ -43,11 +46,44 @@ def test_publish(self, mocker): "apps.odk_publish.etl.odk.publish.PublishService.get_unique_version_by_form_id", return_value="2025-02-01-v1", ) + + assignments = { + "user1": ProjectAppUserAssignment( + projectId=project.central_id, + id=user_form1.app_user.central_id, + type="field_key", + displayName="user1", + createdAt=dt.datetime.now(), + updatedAt=None, + deletedAt=None, + token="token1", + xml_form_ids=["staff_registration_user1"], + ), + "user2": ProjectAppUserAssignment( + projectId=project.central_id, + id=user_form2.app_user.central_id, + type="field_key", + displayName="user2", + createdAt=dt.datetime.now(), + updatedAt=None, + deletedAt=None, + token="token1", + xml_form_ids=["staff_registration_user2"], + ), + } + mock_get_users = mocker.patch( + "apps.odk_publish.etl.odk.publish.PublishService.get_or_create_app_users", + return_value=assignments, + ) mock_create_or_update_form = mocker.patch( "apps.odk_publish.etl.odk.publish.PublishService.create_or_update_form", return_value=mocker.Mock(), ) + mock_assign_app_users_forms = mocker.patch( + "apps.odk_publish.etl.odk.publish.PublishService.assign_app_users_forms" + ) publish_form_template(event=event, user=UserFactory(), send_message=self.send_message) + mock_get_users.assert_called_once() mock_get_version.assert_called_once_with(xml_form_id_base=form_template.form_id_base) mock_create_or_update_form.assert_has_calls( [ @@ -61,16 +97,9 @@ def test_publish(self, mocker): ), ] ) - mock_get_version.assert_called_once_with(xml_form_id_base=form_template.form_id_base) - mock_create_or_update_form.assert_has_calls( + mock_assign_app_users_forms.assert_has_calls( [ - mocker.call( - definition=b"file content", - xml_form_id=user_form1.xml_form_id, - ), - mocker.call( - definition=b"file content", - xml_form_id=user_form2.xml_form_id, - ), + mocker.call(app_users=[assignments["user1"]]), + mocker.call(app_users=[assignments["user2"]]), ] )