Skip to content

Commit

Permalink
publish forms
Browse files Browse the repository at this point in the history
  • Loading branch information
copelco committed Jan 10, 2025
1 parent 943a2e7 commit 836a9bc
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 18 deletions.
2 changes: 1 addition & 1 deletion apps/odk_publish/etl/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def create_or_update_app_users(form_template: FormTemplate):
)
# Link form assignments to app users locally
for app_user_form in app_user_forms:
app_users[app_user_form.app_user.name].form_ids.append(app_user_form.form_id)
app_users[app_user_form.app_user.name].xml_form_ids.append(app_user_form.xml_form_id)
# Create or update the form assignments on the server
client.odk_publish.assign_forms(
app_users=app_users.values(), project_id=form_template.project.project_id
Expand Down
7 changes: 5 additions & 2 deletions apps/odk_publish/etl/odk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
class ODKPublishClient(Client):
"""Extended pyODK Client for interacting with ODK Central."""

def __init__(self, base_url: str):
def __init__(self, base_url: str, project_id: int | None = None):
"""Create an ODK Central-configured client without a config file."""
# Create stub config file if it doesn't exist, so that pyodk doesn't complain
config_path = Path("/tmp/.pyodk_config.toml")
Expand All @@ -33,7 +33,7 @@ def __init__(self, base_url: str):
password=settings.ODK_CENTRAL_PASSWORD,
cache_path=None,
)
super().__init__(config_path=config_path, session=session)
super().__init__(config_path=config_path, session=session, project_id=project_id)
# Update the stub config with the environment-provided authentication
# details
self.config: config.Config = config.objectify_config(
Expand All @@ -48,3 +48,6 @@ def __init__(self, base_url: str):
# Create a ODK Publish service for this client, which provides
# additional functionality for interacting with ODK Central
self.odk_publish: PublishService = PublishService(client=self)

def __enter__(self) -> "ODKPublishClient":
return self.open()
64 changes: 58 additions & 6 deletions apps/odk_publish/etl/odk/publish.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from os import PathLike
from typing import TYPE_CHECKING

import structlog
from pyodk._endpoints import bases
from pyodk._endpoints.form_assignments import FormAssignmentService
from pyodk._endpoints.forms import Form
from pyodk._endpoints.project_app_users import ProjectAppUser, ProjectAppUserService
from pyodk.errors import PyODKError

Expand All @@ -18,7 +20,7 @@
class ProjectAppUserAssignment(ProjectAppUser):
"""Extended ProjectAppUser with additional form_ids attribute."""

form_ids: list[str] = []
xml_form_ids: list[str] = []


class PublishService(bases.Service):
Expand All @@ -39,7 +41,9 @@ def __init__(self, client: "ODKPublishClient"):
def get_app_users(
self, project_id: int, display_names: list[str] = None
) -> dict[str, ProjectAppUserAssignment]:
"""Return a mapping of display names to ProjectAppUserAssignments for the given project."""
"""Return a mapping of display names to ProjectAppUserAssignments for
the given project, filtered by display names if provided.
"""
app_users = {
user.displayName: ProjectAppUserAssignment(**dict(user))
for user in self.project_users.list(project_id=project_id)
Expand Down Expand Up @@ -69,22 +73,70 @@ def get_or_create_app_users(
logger.debug("Retrieved app users", users=list(app_users.keys()), project_id=project_id)
return app_users

def get_forms(self, project_id: int | None = None) -> dict[str, Form]:
"""Return a mapping of form IDs to Form objects for the given project."""
forms = self.client.forms.list(project_id=project_id)
return {form.xmlFormId: form for form in forms}

def create_or_update_form(
self,
xml_form_id: str,
definition: PathLike | bytes,
attachments: list[PathLike | bytes] | None = None,
project_id: int | None = None,
) -> Form:
"""Return forms for the given form IDs, creating them if they don't exist."""
central_forms = self.get_forms(project_id=project_id)
# Updated an existing form if it exists
if xml_form_id in central_forms:
self.client.forms.update(
form_id=xml_form_id,
definition=definition,
attachments=attachments,
project_id=project_id,
)
# Retrieve updated form to get the version
form = self.client.forms.get(form_id=xml_form_id, project_id=project_id)
logger.info(
"Updated form",
project_id=form.projectId,
xml_form_id=form.xmlFormId,
version=form.version,
name=form.name,
)
else:
# Create form if it doesn't exist
form = self.client.forms.create(
form_id=xml_form_id,
definition=definition,
attachments=attachments,
project_id=project_id,
)
logger.info(
"Created form",
project_id=form.projectId,
xml_form_id=form.xmlFormId,
version=form.version,
name=form.name,
)
return form

def assign_app_users_forms(
self, app_users: list[ProjectAppUserAssignment], project_id: int
) -> None:
"""Assign forms to app users."""
for app_user in app_users:
for form_id in app_user.form_ids:
for xml_form_id in app_user.form_ids:
try:
self.form_assignments.assign(
role_id=APP_USER_ROLE_ID,
user_id=app_user.id,
form_id=form_id,
form_id=xml_form_id,
project_id=project_id,
)
logger.debug(
"Assigned form",
form_id=form_id,
form_id=xml_form_id,
app_user=app_user.displayName,
project_id=project_id,
)
Expand All @@ -93,7 +145,7 @@ def assign_app_users_forms(
raise
logger.debug(
"Form already assigned",
form_id=form_id,
form_id=xml_form_id,
app_user=app_user.displayName,
project_id=project_id,
)
21 changes: 15 additions & 6 deletions apps/odk_publish/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ def create_next_version(self, user: User) -> "FormTemplateVersion":
2. Download the Google Sheet Excel file for this form template.
3. Create a new FormTemplateVersion instance with the downloaded file.
"""
with ODKPublishClient(base_url=self.project.central_server.base_url) as client:
with ODKPublishClient(
base_url=self.project.central_server.base_url, project_id=self.project.project_id
) as client:
version = get_unique_version_by_form_id(
client=client, project_id=self.project.project_id, form_id_base=self.form_id_base
)
Expand All @@ -109,7 +111,12 @@ def create_next_version(self, user: User) -> "FormTemplateVersion":
version = FormTemplateVersion.objects.create(
form_template=self, user=user, file=file, version=version
)
version.create_app_user_versions()
app_user_versions = version.create_app_user_versions()
for app_user_version in app_user_versions:
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(),
)
return version


Expand All @@ -131,10 +138,12 @@ class Meta:
def __str__(self):
return self.file.name

def create_app_user_versions(self):
def create_app_user_versions(self) -> list["AppUserFormVersion"]:
app_user_versions = []
for app_user_form in AppUserFormTemplate.objects.filter(form_template=self.form_template):
logger.info("Creating next AppUserFormVersion", app_user_form=app_user_form)
app_user_form.create_next_version(form_template_version=self)
app_user_versions.append(app_user_form.create_next_version(form_template_version=self))
return app_user_versions


class AppUserTemplateVariable(AbstractBaseModel):
Expand Down Expand Up @@ -203,8 +212,8 @@ def __str__(self):
return f"{self.app_user} - {self.form_template}"

@property
def form_id(self) -> str:
"""The ODK Central form_id for this AppUserFormTemplate."""
def xml_form_id(self) -> str:
"""The ODK Central xmlFormId for this AppUserFormTemplate."""
return f"{self.form_template.form_id_base}_{self.app_user.name}"

def create_next_version(self, form_template_version: FormTemplateVersion):
Expand Down
1 change: 1 addition & 0 deletions apps/odk_publish/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class FormTemplateTable(tables.Table):
text="Publish New Version",
orderable=False,
verbose_name="Actions",
attrs={"a": {"class": "text-primary-600 hover:underline"}},
)

class Meta:
Expand Down
9 changes: 6 additions & 3 deletions apps/odk_publish/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.db import models
from django.db import models, transaction
from django.http import HttpRequest
from django.shortcuts import get_object_or_404, redirect, render

from .etl.load import generate_and_save_app_user_collect_qrcodes
from .models import FormTemplateVersion
from .models import FormTemplateVersion, FormTemplate
from .nav import Breadcrumbs
from .tables import FormTemplateTable

Expand Down Expand Up @@ -58,8 +58,11 @@ def form_template_list(request: HttpRequest, odk_project_pk):


@login_required
@transaction.atomic
def form_template_publish_next_version(request: HttpRequest, odk_project_pk, form_template_id):
form_template = get_object_or_404(request.odk_project.form_templates, pk=form_template_id)
form_template: FormTemplate = get_object_or_404(
request.odk_project.form_templates, pk=form_template_id
)
version = form_template.create_next_version(user=request.user)
messages.add_message(request, messages.SUCCESS, f"{version} published.")
return redirect("odk_publish:form-template-list", odk_project_pk=odk_project_pk)

0 comments on commit 836a9bc

Please sign in to comment.