Skip to content

Commit

Permalink
Merge pull request #46 from dimagi/sk/read-app-xml
Browse files Browse the repository at this point in the history
extract module and assessment metadata from app CCZ
  • Loading branch information
snopoke authored Aug 3, 2023
2 parents 561866b + f160832 commit 554bc49
Show file tree
Hide file tree
Showing 11 changed files with 452 additions and 2 deletions.
79 changes: 79 additions & 0 deletions commcare_connect/opportunity/app_xml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import itertools
import re
import tempfile
import xml.etree.ElementTree as ET
import zipfile
from dataclasses import dataclass

import httpx
from django.conf import settings

XMLNS = "http://commcareconnect.com/data/v1/learn"
XMLNS_PREFIX = "{%s}" % XMLNS


@dataclass
class Module:
id: str
name: str
description: str
time_estimate: int


@dataclass
class Assessment:
id: str


def get_connect_blocks_for_app(domain: str, app_id: str) -> list[str]:
form_xmls = get_form_xml_for_app(domain, app_id)
return list(itertools.chain.from_iterable(extract_connect_blocks(form_xml) for form_xml in form_xmls))


def get_form_xml_for_app(domain: str, app_id: str) -> list[str]:
"""Download the CCZ for the given app and return the XML for each form."""
ccz_url = f"{settings.COMMCARE_HQ_URL}/a/{domain}/apps/api/download_ccz/"
params = {
"app_id": app_id,
"latest": "release",
}
response = httpx.get(ccz_url, params=params)
response.raise_for_status()

form_xml = []
with tempfile.NamedTemporaryFile() as file:
file.write(response.content)
file.seek(0)

form_re = re.compile(r"modules-\d+/forms-\d+\.xml")
with zipfile.ZipFile(file, "r") as zip_ref:
for file in zip_ref.namelist():
if form_re.match(file):
with zip_ref.open(file) as xml_file:
form_xml.append(xml_file.read().decode())
return form_xml


def extract_connect_blocks(form_xml):
xml = ET.fromstring(form_xml)
yield from extract_modules(xml)
yield from extract_assessments(xml)


def extract_assessments(xml: ET.ElementTree) -> list[str]:
for block in xml.findall(f".//{XMLNS_PREFIX}assessment"):
yield Assessment(block.get("id"))


def extract_modules(xml: ET.ElementTree):
for block in xml.findall(f".//{XMLNS_PREFIX}module"):
slug = block.get("id")
name = get_element_text(block, "name")
description = get_element_text(block, "description")
time_estimate = get_element_text(block, "time_estimate")
yield Module(slug, name, description, int(time_estimate) if time_estimate is not None else None)


def get_element_text(parent, name) -> str | None:
element = parent.find(f"{XMLNS_PREFIX}{name}")
return element.text if element is not None else None
7 changes: 7 additions & 0 deletions commcare_connect/opportunity/tests/data/demo_app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
This content comes from the https://staging.commcarehq.org/a/skelly/apps/view/a671db914232020002c1f294c675d1d8/ app.

To update it from the app:

1. Download the CCZ: https://staging.commcarehq.org/a/skelly/apps/api/download_ccz/?app_id=a671db914232020002c1f294c675d1d8
2. Extract the contents
3. Update the files in this folder
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<h:html xmlns:h="http://www.w3.org/1999/xhtml" xmlns:orx="http://openrosa.org/jr/xforms" xmlns="http://www.w3.org/2002/xforms" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:jr="http://openrosa.org/javarosa" xmlns:vellum="http://commcarehq.org/xforms/vellum">
<h:head>
<h:title>Module 1</h:title>
<model>
<instance>
<data xmlns:jrm="http://dev.commcarehq.org/jr/xforms" xmlns="http://openrosa.org/formdesigner/52F02F3E-320D-4D91-9EBF-FF4F06226E98" uiVersion="1" version="72" name="Module 1">
<welcome_note/>
<module_1>
<module xmlns="http://commcareconnect.com/data/v1/learn" id="module_1">
<name>Module 1</name>
<description>This is the first module in a series of modules
that will take you through all you need to know.</description>
<time_estimate>1</time_estimate>
</module>
</module_1>
<orx:meta xmlns:cc="http://commcarehq.org/xforms">
<orx:deviceID/>
<orx:timeStart/>
<orx:timeEnd/>
<orx:username/>
<orx:userID/>
<orx:instanceID/>
<cc:appVersion/>
<orx:drift/>
</orx:meta>
</data>
</instance>
<instance id="commcaresession" src="jr://instance/session"/>
<bind nodeset="/data/welcome_note"/>
<itext>
<translation lang="en" default="">
<text id="welcome_note-label">
<value>Welcome to module 1</value>
</text>
</translation>
</itext>
<setvalue ref="/data/meta/deviceID" value="instance('commcaresession')/session/context/deviceid" event="xforms-ready"/>
<setvalue ref="/data/meta/timeStart" value="now()" event="xforms-ready"/>
<bind nodeset="/data/meta/timeStart" type="xsd:dateTime"/>
<setvalue ref="/data/meta/timeEnd" value="now()" event="xforms-revalidate"/>
<bind nodeset="/data/meta/timeEnd" type="xsd:dateTime"/>
<setvalue ref="/data/meta/username" value="instance('commcaresession')/session/context/username" event="xforms-ready"/>
<setvalue ref="/data/meta/userID" value="instance('commcaresession')/session/context/userid" event="xforms-ready"/>
<setvalue ref="/data/meta/instanceID" value="uuid()" event="xforms-ready"/>
<setvalue ref="/data/meta/appVersion" value="instance('commcaresession')/session/context/appversion" event="xforms-ready"/>
<setvalue ref="/data/meta/drift" value="if(count(instance('commcaresession')/session/context/drift) = 1, instance('commcaresession')/session/context/drift, '')" event="xforms-revalidate"/>
</model>
</h:head>
<h:body>
<trigger ref="/data/welcome_note" appearance="minimal">
<label ref="jr:itext('welcome_note-label')"/>
</trigger>
</h:body>
</h:html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<h:html xmlns:h="http://www.w3.org/1999/xhtml" xmlns:orx="http://openrosa.org/jr/xforms" xmlns="http://www.w3.org/2002/xforms" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:jr="http://openrosa.org/javarosa" xmlns:vellum="http://commcarehq.org/xforms/vellum">
<h:head>
<h:title>Module 2</h:title>
<model>
<instance>
<data xmlns:jrm="http://dev.commcarehq.org/jr/xforms" xmlns="http://openrosa.org/formdesigner/EC1AD740-D2C9-4532-AECC-2D5CF5364696" uiVersion="1" version="72" name="Module 2">
<welcome_note/>
<module_2>
<module xmlns="http://commcareconnect.com/data/v1/learn" id="module_2">
<name>Module 2</name>
<description>This is module 2 of the series.</description>
<time_estimate>2</time_estimate>
</module>
</module_2>
<orx:meta xmlns:cc="http://commcarehq.org/xforms">
<orx:deviceID/>
<orx:timeStart/>
<orx:timeEnd/>
<orx:username/>
<orx:userID/>
<orx:instanceID/>
<cc:appVersion/>
<orx:drift/>
</orx:meta>
</data>
</instance>
<instance id="commcaresession" src="jr://instance/session"/>
<bind nodeset="/data/welcome_note"/>
<itext>
<translation lang="en" default="">
<text id="welcome_note-label">
<value>Welcome to module 2</value>
</text>
</translation>
</itext>
<setvalue ref="/data/meta/deviceID" value="instance('commcaresession')/session/context/deviceid" event="xforms-ready"/>
<setvalue ref="/data/meta/timeStart" value="now()" event="xforms-ready"/>
<bind nodeset="/data/meta/timeStart" type="xsd:dateTime"/>
<setvalue ref="/data/meta/timeEnd" value="now()" event="xforms-revalidate"/>
<bind nodeset="/data/meta/timeEnd" type="xsd:dateTime"/>
<setvalue ref="/data/meta/username" value="instance('commcaresession')/session/context/username" event="xforms-ready"/>
<setvalue ref="/data/meta/userID" value="instance('commcaresession')/session/context/userid" event="xforms-ready"/>
<setvalue ref="/data/meta/instanceID" value="uuid()" event="xforms-ready"/>
<setvalue ref="/data/meta/appVersion" value="instance('commcaresession')/session/context/appversion" event="xforms-ready"/>
<setvalue ref="/data/meta/drift" value="if(count(instance('commcaresession')/session/context/drift) = 1, instance('commcaresession')/session/context/drift, '')" event="xforms-revalidate"/>
</model>
</h:head>
<h:body>
<trigger ref="/data/welcome_note" appearance="minimal">
<label ref="jr:itext('welcome_note-label')"/>
</trigger>
</h:body>
</h:html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<h:html xmlns:h="http://www.w3.org/1999/xhtml" xmlns:orx="http://openrosa.org/jr/xforms" xmlns="http://www.w3.org/2002/xforms" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:jr="http://openrosa.org/javarosa" xmlns:vellum="http://commcarehq.org/xforms/vellum">
<h:head>
<h:title>Module 3</h:title>
<model>
<instance>
<data xmlns:jrm="http://dev.commcarehq.org/jr/xforms" xmlns="http://openrosa.org/formdesigner/11151AA2-1599-4C5E-8013-5E2197B6C68E" uiVersion="1" version="72" name="Module 3">
<welcome_note/>
<module_3>
<module xmlns="http://commcareconnect.com/data/v1/learn" id="module_3">
<name>Module 3</name>
<description>Module 3 in the series</description>
<time_estimate>3</time_estimate>
</module>
</module_3>
<orx:meta xmlns:cc="http://commcarehq.org/xforms">
<orx:deviceID/>
<orx:timeStart/>
<orx:timeEnd/>
<orx:username/>
<orx:userID/>
<orx:instanceID/>
<cc:appVersion/>
<orx:drift/>
</orx:meta>
</data>
</instance>
<instance id="commcaresession" src="jr://instance/session"/>
<bind nodeset="/data/welcome_note"/>
<itext>
<translation lang="en" default="">
<text id="welcome_note-label">
<value>Welcome to module 3</value>
</text>
</translation>
</itext>
<setvalue ref="/data/meta/deviceID" value="instance('commcaresession')/session/context/deviceid" event="xforms-ready"/>
<setvalue ref="/data/meta/timeStart" value="now()" event="xforms-ready"/>
<bind nodeset="/data/meta/timeStart" type="xsd:dateTime"/>
<setvalue ref="/data/meta/timeEnd" value="now()" event="xforms-revalidate"/>
<bind nodeset="/data/meta/timeEnd" type="xsd:dateTime"/>
<setvalue ref="/data/meta/username" value="instance('commcaresession')/session/context/username" event="xforms-ready"/>
<setvalue ref="/data/meta/userID" value="instance('commcaresession')/session/context/userid" event="xforms-ready"/>
<setvalue ref="/data/meta/instanceID" value="uuid()" event="xforms-ready"/>
<setvalue ref="/data/meta/appVersion" value="instance('commcaresession')/session/context/appversion" event="xforms-ready"/>
<setvalue ref="/data/meta/drift" value="if(count(instance('commcaresession')/session/context/drift) = 1, instance('commcaresession')/session/context/drift, '')" event="xforms-revalidate"/>
</model>
</h:head>
<h:body>
<trigger ref="/data/welcome_note" appearance="minimal">
<label ref="jr:itext('welcome_note-label')"/>
</trigger>
</h:body>
</h:html>
114 changes: 114 additions & 0 deletions commcare_connect/opportunity/tests/data/demo_app/modules-2/forms-0.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<h:html xmlns:h="http://www.w3.org/1999/xhtml" xmlns:orx="http://openrosa.org/jr/xforms" xmlns="http://www.w3.org/2002/xforms" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:jr="http://openrosa.org/javarosa" xmlns:vellum="http://commcarehq.org/xforms/vellum">
<h:head>
<h:title>Assessment</h:title>
<model>
<instance>
<data xmlns:jrm="http://dev.commcarehq.org/jr/xforms" xmlns="http://openrosa.org/formdesigner/BD70B3D5-6CB4-4A2E-AD5B-C8E3E7BC37A7" uiVersion="1" version="72" name="Assessment">
<did_you_lean_anything/>
<how_many_modules_are_there/>
<score/>
<your_score_is_score/>
<demo>
<assessment xmlns="http://commcareconnect.com/data/v1/learn" id="demo">
<user_score/>
</assessment>
</demo>
<orx:meta xmlns:cc="http://commcarehq.org/xforms">
<orx:deviceID/>
<orx:timeStart/>
<orx:timeEnd/>
<orx:username/>
<orx:userID/>
<orx:instanceID/>
<cc:appVersion/>
<orx:drift/>
</orx:meta>
</data>
</instance>
<instance id="commcaresession" src="jr://instance/session"/>
<bind nodeset="/data/did_you_lean_anything"/>
<bind nodeset="/data/how_many_modules_are_there"/>
<bind nodeset="/data/score" calculate="if(/data/did_you_lean_anything = 'yes', 20, 0) + if(/data/how_many_modules_are_there = 'three', 50, 0)"/>
<bind nodeset="/data/your_score_is_score"/>
<bind nodeset="/data/demo/assessment/user_score" calculate="/data/score"/>
<itext>
<translation lang="en" default="">
<text id="did_you_lean_anything-label">
<value>Did you lean anything?</value>
</text>
<text id="did_you_lean_anything-yes-label">
<value>Yes</value>
</text>
<text id="did_you_lean_anything-no-label">
<value>No</value>
</text>
<text id="how_many_modules_are_there-label">
<value>How many modules are there?</value>
</text>
<text id="how_many_modules_are_there-one-label">
<value>One</value>
</text>
<text id="how_many_modules_are_there-two-label">
<value>Two</value>
</text>
<text id="how_many_modules_are_there-three-label">
<value>Three</value>
</text>
<text id="how_many_modules_are_there-four-label">
<value>Four</value>
</text>
<text id="your_score_is_score-label">
<value>Your score is:
<output value="/data/score"/>
</value>
</text>
</translation>
</itext>
<setvalue ref="/data/meta/deviceID" value="instance('commcaresession')/session/context/deviceid" event="xforms-ready"/>
<setvalue ref="/data/meta/timeStart" value="now()" event="xforms-ready"/>
<bind nodeset="/data/meta/timeStart" type="xsd:dateTime"/>
<setvalue ref="/data/meta/timeEnd" value="now()" event="xforms-revalidate"/>
<bind nodeset="/data/meta/timeEnd" type="xsd:dateTime"/>
<setvalue ref="/data/meta/username" value="instance('commcaresession')/session/context/username" event="xforms-ready"/>
<setvalue ref="/data/meta/userID" value="instance('commcaresession')/session/context/userid" event="xforms-ready"/>
<setvalue ref="/data/meta/instanceID" value="uuid()" event="xforms-ready"/>
<setvalue ref="/data/meta/appVersion" value="instance('commcaresession')/session/context/appversion" event="xforms-ready"/>
<setvalue ref="/data/meta/drift" value="if(count(instance('commcaresession')/session/context/drift) = 1, instance('commcaresession')/session/context/drift, '')" event="xforms-revalidate"/>
</model>
</h:head>
<h:body>
<select ref="/data/did_you_lean_anything">
<label ref="jr:itext('did_you_lean_anything-label')"/>
<item>
<label ref="jr:itext('did_you_lean_anything-yes-label')"/>
<value>yes</value>
</item>
<item>
<label ref="jr:itext('did_you_lean_anything-no-label')"/>
<value>no</value>
</item>
</select>
<select1 ref="/data/how_many_modules_are_there">
<label ref="jr:itext('how_many_modules_are_there-label')"/>
<item>
<label ref="jr:itext('how_many_modules_are_there-one-label')"/>
<value>one</value>
</item>
<item>
<label ref="jr:itext('how_many_modules_are_there-two-label')"/>
<value>two</value>
</item>
<item>
<label ref="jr:itext('how_many_modules_are_there-three-label')"/>
<value>three</value>
</item>
<item>
<label ref="jr:itext('how_many_modules_are_there-four-label')"/>
<value>four</value>
</item>
</select1>
<trigger ref="/data/your_score_is_score" appearance="minimal">
<label ref="jr:itext('your_score_is_score-label')"/>
</trigger>
</h:body>
</h:html>
Loading

0 comments on commit 554bc49

Please sign in to comment.