diff --git a/corehq/apps/app_execution/__init__.py b/corehq/apps/app_execution/__init__.py
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/corehq/apps/app_execution/admin.py b/corehq/apps/app_execution/admin.py
deleted file mode 100644
index 8d1ecb0a0fec..000000000000
--- a/corehq/apps/app_execution/admin.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from django.contrib import admin
-
-from .models import AppWorkflowConfig
-
-
-@admin.register(AppWorkflowConfig)
-class AppWorkflowAdmin(admin.ModelAdmin):
- list_display = ('domain', 'app_id', 'user_id', 'django_user')
diff --git a/corehq/apps/app_execution/api.py b/corehq/apps/app_execution/api.py
deleted file mode 100644
index af0c2b51b73a..000000000000
--- a/corehq/apps/app_execution/api.py
+++ /dev/null
@@ -1,377 +0,0 @@
-import copy
-import dataclasses
-import json
-from enum import Enum
-from functools import cached_property
-from importlib import import_module
-from io import StringIO
-
-import requests
-from django.conf import settings
-from django.contrib.auth import login, logout
-from django.contrib.auth.models import User
-from django.http import HttpRequest
-
-from corehq.apps.app_execution import const
-from corehq.apps.app_execution.data_model import steps, expectations
-from corehq.apps.app_execution.exceptions import AppExecutionError, FormplayerException, ExpectationFailed
-from corehq.apps.app_manager.dbaccessors import get_app
-from corehq.apps.formplayer_api.sync_db import sync_db
-from corehq.apps.formplayer_api.utils import get_formplayer_url
-from corehq.util.hmac_request import get_hmac_digest
-from dimagi.utils.web import get_url_base
-
-
-class BaseFormplayerClient:
- """Client class used to make requests to Formplayer"""
-
- def __init__(self, domain, username, user_id, formplayer_url=None):
- self.domain = domain
- self.username = username
- self.user_id = user_id
- self.formplayer_url = formplayer_url or get_formplayer_url()
-
- def __enter__(self):
- self.session = self._get_requests_session()
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- self.close()
-
- def _get_requests_session(self):
- return requests.Session()
-
- def close(self):
- self.session.close()
- self.session = None
-
- def make_request(self, data, endpoint):
- data_bytes = json.dumps(data).encode('utf-8')
- response_data = self._make_request(endpoint, data_bytes, headers={
- "Content-Type": "application/json",
- "content-length": str(len(data)),
- "X-FORMPLAYER-SESSION": self.user_id,
- })
-
- if response_data.get("exception") or response_data.get("status") == "error":
- raise FormplayerException(response_data.get("exception", "Unknown error"))
- return response_data
-
- def _make_request(self, endpoint, data_bytes, headers):
- raise NotImplementedError()
-
-
-class LocalUserClient(BaseFormplayerClient):
- """Authenticates as a local user to Formplayer.
-
- This fakes a user login in the Django session and uses the session cookie to authenticate with Formplayer."""
-
- @cached_property
- def user(self):
- return User.objects.get(username=self.username)
-
- def _get_requests_session(self):
- session = requests.Session()
-
- engine = import_module(settings.SESSION_ENGINE)
- self.django_session = engine.SessionStore()
-
- # Create a fake request to store login details.
- request = HttpRequest()
- request.session = self.django_session
- login(request, self.user, "django.contrib.auth.backends.ModelBackend")
- # Save the session values.
- request.session.save()
- # Set the cookie to represent the session.
- session_cookie = settings.SESSION_COOKIE_NAME
- session.cookies.set(session_cookie, request.session.session_key)
- return session
-
- def close(self):
- super().close()
- request = HttpRequest()
- request.session = self.django_session
- request.user = self.user
- logout(request)
- self.django_session = None
-
- def _make_request(self, endpoint, data_bytes, headers):
- if 'XSRF-TOKEN' not in self.session.cookies:
- response = self.session.get(f"{self.formplayer_url}/serverup")
- response.raise_for_status()
-
- xsrf_token = self.session.cookies['XSRF-TOKEN']
-
- response = self.session.post(
- url=f"{self.formplayer_url}/{endpoint}",
- data=data_bytes,
- headers={
- "X-XSRF-TOKEN": xsrf_token,
- **headers
- },
- )
- response.raise_for_status()
- return response.json()
-
-
-class UserPasswordClient(LocalUserClient):
- """Authenticates using a username and password.
-
- This client logs in to CommCareHQ and uses the session cookie to authenticate with Formplayer.
- You can use this client with a local or remote CommCareHQ + Formplayer instance.
- """
- def __init__(self, domain, username, user_id, password, commcare_url=None, formplayer_url=None):
- self.password = password
- self.commcare_url = commcare_url or get_url_base()
- super().__init__(domain, username, user_id, formplayer_url)
-
- def _get_requests_session(self):
- session = requests.Session()
- login_url = self.commcare_url + f"/a/{self.domain}/login/"
- session.get(login_url) # csrf
- response = session.post(
- login_url,
- {
- "auth-username": self.username,
- "auth-password": self.password,
- "cloud_care_login_view-current_step": ['auth'], # fake out two_factor ManagementForm
- },
- headers={
- "X-CSRFToken": session.cookies.get('csrftoken'),
- "REFERER": login_url, # csrf requires this for secure requests
- },
- )
- assert (response.status_code == 200)
- return session
-
-
-class HmacAuthClient(BaseFormplayerClient):
- """Authenticates using a shared secret key.
-
- Note: This client does not currently work for case search requests and form submissions."""
-
- def _make_request(self, endpoint, data_bytes, headers):
- response = self.session.post(
- url=f"{self.formplayer_url}/{endpoint}",
- data=data_bytes,
- headers={
- "X-MAC-DIGEST": get_hmac_digest(settings.FORMPLAYER_INTERNAL_AUTH_KEY, data_bytes),
- **headers
- },
- )
- response.raise_for_status()
- return response.json()
-
-
-class ScreenType(str, Enum):
- START = "start"
- MENU = "menu"
- CASE_LIST = "case_list"
- DETAIL = "detail"
- SEARCH = "search"
- SPLIT_SEARCH = "split_search"
- FORM = "form"
-
-
-@dataclasses.dataclass
-class FormplayerSession:
- client: BaseFormplayerClient
- app_id: str
- form_mode: str = const.FORM_MODE_HUMAN
- sync_first: bool = False
- data: dict = None
- _log: StringIO = dataclasses.field(default_factory=StringIO)
-
- def __enter__(self):
- self.client.__enter__()
- return self
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- self.client.__exit__(exc_type, exc_val, exc_tb)
-
- def clone(self):
- return dataclasses.replace(self, data=copy.deepcopy(self.data) if self.data else None)
-
- @cached_property
- def app_build_id(self):
- app = get_app(self.client.domain, self.app_id, latest=True)
- build_on = "Latest Version"
- if app.built_on:
- build_on = app.built_on.strftime("%B %d, %Y")
- self.log(f"Using app '{app.name}' ({app._id} - {app.version} - {build_on})")
- return app._id
-
- def sync(self):
- if not self.sync_first:
- return
- self.log(f"Syncing user data for {self.client.username}")
- sync_db(self.client.domain, self.client.username)
-
- @property
- def current_screen(self):
- return get_screen_type(self.data)
-
- def request_url(self, step):
- screen = self.current_screen
- if screen == ScreenType.START:
- return "navigate_menu_start"
- if screen == ScreenType.FORM:
- return "submit-all" if isinstance(step, steps.SubmitFormStep) else "answer"
- return "navigate_menu"
-
- def get_session_start_data(self):
- return self._get_navigation_data(None)
-
- def get_request_data(self, step):
- if self.current_screen != ScreenType.FORM:
- return self._get_navigation_data(step)
- else:
- return self._get_form_data(step)
-
- def _get_navigation_data(self, step):
- if step:
- assert not step.is_form_step, step
- selections = list(self.data.get("selections", [])) if self.data else []
- data = {
- **self.get_base_data(),
- "app_id": self.app_build_id,
- "locale": "en",
- "cases_per_page": 10,
- "preview": False,
- "offset": 0,
- "selections": selections,
- "query_data": self.data.get("query_data", {}) if self.data else {},
- "search_text": None,
- "sortIndex": None,
- }
- return step.get_request_data(self, data) if step else data
-
- def _get_form_data(self, step):
- assert step.is_form_step, step
- data = {
- **self.get_base_data(),
- "debuggerEnabled": False,
- }
- return step.get_request_data(self, data)
-
- def get_base_data(self):
- return {
- "domain": self.client.domain,
- "restore_as": None,
- "tz_from_browser": "UTC",
- "tz_offset_millis": 0,
- "username": self.client.username,
- }
-
- def start_session(self):
- self.sync()
- data = self.get_session_start_data()
- self.data = self.client.make_request(data, "navigate_menu_start")
- self.log("Starting app session:\n")
-
- def execute_step(self, step):
- if getattr(step, "is_form_step", False) and self.form_mode == const.FORM_MODE_IGNORE:
- self.log_step(step, skipped=True)
- return
- if self.form_mode == const.FORM_MODE_NO_SUBMIT and isinstance(step, steps.SubmitFormStep):
- self.log_step(step, skipped=True)
- return
- if isinstance(step, expectations.Expectation):
- result = step.evaluate(self)
- status = "SUCCESS" if result else "FAILED"
- self.log(f"Expectation {status} for {step}")
- if not result:
- raise ExpectationFailed()
- else:
- data = self.get_request_data(step)
- self.data = self.client.make_request(data, self.request_url(step))
- self.augment_data_from_request(data, ["query_data", "session_id"])
- self.log_step(step)
-
- def augment_data_from_request(self, request_data, fields):
- """Some fields aren't returned by Formplayer, so we need to maintain their value across requests"""
- for field in fields:
- if request_data.get(field) and not self.data.get(field):
- self.data[field] = request_data[field]
-
- def log_step(self, step, indent=" ", skipped=False):
- skipped_log = " (ignored)" if skipped else ""
- self.log(f"Execute step: {step or 'START'} {skipped_log}")
- if skipped:
- return
- double_indent = indent * 2
- screen = self.current_screen
- self.log(f"{indent}New Screen: {screen}")
- if self.data:
- if screen == ScreenType.START:
- self.log("")
- elif screen == ScreenType.MENU:
- for command in self.data["commands"]:
- self.log(f"{double_indent}Command: {command['displayText']}")
- elif screen == ScreenType.CASE_LIST:
- for row in self.data["entities"]:
- self.log(f"{double_indent}Case: {row['id']}")
- elif screen == ScreenType.SEARCH:
- for display in self.data["displays"]:
- self.log(f"{double_indent}Search field: {display['text']}")
- elif screen == ScreenType.SPLIT_SEARCH:
- for display in self.data["queryResponse"]["displays"]:
- self.log(f"{double_indent}Search field: {display['text']}")
- for row in self.data["entities"]:
- self.log(f"{double_indent}Case: {row['id']}")
- elif screen == ScreenType.FORM:
- for item in self.data["tree"]:
- if item["type"] == "question":
- answer = item.get('answer', None) or '""'
- self.log(f"{double_indent}Question: {item['caption']}={answer}")
-
- def get_logs(self):
- return self._log.getvalue()
-
- def log(self, message):
- print(message, file=self._log)
-
-
-def execute_workflow(session: FormplayerSession, workflow):
- with session:
- session.start_session()
- try:
- for step in workflow.steps:
- execute_step(session, step)
- except ExpectationFailed:
- return False
- return True
-
-
-def execute_step(session, step):
- if children := step.get_children():
- for child in children:
- session.execute_step(child)
- else:
- session.execute_step(step)
-
-
-def get_screen_type(current_data):
- if not current_data:
- return ScreenType.START
-
- type_ = current_data.get("type")
- if type_ == "commands":
- return ScreenType.MENU
- if type_ == "entities":
- if current_data.get("queryResponse"):
- return ScreenType.SPLIT_SEARCH
- return ScreenType.CASE_LIST
- if type_ == "query":
- return ScreenType.SEARCH
- data = current_data.get("details")
- if data:
- return ScreenType.DETAIL
- data = current_data.get("tree")
- if data:
- return ScreenType.FORM
- if current_data.get("submitResponseMessage"):
- return get_screen_type(current_data["nextScreen"])
-
- if current_data.get("errors"):
- raise AppExecutionError(current_data["errors"])
- raise AppExecutionError(f"Unknown screen type: {current_data}")
diff --git a/corehq/apps/app_execution/apps.py b/corehq/apps/app_execution/apps.py
deleted file mode 100644
index 535cd42c7b8c..000000000000
--- a/corehq/apps/app_execution/apps.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from django.apps import AppConfig
-
-
-class AppExecutionConfig(AppConfig):
- name = 'corehq.apps.app_execution'
- default_auto_field = "django.db.models.BigAutoField"
diff --git a/corehq/apps/app_execution/const.py b/corehq/apps/app_execution/const.py
deleted file mode 100644
index 6570e78a811f..000000000000
--- a/corehq/apps/app_execution/const.py
+++ /dev/null
@@ -1,3 +0,0 @@
-FORM_MODE_HUMAN = "human"
-FORM_MODE_NO_SUBMIT = "no_submit"
-FORM_MODE_IGNORE = "ignore"
diff --git a/corehq/apps/app_execution/data_model/__init__.py b/corehq/apps/app_execution/data_model/__init__.py
deleted file mode 100644
index b4204ac3018a..000000000000
--- a/corehq/apps/app_execution/data_model/__init__.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from .base import AppWorkflow
-from . import steps
-from . import expectations # noqa
-
-EXAMPLE_WORKFLOW = AppWorkflow(steps=[
- steps.CommandStep(value="My Module"),
- steps.EntitySelectStep(value="clinic_123"),
- steps.QueryStep(inputs={"name": "John Doe"}),
- steps.EntitySelectIndexStep(value=0),
- steps.FormStep(children=[
- steps.AnswerQuestionStep(question_text="Name", value="John Doe"),
- steps.AnswerQuestionStep(question_text="Age", value="30"),
- steps.SubmitFormStep(),
- ]),
-])
diff --git a/corehq/apps/app_execution/data_model/base.py b/corehq/apps/app_execution/data_model/base.py
deleted file mode 100644
index f903e2cf7587..000000000000
--- a/corehq/apps/app_execution/data_model/base.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from __future__ import annotations
-
-import dataclasses
-
-from attr import define
-
-from . import steps as step_models # noqa
-from . import expectations
-
-
-@define
-class AppWorkflow:
- steps: list[step_models.Step | expectations.Expectation] = dataclasses.field(default_factory=list)
-
- def __jsonattrs_to_json__(self):
- return {
- "steps": [step.to_json() for step in self.steps]
- }
-
- @classmethod
- def __jsonattrs_from_json__(cls, data):
- return cls(steps=steps_from_json(data["steps"]))
-
- def __str__(self):
- return " -> ".join(str(step) for step in self.steps)
-
-
-def steps_from_json(json_steps):
- steps = []
- for raw_step in json_steps:
- if raw_step["type"].startswith("expect:"):
- steps.append(expectations.expectation_from_json(raw_step))
- else:
- steps.extend(step_models.steps_from_json([raw_step]))
- return steps
diff --git a/corehq/apps/app_execution/data_model/expectations.py b/corehq/apps/app_execution/data_model/expectations.py
deleted file mode 100644
index 401825456ec6..000000000000
--- a/corehq/apps/app_execution/data_model/expectations.py
+++ /dev/null
@@ -1,208 +0,0 @@
-import copy
-import logging
-import re
-from typing import ClassVar
-
-from attr import asdict, define
-
-from corehq.apps.app_execution.exceptions import AppExecutionError
-from submodules.xml2json import xml2json
-from jsonpath_ng.ext import parse as jsonpath_parse
-
-log = logging.getLogger(__name__)
-
-
-@define
-class Expectation:
- type: ClassVar[str]
-
- def get_children(self):
- """Compatibility with Step.get_children()"""
- return []
-
- def evaluate(self, session):
- try:
- return self._evaluate(session)
- except Exception as e:
- log.exception(f"Error evaluating expectation {self}")
- raise AppExecutionError(f"Error evaluating expectation {self}") from e
-
- def _evaluate(self, session):
- raise NotImplementedError()
-
- def to_json(self):
- return {"type": f"expect:{self.type}", **asdict(self)}
-
- @classmethod
- def from_json(cls, data):
- return cls(**data)
-
-
-@define
-class XpathExpectation(Expectation):
- type: ClassVar[str] = "xpath"
- xpath: str
-
- def _evaluate(self, session):
- return evaluate_xpath(session, self.xpath) == "true"
-
- def __str__(self):
- return f"{self.type}({self.xpath})"
-
-
-@define
-class CasePresent(Expectation):
- type: ClassVar[str] = "case_present"
- xpath_filter: str
-
- def _evaluate(self, session):
- xpath = f"count(instance('casedb')/casedb/case[{self.xpath_filter}]) > 0"
- return evaluate_xpath(session, xpath) == "true"
-
- def __str__(self):
- return f"{self.type}({self.xpath_filter})"
-
-
-@define
-class CaseAbsent(Expectation):
- type: ClassVar[str] = "case_absent"
- xpath_filter: str
-
- def _evaluate(self, session):
- xpath = f"count(instance('casedb')/casedb/case[{self.xpath_filter}]) = 0"
- return evaluate_xpath(session, xpath) == "true"
-
- def __str__(self):
- return f"{self.type}({self.xpath_filter})"
-
-
-@define
-class QuestionValue(Expectation):
- type: ClassVar[str] = "question_value"
- question_path: str
- value: str
-
- def _evaluate(self, session):
- from corehq.apps.app_execution.api import ScreenType
- if not session.current_screen == ScreenType.FORM:
- session.log("QuestionValue expectation only works in form screens")
- return False
-
- result, found = get_question_value_from_tree(self.question_path, session.data.get("tree", []))
- if not found:
- result, found = get_question_value_from_xml(session, self.question_path)
-
- if found:
- return result == self.value
-
- session.log(f"Question {self.question_path} not found")
- return False
-
- def __str__(self):
- return f"{self.type}({self.question_path} = {self.value})"
-
-
-def get_question_value_from_tree(question_id, tree):
- for node in tree:
- if node.get("binding") == question_id:
- return node.get("answer"), True
- if node.get("children"):
- result, found = get_question_value_from_tree(question_id, node.get("children"))
- if found:
- return result, True
- return None, False
-
-
-def get_question_value_from_xml(session, question_id):
- xml = session.data.get("instanceXml", {}).get("output")
- if not xml:
- session.log("No form instance XML found")
- return None, False
-
- try:
- name, json_response = xml2json.xml2json(xml.encode())
- except xml2json.XMLSyntaxError:
- session.log("Unable to parse form instance XML")
- return None, False
- return _get_question_value_from_json(session, question_id, {name: json_response})
-
-
-def _get_question_value_from_json(session, question_id, json_response):
- expr = question_id.replace("/", ".")
- expr = _convert_to_zero_index(expr)
- try:
- jsonpath_expr = jsonpath_parse(f"$.{expr}")
- except Exception:
- session.log(f"Unable to parse the question path {question_id}")
- return None, False
-
- values = [match.value for match in jsonpath_expr.find(json_response)]
- if values:
- return values[0], True
-
- return None, False
-
-
-def _convert_to_zero_index(expr):
- """Xpath is 1-indexed but jsonpath is 0-indexed"""
- for index in re.findall(r"\[(\d+)]", expr):
- expr = expr.replace(f"[{index}]", f"[{int(index) - 1}]")
- return expr
-
-
-def evaluate_xpath(session, xpath):
- data = _get_data(session, xpath)
- url = _get_url(session.current_screen)
- data = session.client.make_request(data, url)
- return _get_result(session, data)
-
-
-def _get_data(session, xpath):
- from corehq.apps.app_execution.api import ScreenType
- data = {
- **session.get_base_data(),
- "app_id": session.app_build_id,
- "selections": session.data.get("selections", []),
- "query_data": session.data.get("query_data", {}),
- "xpath": xpath,
- "debugOutput": "basic"
- }
- if session.current_screen == ScreenType.FORM:
- data["session_id"] = session.data["session_id"]
- return data
-
-
-def _get_result(session, response):
- output = response.get("output")
- if response.get("status") == "validation-error":
- session.log(f"Xpath validation error: {output}")
- return
-
- if not output:
- return
-
- try:
- _, json_response = xml2json.xml2json(output.encode())
- except xml2json.XMLSyntaxError:
- session.log(f"Unable to parse result {output}")
- return
-
- return json_response
-
-
-def _get_url(screen):
- from corehq.apps.app_execution.api import ScreenType
- if screen == ScreenType.FORM:
- return "evaluate-xpath"
- return "evaluate-menu-xpath"
-
-
-TYPE_MAP = {step.type: step for step in Expectation.__subclasses__()}
-
-
-def expectation_from_json(raw_expectation):
- raw_expectation = copy.deepcopy(raw_expectation)
- type_ = raw_expectation.pop("type").replace("expect:", "")
- if type_ not in TYPE_MAP:
- raise ValueError(f"Unknown expectation type {type_}")
- return TYPE_MAP[type_].from_json(raw_expectation)
diff --git a/corehq/apps/app_execution/data_model/steps.py b/corehq/apps/app_execution/data_model/steps.py
deleted file mode 100644
index c14e738f6dc3..000000000000
--- a/corehq/apps/app_execution/data_model/steps.py
+++ /dev/null
@@ -1,346 +0,0 @@
-from __future__ import annotations
-
-import copy
-from typing import Any, ClassVar
-
-from attr import Factory, define
-from attrs import asdict
-
-from ..exceptions import AppExecutionError
-
-
-@define
-class Step:
- type: ClassVar[str]
- is_form_step: ClassVar[bool]
-
- def get_request_data(self, session, data):
- return data
-
- def get_children(self):
- return []
-
- def to_json(self):
- return {"type": self.type, **asdict(self)}
-
- @classmethod
- def from_json(cls, data):
- return cls(**data)
-
-
-@define
-class CommandStep(Step):
- type: ClassVar[str] = "command"
- is_form_step: ClassVar[bool] = False
-
- value: str = ""
- """Display text of the command to execute"""
-
- def get_request_data(self, session, data):
- commands = {c["displayText"].lower(): c for c in session.data.get("commands", [])}
-
- try:
- command = commands[self.value.lower()]
- except KeyError:
- raise AppExecutionError(f"Command not found: {self.value}: {commands.keys()}")
- return _append_selection(data, command["index"])
-
-
-@define
-class CommandIdStep(Step):
- type: ClassVar[str] = "command_id"
- is_form_step: ClassVar[bool] = False
-
- value: str = ""
- """ID of the command to execute"""
-
- def get_request_data(self, session, data):
- return _append_selection(data, self.value)
-
-
-@define
-class EntitySelectStep(Step):
- type: ClassVar[str] = "entity_select"
- is_form_step: ClassVar[bool] = False
-
- value: str
- """ID of the entity to select."""
-
- def get_request_data(self, session, data):
- _validate_entity_ids(session, [self.value])
- return _append_selection(data, self.value)
-
- def __str__(self):
- return f"Entity Select: {self.value}"
-
-
-@define
-class MultipleEntitySelectStep(Step):
- type: ClassVar[str] = "multiple_entity_select"
- is_form_step: ClassVar[bool] = False
-
- values: list[str]
-
- def get_request_data(self, session, data):
- _validate_entity_ids(session, self.values)
- data = _append_selection(data, "use_selected_values")
- data["selectedValues"] = self.values
- return data
-
-
-def _validate_entity_ids(session, entity_ids):
- entities = {entity["id"] for entity in session.data.get("entities", [])}
- if not entities:
- raise AppExecutionError("No entities found")
- missing = set(entity_ids) - entities
- if missing:
- raise AppExecutionError(f"Entities not found: {missing}: {list(entities)}")
-
-
-@define
-class EntitySelectIndexStep(Step):
- type: ClassVar[str] = "entity_select_index"
- is_form_step: ClassVar[bool] = False
-
- value: int
- """Zero-based index of the entity to select."""
-
- def get_request_data(self, session, data):
- selected = _select_entities_by_index(session, [self.value])
- return _append_selection(data, selected[0])
-
- def __str__(self):
- return f"Entity Select: {self.value}"
-
-
-@define
-class MultipleEntitySelectByIndexStep(Step):
- type: ClassVar[str] = "multiple_entity_select_by_index"
- is_form_step: ClassVar[bool] = False
-
- values: list[int]
-
- def get_request_data(self, session, data):
- selected = _select_entities_by_index(session, self.values)
- data = _append_selection(data, "use_selected_values")
- data["selectedValues"] = selected
- return data
-
-
-def _select_entities_by_index(session, indexes):
- entities = [entity["id"] for entity in session.data.get("entities", [])]
- if not entities:
- raise AppExecutionError("No entities found")
- if max(indexes) >= len(entities):
- raise AppExecutionError(f"Entity index out of range: {max(indexes)}: {list(entities)}")
- return [entities[index] for index in indexes]
-
-
-@define
-class QueryInputValidationStep(Step):
- type: ClassVar[str] = "query_input_validation"
- is_form_step: ClassVar[bool] = False
-
- inputs: dict
- """Search inputs dict. Keys are field names and values are search values."""
-
- def get_request_data(self, session, data):
- query_key = session.data["queryKey"]
- return {
- **data,
- # DataDog tag value
- "requestInitiatedByTag": "field_change",
- "query_data": {
- query_key: {
- "execute": False,
- "force_manual_search": True,
- "inputs": self.inputs,
- }
- },
- }
-
-
-@define
-class QueryStep(Step):
- type: ClassVar[str] = "query"
- is_form_step: ClassVar[bool] = False
-
- inputs: dict
- """Search inputs dict. Keys are field names and values are search values."""
-
- validate_inputs: bool = False
- """Simulate updating search inputs on the UI. One request per update."""
-
- def get_children(self):
- children = []
- if self.validate_inputs:
- # children will be executed instead of the parent
- inputs = {}
- for field, value in self.inputs.items():
- inputs[field] = value
- children.append(QueryInputValidationStep(inputs=inputs.copy()))
- children.append(QueryStep(inputs=self.inputs)) # execute query
- return children
-
- def get_request_data(self, session, data):
- query_key = session.data["queryKey"]
- return {
- **data,
- "query_data": {
- query_key: {
- "execute": True,
- "inputs": self.inputs,
- }
- },
- }
-
- def __str__(self):
- return f"Query: {self.inputs}"
-
-
-@define
-class ClearQueryStep(Step):
- """This is implemented in the suite file with `redo_last="true"` and appears on the UI
- as "Search Again"
- """
- type: ClassVar[str] = "clear_query"
- is_form_step: ClassVar[bool] = False
-
- def get_request_data(self, session, data):
- query_key = session.data["queryKey"]
- return {
- **data,
- "query_data": {
- query_key: {
- "inputs": None,
- "execute": False,
- "force_manual_search": True,
- }
- },
- }
-
- def __str__(self):
- return "ClearQuery"
-
-
-@define
-class AnswerQuestionStep(Step):
- type: ClassVar[str] = "answer_question"
- is_form_step: ClassVar[bool] = True
- question_text: str
- value: str
-
- @classmethod
- def from_json(cls, data):
- data.pop("question_id", None)
- return cls(**data)
-
- def get_request_data(self, session, data):
- return {
- **data,
- **_get_answer_data(session, "caption", self.question_text, self.value)
- }
-
- def __str__(self):
- return f"Answer Question: {self.question_text} = {self.value}"
-
-
-@define
-class AnswerQuestionIdStep(Step):
- type: ClassVar[str] = "answer_question_id"
- is_form_step: ClassVar[bool] = True
- question_id: str
- value: str
-
- def get_request_data(self, session, data):
- return {
- **data,
- **_get_answer_data(session, "question_id", self.question_id, self.value)
- }
-
- def __str__(self):
- return f"Answer Question: {self.question_id} = {self.value}"
-
-
-def _get_answer_data(session, node_field_name, node_value, answer):
- try:
- question = [
- node for node in session.data["tree"]
- if node[node_field_name] == node_value
- ][0]
- except IndexError:
- raise AppExecutionError(f"Question not found: {node_value}")
-
- return {
- "action": "answer",
- "answersToValidate": {},
- "answer": answer,
- "ix": question["ix"],
- "session_id": session.data["session_id"]
- }
-
-
-@define
-class SubmitFormStep(Step):
- type: ClassVar[str] = "submit_form"
- is_form_step: ClassVar[bool] = True
-
- def get_request_data(self, session, data):
- answers = {
- node["ix"]: node["answer"]
- for node in session.data["tree"]
- if "answer" in node
- }
- return {
- **data,
- "action": "submit-all",
- "prevalidated": True,
- "answers": answers,
- "session_id": session.data["session_id"]
- }
-
-
-@define
-class FormStep(Step):
- type: ClassVar[str] = "form"
- is_form_step: ClassVar[bool] = False # the children are all form steps but this one isn't
- children: list[Any] = Factory(list)
-
- def to_json(self):
- return {
- "type": self.type,
- "children": [child.to_json() for child in self.children]
- }
-
- def get_children(self):
- return self.children
-
- @classmethod
- def from_json(cls, data):
- from .base import steps_from_json
- return cls(children=steps_from_json(data["children"]))
-
-
-@define
-class RawNavigationStep(Step):
- type: ClassVar[str] = "raw_navigation"
- is_form_step: ClassVar[bool] = False
-
- request_data: dict
-
- def get_request_data(self, session, data):
- return self.request_data
-
-
-def _append_selection(data, selection):
- selections = data.get("selections", [])
- selections.append(selection)
- return {**data, "selections": selections}
-
-
-STEP_MAP = {step.type: step for step in Step.__subclasses__()}
-
-
-def steps_from_json(raw_steps):
- raw_steps = copy.deepcopy(raw_steps)
- return [STEP_MAP[child.pop("type")].from_json(child) for child in raw_steps]
diff --git a/corehq/apps/app_execution/db_accessors.py b/corehq/apps/app_execution/db_accessors.py
deleted file mode 100644
index b23e281868ee..000000000000
--- a/corehq/apps/app_execution/db_accessors.py
+++ /dev/null
@@ -1,95 +0,0 @@
-from collections import defaultdict
-from datetime import timedelta
-
-from django.db.models import Avg, Count, DateTimeField, DurationField, ExpressionWrapper, F, Max
-from django.db.models.functions import Trunc
-
-from corehq.apps.app_execution.models import AppExecutionLog, AppWorkflowConfig
-
-
-def get_avg_duration_data(domain, start, end, workflow_id=None):
- query = AppExecutionLog.objects.filter(workflow__domain=domain, started__gte=start, started__lt=end)
- if workflow_id:
- query = query.filter(workflow_id=workflow_id)
-
- chart_logs = (
- query.annotate(
- date=Trunc("started", "hour", output_field=DateTimeField()),
- duration=ExpressionWrapper(F("completed") - F("started"), output_field=DurationField()),
- ).values("date", "workflow_id")
- .annotate(avg_duration=Avg('duration'))
- .annotate(max_duration=Max('duration'))
- )
-
- data = defaultdict(list)
- seen_dates = defaultdict(set)
- for row in chart_logs:
- data[row["workflow_id"]].append({
- "date": row["date"].isoformat(),
- "avg_duration": row["avg_duration"].total_seconds(),
- "max_duration": row["max_duration"].total_seconds(),
- })
- seen_dates[row["workflow_id"]].add(row["date"])
-
- start = start.replace(minute=0, second=0, microsecond=0)
- current = start
- while current < end:
- for workflow_id, dates in seen_dates.items():
- if current not in dates:
- data[workflow_id].append({"date": current.isoformat(), "avg_duration": None, "max_duration": None})
- current += timedelta(hours=1)
-
- workflow_names = {
- workflow["id"]: workflow["name"]
- for workflow in AppWorkflowConfig.objects.filter(id__in=list(data)).values("id", "name")
- }
- return [
- {
- "key": workflow_id,
- "label": workflow_names[workflow_id],
- "values": sorted(data, key=lambda x: x["date"])
- }
- for workflow_id, data in data.items()
- ]
-
-
-def get_status_data(domain, start, end, workflow_id=None):
- query = AppExecutionLog.objects.filter(workflow__domain=domain, started__gte=start, started__lt=end)
- if workflow_id:
- query = query.filter(workflow_id=workflow_id)
-
- chart_logs = (
- query.annotate(date=Trunc("started", "hour", output_field=DateTimeField()))
- .values("date", "success")
- .annotate(count=Count("success"))
- )
-
- success = []
- error = []
- seen_success_dates = set()
- seen_error_dates = set()
- for row in chart_logs:
- item = {
- "date": row["date"].isoformat(),
- "count": row["count"],
- }
- if row["success"]:
- success.append(item)
- seen_success_dates.add(row["date"])
- else:
- error.append(item)
- seen_error_dates.add(row["date"])
-
- start = start.replace(minute=0, second=0, microsecond=0)
- current = start
- while current < end:
- if current not in seen_error_dates:
- error.append({"date": current.isoformat(), "count": 0})
- if current not in seen_success_dates:
- success.append({"date": current.isoformat(), "count": 0})
- current += timedelta(hours=1)
-
- return [
- {"key": "Success", "values": sorted(success, key=lambda x: x["date"])},
- {"key": "Error", "values": sorted(error, key=lambda x: x["date"])},
- ]
diff --git a/corehq/apps/app_execution/exceptions.py b/corehq/apps/app_execution/exceptions.py
deleted file mode 100644
index b2114ae0da0e..000000000000
--- a/corehq/apps/app_execution/exceptions.py
+++ /dev/null
@@ -1,10 +0,0 @@
-class AppExecutionError(Exception):
- pass
-
-
-class FormplayerException(Exception):
- pass
-
-
-class ExpectationFailed(Exception):
- """Special exception to signal that the execution should stop immediately."""
diff --git a/corehq/apps/app_execution/forms.py b/corehq/apps/app_execution/forms.py
deleted file mode 100644
index 7e6e40f0d1db..000000000000
--- a/corehq/apps/app_execution/forms.py
+++ /dev/null
@@ -1,141 +0,0 @@
-from couchdbkit import NoResultFound, ResourceNotFound
-from crispy_forms import bootstrap as twbscrispy
-from crispy_forms import layout as crispy
-from crispy_forms.bootstrap import InlineField
-from django import forms
-from django.utils.translation import gettext as _
-
-from corehq.apps.app_execution.exceptions import AppExecutionError
-from corehq.apps.app_execution.models import AppWorkflowConfig
-from corehq.apps.app_execution.workflow_dsl import dsl_to_workflow, workflow_to_dsl
-from corehq.apps.app_manager.dbaccessors import get_brief_app
-from corehq.apps.hqwebapp import crispy as hqcrispy
-from corehq.apps.users.models import CommCareUser
-from corehq.apps.users.util import generate_mobile_username
-
-
-class AppWorkflowConfigForm(forms.ModelForm):
- run_every = forms.IntegerField(min_value=1, required=False, label=_("Run Every (minutes)"))
- username = forms.CharField(max_length=255, label=_("Username"),
- help_text=_("Username of the user to run the workflow"))
- har_file = forms.FileField(label=_("HAR File"), required=False)
- workflow_dsl = forms.CharField(
- label="Workflow",
- required=False,
- widget=forms.Textarea(attrs={"rows": 20, "class": "textarea form-control"})
- )
-
- class Meta:
- model = AppWorkflowConfig
- fields = (
- "name",
- "app_id",
- "workflow",
- "sync_before_run",
- "form_mode",
- "run_every",
- "notification_emails"
- )
- widgets = {
- "form_mode": forms.RadioSelect(),
- "workflow": forms.Textarea(attrs={"rows": 20}),
- }
-
- def __init__(self, request, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.request = request
- self.is_raw_mode = request.GET.get("mode", "dsl") == "raw"
- if self.is_raw_mode:
- del self.fields["workflow_dsl"]
- else:
- del self.fields["workflow"]
- workflow = self.instance.workflow
- if not workflow:
- workflow = self.initial.get("workflow")
- self.fields["workflow_dsl"].initial = workflow_to_dsl(workflow)
-
- if self.instance.id:
- self.fields["username"].initial = self.instance.django_user.username
- self.helper = hqcrispy.HQFormHelper()
-
- fields = [
- "name",
- "app_id",
- "username",
- "sync_before_run",
- "form_mode",
- ]
- if request.user.is_superuser:
- fields += ["run_every", "notification_emails"]
-
- har_help = _("HAR file recording should start with the selection of the app (navigate_menu_start).")
- self.helper.layout = crispy.Layout(
- crispy.Div(
- crispy.Div(
- *fields,
- css_class="col",
- ),
- crispy.Div(
- crispy.HTML(f"
{har_help}
"),
- "har_file",
- twbscrispy.StrictButton(
- _("Populate workflow from HAR file"),
- type='submit', css_class='btn-secondary', name="import_har", value="1",
- formnovalidate=True,
- ),
- crispy.HTML("
"),
- crispy.HTML(f"{_('Workflow:')}
"),
- InlineField("workflow") if self.is_raw_mode else InlineField("workflow_dsl"),
- css_class="col"
- ),
- css_class="row mb-3"
- ),
- hqcrispy.FormActions(
- twbscrispy.StrictButton(_("Save"), type='submit', css_class='btn-primary')
- ),
- )
-
- def clean_workflow_dsl(self):
- if "workflow_dsl" in self.cleaned_data:
- dsl = self.cleaned_data["workflow_dsl"]
- try:
- dsl_to_workflow(dsl)
- except AppExecutionError as e:
- raise forms.ValidationError(str(e))
- return dsl
-
- def clean_username(self):
- domain = self.request.domain
- username = generate_mobile_username(self.cleaned_data.get("username"), domain, is_unique=False)
- try:
- self.commcare_user = CommCareUser.get_by_username(username)
- except ResourceNotFound:
- raise forms.ValidationError(_("User not found"))
-
- if not self.commcare_user or self.commcare_user.domain != domain:
- raise forms.ValidationError(_("User not found"))
-
- return username
-
- def clean_app_id(self):
- domain = self.request.domain
- app_id = self.cleaned_data.get("app_id")
- try:
- get_brief_app(domain, app_id)
- except NoResultFound:
- raise forms.ValidationError(_("App not found in domain: {domain}:{app_id}").format(
- domain=domain, app_id=app_id
- ))
-
- return app_id
-
- def clean(self):
- if "workflow_dsl" in self.cleaned_data:
- workflow = dsl_to_workflow(self.cleaned_data["workflow_dsl"])
- self.cleaned_data["workflow"] = workflow
- return self.cleaned_data
-
- def save(self, commit=True):
- self.instance.domain = self.request.domain
- self.instance.django_user = self.commcare_user.get_django_user()
- return super().save(commit=commit)
diff --git a/corehq/apps/app_execution/har_parser.py b/corehq/apps/app_execution/har_parser.py
deleted file mode 100644
index 97409fab065d..000000000000
--- a/corehq/apps/app_execution/har_parser.py
+++ /dev/null
@@ -1,170 +0,0 @@
-import json
-
-from corehq.apps.app_execution.data_model import AppWorkflow, steps
-from corehq.apps.app_execution.api import ScreenType, get_screen_type
-from corehq.apps.app_execution.models import AppWorkflowConfig
-
-NON_FORM_ENDPOINTS = {"navigate_menu_start", "navigate_menu", "get_details"}
-FORM_ENDPOINTS = {"answer", "submit-all"}
-ENDPOINTS = NON_FORM_ENDPOINTS | FORM_ENDPOINTS
-
-
-def parse_har_from_string(har_string):
- return HarParser().parse(json.loads(har_string))
-
-
-class HarParser:
- def __init__(self):
- self.domain = None
- self.app_id = None
- self.steps = []
- self.current_screen = None
- self.screen_data = None
- self.form_step = None
-
- def parse(self, har_data):
- """
- Extracts a workflow from a HAR file and returns it as a dictionary.
- """
- for endpoint, entry in get_formplayer_entries(har_data['log']['entries']):
- if not self.current_screen and endpoint != 'navigate_menu_start':
- # Skip until we get the first navigate_menu_start
- continue
-
- request_data = json.loads(entry['request']['postData']['text'])
- response_data = json.loads(entry['response']['content']['text'])
-
- if endpoint in FORM_ENDPOINTS:
- self.set_form_step()
- else:
- self.clear_form_step()
-
- if endpoint == 'navigate_menu_start' or self.current_screen == ScreenType.START:
- if self.current_screen:
- assert self.domain == request_data["domain"]
- assert self.app_id == request_data["app_id"]
- else:
- self.current_screen = ScreenType.START
- self.domain = request_data["domain"]
- self.app_id = request_data["app_id"]
-
- elif endpoint == 'navigate_menu':
- step = self._extract_navigation_step(request_data)
- if step:
- self.steps.append(step)
- elif endpoint == 'answer':
- self.form_step.children.append(self._extract_form_answer_step(request_data))
- elif endpoint == 'submit-all':
- self.form_step.children.append(steps.SubmitFormStep())
- elif endpoint == 'get_details':
- # skip over detail screens and don't update current screen and screen data
- continue
-
- self.current_screen = get_screen_type(response_data)
- self.screen_data = response_data
-
- return AppWorkflowConfig(
- domain=self.domain, app_id=self.app_id, workflow=AppWorkflow(steps=self.steps)
- )
-
- def _extract_navigation_step(self, request_data):
- selections_changed = request_data.get("selections") != self.screen_data.get("selections")
- if self.current_screen == ScreenType.MENU:
- last_selection = _get_last_selection(request_data, int)
- command = self.screen_data["commands"][last_selection]["displayText"]
- return steps.CommandStep(value=command)
- elif self.current_screen == ScreenType.CASE_LIST:
- if selections_changed:
- return self._get_entity_select_step(request_data)
- elif self._get_query_data(request_data).get("inputs") is None:
- return steps.ClearQueryStep()
- elif self.current_screen == ScreenType.SEARCH:
- return self._get_search_step(request_data)
- elif self.current_screen == ScreenType.SPLIT_SEARCH:
- if not selections_changed:
- return self._get_search_step(request_data)
- else:
- return self._get_entity_select_step(request_data)
-
- elif self.current_screen == ScreenType.DETAIL:
- pass
- else:
- raise Exception(f"Unexpected screen type: {self.current_screen}")
-
- def _get_entity_select_step(self, request_data):
- last_selection = _get_last_selection(request_data)
- if is_action(last_selection):
- return steps.CommandIdStep(value=last_selection)
- elif is_multi_select(last_selection):
- return steps.MultipleEntitySelectStep(values=request_data["selectedValues"])
- else:
- return steps.EntitySelectStep(value=last_selection)
-
- def _get_search_step(self, request_data):
- query_data = self._get_query_data(request_data)
- if query_data and query_data["execute"]:
- return steps.QueryStep(inputs=query_data["inputs"])
- else:
- return steps.QueryInputValidationStep(inputs=query_data["inputs"])
-
- def _get_query_data(self, request_data):
- query_key = self.screen_data["queryKey"]
- query_data = request_data["query_data"].get(query_key)
- return query_data
-
- def _extract_form_answer_step(self, request_data):
- tree = self.screen_data["tree"]
- tree_item = [question for question in tree if question["ix"] == request_data["ix"]][0]
- step = steps.AnswerQuestionIdStep(
- question_id=tree_item["question_id"],
- value=request_data["answer"])
- return step
-
- def set_form_step(self):
- if not self.form_step:
- self.form_step = steps.FormStep(children=[])
- self.steps.append(self.form_step)
-
- def clear_form_step(self):
- self.form_step = None
-
-
-def _get_last_selection(data, cast=None):
- selections = data["selections"]
- if not selections:
- return None
- selection = selections[-1]
- return cast(selection) if cast else selection
-
-
-def is_action(selection):
- return selection.startswith("action")
-
-
-def is_multi_select(selection):
- return selection == "use_selected_values"
-
-
-def get_formplayer_entries(entries):
- base_url = None
- for entry in entries:
- request = entry['request']
- if request['method'] != 'POST':
- continue
-
- url = request['url']
- if not base_url:
- base_url = _get_formplayer_base_url(url)
-
- if not base_url:
- continue
-
- endpoint = url.removeprefix(base_url)
- yield endpoint, entry
-
-
-def _get_formplayer_base_url(url):
- endpoint = url.split("/")[-1]
- if endpoint not in ENDPOINTS:
- return None
- return "/".join(url.split("/")[:-1]) + "/"
diff --git a/corehq/apps/app_execution/management/__init__.py b/corehq/apps/app_execution/management/__init__.py
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/corehq/apps/app_execution/management/commands/__init__.py b/corehq/apps/app_execution/management/commands/__init__.py
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/corehq/apps/app_execution/management/commands/clean_har_file.py b/corehq/apps/app_execution/management/commands/clean_har_file.py
deleted file mode 100644
index 9e02265609ed..000000000000
--- a/corehq/apps/app_execution/management/commands/clean_har_file.py
+++ /dev/null
@@ -1,54 +0,0 @@
-import json
-import pathlib
-
-from django.core.management import BaseCommand
-
-from corehq.apps.app_execution.har_parser import get_formplayer_entries
-
-
-class Command(BaseCommand):
- help = """
- Filter a HAR file for entries relevant to Formplayer and remove unnecessary data.
- This is mostly used for reducing HAR file size for testing purposes.
- """
-
- def add_arguments(self, parser):
- parser.add_argument("har_file", type=pathlib.Path)
- parser.add_argument("-o", "--output", type=pathlib.Path,
- help="Output file path. If not provided, will print to stdout.")
-
- def handle(self, har_file, output=None, *args, **options):
- with har_file.open() as f:
- har_data = json.load(f)
-
- formplayer_entries = get_formplayer_entries(har_data["log"]["entries"])
- cleaned_har = {
- "log": {
- "entries": [_clean_entry(entry[1]) for entry in formplayer_entries]
-
- }
- }
- har_dump = json.dumps(cleaned_har, indent=2)
- if output:
- with output.open("w") as f:
- f.write(har_dump)
- else:
- print(har_dump)
-
-
-def _clean_entry(entry):
- request = entry["request"]
- return {
- "request": {
- "method": request["method"],
- "url": request["url"],
- "postData": {
- "text": request["postData"]["text"]
- }
- },
- "response": {
- "content": {
- "text": entry["response"]["content"]["text"]
- }
- }
- }
diff --git a/corehq/apps/app_execution/migrations/0001_initial.py b/corehq/apps/app_execution/migrations/0001_initial.py
deleted file mode 100644
index 040525ca7c73..000000000000
--- a/corehq/apps/app_execution/migrations/0001_initial.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# Generated by Django 3.2.25 on 2024-04-23 13:02
-
-from django.conf import settings
-import django.contrib.postgres.fields
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ]
-
- operations = [
- migrations.CreateModel(
- name='AppWorkflowConfig',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=255)),
- ('domain', models.CharField(max_length=255)),
- ('app_id', models.CharField(max_length=255)),
- ('user_id', models.CharField(max_length=36)),
- ('workflow', models.JSONField()),
- ('form_mode', models.CharField(choices=[('human', 'Human: Answer each question individually and submit form'), ('no_submit', "No Submit: Answer all questions but don't submit the form"), ('ignore', 'Ignore: Do not complete or submit forms')], max_length=255)),
- ('sync_before_run', models.BooleanField(default=False, help_text='Sync user data before running')),
- ('run_every', models.IntegerField(default=0, help_text='Number of minutes between runs')),
- ('last_run', models.DateTimeField(blank=True, null=True)),
- ('notification_emails', django.contrib.postgres.fields.ArrayField(base_field=models.EmailField(max_length=254), default=list, help_text='Emails to notify on failure', size=None)),
- ('django_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
- ],
- options={
- 'unique_together': {('domain', 'user_id')},
- },
- ),
- ]
diff --git a/corehq/apps/app_execution/migrations/0002_alter_appworkflowconfig_unique_together.py b/corehq/apps/app_execution/migrations/0002_alter_appworkflowconfig_unique_together.py
deleted file mode 100644
index 3bcb2ec51b1c..000000000000
--- a/corehq/apps/app_execution/migrations/0002_alter_appworkflowconfig_unique_together.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# Generated by Django 3.2.25 on 2024-04-30 11:10
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('app_execution', '0001_initial'),
- ]
-
- operations = [
- migrations.AlterUniqueTogether(
- name='appworkflowconfig',
- unique_together=set(),
- ),
- ]
diff --git a/corehq/apps/app_execution/migrations/0003_appexecutionlog_updated.py b/corehq/apps/app_execution/migrations/0003_appexecutionlog_updated.py
deleted file mode 100644
index 56a7f57f6c30..000000000000
--- a/corehq/apps/app_execution/migrations/0003_appexecutionlog_updated.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# Generated by Django 4.2.11 on 2024-05-10 11:15
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('app_execution', '0002_alter_appworkflowconfig_unique_together'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='AppExecutionLog',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('started', models.DateTimeField(auto_now_add=True)),
- ('completed', models.DateTimeField(blank=True, null=True)),
- ('success', models.BooleanField(default=False)),
- ('output', models.TextField(blank=True)),
- ('workflow', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app_execution.appworkflowconfig')),
- ('error', models.TextField(blank=True)),
- ],
- ),
- ]
diff --git a/corehq/apps/app_execution/migrations/0004_alter_appworkflowconfig_notification_emails_and_more.py b/corehq/apps/app_execution/migrations/0004_alter_appworkflowconfig_notification_emails_and_more.py
deleted file mode 100644
index d15638e72b1f..000000000000
--- a/corehq/apps/app_execution/migrations/0004_alter_appworkflowconfig_notification_emails_and_more.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# Generated by Django 4.2.11 on 2024-05-21 15:36
-
-import django.contrib.postgres.fields
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('app_execution', '0003_appexecutionlog_updated'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='appworkflowconfig',
- name='notification_emails',
- field=django.contrib.postgres.fields.ArrayField(base_field=models.EmailField(max_length=254), blank=True, default=list, help_text='Emails to notify on failure', size=None),
- ),
- migrations.AlterField(
- model_name='appworkflowconfig',
- name='run_every',
- field=models.IntegerField(blank=True, help_text='Number of minutes between runs', null=True),
- ),
- ]
diff --git a/corehq/apps/app_execution/migrations/__init__.py b/corehq/apps/app_execution/migrations/__init__.py
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/corehq/apps/app_execution/models.py b/corehq/apps/app_execution/models.py
deleted file mode 100644
index 0a75457267b9..000000000000
--- a/corehq/apps/app_execution/models.py
+++ /dev/null
@@ -1,84 +0,0 @@
-from functools import cached_property
-
-from django.contrib.auth.models import User
-from django.contrib.postgres.fields import ArrayField
-from django.db import models
-from django.db.models import functions
-
-from corehq.apps.app_execution import const
-from corehq.apps.app_execution.api import FormplayerSession, LocalUserClient
-from corehq.apps.app_execution.data_model import AppWorkflow
-from corehq.apps.app_manager.dbaccessors import get_brief_app
-from corehq.sql_db.functions import MakeInterval
-from corehq.util.jsonattrs import AttrsObject
-from django.utils.translation import gettext_lazy
-
-
-class AppWorkflowManager(models.Manager):
- def get_due(self):
- cutoff = functions.Now() - MakeInterval("mins", models.F("run_every"))
- return self.filter(run_every__isnull=False, last_run__isnull=True) | self.filter(
- last_run__lt=cutoff
- )
-
-
-class AppWorkflowConfig(models.Model):
- FORM_MODE_CHOICES = [
- (const.FORM_MODE_HUMAN, gettext_lazy("Human: Answer each question individually and submit form")),
- (const.FORM_MODE_NO_SUBMIT, gettext_lazy("No Submit: Answer all questions but don't submit the form")),
- (const.FORM_MODE_IGNORE, gettext_lazy("Ignore: Do not complete or submit forms")),
- ]
- name = models.CharField(max_length=255)
- domain = models.CharField(max_length=255)
- app_id = models.CharField(max_length=255)
- user_id = models.CharField(max_length=36)
- django_user = models.ForeignKey(User, on_delete=models.CASCADE)
- workflow = AttrsObject(AppWorkflow)
- form_mode = models.CharField(max_length=255, choices=FORM_MODE_CHOICES)
- sync_before_run = models.BooleanField(default=False, help_text=gettext_lazy("Sync user data before running"))
- run_every = models.IntegerField(
- help_text=gettext_lazy("Number of minutes between runs"), null=True, blank=True)
- last_run = models.DateTimeField(null=True, blank=True)
- notification_emails = ArrayField(
- models.EmailField(), default=list, help_text=gettext_lazy("Emails to notify on failure"), blank=True
- )
-
- objects = AppWorkflowManager()
-
- @cached_property
- def app_name(self):
- app = get_brief_app(self.domain, self.app_id)
- return app.name
-
- @property
- def workflow_json(self):
- return AppWorkflowConfig.workflow_object_to_json_string(self.workflow)
-
- @staticmethod
- def workflow_object_to_json_string(workflow):
- return AppWorkflowConfig._meta.get_field("workflow").formfield().prepare_value(workflow)
-
- def get_formplayer_session(self):
- client = LocalUserClient(
- domain=self.domain,
- username=self.django_user.username,
- user_id=self.user_id
- )
- return FormplayerSession(client, self.app_id, self.form_mode, self.sync_before_run)
-
-
-class AppExecutionLog(models.Model):
- workflow = models.ForeignKey(AppWorkflowConfig, on_delete=models.CASCADE)
- started = models.DateTimeField(auto_now_add=True)
- completed = models.DateTimeField(null=True, blank=True)
- success = models.BooleanField(default=False)
- output = models.TextField(blank=True)
- error = models.TextField(blank=True)
-
- @property
- def duration(self):
- if self.completed:
- return self.completed - self.started
-
- def __str__(self):
- return f"{self.workflow.name} - {self.started}"
diff --git a/corehq/apps/app_execution/static/app_execution/js/workflow_charts.js b/corehq/apps/app_execution/static/app_execution/js/workflow_charts.js
deleted file mode 100644
index 25569cd18547..000000000000
--- a/corehq/apps/app_execution/static/app_execution/js/workflow_charts.js
+++ /dev/null
@@ -1,97 +0,0 @@
-import "commcarehq";
-import $ from "jquery";
-import moment from "moment/moment";
-import d3 from "d3/d3.min";
-import nv from "nvd3/nv.d3.latest.min"; // version 1.1.10 has a bug that affects line charts with multiple series
-import "nvd3-1.8.6/build/nv.d3.css";
-
-
-function getSeries(data, includeSeries) {
- return includeSeries.map((seriesMeta) => {
- return {
- // include key in the label to differentiate between series with the same label
- key: `${data.label}${seriesMeta.label}[${data.key}]`,
- values: data.values.map((item) => {
- return {
- x: moment(item.date),
- y: item[seriesMeta.key],
- };
- }),
- };
- });
-}
-
-function buildChart(yLabel) {
- let chart = nv.models.lineChart()
- .showYAxis(true)
- .showXAxis(true);
-
- chart.yAxis
- .axisLabel(yLabel);
- chart.forceY(0);
- chart.xScale(d3.time.scale());
- chart.margin({left: 80, bottom: 100});
- chart.xAxis.rotateLabels(-45)
- .tickFormat(function (d) {
- return moment(d).format("MMM DD [@] HH");
- });
-
- nv.utils.windowResize(chart.update);
- return chart;
-}
-
-function setupTimingChart(data, includeSeries) {
- const timingSeries = data.timing.flatMap((series) => getSeries(series, includeSeries));
-
- nv.addGraph(() => {
- let chart = buildChart(gettext("Seconds"));
- chart.yAxis.tickFormat(d3.format(".1f"));
- // remove the key from the label
- chart.legend.key((d) => d.key.split("[")[0]);
- chart.tooltip.keyFormatter((d) => {
- return d.split("[")[0];
- });
-
- d3.select('#timing_linechart svg')
- .datum(timingSeries)
- .call(chart);
-
- return chart;
- });
-}
-
-function setupStatusChart(data) {
- const colors = {
- "Success": "#6dcc66",
- "Error": "#f44",
- };
- let seriesData = data.status.map((series) => {
- return {
- key: series.key,
- values: series.values.map((item) => {
- return {
- x: moment(item.date),
- y: item.count,
- };
- }),
- color: colors[series.key],
- };
- });
-
- nv.addGraph(() => {
- let chart = buildChart(gettext("Chart"));
-
- d3.select('#status_barchart svg')
- .datum(seriesData)
- .call(chart);
-
- return chart;
- });
-}
-
-$(document).ready(function () {
- const data = JSON.parse(document.getElementById('chart_data').textContent);
- const includeSeries = JSON.parse(document.getElementById('timingSeries').textContent);
- setupTimingChart(data, includeSeries);
- setupStatusChart(data);
-});
diff --git a/corehq/apps/app_execution/static/app_execution/js/workflow_logs.js b/corehq/apps/app_execution/static/app_execution/js/workflow_logs.js
deleted file mode 100644
index 52f2d00239d0..000000000000
--- a/corehq/apps/app_execution/static/app_execution/js/workflow_logs.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import "commcarehq";
-import $ from "jquery";
-import ko from "knockout";
-import initialPageData from "hqwebapp/js/initial_page_data";
-import hqTempusDominus from "hqwebapp/js/tempus_dominus";
-import "app_execution/js/workflow_charts";
-import "hqwebapp/js/components/pagination";
-
-let logsModel = function () {
- let self = {};
-
- self.statusFilter = ko.observable("");
- let allDatesText = gettext("Show All Dates");
- self.dateRange = ko.observable(allDatesText);
- self.items = ko.observableArray();
- self.totalItems = ko.observable(initialPageData.get('total_items'));
- self.perPage = ko.observable(25);
- self.goToPage = function (page) {
- let params = {page: page, per_page: self.perPage()};
- const url = initialPageData.reverse('app_execution:logs_json');
- if (self.statusFilter()) {
- params.status = self.statusFilter();
- }
- if (self.dateRange() && self.dateRange() !== allDatesText) {
- const separator = hqTempusDominus.getDateRangeSeparator(),
- dates = self.dateRange().split(separator);
- params.startDate = dates[0];
- params.endDate = dates[1] || dates[0];
- }
- $.getJSON(url, params, function (data) {
- self.items(data.logs);
- });
- };
-
- self.filter = ko.computed(() => {
- self.statusFilter();
- if (self.dateRange().includes(hqTempusDominus.getDateRangeSeparator())) {
- self.goToPage(1);
- }
- }).extend({throttle: 500});
-
- self.onLoad = function () {
- self.goToPage(1);
- };
-
- hqTempusDominus.createDefaultDateRangePicker(document.getElementById('id_date_range'));
-
- return self;
-};
-
-$("#workflow-logs").koApplyBindings(logsModel());
diff --git a/corehq/apps/app_execution/tasks.py b/corehq/apps/app_execution/tasks.py
deleted file mode 100644
index ff2c1a83b948..000000000000
--- a/corehq/apps/app_execution/tasks.py
+++ /dev/null
@@ -1,78 +0,0 @@
-import traceback
-
-from celery.schedules import crontab
-from django.utils import timezone
-from django.utils.html import format_html
-
-from corehq.apps.app_execution.api import execute_workflow
-from corehq.apps.app_execution.models import AppWorkflowConfig
-from corehq.apps.celery import periodic_task
-from corehq.util import reverse
-from corehq.util.log import send_HTML_email
-from dimagi.utils.rate_limit import rate_limit
-
-
-@periodic_task(run_every=crontab()) # run every minute
-def run_app_workflows():
-
- for config in AppWorkflowConfig.objects.get_due():
- run_app_workflow(config, email_on_error=True)
-
-
-def run_app_workflow(config, email_on_error=False):
- session = config.get_formplayer_session()
- log = config.appexecutionlog_set.create()
- try:
- success = execute_workflow(session, config.workflow)
- except Exception as e:
- log.success = False
- log.error = str(e)
-
- # rate limit to prevent spamming: 1 email per config per 10 minutes
- if email_on_error and rate_limit(
- f"task-execution-error-{config.pk}", actions_allowed=1, how_often=10 * 60
- ):
- _email_error(config, e, log)
- else:
- log.success = success
- finally:
- config.last_run = timezone.now()
- config.save()
- log.output = session.get_logs()
- log.completed = timezone.now()
- log.save()
- return log
-
-
-def _email_error(config, e, log):
- url = reverse('app_execution:edit_workflow', args=[config.domain, config.pk], absolute=True)
- log_url = reverse('app_execution:workflow_log', args=[config.domain, log.pk], absolute=True)
- all_logs_url = reverse('app_execution:workflow_logs', args=[config.domain, config.pk], absolute=True)
- message = format_html(
- """Error executing workflow: {name}
-
- Log: {log_url}
- All Logs: {all_logs_url}
- {traceback}
- Error:
- {error}
- """,
- url=url,
- name=config.name,
- log_url=log_url,
- all_logs_url=all_logs_url,
- traceback=traceback.format_exc(),
- error=e,
- )
- send_HTML_email(
- f"App Execution Workflow Failure: {config.name}",
- config.notification_emails,
- message,
- )
-
-
-@periodic_task(run_every=crontab(minute=0, hour=0)) # run every day at midnight
-def clear_old_logs():
- AppWorkflowConfig.objects.filter(
- appexecutionlog__completed__lt=timezone.now() - timezone.timedelta(days=30)
- ).delete()
diff --git a/corehq/apps/app_execution/templates/app_execution/components/logs.html b/corehq/apps/app_execution/templates/app_execution/components/logs.html
deleted file mode 100644
index 90f2178ffc3d..000000000000
--- a/corehq/apps/app_execution/templates/app_execution/components/logs.html
+++ /dev/null
@@ -1,28 +0,0 @@
-{% load i18n %}
-
-
-
Logs
-
{{ log.output }}
- {% if log.success %}
-
- {% trans "Success" %}
-
- {% else %}
-
- {% trans "Error:" %} {{ log.error }}
-
- {% endif %}
-
-
-
- {% trans "Workflow" %}
-
-
-
{{ workflow_json }}
-
-
diff --git a/corehq/apps/app_execution/templates/app_execution/components/title_bar.html b/corehq/apps/app_execution/templates/app_execution/components/title_bar.html
deleted file mode 100644
index 33e05bdfd99d..000000000000
--- a/corehq/apps/app_execution/templates/app_execution/components/title_bar.html
+++ /dev/null
@@ -1,25 +0,0 @@
-{% load i18n %}
-
-
-
-
{{ title_text }}
-
-
-
-
diff --git a/corehq/apps/app_execution/templates/app_execution/workflow_form.html b/corehq/apps/app_execution/templates/app_execution/workflow_form.html
deleted file mode 100644
index c90c68cdb7b0..000000000000
--- a/corehq/apps/app_execution/templates/app_execution/workflow_form.html
+++ /dev/null
@@ -1,16 +0,0 @@
-{% extends "hqwebapp/bootstrap5/base_section.html" %}
-{% load i18n %}
-{% load crispy_forms_tags %}
-{% block page_content %}
- {% if workflow %}
- {% blocktrans asvar title_text with workflow_name=workflow.name %}
- Editing "{{ workflow_name }}"
- {% endblocktrans %}
- {% include "app_execution/components/title_bar.html" with workflow_id=workflow.id title_text=title_text show_edit=False %}
- {% else %}
-
-
{% translate "New workflow" %}
-
- {% endif %}
- {% crispy form %}
-{% endblock %}
diff --git a/corehq/apps/app_execution/templates/app_execution/workflow_list.html b/corehq/apps/app_execution/templates/app_execution/workflow_list.html
deleted file mode 100644
index df82f6887ff8..000000000000
--- a/corehq/apps/app_execution/templates/app_execution/workflow_list.html
+++ /dev/null
@@ -1,69 +0,0 @@
-{% extends "hqwebapp/bootstrap5/base_section.html" %}
-{% load hq_shared_tags %}
-{% load compress %}
-{% load i18n %}
-
-{% js_entry 'app_execution/js/workflow_charts' %}
-
-{% block page_content %}
-
-
-
-
{% trans "Average Timings" %}
-
-
-
-
{% trans "Status" %}
-
-
-
-
-
-
- | {% translate 'Name' %} |
- {% translate 'App Name' %} |
- {% translate 'User' %} |
- {% translate 'Last Run' %} |
- {% translate 'Last 10 Runs' %} |
- |
-
-
-
- {% for workflow in workflows %}
-
- | {{ workflow.name }} |
- {{ workflow.app_name }} |
- {{ workflow.django_user.username }} |
- {{ workflow.last_run|default:""|date:"DATETIME_FORMAT" }} |
-
-
- {% for status in workflow.last_n %}
- {% if not status %}
-
- {% else %}
-
- {% endif %}
- {% endfor %}
-
- |
-
-
- |
-
- {% endfor %}
-
-
-
-{{ chart_data|json_script:"chart_data" }}
-
-{% endblock %}
diff --git a/corehq/apps/app_execution/templates/app_execution/workflow_log.html b/corehq/apps/app_execution/templates/app_execution/workflow_log.html
deleted file mode 100644
index 6925f72f55ee..000000000000
--- a/corehq/apps/app_execution/templates/app_execution/workflow_log.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{% extends "hqwebapp/bootstrap5/base_section.html" %}
-{% load i18n %}
-{% block page_content %}
- {% blocktrans asvar title_text with workflow_name=log.config.name date=log.started|date %}
- Workflow Execution Log for {{ workflow_name }}: {{ date }}
- {% endblocktrans %}
- {% include "app_execution/components/title_bar.html" with workflow_id=log.workflow_id title_text=title_text%}
- {% translate 'Details' %}
-
-
- - {% blocktrans with date_started=log.started|date:"DATETIME_FORMAT" %}Started: {{ date_started }}{% endblocktrans %}
- - {% blocktrans with date_ended=log.completed|date:"DATETIME_FORMAT" %}Ended: {{ date_ended }}{% endblocktrans %}
- - {% blocktrans with duration=log.duration %}Duration: {{ duration }}{% endblocktrans %}
- - {% translate 'Status:' %}
- {% if log.success %}{% translate 'Success' %}{% else %}{% translate 'Error' %}{% endif %}
-
-
-
- {% include "app_execution/components/logs.html" with log=log %}
-{% endblock page_content %}
diff --git a/corehq/apps/app_execution/templates/app_execution/workflow_log_list.html b/corehq/apps/app_execution/templates/app_execution/workflow_log_list.html
deleted file mode 100644
index 02d7f3ff7bc4..000000000000
--- a/corehq/apps/app_execution/templates/app_execution/workflow_log_list.html
+++ /dev/null
@@ -1,79 +0,0 @@
-{% extends "hqwebapp/bootstrap5/base_section.html" %}
-{% load hq_shared_tags %}
-{% load compress %}
-{% load i18n %}
-
-{% block stylesheets %}{{ block.super }}
-{% compress css %}
-
- {% endcompress %}
-{% endblock %}
-
-{% js_entry 'app_execution/js/workflow_logs' %}
-
-{% block page_content %}
- {% initial_page_data 'total_items' total %}
- {% registerurl "app_execution:logs_json" request.domain workflow.id %}
- {% blocktranslate asvar title_text with name=workflow.name %}Logs for workflow "{{ name }}"{% endblocktranslate %}
- {% include "app_execution/components/title_bar.html" with workflow_id=workflow.id show_logs=False title_text=title_text %}
-
-
-
{% translate "Average Timings" %}
-
-
-
-
{% translate "Log Status" %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- | {% translate 'Status' %} |
- {% translate 'Started' %} |
- {% translate 'Duration' %} |
- |
-
-
-
-
- |
- |
- |
- |
- {% translate 'Details' %} |
-
-
-
-
-
-{{ chart_data|json_script:"chart_data" }}
-
-{% endblock %}
diff --git a/corehq/apps/app_execution/templates/app_execution/workflow_run.html b/corehq/apps/app_execution/templates/app_execution/workflow_run.html
deleted file mode 100644
index cd8b8ed2a03f..000000000000
--- a/corehq/apps/app_execution/templates/app_execution/workflow_run.html
+++ /dev/null
@@ -1,22 +0,0 @@
-{% extends "hqwebapp/bootstrap5/base_section.html" %}
-{% load i18n %}
-{% block js-inline %} {{ block.super }}
-
-{% endblock %}
-{% block page_content %}
- {% blocktrans asvar title_text with name=workflow.name %}Testing {{ name }}{% endblocktrans %}
- {% include "app_execution/components/title_bar.html" with workflow_id=workflow.id show_run=False title_text=title_text %}
-
-{% endblock %}
diff --git a/corehq/apps/app_execution/tests/__init__.py b/corehq/apps/app_execution/tests/__init__.py
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/corehq/apps/app_execution/tests/data/case_list.json b/corehq/apps/app_execution/tests/data/case_list.json
deleted file mode 100644
index 0ecc3ae92eda..000000000000
--- a/corehq/apps/app_execution/tests/data/case_list.json
+++ /dev/null
@@ -1,90 +0,0 @@
-{
- "log": {
- "entries": [
- {
- "request": {
- "method": "POST",
- "url": "https://www.commcarehq.org/formplayer/navigate_menu_start",
- "postData": {
- "text": "{\"username\":\"user@example.com\",\"restoreAs\":null,\"domain\":\"demo_domain\",\"app_id\":\"demo_app_id\",\"locale\":\"en\",\"offset\":null,\"cases_per_page\":10,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"\\\">\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"demo_app_id\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 144\",\"selections\":null,\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"locales\":[\"default\",\"en\"],\"breadcrumbs\":[\"\\\">\"],\"persistentCaseTile\":null,\"queryKey\":null,\"commands\":[{\"index\":0,\"displayText\":\"Register\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"},{\"index\":1,\"displayText\":\"Format test\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"},{\"index\":2,\"displayText\":\"Case List\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"},{\"index\":3,\"displayText\":\"Child cases from repeat 1\",\"navigationState\":\"JUMP\",\"badgeText\":\"\"},{\"index\":4,\"displayText\":\"Child cases from repeat 2\",\"navigationState\":\"JUMP\",\"badgeText\":\"\"},{\"index\":5,\"displayText\":\"Child cases from repeat 3\",\"navigationState\":\"JUMP\",\"badgeText\":\"\"},{\"index\":6,\"displayText\":\"define cat_owner casetype\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"}],\"type\":\"commands\",\"layoutStyle\":null}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://www.commcarehq.org/formplayer/navigate_menu",
- "postData": {
- "text": "{\"username\":\"user@example.com\",\"restoreAs\":null,\"domain\":\"demo_domain\",\"app_id\":\"demo_app_id\",\"locale\":\"en\",\"selections\":[\"0\"],\"offset\":0,\"search_text\":null,\"form_session_id\":null,\"query_data\":{},\"cases_per_page\":10,\"sortIndex\":null,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"Register\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"demo_app_id\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 144\",\"selections\":[\"0\"],\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"locales\":[\"default\",\"en\"],\"breadcrumbs\":[\"\\\">\",\"Register\"],\"persistentCaseTile\":null,\"queryKey\":null,\"commands\":[{\"index\":0,\"displayText\":\"Register cat\",\"navigationState\":\"JUMP\",\"badgeText\":\"\"},{\"index\":1,\"displayText\":\"Cat sighting\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"}],\"type\":\"commands\",\"layoutStyle\":null}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://www.commcarehq.org/formplayer/navigate_menu",
- "postData": {
- "text": "{\"username\":\"user@example.com\",\"restoreAs\":null,\"domain\":\"demo_domain\",\"app_id\":\"demo_app_id\",\"locale\":\"en\",\"selections\":[\"0\",\"1\"],\"offset\":0,\"search_text\":null,\"form_session_id\":null,\"query_data\":{},\"cases_per_page\":10,\"sortIndex\":null,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"Cat sighting\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"demo_app_id\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 144\",\"selections\":[\"0\",\"1\"],\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"locales\":[\"default\",\"en\"],\"breadcrumbs\":[\"\\\">\",\"Register\",\"Cat sighting\"],\"persistentCaseTile\":null,\"queryKey\":null,\"entities\":[{\"id\":\"7a6d6f96-9bd4-41c0-8e52-e499497c4991\",\"data\":[\"fluffy\"],\"groupKey\":null,\"altText\":[null]}],\"actions\":[],\"redoLast\":null,\"styles\":[{\"fontSize\":0,\"widthHint\":-1,\"horizontalAlign\":null,\"verticalAlign\":null,\"showBorder\":false,\"showShading\":false,\"displayFormat\":null}],\"endpointActions\":[null],\"headers\":[\"Name\"],\"tiles\":null,\"widthHints\":[100],\"numEntitiesPerRow\":0,\"useUniformUnits\":false,\"sortIndices\":[0],\"noItemsText\":null,\"selectText\":null,\"pageCount\":0,\"currentPage\":0,\"type\":\"entities\",\"usesCaseTiles\":false,\"maxWidth\":0,\"maxHeight\":0,\"maxSelectValue\":-1,\"hasDetails\":true,\"queryResponse\":null,\"groupHeaderRows\":-1,\"multiSelect\":false}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://www.commcarehq.org/formplayer/get_details",
- "postData": {
- "text": "{\"username\":\"user@example.com\",\"restoreAs\":null,\"domain\":\"demo_domain\",\"app_id\":\"demo_app_id\",\"locale\":\"en\",\"selections\":[\"0\",\"1\",\"7a6d6f96-9bd4-41c0-8e52-e499497c4991\"],\"offset\":0,\"search_text\":null,\"form_session_id\":null,\"query_data\":{},\"cases_per_page\":10,\"sortIndex\":null,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\",\"isShortDetail\":false}"
- }
- },
- "response": {
- "content": {
- "text": "{\"details\":[{\"details\":[\"fluffy\"],\"entities\":null,\"styles\":[{\"fontSize\":0,\"widthHint\":-1,\"horizontalAlign\":null,\"verticalAlign\":null,\"showBorder\":false,\"showShading\":false,\"displayFormat\":null}],\"headers\":[\"Name\"],\"altText\":[null],\"title\":\"Case\",\"usesCaseTiles\":false,\"maxWidth\":0,\"maxHeight\":0,\"numEntitiesPerRow\":0,\"tiles\":null,\"useUniformUnits\":false,\"hasInlineTile\":false,\"useNodeset\":false}],\"isPersistentDetail\":false}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://www.commcarehq.org/formplayer/navigate_menu",
- "postData": {
- "text": "{\"username\":\"user@example.com\",\"restoreAs\":null,\"domain\":\"demo_domain\",\"app_id\":\"demo_app_id\",\"locale\":\"en\",\"selections\":[\"0\",\"1\",\"7a6d6f96-9bd4-41c0-8e52-e499497c4991\"],\"offset\":0,\"search_text\":null,\"form_session_id\":null,\"query_data\":{},\"cases_per_page\":10,\"sortIndex\":null,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"Cat sighting\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"demo_app_id\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 144\",\"selections\":[\"0\",\"1\",\"7a6d6f96-9bd4-41c0-8e52-e499497c4991\"],\"translations\":{\"forms.m0f1.submit_label\":\"Submit\"},\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"instanceXml\":{\"output\":\"\\n \\n \\n fluffy\\n \\n \\n \\n \\n \\n \\n \\n Formplayer\\n 2024-05-14T13:39:14.292+02\\n \\n user@example.com\\n 655a8581da22f05fb953b19d8561b079\\n 8c680880-dd1b-48b3-89e5-36271229e9f5\\n Formplayer Version: 2.53\\n \\n \\n\\n\"},\"tree\":[{\"caption_audio\":null,\"caption_video\":null,\"caption_image\":null,\"caption_markdown\":null,\"caption\":\"Confirmed sighting of fluffy\",\"binding\":\"/data/label\",\"question_id\":\"label\",\"required\":0,\"relevant\":0,\"answer\":null,\"datatype\":\"info\",\"style\":{\"raw\":\"minimal\"},\"type\":\"question\",\"ix\":\"0\",\"choices\":null,\"repeatable\":null,\"exists\":null,\"header\":null,\"control\":9,\"help\":null,\"help_image\":null,\"help_audio\":null,\"help_video\":null,\"hint\":null,\"output\":null,\"add-choice\":null},{\"caption_audio\":null,\"caption_video\":null,\"caption_image\":null,\"caption_markdown\":null,\"caption\":\"When?\",\"binding\":\"/data/time_of_sighting\",\"question_id\":\"time_of_sighting\",\"required\":0,\"relevant\":0,\"answer\":null,\"datatype\":\"datetime\",\"style\":null,\"type\":\"question\",\"ix\":\"1\",\"choices\":null,\"repeatable\":null,\"exists\":null,\"header\":null,\"control\":1,\"help\":null,\"help_image\":null,\"help_audio\":null,\"help_video\":null,\"hint\":null,\"output\":null,\"add-choice\":null},{\"caption_audio\":null,\"caption_video\":null,\"caption_image\":null,\"caption_markdown\":null,\"caption\":null,\"binding\":null,\"question_id\":null,\"required\":0,\"relevant\":0,\"answer\":null,\"datatype\":null,\"style\":{},\"type\":\"repeat-juncture\",\"ix\":\"2_-10\",\"choices\":null,\"repeatable\":null,\"exists\":null,\"header\":null,\"control\":0,\"help\":null,\"help_image\":null,\"help_audio\":null,\"help_video\":null,\"hint\":null,\"children\":[],\"output\":null,\"add-choice\":\"None - Add null\"},{\"caption_audio\":null,\"caption_video\":null,\"caption_image\":null,\"caption_markdown\":null,\"caption\":null,\"binding\":\"/data/question1\",\"question_id\":\"question1\",\"required\":0,\"relevant\":0,\"answer\":null,\"datatype\":\"select\",\"style\":null,\"type\":\"question\",\"ix\":\"3\",\"choices\":[],\"repeatable\":null,\"exists\":null,\"header\":null,\"control\":2,\"help\":null,\"help_image\":null,\"help_audio\":null,\"help_video\":null,\"hint\":null,\"output\":null,\"add-choice\":null}],\"langs\":[\"en\"],\"breadcrumbs\":[\"\\\">\",\"Register\",\"Cat sighting\",\"fluffy\"],\"persistentCaseTile\":null,\"event\":null,\"session_id\":\"0bd6b52b-f6ff-4112-bef3-66f04e57475b\",\"seq_id\":0}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://www.commcarehq.org/formplayer/submit-all",
- "postData": {
- "text": "{\"action\":\"submit-all\",\"answers\":{\"0\":\"OK\",\"1\":\"Not Supported by Web Entry\",\"3\":null},\"prevalidated\":true,\"domain\":\"demo_domain\",\"username\":\"user@example.com\",\"restoreAs\":null,\"session-id\":\"0bd6b52b-f6ff-4112-bef3-66f04e57475b\",\"session_id\":\"0bd6b52b-f6ff-4112-bef3-66f04e57475b\",\"debuggerEnabled\":true,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":null,\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":null,\"appVersion\":null,\"selections\":null,\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":null,\"instanceXml\":null,\"status\":\"success\",\"submitResponseMessage\":\"'Register cat' successfully saved!\\n\\nYou submitted [this form](/a/demo_domain/reports/form_data/c0812de6-d8cc-4123-8204-3dda5b2b87a6/), which affected [this case](/a/demo_domain/reports/case_data/7a6d6f96-9bd4-41c0-8e52-e499497c4991/).\\n\\nClick to export your [case](/a/demo_domain/data/export/custom/case/) or [form](/a/demo_domain/data/export/custom/form/) data.\",\"errors\":{},\"nextScreen\":null,\"session_id\":null,\"seq_id\":0}"
- }
- }
- }
- ]
- }
-}
diff --git a/corehq/apps/app_execution/tests/data/case_list_action.json b/corehq/apps/app_execution/tests/data/case_list_action.json
deleted file mode 100644
index aa7ee30ead07..000000000000
--- a/corehq/apps/app_execution/tests/data/case_list_action.json
+++ /dev/null
@@ -1,48 +0,0 @@
-{
- "log": {
- "entries": [
- {
- "request": {
- "method": "POST",
- "url": "https://www.commcarehq.org/formplayer/navigate_menu_start",
- "postData": {
- "text": "{\"username\":\"skelly@dimagi.com\",\"restoreAs\":null,\"domain\":\"skelly\",\"app_id\":\"b5e1e70227b04da49131361f82537f7f\",\"locale\":\"en\",\"offset\":null,\"cases_per_page\":10,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"Baby log\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"b5e1e70227b04da49131361f82537f7f\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 102\",\"selections\":null,\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"locales\":[\"default\",\"en\"],\"breadcrumbs\":[\"Baby log\"],\"persistentCaseTile\":null,\"queryKey\":null,\"commands\":[{\"index\":0,\"displayText\":\"Baby Log\",\"imageUri\":\"jr://file/commcare/image/babylog.png\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"},{\"index\":1,\"displayText\":\"Registration\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"}],\"type\":\"commands\",\"layoutStyle\":null}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://www.commcarehq.org/formplayer/navigate_menu",
- "postData": {
- "text": "{\"username\":\"skelly@dimagi.com\",\"restoreAs\":null,\"domain\":\"skelly\",\"app_id\":\"b5e1e70227b04da49131361f82537f7f\",\"locale\":\"en\",\"selections\":[\"0\"],\"offset\":0,\"search_text\":null,\"query_data\":{},\"cases_per_page\":10,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"Baby Log\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"b5e1e70227b04da49131361f82537f7f\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 102\",\"selections\":[\"0\"],\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"locales\":[\"default\",\"en\"],\"breadcrumbs\":[\"Baby log\",\"Baby Log\"],\"persistentCaseTile\":null,\"queryKey\":null,\"entities\":[],\"actions\":[{\"text\":\"Continue To Registry\",\"id\":null,\"input\":null,\"value\":null,\"receive\":null,\"hidden\":null,\"itemsetChoices\":null,\"itemsetChoicesKey\":null,\"hint\":null,\"error\":null,\"groupKey\":null,\"audio_uri\":null,\"image_uri\":null,\"allow_blank_value\":false,\"required\":false,\"required_msg\":null}],\"redoLast\":null,\"styles\":[{\"fontSize\":0,\"widthHint\":-1,\"horizontalAlign\":null,\"verticalAlign\":null,\"showBorder\":false,\"showShading\":false,\"displayFormat\":null}],\"endpointActions\":[null],\"headers\":[\"Baby Name\"],\"tiles\":null,\"widthHints\":[100],\"numEntitiesPerRow\":0,\"useUniformUnits\":false,\"sortIndices\":[0],\"noItemsText\":null,\"selectText\":null,\"pageCount\":0,\"currentPage\":0,\"type\":\"entities\",\"usesCaseTiles\":false,\"maxWidth\":0,\"maxHeight\":0,\"maxSelectValue\":-1,\"hasDetails\":true,\"queryResponse\":null,\"groupHeaderRows\":-1,\"multiSelect\":false}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://www.commcarehq.org/formplayer/navigate_menu",
- "postData": {
- "text": "{\"username\":\"skelly@dimagi.com\",\"restoreAs\":null,\"domain\":\"skelly\",\"app_id\":\"b5e1e70227b04da49131361f82537f7f\",\"locale\":\"en\",\"selections\":[\"0\",\"action 0\"],\"offset\":0,\"search_text\":null,\"query_data\":{},\"cases_per_page\":10,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"Registry\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"b5e1e70227b04da49131361f82537f7f\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 102\",\"selections\":[\"0\",\"action 0\"],\"translations\":{\"forms.m1f0.submit_label\":\"Submit\"},\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"instanceXml\":{\"output\":\"\\n \\n \\n \\n \\n \\n 655a8581da22f05fb953b19d8561b079\\n babylog\\n \\n \\n \\n \\n \\n \\n \\n Formplayer\\n 2024-05-17T11:38:12.880+02\\n \\n skelly@dimagi.com\\n 655a8581da22f05fb953b19d8561b079\\n da5378f2-320b-4979-9c95-03d32fb73874\\n Formplayer Version: 2.53\\n \\n \\n\\n\"},\"tree\":[{\"caption_audio\":null,\"caption_video\":null,\"caption_image\":null,\"caption_markdown\":null,\"caption\":\"Name\",\"binding\":\"/data/baby_name\",\"question_id\":\"baby_name\",\"required\":1,\"relevant\":0,\"answer\":null,\"datatype\":\"str\",\"style\":null,\"type\":\"question\",\"ix\":\"0\",\"choices\":null,\"repeatable\":null,\"exists\":null,\"header\":null,\"control\":1,\"help\":null,\"help_image\":null,\"help_audio\":null,\"help_video\":null,\"hint\":null,\"output\":null,\"add-choice\":null},{\"caption_audio\":null,\"caption_video\":null,\"caption_image\":null,\"caption_markdown\":null,\"caption\":\"Date of Birth\",\"binding\":\"/data/dob\",\"question_id\":\"dob\",\"required\":1,\"relevant\":0,\"answer\":null,\"datatype\":\"datetime\",\"style\":null,\"type\":\"question\",\"ix\":\"1\",\"choices\":null,\"repeatable\":null,\"exists\":null,\"header\":null,\"control\":1,\"help\":null,\"help_image\":null,\"help_audio\":null,\"help_video\":null,\"hint\":null,\"output\":null,\"add-choice\":null}],\"langs\":[\"en\"],\"breadcrumbs\":[\"Baby log\",\"Baby Log\",\"Registry\"],\"persistentCaseTile\":null,\"event\":null,\"session_id\":\"ffc424a6-9649-4de3-b1a2-65cb3cc40487\",\"seq_id\":0}"
- }
- }
- }
- ]
- }
-}
diff --git a/corehq/apps/app_execution/tests/data/reg_form.json b/corehq/apps/app_execution/tests/data/reg_form.json
deleted file mode 100644
index e405ddcd3a34..000000000000
--- a/corehq/apps/app_execution/tests/data/reg_form.json
+++ /dev/null
@@ -1,90 +0,0 @@
-{
- "log": {
- "entries": [
- {
- "request": {
- "method": "POST",
- "url": "https://www.commcarehq.org/formplayer/navigate_menu_start",
- "postData": {
- "text": "{\"username\":\"user@example.com\",\"restoreAs\":null,\"domain\":\"demo_domain\",\"app_id\":\"demo_app_id\",\"locale\":\"en\",\"offset\":null,\"cases_per_page\":10,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"\\\">\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"demo_app_id\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 144\",\"selections\":null,\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"locales\":[\"default\",\"en\"],\"breadcrumbs\":[\"\\\">\"],\"persistentCaseTile\":null,\"queryKey\":null,\"commands\":[{\"index\":0,\"displayText\":\"Register\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"},{\"index\":1,\"displayText\":\"Format test\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"},{\"index\":2,\"displayText\":\"Case List\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"},{\"index\":3,\"displayText\":\"Child cases from repeat 1\",\"navigationState\":\"JUMP\",\"badgeText\":\"\"},{\"index\":4,\"displayText\":\"Child cases from repeat 2\",\"navigationState\":\"JUMP\",\"badgeText\":\"\"},{\"index\":5,\"displayText\":\"Child cases from repeat 3\",\"navigationState\":\"JUMP\",\"badgeText\":\"\"},{\"index\":6,\"displayText\":\"define cat_owner casetype\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"}],\"type\":\"commands\",\"layoutStyle\":null}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://www.commcarehq.org/formplayer/navigate_menu",
- "postData": {
- "text": "{\"username\":\"user@example.com\",\"restoreAs\":null,\"domain\":\"demo_domain\",\"app_id\":\"demo_app_id\",\"locale\":\"en\",\"selections\":[\"0\"],\"offset\":0,\"search_text\":null,\"query_data\":{},\"cases_per_page\":10,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"Register\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"demo_app_id\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 144\",\"selections\":[\"0\"],\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"locales\":[\"default\",\"en\"],\"breadcrumbs\":[\"\\\">\",\"Register\"],\"persistentCaseTile\":null,\"queryKey\":null,\"commands\":[{\"index\":0,\"displayText\":\"Register cat\",\"navigationState\":\"JUMP\",\"badgeText\":\"\"},{\"index\":1,\"displayText\":\"Cat sighting\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"}],\"type\":\"commands\",\"layoutStyle\":null}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://www.commcarehq.org/formplayer/navigate_menu",
- "postData": {
- "text": "{\"username\":\"user@example.com\",\"restoreAs\":null,\"domain\":\"demo_domain\",\"app_id\":\"demo_app_id\",\"locale\":\"en\",\"selections\":[\"0\",\"0\"],\"offset\":0,\"search_text\":null,\"query_data\":{},\"cases_per_page\":10,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"Register cat\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"demo_app_id\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 144\",\"selections\":[\"0\",\"0\"],\"translations\":{\"forms.m0f0.submit_label\":\"Submit\"},\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"instanceXml\":{\"output\":\"\\n \\n \\n \\n \\n \\n 655a8581da22f05fb953b19d8561b079\\n cat\\n \\n \\n \\n Formplayer\\n 2024-05-14T13:38:56.606+02\\n \\n user@example.com\\n 655a8581da22f05fb953b19d8561b079\\n c0812de6-d8cc-4123-8204-3dda5b2b87a6\\n Formplayer Version: 2.53\\n \\n \\n\\n\"},\"tree\":[{\"caption_audio\":null,\"caption_video\":null,\"caption_image\":null,\"caption_markdown\":null,\"caption\":\"Name\",\"binding\":\"/data/name\",\"question_id\":\"name\",\"required\":1,\"relevant\":0,\"answer\":null,\"datatype\":\"str\",\"style\":null,\"type\":\"question\",\"ix\":\"0\",\"choices\":null,\"repeatable\":null,\"exists\":null,\"header\":null,\"control\":1,\"help\":null,\"help_image\":null,\"help_audio\":null,\"help_video\":null,\"hint\":null,\"output\":null,\"add-choice\":null},{\"caption_audio\":null,\"caption_video\":null,\"caption_image\":null,\"caption_markdown\":null,\"caption\":\"Date\",\"binding\":\"/data/date\",\"question_id\":\"date\",\"required\":0,\"relevant\":0,\"answer\":null,\"datatype\":\"date\",\"style\":null,\"type\":\"question\",\"ix\":\"1\",\"choices\":null,\"repeatable\":null,\"exists\":null,\"header\":null,\"control\":1,\"help\":null,\"help_image\":null,\"help_audio\":null,\"help_video\":null,\"hint\":null,\"output\":null,\"add-choice\":null}],\"langs\":[\"en\"],\"breadcrumbs\":[\"\\\">\",\"Register\",\"Register cat\"],\"persistentCaseTile\":null,\"event\":null,\"session_id\":\"db67e0bf-a770-4b73-9e48-2ecbb2faefa1\",\"seq_id\":0}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://www.commcarehq.org/formplayer/answer",
- "postData": {
- "text": "{\"action\":\"answer\",\"ix\":\"0\",\"answer\":\"fluffy\",\"answersToValidate\":{},\"domain\":\"demo_domain\",\"username\":\"user@example.com\",\"restoreAs\":null,\"session-id\":\"db67e0bf-a770-4b73-9e48-2ecbb2faefa1\",\"session_id\":\"db67e0bf-a770-4b73-9e48-2ecbb2faefa1\",\"debuggerEnabled\":true,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"Register cat\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":null,\"appVersion\":null,\"selections\":null,\"translations\":{},\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":null,\"instanceXml\":{\"output\":\"\\n fluffy\\n \\n \\n \\n fluffy\\n 655a8581da22f05fb953b19d8561b079\\n cat\\n \\n \\n \\n Formplayer\\n 2024-05-14T13:38:56.606+02\\n \\n user@example.com\\n 655a8581da22f05fb953b19d8561b079\\n c0812de6-d8cc-4123-8204-3dda5b2b87a6\\n Formplayer Version: 2.53\\n \\n \\n\\n\"},\"tree\":[{\"caption_audio\":null,\"caption_video\":null,\"caption_image\":null,\"caption_markdown\":null,\"caption\":\"Name\",\"binding\":\"/data/name\",\"question_id\":\"name\",\"required\":1,\"relevant\":0,\"answer\":\"fluffy\",\"datatype\":\"str\",\"style\":null,\"type\":\"question\",\"ix\":\"0\",\"choices\":null,\"repeatable\":null,\"exists\":null,\"header\":null,\"control\":1,\"help\":null,\"help_image\":null,\"help_audio\":null,\"help_video\":null,\"hint\":null,\"output\":null,\"add-choice\":null},{\"caption_audio\":null,\"caption_video\":null,\"caption_image\":null,\"caption_markdown\":null,\"caption\":\"Date\",\"binding\":\"/data/date\",\"question_id\":\"date\",\"required\":0,\"relevant\":0,\"answer\":null,\"datatype\":\"date\",\"style\":null,\"type\":\"question\",\"ix\":\"1\",\"choices\":null,\"repeatable\":null,\"exists\":null,\"header\":null,\"control\":1,\"help\":null,\"help_image\":null,\"help_audio\":null,\"help_video\":null,\"hint\":null,\"output\":null,\"add-choice\":null}],\"status\":\"accepted\",\"reason\":null,\"type\":null,\"event\":null,\"errors\":{},\"session_id\":null,\"seq_id\":1}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://www.commcarehq.org/formplayer/answer",
- "postData": {
- "text": "{\"action\":\"answer\",\"ix\":\"1\",\"answer\":\"2024-05-14\",\"answersToValidate\":{},\"domain\":\"demo_domain\",\"username\":\"user@example.com\",\"restoreAs\":null,\"session-id\":\"db67e0bf-a770-4b73-9e48-2ecbb2faefa1\",\"session_id\":\"db67e0bf-a770-4b73-9e48-2ecbb2faefa1\",\"debuggerEnabled\":true,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"Register cat\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":null,\"appVersion\":null,\"selections\":null,\"translations\":{},\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":null,\"instanceXml\":{\"output\":\"\\n fluffy\\n 2024-05-14\\n \\n \\n fluffy\\n 655a8581da22f05fb953b19d8561b079\\n cat\\n \\n \\n \\n Formplayer\\n 2024-05-14T13:38:56.606+02\\n \\n user@example.com\\n 655a8581da22f05fb953b19d8561b079\\n c0812de6-d8cc-4123-8204-3dda5b2b87a6\\n Formplayer Version: 2.53\\n \\n \\n\\n\"},\"tree\":[{\"caption_audio\":null,\"caption_video\":null,\"caption_image\":null,\"caption_markdown\":null,\"caption\":\"Name\",\"binding\":\"/data/name\",\"question_id\":\"name\",\"required\":1,\"relevant\":0,\"answer\":\"fluffy\",\"datatype\":\"str\",\"style\":null,\"type\":\"question\",\"ix\":\"0\",\"choices\":null,\"repeatable\":null,\"exists\":null,\"header\":null,\"control\":1,\"help\":null,\"help_image\":null,\"help_audio\":null,\"help_video\":null,\"hint\":null,\"output\":null,\"add-choice\":null},{\"caption_audio\":null,\"caption_video\":null,\"caption_image\":null,\"caption_markdown\":null,\"caption\":\"Date\",\"binding\":\"/data/date\",\"question_id\":\"date\",\"required\":0,\"relevant\":0,\"answer\":\"2024-05-14\",\"datatype\":\"date\",\"style\":null,\"type\":\"question\",\"ix\":\"1\",\"choices\":null,\"repeatable\":null,\"exists\":null,\"header\":null,\"control\":1,\"help\":null,\"help_image\":null,\"help_audio\":null,\"help_video\":null,\"hint\":null,\"output\":null,\"add-choice\":null}],\"status\":\"accepted\",\"reason\":null,\"type\":null,\"event\":null,\"errors\":{},\"session_id\":null,\"seq_id\":2}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://www.commcarehq.org/formplayer/submit-all",
- "postData": {
- "text": "{\"action\":\"submit-all\",\"answers\":{\"0\":\"fluffy\",\"1\":\"2024-05-14\"},\"prevalidated\":true,\"domain\":\"demo_domain\",\"username\":\"user@example.com\",\"restoreAs\":null,\"session-id\":\"db67e0bf-a770-4b73-9e48-2ecbb2faefa1\",\"session_id\":\"db67e0bf-a770-4b73-9e48-2ecbb2faefa1\",\"debuggerEnabled\":true,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":null,\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":null,\"appVersion\":null,\"selections\":null,\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":null,\"instanceXml\":null,\"status\":\"success\",\"submitResponseMessage\":\"'Register cat' successfully saved!\\n\\nYou submitted [this form](/a/demo_domain/reports/form_data/c0812de6-d8cc-4123-8204-3dda5b2b87a6/), which affected [this case](/a/demo_domain/reports/case_data/7a6d6f96-9bd4-41c0-8e52-e499497c4991/).\\n\\nClick to export your [case](/a/demo_domain/data/export/custom/case/) or [form](/a/demo_domain/data/export/custom/form/) data.\",\"errors\":{},\"nextScreen\":null,\"session_id\":null,\"seq_id\":0}"
- }
- }
- }
- ]
- }
-}
diff --git a/corehq/apps/app_execution/tests/data/search_again.json b/corehq/apps/app_execution/tests/data/search_again.json
deleted file mode 100644
index 3c97ea85b442..000000000000
--- a/corehq/apps/app_execution/tests/data/search_again.json
+++ /dev/null
@@ -1,118 +0,0 @@
-{
- "log": {
- "entries": [
- {
- "request": {
- "method": "POST",
- "url": "https://www.commcarehq.org/formplayer/navigate_menu_start",
- "postData": {
- "text": "{\"username\":\"skelly@dimagi.com\",\"restoreAs\":null,\"domain\":\"ush-envelope-testing\",\"app_id\":\"08ed729aa0fc402cbdcc8b6ba5d3b6b1\",\"locale\":\"en\",\"offset\":null,\"cases_per_page\":10,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"Case Search Baseline App\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"08ed729aa0fc402cbdcc8b6ba5d3b6b1\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 66\",\"selections\":null,\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"locales\":[\"default\",\"en\"],\"breadcrumbs\":[\"Case Search Baseline App\"],\"persistentCaseTile\":null,\"queryKey\":null,\"commands\":[{\"index\":0,\"displayText\":\"Subcase-exists search\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"},{\"index\":1,\"displayText\":\"Subcase-count search\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"},{\"index\":2,\"displayText\":\"Fuzzy Match Search\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"},{\"index\":3,\"displayText\":\"Phonetic Match Search\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"},{\"index\":4,\"displayText\":\"Baseline Case Search\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"},{\"index\":5,\"displayText\":\"Include Related Cases\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"}],\"type\":\"commands\",\"layoutStyle\":null}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://www.commcarehq.org/formplayer/navigate_menu",
- "postData": {
- "text": "{\"username\":\"skelly@dimagi.com\",\"restoreAs\":null,\"domain\":\"ush-envelope-testing\",\"app_id\":\"08ed729aa0fc402cbdcc8b6ba5d3b6b1\",\"locale\":\"en\",\"selections\":[\"5\"],\"offset\":0,\"search_text\":null,\"query_data\":{},\"cases_per_page\":10,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"Search All Cases\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"08ed729aa0fc402cbdcc8b6ba5d3b6b1\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 66\",\"selections\":[\"5\"],\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"locales\":[\"default\",\"en\"],\"breadcrumbs\":[\"Case Search Baseline App\",\"Include Related Cases\"],\"persistentCaseTile\":null,\"queryKey\":\"search_command.m5_results\",\"entities\":[{\"id\":\"d58917e568e74f9c9ae564ec823010f1\",\"data\":[\"Abdiel Munoz\",\"d58917e568e74f9c9ae564ec823010f1\"],\"groupKey\":null,\"altText\":[null,null]},{\"id\":\"37ca1b7c771f4b96b67ff90933cc8c5c\",\"data\":[\"Ace Whitaker\",\"37ca1b7c771f4b96b67ff90933cc8c5c\"],\"groupKey\":null,\"altText\":[null,null]},{\"id\":\"471b51187cce4e138bc63a4630dd6a67\",\"data\":[\"Adler Meyers\",\"471b51187cce4e138bc63a4630dd6a67\"],\"groupKey\":null,\"altText\":[null,null]},{\"id\":\"3a077d1e80d644f18f5590b462da8382\",\"data\":[\"Agustin House\",\"3a077d1e80d644f18f5590b462da8382\"],\"groupKey\":null,\"altText\":[null,null]},{\"id\":\"041bb34bc12e4437bc5b541c33bde258\",\"data\":[\"Ahmad Duran\",\"041bb34bc12e4437bc5b541c33bde258\"],\"groupKey\":null,\"altText\":[null,null]},{\"id\":\"fbdbc9558a7e4958ac25f07b6ca3401a\",\"data\":[\"Ahmad Garrison\",\"fbdbc9558a7e4958ac25f07b6ca3401a\"],\"groupKey\":null,\"altText\":[null,null]},{\"id\":\"5c3bd68da16a4a7c9c632d1a722f8b8d\",\"data\":[\"Alan Harrell\",\"5c3bd68da16a4a7c9c632d1a722f8b8d\"],\"groupKey\":null,\"altText\":[null,null]},{\"id\":\"3baa2919b6f845aba9e99cfdf709bcbd\",\"data\":[\"Albert Little\",\"3baa2919b6f845aba9e99cfdf709bcbd\"],\"groupKey\":null,\"altText\":[null,null]},{\"id\":\"7c618a2715934badac384f1b57c2e925\",\"data\":[\"Alberto Gates\",\"7c618a2715934badac384f1b57c2e925\"],\"groupKey\":null,\"altText\":[null,null]},{\"id\":\"e55bfc3e69354a7092e5fa818ac10a90\",\"data\":[\"Alberto Gutierrez\",\"e55bfc3e69354a7092e5fa818ac10a90\"],\"groupKey\":null,\"altText\":[null,null]}],\"actions\":[{\"text\":\"Search Again\",\"id\":null,\"input\":null,\"value\":null,\"receive\":null,\"hidden\":null,\"itemsetChoices\":null,\"itemsetChoicesKey\":null,\"hint\":null,\"error\":null,\"groupKey\":null,\"audio_uri\":null,\"image_uri\":null,\"allow_blank_value\":false,\"required\":false,\"required_msg\":null}],\"redoLast\":\"action 0\",\"styles\":[{\"fontSize\":0,\"widthHint\":-1,\"horizontalAlign\":null,\"verticalAlign\":null,\"showBorder\":false,\"showShading\":false,\"displayFormat\":null},{\"fontSize\":0,\"widthHint\":-1,\"horizontalAlign\":null,\"verticalAlign\":null,\"showBorder\":false,\"showShading\":false,\"displayFormat\":null}],\"endpointActions\":[null,null],\"headers\":[\"Name\",\"Case ID\"],\"tiles\":null,\"widthHints\":[50,50],\"numEntitiesPerRow\":0,\"useUniformUnits\":false,\"sortIndices\":[0],\"noItemsText\":null,\"selectText\":\"Continue\",\"pageCount\":50,\"currentPage\":0,\"type\":\"entities\",\"usesCaseTiles\":false,\"maxWidth\":0,\"maxHeight\":0,\"maxSelectValue\":-1,\"hasDetails\":true,\"queryResponse\":null,\"groupHeaderRows\":-1,\"multiSelect\":false}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://www.commcarehq.org/formplayer/navigate_menu",
- "postData": {
- "text": "{\"username\":\"skelly@dimagi.com\",\"restoreAs\":null,\"domain\":\"ush-envelope-testing\",\"app_id\":\"08ed729aa0fc402cbdcc8b6ba5d3b6b1\",\"locale\":\"en\",\"selections\":[\"5\"],\"offset\":0,\"search_text\":null,\"query_data\":{\"search_command.m5_results\":{\"execute\":false,\"force_manual_search\":true,\"selections\":[\"5\"],\"inputs\":null}},\"cases_per_page\":10,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"\u00a0\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"08ed729aa0fc402cbdcc8b6ba5d3b6b1\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 66\",\"selections\":[\"5\"],\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"locales\":[\"default\",\"en\"],\"breadcrumbs\":[\"Case Search Baseline App\",\"Include Related Cases\"],\"persistentCaseTile\":null,\"queryKey\":\"search_command.m5_results\",\"displays\":[{\"text\":\"First Name\",\"id\":\"first_name\",\"input\":null,\"value\":null,\"receive\":null,\"hidden\":null,\"itemsetChoices\":null,\"itemsetChoicesKey\":null,\"hint\":null,\"error\":null,\"groupKey\":null,\"audio_uri\":null,\"image_uri\":null,\"allow_blank_value\":false,\"required\":false,\"required_msg\":\"Sorry, this response is required!\"},{\"text\":\"Last Name\",\"id\":\"last_name\",\"input\":null,\"value\":null,\"receive\":null,\"hidden\":null,\"itemsetChoices\":null,\"itemsetChoicesKey\":null,\"hint\":null,\"error\":null,\"groupKey\":null,\"audio_uri\":null,\"image_uri\":null,\"allow_blank_value\":false,\"required\":false,\"required_msg\":\"Sorry, this response is required!\"}],\"type\":\"query\",\"description\":\"\u00a0\",\"groupHeaders\":{},\"searchOnClear\":false}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://www.commcarehq.org/formplayer/navigate_menu",
- "postData": {
- "text": "{\"username\":\"skelly@dimagi.com\",\"restoreAs\":null,\"domain\":\"ush-envelope-testing\",\"app_id\":\"08ed729aa0fc402cbdcc8b6ba5d3b6b1\",\"locale\":\"en\",\"selections\":[\"5\"],\"offset\":0,\"search_text\":null,\"query_data\":{\"search_command.m5_results\":{\"inputs\":{\"first_name\":\"Lucca\"},\"execute\":false,\"force_manual_search\":true,\"selections\":[\"5\"]}},\"cases_per_page\":10,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\",\"requestInitiatedByTag\":\"field_change\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"\u00a0\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"08ed729aa0fc402cbdcc8b6ba5d3b6b1\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 66\",\"selections\":[\"5\"],\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"locales\":[\"default\",\"en\"],\"breadcrumbs\":[\"Case Search Baseline App\",\"Include Related Cases\"],\"persistentCaseTile\":null,\"queryKey\":\"search_command.m5_results\",\"displays\":[{\"text\":\"First Name\",\"id\":\"first_name\",\"input\":null,\"value\":\"Lucca\",\"receive\":null,\"hidden\":null,\"itemsetChoices\":null,\"itemsetChoicesKey\":null,\"hint\":null,\"error\":null,\"groupKey\":null,\"audio_uri\":null,\"image_uri\":null,\"allow_blank_value\":false,\"required\":false,\"required_msg\":\"Sorry, this response is required!\"},{\"text\":\"Last Name\",\"id\":\"last_name\",\"input\":null,\"value\":null,\"receive\":null,\"hidden\":null,\"itemsetChoices\":null,\"itemsetChoicesKey\":null,\"hint\":null,\"error\":null,\"groupKey\":null,\"audio_uri\":null,\"image_uri\":null,\"allow_blank_value\":false,\"required\":false,\"required_msg\":\"Sorry, this response is required!\"}],\"type\":\"query\",\"description\":\"\u00a0\",\"groupHeaders\":{},\"searchOnClear\":false}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://www.commcarehq.org/formplayer/navigate_menu",
- "postData": {
- "text": "{\"username\":\"skelly@dimagi.com\",\"restoreAs\":null,\"domain\":\"ush-envelope-testing\",\"app_id\":\"08ed729aa0fc402cbdcc8b6ba5d3b6b1\",\"locale\":\"en\",\"selections\":[\"5\"],\"offset\":0,\"search_text\":null,\"query_data\":{\"search_command.m5_results\":{\"inputs\":{\"first_name\":\"Lucca\",\"last_name\":\"Mcpherson\"},\"execute\":false,\"force_manual_search\":true,\"selections\":[\"5\"]}},\"cases_per_page\":10,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\",\"requestInitiatedByTag\":\"field_change\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"\u00a0\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"08ed729aa0fc402cbdcc8b6ba5d3b6b1\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 66\",\"selections\":[\"5\"],\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"locales\":[\"default\",\"en\"],\"breadcrumbs\":[\"Case Search Baseline App\",\"Include Related Cases\"],\"persistentCaseTile\":null,\"queryKey\":\"search_command.m5_results\",\"displays\":[{\"text\":\"First Name\",\"id\":\"first_name\",\"input\":null,\"value\":\"Lucca\",\"receive\":null,\"hidden\":null,\"itemsetChoices\":null,\"itemsetChoicesKey\":null,\"hint\":null,\"error\":null,\"groupKey\":null,\"audio_uri\":null,\"image_uri\":null,\"allow_blank_value\":false,\"required\":false,\"required_msg\":\"Sorry, this response is required!\"},{\"text\":\"Last Name\",\"id\":\"last_name\",\"input\":null,\"value\":\"Mcpherson\",\"receive\":null,\"hidden\":null,\"itemsetChoices\":null,\"itemsetChoicesKey\":null,\"hint\":null,\"error\":null,\"groupKey\":null,\"audio_uri\":null,\"image_uri\":null,\"allow_blank_value\":false,\"required\":false,\"required_msg\":\"Sorry, this response is required!\"}],\"type\":\"query\",\"description\":\"\u00a0\",\"groupHeaders\":{},\"searchOnClear\":false}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://www.commcarehq.org/formplayer/navigate_menu",
- "postData": {
- "text": "{\"username\":\"skelly@dimagi.com\",\"restoreAs\":null,\"domain\":\"ush-envelope-testing\",\"app_id\":\"08ed729aa0fc402cbdcc8b6ba5d3b6b1\",\"locale\":\"en\",\"selections\":[\"5\"],\"offset\":0,\"search_text\":null,\"query_data\":{\"search_command.m5_results\":{\"execute\":true,\"force_manual_search\":true,\"selections\":[\"5\"],\"inputs\":{\"first_name\":\"Lucca\",\"last_name\":\"Mcpherson\"}}},\"cases_per_page\":10,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"Search All Cases\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"08ed729aa0fc402cbdcc8b6ba5d3b6b1\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 66\",\"selections\":[\"5\"],\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"locales\":[\"default\",\"en\"],\"breadcrumbs\":[\"Case Search Baseline App\",\"Include Related Cases\"],\"persistentCaseTile\":null,\"queryKey\":\"search_command.m5_results\",\"entities\":[{\"id\":\"18e434037dae4d87b98e77687a2aeff4\",\"data\":[\"Lucca Mcpherson\",\"18e434037dae4d87b98e77687a2aeff4\"],\"groupKey\":null,\"altText\":[null,null]}],\"actions\":[{\"text\":\"Search Again\",\"id\":null,\"input\":null,\"value\":null,\"receive\":null,\"hidden\":null,\"itemsetChoices\":null,\"itemsetChoicesKey\":null,\"hint\":null,\"error\":null,\"groupKey\":null,\"audio_uri\":null,\"image_uri\":null,\"allow_blank_value\":false,\"required\":false,\"required_msg\":null}],\"redoLast\":\"action 0\",\"styles\":[{\"fontSize\":0,\"widthHint\":-1,\"horizontalAlign\":null,\"verticalAlign\":null,\"showBorder\":false,\"showShading\":false,\"displayFormat\":null},{\"fontSize\":0,\"widthHint\":-1,\"horizontalAlign\":null,\"verticalAlign\":null,\"showBorder\":false,\"showShading\":false,\"displayFormat\":null}],\"endpointActions\":[null,null],\"headers\":[\"Name\",\"Case ID\"],\"tiles\":null,\"widthHints\":[50,50],\"numEntitiesPerRow\":0,\"useUniformUnits\":false,\"sortIndices\":[0],\"noItemsText\":null,\"selectText\":\"Continue\",\"pageCount\":0,\"currentPage\":0,\"type\":\"entities\",\"usesCaseTiles\":false,\"maxWidth\":0,\"maxHeight\":0,\"maxSelectValue\":-1,\"hasDetails\":true,\"queryResponse\":null,\"groupHeaderRows\":-1,\"multiSelect\":false}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://www.commcarehq.org/formplayer/get_details",
- "postData": {
- "text": "{\"username\":\"skelly@dimagi.com\",\"restoreAs\":null,\"domain\":\"ush-envelope-testing\",\"app_id\":\"08ed729aa0fc402cbdcc8b6ba5d3b6b1\",\"locale\":\"en\",\"selections\":[\"5\",\"18e434037dae4d87b98e77687a2aeff4\"],\"offset\":0,\"search_text\":null,\"query_data\":{\"search_command.m5_results\":{\"execute\":true,\"force_manual_search\":true,\"selections\":[\"5\"],\"inputs\":{\"first_name\":\"Lucca\",\"last_name\":\"Mcpherson\"}}},\"cases_per_page\":10,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\",\"isShortDetail\":false}"
- }
- },
- "response": {
- "content": {
- "text": "{\"details\":[{\"details\":[\"Lucca Mcpherson\"],\"entities\":null,\"styles\":[{\"fontSize\":0,\"widthHint\":-1,\"horizontalAlign\":null,\"verticalAlign\":null,\"showBorder\":false,\"showShading\":false,\"displayFormat\":null}],\"headers\":[\"Name\"],\"altText\":[null],\"title\":\"Case\",\"usesCaseTiles\":false,\"maxWidth\":0,\"maxHeight\":0,\"numEntitiesPerRow\":0,\"tiles\":null,\"useUniformUnits\":false,\"hasInlineTile\":false,\"useNodeset\":false}],\"isPersistentDetail\":false}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://www.commcarehq.org/formplayer/navigate_menu",
- "postData": {
- "text": "{\"username\":\"skelly@dimagi.com\",\"restoreAs\":null,\"domain\":\"ush-envelope-testing\",\"app_id\":\"08ed729aa0fc402cbdcc8b6ba5d3b6b1\",\"locale\":\"en\",\"selections\":[\"5\",\"18e434037dae4d87b98e77687a2aeff4\"],\"offset\":0,\"search_text\":null,\"query_data\":{\"search_command.m5_results\":{\"execute\":true,\"force_manual_search\":true,\"selections\":[\"5\"],\"inputs\":{\"first_name\":\"Lucca\",\"last_name\":\"Mcpherson\"}}},\"cases_per_page\":10,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"Followup\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"08ed729aa0fc402cbdcc8b6ba5d3b6b1\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 66\",\"selections\":[\"5\",\"18e434037dae4d87b98e77687a2aeff4\"],\"translations\":{\"forms.m5f0.submit_label\":\"Submit\"},\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":true,\"appInstall\":false},\"instanceXml\":{\"output\":\"\\n \\n \\n \\n Formplayer\\n 2024-05-17T10:35:36.603+02\\n \\n skelly@dimagi.com\\n 655a8581da22f05fb953b19d8561b079\\n b902640e-65b7-4d96-b0c5-b9d1bc0dd063\\n Formplayer Version: 2.53\\n \\n \\n\\n\"},\"tree\":[{\"caption_audio\":null,\"caption_video\":null,\"caption_image\":null,\"caption_markdown\":null,\"caption\":\"test\",\"binding\":\"/data/test\",\"question_id\":\"test\",\"required\":0,\"relevant\":0,\"answer\":null,\"datatype\":\"str\",\"style\":null,\"type\":\"question\",\"ix\":\"0\",\"choices\":null,\"repeatable\":null,\"exists\":null,\"header\":null,\"control\":1,\"help\":null,\"help_image\":null,\"help_audio\":null,\"help_video\":null,\"hint\":null,\"output\":null,\"add-choice\":null}],\"langs\":[\"en\"],\"breadcrumbs\":[\"Case Search Baseline App\",\"Include Related Cases\",\"Lucca Mcpherson\"],\"persistentCaseTile\":null,\"event\":null,\"session_id\":\"a66abec1-d0d1-4944-9334-6af8ec3cb1ac\",\"seq_id\":0}"
- }
- }
- }
- ]
- }
-}
diff --git a/corehq/apps/app_execution/tests/data/split_case_search_search.json b/corehq/apps/app_execution/tests/data/split_case_search_search.json
deleted file mode 100644
index db3c37bb2cd2..000000000000
--- a/corehq/apps/app_execution/tests/data/split_case_search_search.json
+++ /dev/null
@@ -1,90 +0,0 @@
-{
- "log": {
- "entries": [
- {
- "request": {
- "method": "POST",
- "url": "https://staging.commcarehq.org/formplayer/navigate_menu_start",
- "postData": {
- "text": "{\"username\":\"skelly@dimagi.com\",\"restoreAs\":null,\"domain\":\"skelly\",\"app_id\":\"85243995d0274a6699ba92650df77787\",\"locale\":\"en\",\"offset\":null,\"cases_per_page\":10,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"Stale case search data test\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"85243995d0274a6699ba92650df77787\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 1\",\"selections\":null,\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"locales\":[\"default\",\"en\"],\"breadcrumbs\":[\"Stale case search data test\"],\"persistentCaseTile\":null,\"queryKey\":null,\"commands\":[{\"index\":0,\"displayText\":\"Registration\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"},{\"index\":1,\"displayText\":\"Pending Cases\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"},{\"index\":2,\"displayText\":\"Case List\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"}],\"type\":\"commands\",\"layoutStyle\":null}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://staging.commcarehq.org/formplayer/navigate_menu",
- "postData": {
- "text": "{\"username\":\"skelly@dimagi.com\",\"restoreAs\":null,\"domain\":\"skelly\",\"app_id\":\"85243995d0274a6699ba92650df77787\",\"locale\":\"en\",\"selections\":[\"1\"],\"offset\":0,\"search_text\":null,\"query_data\":{},\"cases_per_page\":10,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"Search All Cases\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"85243995d0274a6699ba92650df77787\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 1\",\"selections\":[\"1\"],\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"locales\":[\"default\",\"en\"],\"breadcrumbs\":[\"Stale case search data test\",\"Pending Cases\"],\"persistentCaseTile\":null,\"queryKey\":\"search_command.m1_results\",\"entities\":[{\"id\":\"0da3e5c6-f069-49be-aab3-53f2b9b7ebd0\",\"data\":[\"stale1\",\"pending\",\"pending\"],\"groupKey\":null,\"altText\":[null,null,null]}],\"actions\":[],\"redoLast\":null,\"styles\":[{\"fontSize\":0,\"widthHint\":-1,\"horizontalAlign\":null,\"verticalAlign\":null,\"showBorder\":false,\"showShading\":false,\"displayFormat\":null},{\"fontSize\":0,\"widthHint\":-1,\"horizontalAlign\":null,\"verticalAlign\":null,\"showBorder\":false,\"showShading\":false,\"displayFormat\":null},{\"fontSize\":0,\"widthHint\":-1,\"horizontalAlign\":null,\"verticalAlign\":null,\"showBorder\":false,\"showShading\":false,\"displayFormat\":null}],\"endpointActions\":[null,null,null],\"headers\":[\"Name\",\"Current Status\",\"casedb status\"],\"tiles\":null,\"widthHints\":[33,33,33],\"numEntitiesPerRow\":0,\"useUniformUnits\":false,\"sortIndices\":[0],\"noItemsText\":null,\"selectText\":null,\"pageCount\":0,\"currentPage\":0,\"type\":\"entities\",\"usesCaseTiles\":false,\"maxWidth\":0,\"maxHeight\":0,\"maxSelectValue\":-1,\"hasDetails\":true,\"queryResponse\":{\"notification\":null,\"title\":\"Case Claim\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":null,\"appVersion\":null,\"selections\":null,\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":true,\"metaData\":null,\"locales\":[\"default\",\"en\"],\"breadcrumbs\":null,\"persistentCaseTile\":null,\"queryKey\":\"search_command.m1_results\",\"displays\":[{\"text\":\"Name\",\"id\":\"name\",\"input\":null,\"value\":null,\"receive\":null,\"hidden\":null,\"itemsetChoices\":null,\"itemsetChoicesKey\":null,\"hint\":null,\"error\":null,\"groupKey\":null,\"audio_uri\":null,\"image_uri\":null,\"allow_blank_value\":false,\"required\":false,\"required_msg\":\"Sorry, this response is required!\"}],\"type\":\"query\",\"description\":\"\u00a0\",\"groupHeaders\":{},\"searchOnClear\":false},\"groupHeaderRows\":-1,\"multiSelect\":false}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://staging.commcarehq.org/formplayer/navigate_menu",
- "postData": {
- "text": "{\"username\":\"skelly@dimagi.com\",\"restoreAs\":null,\"domain\":\"skelly\",\"app_id\":\"85243995d0274a6699ba92650df77787\",\"locale\":\"en\",\"selections\":[\"1\"],\"offset\":0,\"search_text\":null,\"query_data\":{\"search_command.m1_results\":{\"inputs\":{\"name\":\"stale1\"},\"execute\":false,\"force_manual_search\":true,\"selections\":[\"1\"]}},\"cases_per_page\":10,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\",\"requestInitiatedByTag\":\"field_change\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"Case Claim\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"85243995d0274a6699ba92650df77787\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 1\",\"selections\":[\"1\"],\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":true,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"locales\":[\"default\",\"en\"],\"breadcrumbs\":[\"Stale case search data test\",\"Pending Cases\"],\"persistentCaseTile\":null,\"queryKey\":\"search_command.m1_results\",\"displays\":[{\"text\":\"Name\",\"id\":\"name\",\"input\":null,\"value\":\"stale1\",\"receive\":null,\"hidden\":null,\"itemsetChoices\":null,\"itemsetChoicesKey\":null,\"hint\":null,\"error\":null,\"groupKey\":null,\"audio_uri\":null,\"image_uri\":null,\"allow_blank_value\":false,\"required\":false,\"required_msg\":\"Sorry, this response is required!\"}],\"type\":\"query\",\"description\":\"\u00a0\",\"groupHeaders\":{},\"searchOnClear\":false}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://staging.commcarehq.org/formplayer/navigate_menu",
- "postData": {
- "text": "{\"username\":\"skelly@dimagi.com\",\"restoreAs\":null,\"domain\":\"skelly\",\"app_id\":\"85243995d0274a6699ba92650df77787\",\"locale\":\"en\",\"selections\":[\"1\"],\"offset\":0,\"search_text\":null,\"query_data\":{\"search_command.m1_results\":{\"execute\":true,\"force_manual_search\":true,\"selections\":[\"1\"],\"inputs\":{\"name\":\"stale1\"}}},\"cases_per_page\":10,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"Search All Cases\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"85243995d0274a6699ba92650df77787\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 1\",\"selections\":[\"1\"],\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"locales\":[\"default\",\"en\"],\"breadcrumbs\":[\"Stale case search data test\",\"Pending Cases\"],\"persistentCaseTile\":null,\"queryKey\":\"search_command.m1_results\",\"entities\":[{\"id\":\"0da3e5c6-f069-49be-aab3-53f2b9b7ebd0\",\"data\":[\"stale1\",\"pending\",\"pending\"],\"groupKey\":null,\"altText\":[null,null,null]}],\"actions\":[],\"redoLast\":null,\"styles\":[{\"fontSize\":0,\"widthHint\":-1,\"horizontalAlign\":null,\"verticalAlign\":null,\"showBorder\":false,\"showShading\":false,\"displayFormat\":null},{\"fontSize\":0,\"widthHint\":-1,\"horizontalAlign\":null,\"verticalAlign\":null,\"showBorder\":false,\"showShading\":false,\"displayFormat\":null},{\"fontSize\":0,\"widthHint\":-1,\"horizontalAlign\":null,\"verticalAlign\":null,\"showBorder\":false,\"showShading\":false,\"displayFormat\":null}],\"endpointActions\":[null,null,null],\"headers\":[\"Name\",\"Current Status\",\"casedb status\"],\"tiles\":null,\"widthHints\":[33,33,33],\"numEntitiesPerRow\":0,\"useUniformUnits\":false,\"sortIndices\":[0],\"noItemsText\":null,\"selectText\":null,\"pageCount\":0,\"currentPage\":0,\"type\":\"entities\",\"usesCaseTiles\":false,\"maxWidth\":0,\"maxHeight\":0,\"maxSelectValue\":-1,\"hasDetails\":true,\"queryResponse\":{\"notification\":null,\"title\":\"Case Claim\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":null,\"appVersion\":null,\"selections\":null,\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":true,\"metaData\":null,\"locales\":[\"default\",\"en\"],\"breadcrumbs\":null,\"persistentCaseTile\":null,\"queryKey\":\"search_command.m1_results\",\"displays\":[{\"text\":\"Name\",\"id\":\"name\",\"input\":null,\"value\":\"stale1\",\"receive\":null,\"hidden\":null,\"itemsetChoices\":null,\"itemsetChoicesKey\":null,\"hint\":null,\"error\":null,\"groupKey\":null,\"audio_uri\":null,\"image_uri\":null,\"allow_blank_value\":false,\"required\":false,\"required_msg\":\"Sorry, this response is required!\"}],\"type\":\"query\",\"description\":\"\u00a0\",\"groupHeaders\":{},\"searchOnClear\":false},\"groupHeaderRows\":-1,\"multiSelect\":false}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://staging.commcarehq.org/formplayer/get_details",
- "postData": {
- "text": "{\"username\":\"skelly@dimagi.com\",\"restoreAs\":null,\"domain\":\"skelly\",\"app_id\":\"85243995d0274a6699ba92650df77787\",\"locale\":\"en\",\"selections\":[\"1\",\"0da3e5c6-f069-49be-aab3-53f2b9b7ebd0\"],\"offset\":0,\"search_text\":null,\"query_data\":{\"search_command.m1_results\":{\"execute\":true,\"force_manual_search\":true,\"selections\":[\"1\"],\"inputs\":{\"name\":\"stale1\"}}},\"cases_per_page\":10,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\",\"isShortDetail\":false}"
- }
- },
- "response": {
- "content": {
- "text": "{\"details\":[{\"details\":[\"stale1\"],\"entities\":null,\"styles\":[{\"fontSize\":0,\"widthHint\":-1,\"horizontalAlign\":null,\"verticalAlign\":null,\"showBorder\":false,\"showShading\":false,\"displayFormat\":null}],\"headers\":[\"Name\"],\"altText\":[null],\"title\":\"Case\",\"usesCaseTiles\":false,\"maxWidth\":0,\"maxHeight\":0,\"numEntitiesPerRow\":0,\"tiles\":null,\"useUniformUnits\":false,\"hasInlineTile\":false,\"useNodeset\":false}],\"isPersistentDetail\":false}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://staging.commcarehq.org/formplayer/navigate_menu",
- "postData": {
- "text": "{\"username\":\"skelly@dimagi.com\",\"restoreAs\":null,\"domain\":\"skelly\",\"app_id\":\"85243995d0274a6699ba92650df77787\",\"locale\":\"en\",\"selections\":[\"1\",\"0da3e5c6-f069-49be-aab3-53f2b9b7ebd0\"],\"offset\":0,\"search_text\":null,\"query_data\":{\"search_command.m1_results\":{\"execute\":true,\"force_manual_search\":true,\"selections\":[\"1\"],\"inputs\":{\"name\":\"stale1\"}}},\"cases_per_page\":10,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"Pending Cases\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"85243995d0274a6699ba92650df77787\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 1\",\"selections\":[\"1\",\"0da3e5c6-f069-49be-aab3-53f2b9b7ebd0\"],\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"locales\":[\"default\",\"en\"],\"breadcrumbs\":[\"Stale case search data test\",\"Pending Cases\",\"stale1\"],\"persistentCaseTile\":null,\"queryKey\":null,\"commands\":[{\"index\":0,\"displayText\":\"Followup Form Pending\",\"navigationState\":\"JUMP\",\"badgeText\":\"\"},{\"index\":1,\"displayText\":\"Duplicate search\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"}],\"type\":\"commands\",\"layoutStyle\":null}"
- }
- }
- }
- ]
- }
-}
diff --git a/corehq/apps/app_execution/tests/data/split_case_search_select.json b/corehq/apps/app_execution/tests/data/split_case_search_select.json
deleted file mode 100644
index 585b5b461d1c..000000000000
--- a/corehq/apps/app_execution/tests/data/split_case_search_select.json
+++ /dev/null
@@ -1,62 +0,0 @@
-{
- "log": {
- "entries": [
- {
- "request": {
- "method": "POST",
- "url": "https://staging.commcarehq.org/formplayer/navigate_menu_start",
- "postData": {
- "text": "{\"username\":\"skelly@dimagi.com\",\"restoreAs\":null,\"domain\":\"skelly\",\"app_id\":\"85243995d0274a6699ba92650df77787\",\"locale\":\"en\",\"offset\":null,\"cases_per_page\":10,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"Stale case search data test\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"85243995d0274a6699ba92650df77787\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 1\",\"selections\":null,\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"locales\":[\"default\",\"en\"],\"breadcrumbs\":[\"Stale case search data test\"],\"persistentCaseTile\":null,\"queryKey\":null,\"commands\":[{\"index\":0,\"displayText\":\"Registration\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"},{\"index\":1,\"displayText\":\"Pending Cases\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"},{\"index\":2,\"displayText\":\"Case List\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"}],\"type\":\"commands\",\"layoutStyle\":null}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://staging.commcarehq.org/formplayer/navigate_menu",
- "postData": {
- "text": "{\"username\":\"skelly@dimagi.com\",\"restoreAs\":null,\"domain\":\"skelly\",\"app_id\":\"85243995d0274a6699ba92650df77787\",\"locale\":\"en\",\"selections\":[\"1\"],\"offset\":0,\"search_text\":null,\"query_data\":{},\"cases_per_page\":10,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"Search All Cases\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"85243995d0274a6699ba92650df77787\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 1\",\"selections\":[\"1\"],\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"locales\":[\"default\",\"en\"],\"breadcrumbs\":[\"Stale case search data test\",\"Pending Cases\"],\"persistentCaseTile\":null,\"queryKey\":\"search_command.m1_results\",\"entities\":[{\"id\":\"0da3e5c6-f069-49be-aab3-53f2b9b7ebd0\",\"data\":[\"stale1\",\"pending\",\"pending\"],\"groupKey\":null,\"altText\":[null,null,null]}],\"actions\":[],\"redoLast\":null,\"styles\":[{\"fontSize\":0,\"widthHint\":-1,\"horizontalAlign\":null,\"verticalAlign\":null,\"showBorder\":false,\"showShading\":false,\"displayFormat\":null},{\"fontSize\":0,\"widthHint\":-1,\"horizontalAlign\":null,\"verticalAlign\":null,\"showBorder\":false,\"showShading\":false,\"displayFormat\":null},{\"fontSize\":0,\"widthHint\":-1,\"horizontalAlign\":null,\"verticalAlign\":null,\"showBorder\":false,\"showShading\":false,\"displayFormat\":null}],\"endpointActions\":[null,null,null],\"headers\":[\"Name\",\"Current Status\",\"casedb status\"],\"tiles\":null,\"widthHints\":[33,33,33],\"numEntitiesPerRow\":0,\"useUniformUnits\":false,\"sortIndices\":[0],\"noItemsText\":null,\"selectText\":null,\"pageCount\":0,\"currentPage\":0,\"type\":\"entities\",\"usesCaseTiles\":false,\"maxWidth\":0,\"maxHeight\":0,\"maxSelectValue\":-1,\"hasDetails\":true,\"queryResponse\":{\"notification\":null,\"title\":\"Case Claim\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":null,\"appVersion\":null,\"selections\":null,\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":true,\"metaData\":null,\"locales\":[\"default\",\"en\"],\"breadcrumbs\":null,\"persistentCaseTile\":null,\"queryKey\":\"search_command.m1_results\",\"displays\":[{\"text\":\"Name\",\"id\":\"name\",\"input\":null,\"value\":null,\"receive\":null,\"hidden\":null,\"itemsetChoices\":null,\"itemsetChoicesKey\":null,\"hint\":null,\"error\":null,\"groupKey\":null,\"audio_uri\":null,\"image_uri\":null,\"allow_blank_value\":false,\"required\":false,\"required_msg\":\"Sorry, this response is required!\"}],\"type\":\"query\",\"description\":\"\u00a0\",\"groupHeaders\":{},\"searchOnClear\":false},\"groupHeaderRows\":-1,\"multiSelect\":false}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://staging.commcarehq.org/formplayer/get_details",
- "postData": {
- "text": "{\"username\":\"skelly@dimagi.com\",\"restoreAs\":null,\"domain\":\"skelly\",\"app_id\":\"85243995d0274a6699ba92650df77787\",\"locale\":\"en\",\"selections\":[\"1\",\"0da3e5c6-f069-49be-aab3-53f2b9b7ebd0\"],\"offset\":0,\"search_text\":null,\"query_data\":{},\"cases_per_page\":10,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\",\"isShortDetail\":false}"
- }
- },
- "response": {
- "content": {
- "text": "{\"details\":[{\"details\":[\"stale1\"],\"entities\":null,\"styles\":[{\"fontSize\":0,\"widthHint\":-1,\"horizontalAlign\":null,\"verticalAlign\":null,\"showBorder\":false,\"showShading\":false,\"displayFormat\":null}],\"headers\":[\"Name\"],\"altText\":[null],\"title\":\"Case\",\"usesCaseTiles\":false,\"maxWidth\":0,\"maxHeight\":0,\"numEntitiesPerRow\":0,\"tiles\":null,\"useUniformUnits\":false,\"hasInlineTile\":false,\"useNodeset\":false}],\"isPersistentDetail\":false}"
- }
- }
- },
- {
- "request": {
- "method": "POST",
- "url": "https://staging.commcarehq.org/formplayer/navigate_menu",
- "postData": {
- "text": "{\"username\":\"skelly@dimagi.com\",\"restoreAs\":null,\"domain\":\"skelly\",\"app_id\":\"85243995d0274a6699ba92650df77787\",\"locale\":\"en\",\"selections\":[\"1\",\"0da3e5c6-f069-49be-aab3-53f2b9b7ebd0\"],\"offset\":0,\"search_text\":null,\"query_data\":{},\"cases_per_page\":10,\"preview\":false,\"tz_offset_millis\":7200000,\"tz_from_browser\":\"Africa/Johannesburg\"}"
- }
- },
- "response": {
- "content": {
- "text": "{\"notification\":null,\"title\":\"Pending Cases\",\"clearSession\":false,\"shouldAutoSubmit\":false,\"appId\":\"85243995d0274a6699ba92650df77787\",\"appVersion\":\"Formplayer Version: 2.53, App Version: 1\",\"selections\":[\"1\",\"0da3e5c6-f069-49be-aab3-53f2b9b7ebd0\"],\"translations\":null,\"smartLinkRedirect\":null,\"dynamicSearch\":false,\"metaData\":{\"attemptRestore\":false,\"appInstall\":false},\"locales\":[\"default\",\"en\"],\"breadcrumbs\":[\"Stale case search data test\",\"Pending Cases\",\"stale1\"],\"persistentCaseTile\":null,\"queryKey\":null,\"commands\":[{\"index\":0,\"displayText\":\"Followup Form Pending\",\"navigationState\":\"JUMP\",\"badgeText\":\"\"},{\"index\":1,\"displayText\":\"Duplicate search\",\"navigationState\":\"NEXT\",\"badgeText\":\"\"}],\"type\":\"commands\",\"layoutStyle\":null}"
- }
- }
- }
- ]
- }
-}
diff --git a/corehq/apps/app_execution/tests/mock_formplayer.py b/corehq/apps/app_execution/tests/mock_formplayer.py
deleted file mode 100644
index 6fdff5dff678..000000000000
--- a/corehq/apps/app_execution/tests/mock_formplayer.py
+++ /dev/null
@@ -1,108 +0,0 @@
-import dataclasses
-import json
-from functools import cached_property
-
-from corehq.apps.app_execution.api import BaseFormplayerClient
-from corehq.apps.app_execution.tests import response_factory as factory
-
-
-@dataclasses.dataclass
-class Screen:
- name: str
- children: list
-
- def process_selections(self, selections, data):
- output = {}
- option = self
- for selection in selections:
- option = option.get_next(selection)
- option, partial_data = option.execute(data)
- output.update(partial_data)
-
- return {**output, **option.get_response_data(selections)}
-
- def get_next(self, selection):
- return self.children[int(selection)]
-
- def execute(self, data):
- return self, {}
-
- def get_response_data(self, selections):
- pass
-
-
-@dataclasses.dataclass
-class Menu(Screen):
- def get_response_data(self, selections):
- return factory.command_response(selections, [child.name for child in self.children])
-
-
-@dataclasses.dataclass
-class CaseList(Screen):
- cases: list = dataclasses.field(default_factory=list)
-
- @cached_property
- def entities(self):
- return factory.make_entities(self.cases)
-
- def get_response_data(self, selections):
- return factory.entity_list_response(selections, self.entities)
-
- def get_next(self, selection):
- assert selection in [e["id"] for e in self.entities], selection
- return Menu(name="Forms", children=self.children)
-
-
-@dataclasses.dataclass
-class CaseSearch(Screen):
- query_key: str
- displays: list = dataclasses.field(default_factory=list)
-
- def __post_init__(self):
- assert len(self.children) == 1, len(self.children)
- assert isinstance(self.children[0], CaseList), self.children[0]
-
- def get_response_data(self, selections):
- return factory.query_response(selections, self.query_key, self.displays)
-
- def execute(self, data):
- query_data = data.get("query_data", {})
- if query_data.get(self.query_key, {}).get("execute"):
- return self.children[0], {"query_data": query_data}
- return self, {"query_data": query_data}
-
- def get_next(self, selection):
- raise NotImplementedError("CaseSearch does not support selections")
-
-
-@dataclasses.dataclass
-class Form(Screen):
-
- def get_response_data(self, selections):
- return factory.form_response(selections, self.children)
-
-
-class MockFormplayerClient(BaseFormplayerClient):
- def __init__(self, app):
- self.app = app
- self.form_session = {}
- super().__init__("domain", "username", "user_id")
-
- def _make_request(self, endpoint, data_bytes, headers):
- data = json.loads(data_bytes.decode("utf-8"))
- if "navigate_menu" in endpoint:
- selections = data["selections"]
- output = self.app.process_selections(selections, data)
- if "tree" in output:
- self.form_session = output
- return output
- else:
- # form response
- if not self.form_session:
- raise ValueError("No session data")
- assert data.get("session_id") == self.form_session["session_id"]
- if data["action"] == "answer":
- self.form_session["tree"][int(data["ix"])]["answer"] = data["answer"]
- elif data["action"] == "submit-all":
- return {"submitResponseMessage": "success", "nextScreen": None}
- return self.form_session
diff --git a/corehq/apps/app_execution/tests/response_factory.py b/corehq/apps/app_execution/tests/response_factory.py
deleted file mode 100644
index ccfb844504e3..000000000000
--- a/corehq/apps/app_execution/tests/response_factory.py
+++ /dev/null
@@ -1,100 +0,0 @@
-import uuid
-
-
-def command_response(selections, commands):
- return {
- "title": "Simple app",
- "selections": selections,
- "commands": [{"index": index, "displayText": command} for index, command in enumerate(commands)],
- "type": "commands",
- }
-
-
-def entity_list_response(selections, entities):
- """
- Returns a response for a form screen
-
- Args:
- selections: list of selections
- entities: list of entities
- id: str
- data: list[str]
- """
- return {
- "title": "Followup Form",
- "type": "entities",
- "selections": selections,
- "entities": entities,
- }
-
-
-def query_response(selections, query_key, displays):
- """
- Returns a response for a search screen
-
- Args:
- selections: list of selections
- query_key: query key
- displays: list of displays
- id: str
- value: str
- required: bool
- allow_blank_value: bool
- """
- return {
- "title": "Case Search",
- "type": "query",
- "queryKey": query_key,
- "selections": selections,
- "displays": displays,
- }
-
-
-def form_response(selections, questions):
- """
- Returns a response for a form screen
-
- Args:
- selections: list of selections
- questions: list of questions
- ix: str
- caption: str
- question_id: str
- answer: str | None
- datatype: str
- type: str
- choices: list[str] | None
- """
- return {
- "title": "Survey",
- "selections": selections,
- "tree": questions,
- "session_id": "8e212c16-00ac-4060-bcee-a42ad430f614"
- }
-
-
-def make_entities(case_data):
- return [make_entity(case) for case in case_data]
-
-
-def make_entity(case):
- return {"id": case.get("id", str(uuid.uuid4())), "data": [case["name"]]}
-
-
-def make_questions(captions, datatype="str"):
- return [
- make_question(ix, caption, f"question_{ix}", datatype=datatype)
- for ix, caption in enumerate(captions)
- ]
-
-
-def make_question(ix, caption, question_id, answer=None, datatype="str", type_="question", choices=None):
- return {
- "ix": ix,
- "caption": caption,
- "question_id": question_id,
- "answer": answer,
- "datatype": datatype,
- "type": type_,
- "choices": choices,
- }
diff --git a/corehq/apps/app_execution/tests/test_dsl.py b/corehq/apps/app_execution/tests/test_dsl.py
deleted file mode 100644
index 90838db50088..000000000000
--- a/corehq/apps/app_execution/tests/test_dsl.py
+++ /dev/null
@@ -1,98 +0,0 @@
-from django.test import SimpleTestCase
-from testil import eq
-
-from corehq.apps.app_execution import data_model
-from corehq.apps.app_execution.data_model import AppWorkflow
-from corehq.apps.app_execution.exceptions import AppExecutionError
-from corehq.apps.app_execution.tests.test_expectations import get_workflow_with_all_expectation_steps
-from corehq.apps.app_execution.tests.test_steps import get_workflow_with_all_steps
-from corehq.apps.app_execution.workflow_dsl import DSL_MAP, dsl_to_workflow, workflow_to_dsl
-from corehq.apps.app_manager.tests.views.test_apply_patch import assert_no_diff
-
-
-class TestDsl(SimpleTestCase):
-
- def test_map_has_all_steps(self):
- missing = set(DSL_MAP) - set(data_model.steps.STEP_MAP) - set(data_model.expectations.TYPE_MAP)
- self.assertEqual(missing, set())
-
- def test_workflow_to_dsl(self):
- workflow = _get_workflow()
- dsl = workflow_to_dsl(workflow)
- assert_no_diff(_get_dsl(), dsl)
-
- def test_dsl_to_workflow(self):
- workflow = dsl_to_workflow(_get_dsl())
- eq(workflow, _get_workflow())
-
- def test_dsl_to_workflow_invalid_step(self):
- dsl = "make me a sandwich"
- message = "Invalid step: make me a sandwich"
- with self.assertRaisesMessage(AppExecutionError, message):
- dsl_to_workflow(dsl)
-
- def test_dsl_blank_lines_and_comments(self):
- dsl = """
- # blank lines and comments are ignored
- Select menu "Case Search"
-
- # indents are ignored
- Select menu with ID "action 0"
- """
- workflow = dsl_to_workflow(dsl)
- eq(workflow, data_model.AppWorkflow(steps=[
- data_model.steps.CommandStep("Case Search"),
- data_model.steps.CommandIdStep("action 0"),
- ]))
-
- def test_expectations_in_form(self):
- dsl = """
- Start form
- Expect case present @case_id = '123'
- Answer question "Name" with "str"
- Expect question "/data/question1" with "123"
- Submit form
- End form
- """
- workflow = dsl_to_workflow(dsl)
- eq(workflow, data_model.AppWorkflow(steps=[
- data_model.steps.FormStep(children=[
- data_model.expectations.CasePresent(xpath_filter="@case_id = '123'"),
- data_model.steps.AnswerQuestionStep("Name", "str"),
- data_model.expectations.QuestionValue("/data/question1", "123"),
- data_model.steps.SubmitFormStep(),
- ]),
- ]))
-
-
-def _get_workflow():
- steps = get_workflow_with_all_steps().steps
- expectations = get_workflow_with_all_expectation_steps().steps
- return AppWorkflow(steps=steps + expectations)
-
-
-def _get_dsl():
- lines = [
- 'Select menu "Case Search"',
- 'Select menu with ID "action 0"',
- 'Update search parameters first_name="query value"',
- 'Update search parameters last_name="query value"',
- 'Search with parameters first_name="query value", last_name="query value"',
- 'Select entity with ID "123"',
- 'Select entity at index 2',
- 'Clear search',
- 'Raw navigation request data {"selections": ["0", "1", "123abc"]}',
- 'Select menu "Followup Case"',
- 'Select entities with IDs "xyz, abc"',
- 'Select entities at indexes 0, 2',
- 'Start form',
- ' Answer question "Name" with "str"',
- ' Answer question with ID "name" with "str"',
- ' Submit form',
- 'End form',
- "Expect xpath instance('commcaresession')/session/data/case/@case_id = '123'",
- "Expect case present @case_id = '123'",
- "Expect case absent @case_id = '345'",
- 'Expect question "/data/question1" with "123"',
- ]
- return '\n'.join(lines)
diff --git a/corehq/apps/app_execution/tests/test_execution.py b/corehq/apps/app_execution/tests/test_execution.py
deleted file mode 100644
index e5b661235243..000000000000
--- a/corehq/apps/app_execution/tests/test_execution.py
+++ /dev/null
@@ -1,39 +0,0 @@
-from django.test import SimpleTestCase
-from testil import eq
-
-from . import response_factory as factory
-from .mock_formplayer import CaseList, Form, Menu, MockFormplayerClient
-from ..api import FormplayerSession, execute_workflow
-from ..data_model import AppWorkflow, steps
-
-CASES = [{"id": "123", "name": "Case1"}, {"id": "456", "name": "Case2"}]
-APP = Menu(
- name="App1",
- children=[
- Menu(name="Case List", children=[
- CaseList(name="Followup", cases=CASES, children=[
- Form(name="Followup Case", children=[
- factory.make_question("0", "Name", "name", ""),
- ])
- ]),
- ]),
- ]
-)
-
-
-class TestExecution(SimpleTestCase):
- def test_execution(self):
- workflow = AppWorkflow(steps=[
- steps.CommandStep("Case List"),
- steps.CommandStep("Followup"),
- steps.EntitySelectStep("123"),
- steps.CommandStep("Followup Case"),
- steps.FormStep(children=[
- steps.AnswerQuestionStep(question_text='Name', value='str'),
- steps.SubmitFormStep(),
- ])
- ])
- session = FormplayerSession(MockFormplayerClient(APP), app_id="app_id")
- session.__dict__["app_build_id"] = "app_build_id" # prime cache to avoid DB hit
- success = execute_workflow(session, workflow)
- eq(success, True)
diff --git a/corehq/apps/app_execution/tests/test_expect_xpath.py b/corehq/apps/app_execution/tests/test_expect_xpath.py
deleted file mode 100644
index deefd7bb1b10..000000000000
--- a/corehq/apps/app_execution/tests/test_expect_xpath.py
+++ /dev/null
@@ -1,57 +0,0 @@
-from unittest.mock import Mock
-
-from testil import eq
-
-from corehq.apps.app_execution.api import FormplayerSession, ScreenType
-from corehq.apps.app_execution.data_model.expectations import XpathExpectation, _get_result
-
-RESULT_XML = "{}"
-
-
-def test_expect_xpath_true():
- result = _test_expect_xpath("true")
- eq(result, True)
-
-
-def test_expect_xpath_false():
- result = _test_expect_xpath("anything else")
- eq(result, False)
-
-
-def _test_expect_xpath(result):
- session = Mock(
- current_screen=ScreenType.FORM,
- app_build_id="123",
- data={"selections": ["0"], "session_id": "123"},
- client=Mock(),
- spec=FormplayerSession,
- )
- session.get_base_data.return_value = {}
- session.client.make_request.return_value = {"output": RESULT_XML.format(result)}
- expectation = XpathExpectation(xpath="1 = 2")
- return expectation.evaluate(session)
-
-
-def test_expect_xpath_get_result():
- response = {"output": RESULT_XML.format("test")}
- result = _get_result(Mock(log=_raise_log), response=response)
- eq(result, {"session": "test"})
-
-
-def test_expect_xpath_get_result_error():
- response = {"output": RESULT_XML.format("")}
- logs = []
- result = _get_result(Mock(log=_collect_log(logs)), response=response)
- eq(result, None)
- eq(logs, [f"Unable to parse result {response['output']}"])
-
-
-def _raise_log(x):
- raise Exception(x)
-
-
-def _collect_log(logs):
- def log(x):
- logs.append(x)
-
- return log
diff --git a/corehq/apps/app_execution/tests/test_expectations.py b/corehq/apps/app_execution/tests/test_expectations.py
deleted file mode 100644
index 1beb6159d222..000000000000
--- a/corehq/apps/app_execution/tests/test_expectations.py
+++ /dev/null
@@ -1,49 +0,0 @@
-from django.test import SimpleTestCase
-from testil import eq
-
-from .utils import assert_json_dict_equal
-from ..data_model import AppWorkflow
-from ..data_model import expectations
-from ..data_model import steps
-
-
-class ExpectationModelTest(SimpleTestCase):
-
- def test_workflow_has_all_step_types(self):
- all_steps = get_workflow_with_all_expectation_steps().steps
- types_ = {step.type for step in all_steps}
- missing = expectations.TYPE_MAP.keys() - types_
- if missing:
- raise AssertionError(f"Missing expectation types: {missing}")
-
- def test_no_overlap(self):
- overlap = set(steps.STEP_MAP).intersection(set(expectations.TYPE_MAP))
- self.assertEqual(overlap, set())
-
- def test_to_json(self):
- workflow = get_workflow_with_all_expectation_steps()
- assert_json_dict_equal(workflow.__jsonattrs_to_json__(), _get_workflow_json())
-
- def test_from_json(self):
- workflow = AppWorkflow.__jsonattrs_from_json__(_get_workflow_json())
- eq(workflow, get_workflow_with_all_expectation_steps())
-
-
-def get_workflow_with_all_expectation_steps():
- return AppWorkflow(steps=[
- expectations.XpathExpectation(xpath="instance('commcaresession')/session/data/case/@case_id = '123'"),
- expectations.CasePresent(xpath_filter="@case_id = '123'"),
- expectations.CaseAbsent(xpath_filter="@case_id = '345'"),
- expectations.QuestionValue(question_path="/data/question1", value="123"),
- ])
-
-
-def _get_workflow_json():
- return {
- "steps": [
- {"type": "expect:xpath", "xpath": "instance('commcaresession')/session/data/case/@case_id = '123'"},
- {"type": "expect:case_present", "xpath_filter": "@case_id = '123'"},
- {"type": "expect:case_absent", "xpath_filter": "@case_id = '345'"},
- {"type": "expect:question_value", "question_path": "/data/question1", "value": "123"},
- ]
- }
diff --git a/corehq/apps/app_execution/tests/test_har_extract.py b/corehq/apps/app_execution/tests/test_har_extract.py
deleted file mode 100644
index 6d72d731edae..000000000000
--- a/corehq/apps/app_execution/tests/test_har_extract.py
+++ /dev/null
@@ -1,98 +0,0 @@
-import os
-
-from django.test import SimpleTestCase
-
-from corehq.apps.app_execution.data_model import steps
-from corehq.apps.app_execution.har_parser import HarParser
-from corehq.util.test_utils import TestFileMixin
-
-
-class TestHarExtraction(SimpleTestCase, TestFileMixin):
- file_path = ('data',)
- root = os.path.dirname(__file__)
-
- def test_extraction_reg_form(self):
- har_data = self.get_json("reg_form")
- config = HarParser().parse(har_data)
- self.assertEqual(config.domain, "demo_domain")
- self.assertEqual(config.app_id, "demo_app_id")
- self.assertEqual(config.workflow.steps, get_reg_form_steps())
-
- def test_extraction_case_list(self):
- har_data = self.get_json("case_list")
- config = HarParser().parse(har_data)
- self.assertEqual(config.domain, "demo_domain")
- self.assertEqual(config.app_id, "demo_app_id")
- self.assertEqual(config.workflow.steps, get_case_list_steps())
-
- def test_extraction_combined(self):
- reg_form_har_data = self.get_json("reg_form")
- case_list_har_data = self.get_json("case_list")
- combined = {
- 'log': {
- 'entries': reg_form_har_data['log']['entries'] + case_list_har_data['log']['entries']
- }
- }
- config = HarParser().parse(combined)
- self.assertEqual(config.domain, "demo_domain")
- self.assertEqual(config.app_id, "demo_app_id")
- self.assertEqual(config.workflow.steps, get_reg_form_steps() + get_case_list_steps())
-
- def test_split_screen_case_search_select(self):
- har = self.get_json("split_case_search_select")
- config = HarParser().parse(har)
- self.assertEqual(config.workflow.steps, [
- steps.CommandStep(value='Pending Cases'),
- steps.EntitySelectStep(value='0da3e5c6-f069-49be-aab3-53f2b9b7ebd0')
- ])
-
- def test_split_screen_case_search_search(self):
- har = self.get_json("split_case_search_search")
- config = HarParser().parse(har)
- self.assertEqual(config.workflow.steps, [
- steps.CommandStep(value='Pending Cases'),
- steps.QueryInputValidationStep(inputs={'name': 'stale1'}),
- steps.QueryStep(inputs={'name': 'stale1'}),
- steps.EntitySelectStep(value='0da3e5c6-f069-49be-aab3-53f2b9b7ebd0'),
- ])
-
- def test_search_again(self):
- har = self.get_json("search_again")
- config = HarParser().parse(har)
- self.assertEqual(config.workflow.steps, [
- steps.CommandStep(value='Include Related Cases'),
- steps.ClearQueryStep(),
- steps.QueryInputValidationStep(inputs={'first_name': 'Lucca'}),
- steps.QueryInputValidationStep(inputs={'first_name': 'Lucca', 'last_name': 'Mcpherson'}),
- steps.QueryStep(inputs={'first_name': 'Lucca', 'last_name': 'Mcpherson'}),
- steps.EntitySelectStep(value='18e434037dae4d87b98e77687a2aeff4'),
- ])
-
- def test_case_list_action(self):
- har = self.get_json("case_list_action")
- config = HarParser().parse(har)
- self.assertEqual(config.workflow.steps, [
- steps.CommandStep(value='Baby Log'),
- steps.CommandIdStep(value='action 0')
- ])
-
-
-def get_reg_form_steps():
- return [
- steps.CommandStep(value='Register'),
- steps.CommandStep(value='Register cat'),
- steps.FormStep(children=[
- steps.AnswerQuestionIdStep(question_id='name', value='fluffy'),
- steps.AnswerQuestionIdStep(question_id='date', value='2024-05-14'),
- steps.SubmitFormStep()
- ])
- ]
-
-
-def get_case_list_steps():
- return [
- steps.CommandStep(value='Register'),
- steps.CommandStep(value='Cat sighting'),
- steps.EntitySelectStep(value='7a6d6f96-9bd4-41c0-8e52-e499497c4991'),
- steps.FormStep(children=[steps.SubmitFormStep()])
- ]
diff --git a/corehq/apps/app_execution/tests/test_question_value.py b/corehq/apps/app_execution/tests/test_question_value.py
deleted file mode 100644
index c6ae0d5a7e26..000000000000
--- a/corehq/apps/app_execution/tests/test_question_value.py
+++ /dev/null
@@ -1,57 +0,0 @@
-from unittest.mock import Mock
-
-from testil import eq
-
-from corehq.apps.app_execution.data_model.expectations import get_question_value_from_tree, \
- get_question_value_from_xml
-
-
-def test_get_question_value_from_tree():
- answer, found = get_question_value_from_tree("/data/nested/group/list_q_2", [
- {"binding": "/data/question1", "answer": None},
- {"binding": "/data/nested", "children": [
- {"binding": "/data/nested/group", "children": [
- {"binding": "/data/nested/group/list_q_2", "answer": "123"}
- ]}
- ]},
- ])
- eq(found, True)
- eq(answer, "123")
-
-
-def test_get_question_value_from_tree_not_found():
- answer, found = get_question_value_from_tree("/data/nested/group/list_q_2", [])
- eq(found, False)
- eq(answer, None)
-
-
-def test_get_question_value_from_xml():
- answer, found = get_question_value_from_xml(_get_session(), "/data/nested/group/list_q_2")
- eq(found, True)
- eq(answer, "123")
-
-
-def test_get_question_value_from_xml_not_found():
- answer, found = get_question_value_from_xml(_get_session(), "/data/nested/group/list_q_3")
- eq(found, False)
- eq(answer, None)
-
-
-def test_get_question_value_from_xml_repeat():
- answer, found = get_question_value_from_xml(_get_session(), "/data/repeat[2]/q1")
- eq(found, True)
- eq(answer, "def")
-
-
-FORM_XML = """
- 123
- abc
- def
- ghi
-"""
-
-
-def _get_session():
- return Mock(data={
- "instanceXml": {"output": FORM_XML}
- })
diff --git a/corehq/apps/app_execution/tests/test_steps.py b/corehq/apps/app_execution/tests/test_steps.py
deleted file mode 100644
index 34c6345226a2..000000000000
--- a/corehq/apps/app_execution/tests/test_steps.py
+++ /dev/null
@@ -1,92 +0,0 @@
-import itertools
-
-from django.test import SimpleTestCase
-from testil import eq
-
-from corehq.apps.app_execution.data_model import AppWorkflow, steps
-from corehq.apps.app_execution.tests.utils import assert_json_dict_equal
-
-
-class StepModelTest(SimpleTestCase):
-
- def test_workflow_has_all_step_types(self):
- workflow = get_workflow_with_all_steps()
- all_steps = []
- new_steps = [step for step in workflow.steps]
- while new_steps:
- all_steps.extend(new_steps)
- new_steps = list(itertools.chain.from_iterable([
- step.get_children() for step in new_steps if step.get_children()
- ]))
- step_types = {step.type for step in all_steps}
- missing = steps.STEP_MAP.keys() - step_types
- if missing:
- raise AssertionError(f"Missing step types: {missing}")
-
- def test_to_json(self):
- assert_json_dict_equal(get_workflow_with_all_steps().__jsonattrs_to_json__(), _get_workflow_json())
-
- def test_from_json(self):
- workflow = AppWorkflow.__jsonattrs_from_json__(_get_workflow_json())
- eq(workflow, get_workflow_with_all_steps())
-
-
-def get_workflow_with_all_steps():
- return AppWorkflow(steps=[
- steps.CommandStep("Case Search"),
- steps.CommandIdStep("action 0"),
- steps.QueryInputValidationStep({"first_name": "query value"}),
- steps.QueryInputValidationStep({"last_name": "query value"}),
- steps.QueryStep({"first_name": "query value", "last_name": "query value"}),
- steps.EntitySelectStep("123"),
- steps.EntitySelectIndexStep(2),
- steps.ClearQueryStep(),
- steps.RawNavigationStep(request_data={"selections": ["0", "1", "123abc"]}),
- steps.CommandStep("Followup Case"),
- steps.MultipleEntitySelectStep(values=["xyz", "abc"]),
- steps.MultipleEntitySelectByIndexStep(values=[0, 2]),
- steps.FormStep(children=[
- steps.AnswerQuestionStep(question_text='Name', value='str'),
- steps.AnswerQuestionIdStep(question_id='name', value='str'),
- steps.SubmitFormStep()
- ]),
- ])
-
-
-def _get_workflow_json():
- return {
- "steps": [
- {"type": "command", "value": "Case Search"},
- {"type": "command_id", "value": "action 0"},
- {"type": "query_input_validation", "inputs": {"first_name": "query value"}},
- {"type": "query_input_validation", "inputs": {"last_name": "query value"}},
- {
- "type": "query",
- "inputs": {"first_name": "query value", "last_name": "query value"},
- "validate_inputs": False,
- },
- {"type": "entity_select", "value": "123"},
- {"type": "entity_select_index", "value": 2},
- {"type": "clear_query"},
- {"type": "raw_navigation", "request_data": {"selections": ["0", "1", "123abc"]}},
- {"type": "command", "value": "Followup Case"},
- {"type": "multiple_entity_select", "values": ["xyz", "abc"]},
- {"type": "multiple_entity_select_by_index", "values": [0, 2]},
- {
- "type": "form",
- "children": [
- {
- "type": "answer_question",
- "question_text": "Name",
- "value": "str",
- },
- {
- "type": "answer_question_id",
- "question_id": "name",
- "value": "str",
- },
- {"type": "submit_form"}
- ]
- }
- ]
- }
diff --git a/corehq/apps/app_execution/tests/utils.py b/corehq/apps/app_execution/tests/utils.py
deleted file mode 100644
index 6f6241ef59f4..000000000000
--- a/corehq/apps/app_execution/tests/utils.py
+++ /dev/null
@@ -1,8 +0,0 @@
-import json
-
-from corehq.apps.app_manager.tests.views.test_apply_patch import assert_no_diff
-
-
-def assert_json_dict_equal(expected, actual):
- if expected != actual:
- assert_no_diff(json.dumps(expected, indent=2), json.dumps(actual, indent=2))
diff --git a/corehq/apps/app_execution/urls.py b/corehq/apps/app_execution/urls.py
deleted file mode 100644
index ffc25708911c..000000000000
--- a/corehq/apps/app_execution/urls.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from django.urls import path
-
-from . import views
-
-app_name = "app_execution"
-
-urlpatterns = [
- path('', views.workflow_list, name="workflow_list"),
- path('new/', views.new_workflow, name="new_workflow"),
- path('/edit/', views.edit_workflow, name="edit_workflow"),
- path('/run/', views.run_workflow, name="run_workflow"),
- path('/logs/', views.workflow_log_list, name="workflow_logs"),
- path('/json_logs/', views.workflow_logs_json, name="logs_json"),
- path('/delete/', views.delete_workflow, name="delete_workflow"),
- path('log/', views.workflow_log, name="workflow_log"),
-]
diff --git a/corehq/apps/app_execution/views.py b/corehq/apps/app_execution/views.py
deleted file mode 100644
index 6538df94a1ff..000000000000
--- a/corehq/apps/app_execution/views.py
+++ /dev/null
@@ -1,237 +0,0 @@
-from datetime import datetime
-
-from dateutil.relativedelta import relativedelta
-from django.contrib import messages
-from django.http import JsonResponse
-from django.shortcuts import get_object_or_404, redirect, render
-from django.urls import reverse
-
-from corehq.apps.app_execution import const
-from corehq.apps.app_execution.data_model import EXAMPLE_WORKFLOW
-from corehq.apps.app_execution.db_accessors import get_avg_duration_data, get_status_data
-from corehq.apps.app_execution.forms import AppWorkflowConfigForm
-from corehq.apps.app_execution.har_parser import parse_har_from_string
-from corehq.apps.app_execution.models import AppExecutionLog, AppWorkflowConfig
-from corehq.apps.app_execution.tasks import run_app_workflow
-from corehq.apps.app_execution.workflow_dsl import workflow_to_dsl
-from corehq.apps.domain.decorators import require_superuser_or_contractor
-from corehq.apps.hqwebapp.decorators import use_bootstrap5
-from corehq.util.timezones.utils import get_timezone_for_user
-from corehq.util.view_utils import get_date_param
-from django.utils.translation import gettext as _
-
-
-@require_superuser_or_contractor
-@use_bootstrap5
-def workflow_list(request, domain):
- workflows = AppWorkflowConfig.objects.filter(domain=domain)
- _augment_with_logs(workflows)
- utcnow = datetime.utcnow()
- start = utcnow - relativedelta(months=1)
- context = _get_context(
- request,
- _("Automatically Executed App Workflows"),
- reverse("app_execution:workflow_list", args=[domain]),
- workflows=workflows,
- chart_data={
- "timing": get_avg_duration_data(domain, start=start, end=utcnow),
- "status": get_status_data(domain, start=start, end=utcnow),
- }
- )
- return render(request, "app_execution/workflow_list.html", context)
-
-
-def _augment_with_logs(workflows):
- """Add log records to each workflow object. Always at 10 even if there are less than 10 logs.
- """
- for workflow in workflows:
- log_status = [{}] * 10
- logs = AppExecutionLog.objects.filter(workflow=workflow).order_by("-started")[:10]
- for i, log in enumerate(logs):
- log_status[9 - i] = {"success": log.success, "id": log.id}
- workflow.last_n = log_status
-
-
-@require_superuser_or_contractor
-@use_bootstrap5
-def delete_workflow(request, domain, pk):
- workflow = get_object_or_404(AppWorkflowConfig, domain=domain, pk=pk)
- workflow.delete()
- return redirect("app_execution:workflow_list", domain)
-
-
-@require_superuser_or_contractor
-@use_bootstrap5
-def new_workflow(request, domain):
- form = AppWorkflowConfigForm(request, initial={
- "workflow": EXAMPLE_WORKFLOW, "run_every": 1, "form_mode": const.FORM_MODE_HUMAN
- })
- if request.method == "POST":
- import_har = request.POST.get("import_har")
- har_file = request.FILES.get("har_file")
- if import_har and har_file:
- form = _get_form_from_har(har_file.read(), request)
- else:
- form = AppWorkflowConfigForm(request, request.POST)
- if form.is_valid():
- form.save()
- return redirect("app_execution:workflow_list", domain)
-
- context = _get_context(
- request,
- _("New App Workflow"),
- reverse("app_execution:new_workflow", args=[domain]),
- add_parent=True,
- form=form
- )
- return render(request, "app_execution/workflow_form.html", context)
-
-
-@require_superuser_or_contractor
-@use_bootstrap5
-def edit_workflow(request, domain, pk):
- config = get_object_or_404(AppWorkflowConfig, domain=domain, pk=pk)
- form = AppWorkflowConfigForm(request, instance=config)
- if request.method == "POST":
- form = AppWorkflowConfigForm(request, request.POST, instance=config)
- import_har = request.POST.get("import_har")
- har_file = request.FILES.get("har_file")
- if import_har and har_file:
- form = _get_form_from_har(har_file.read(), request, instance=config)
- elif har_file:
- messages.error(request, _("You must use the 'Import HAR' button to upload a HAR file."))
- else:
- if form.is_valid():
- form.save()
- return redirect("app_execution:workflow_list", domain)
-
- context = _get_context(
- request, _("Edit App Workflow: {name}").format(name=config.name),
- reverse("app_execution:edit_workflow", args=[domain, pk]),
- add_parent=True, form=form, workflow=config
- )
- return render(request, "app_execution/workflow_form.html", context)
-
-
-def _get_form_from_har(har_data_string, request, instance=None):
- post_data = request.POST.copy()
- try:
- config = parse_har_from_string(har_data_string)
- post_data["domain"] = config.domain
- post_data["app_id"] = config.app_id
- post_data["workflow"] = AppWorkflowConfig.workflow_object_to_json_string(config.workflow)
- except Exception as e:
- messages.error(request, _("Unable to process HAR file: {error}").format(error=str(e)))
-
- return AppWorkflowConfigForm(request, post_data, instance=instance)
-
-
-@require_superuser_or_contractor
-@use_bootstrap5
-def run_workflow(request, domain, pk):
- config = get_object_or_404(AppWorkflowConfig, domain=domain, pk=pk)
-
- context = _get_context(
- request, f"Test App Workflow: {config.name}", reverse("app_execution:run_workflow", args=[domain, pk]),
- add_parent=True, workflow=config
- )
-
- if request.method == "POST":
- log = run_app_workflow(config)
- return redirect("app_execution:workflow_log", domain, log.id)
-
- return render(request, "app_execution/workflow_run.html", context)
-
-
-def _get_context(request, title, url, add_parent=False, **kwargs):
- parents = [{
- "title": "App Testing",
- "url": reverse("app_execution:workflow_list", args=[request.domain]),
- }]
- context = {
- "domain": request.domain,
- "current_page": {
- "page_name": title,
- "title": title,
- "url": url,
- "parents": parents if add_parent else [],
- },
- "section": {"page_name": "Apps", "url": "#"},
- }
- return {**context, **kwargs}
-
-
-@require_superuser_or_contractor
-@use_bootstrap5
-def workflow_log_list(request, domain, pk):
- utcnow = datetime.utcnow()
- start = utcnow - relativedelta(months=1)
- context = _get_context(
- request,
- _("Automatically Executed App Workflow Logs"),
- reverse("app_execution:workflow_logs", args=[domain, pk]),
- add_parent=True,
- workflow=AppWorkflowConfig.objects.get(id=pk),
- total=AppExecutionLog.objects.filter(workflow__domain=domain, workflow_id=pk).count(),
- chart_data={
- "timing": get_avg_duration_data(domain, start=start, end=utcnow, workflow_id=pk),
- "status": get_status_data(domain, start=start, end=utcnow, workflow_id=pk),
- },
- )
- return render(request, "app_execution/workflow_log_list.html", context)
-
-
-@require_superuser_or_contractor
-@use_bootstrap5
-def workflow_logs_json(request, domain, pk):
- status = request.GET.get('status', None)
- limit = int(request.GET.get('per_page', 10))
- page = int(request.GET.get('page', 1))
- skip = limit * (page - 1)
-
- timezone = get_timezone_for_user(request.couch_user, domain)
- try:
- start_date = get_date_param(request, 'startDate', timezone=timezone)
- end_date = get_date_param(request, 'endDate', timezone=timezone)
- except ValueError:
- return JsonResponse({"error": _("Invalid date parameter")})
-
- query = AppExecutionLog.objects.filter(workflow__domain=domain, workflow_id=pk)
- if status:
- query = query.filter(success=status == "success")
- if start_date:
- query = query.filter(started__gte=start_date)
- if end_date:
- query = query.filter(started__lte=datetime.combine(end_date, datetime.max.time()))
-
- logs = query.order_by("-started")[skip:skip + limit]
- return JsonResponse({
- "logs": [
- {
- "id": log.id,
- "started": log.started.strftime("%Y-%m-%d %H:%M:%SZ"),
- "completed": log.completed.strftime("%Y-%m-%d %H:%M:%SZ") if log.completed else None,
- "success": log.success,
- "duration": str(log.duration) if log.duration else "",
- "url": reverse("app_execution:workflow_log", args=[domain, log.id])
- }
- for log in logs
- ]
- })
-
-
-@require_superuser_or_contractor
-@use_bootstrap5
-def workflow_log(request, domain, pk):
- log = get_object_or_404(AppExecutionLog, workflow__domain=domain, pk=pk)
- return render(
- request, "app_execution/workflow_log.html",
- _get_context(
- request,
- _("Workflow Log: {name}").format(name=log.workflow.name),
- reverse("app_execution:workflow_log", args=[domain, pk]),
- add_parent=True,
- log=log,
- workflow_json=workflow_to_dsl(log.workflow.workflow)
- )
- )
diff --git a/corehq/apps/app_execution/workflow_dsl.py b/corehq/apps/app_execution/workflow_dsl.py
deleted file mode 100644
index 8dbafab0628b..000000000000
--- a/corehq/apps/app_execution/workflow_dsl.py
+++ /dev/null
@@ -1,332 +0,0 @@
-import dataclasses
-import json
-import re
-from operator import attrgetter
-from typing import Any, Callable
-
-from corehq.apps.app_execution import data_model
-from corehq.apps.app_execution.exceptions import AppExecutionError
-
-
-def dsl_to_workflow(dsl_str):
- """Parse a DSL string into an AppWorkflow object
- Rules:
- - Lines starting with '#' are ignored
- - Blank lines are ignored
- - Line indentation is ignored
- - All other lines must map to a workflow step
- """
- lines = dsl_str.splitlines()
- steps = []
- nested = []
- for line in lines:
- line = line.strip()
- if not line or line.startswith('#'):
- continue
-
- if nested and nested[-1].match_end(line):
- nested.pop()
- continue
-
- step, dsl = _line_to_step(line)
- if nested:
- steps[-1].children.append(step)
- else:
- steps.append(step)
-
- if isinstance(dsl, NestedDsl):
- nested.append(dsl)
-
- return data_model.AppWorkflow(steps=steps)
-
-
-def _line_to_step(line):
- for dsl in DSL:
- try:
- step = dsl.from_dsl(line)
- except Exception as e:
- raise AppExecutionError(f"Error parsing line '{line}': {e}")
-
- if not step:
- # try next parser
- continue
-
- return step, dsl
-
- raise AppExecutionError(f"Invalid step: {line}")
-
-
-def workflow_to_dsl(workflow):
- """Convert an AppWorkflow object into a DSL string"""
- lines = _steps_to_dsl_lines(workflow.steps)
- return '\n'.join(lines)
-
-
-def _steps_to_dsl_lines(steps, depth=0):
- lines = []
- for step in steps:
- dsl = DSL_MAP.get(step.type)
- if not dsl:
- raise AppExecutionError(f"Unsupported step type: {step.type}")
- indent = " " * depth
- dsl_out = dsl.to_dsl(step)
- if isinstance(dsl_out, list):
- lines.extend([indent + line for line in dsl_out])
- else:
- lines.append(indent + dsl_out)
- return lines
-
-
-def identity(x):
- """Identity function"""
- return x
-
-
-@dataclasses.dataclass
-class SimpleDsl:
- """DSL class that maps a step class to a DSL format string and regex parser
-
- By default, the step is assumed to have a `value` attribute which is passed to the
- format string unchanged.
-
- The line regex should use named groups to extract values from the line which are passed
- to the Step constructor as keyword arguments.
- """
- step_cls: type[Any]
- format_str: str
- parse_regex: str
- value_to_str: Callable[[Any], str] = attrgetter('value')
- dict_to_kwargs: Callable[[dict], dict] = identity
-
- def to_dsl(self, step):
- return self.format_str.format(step=step, value=self.value_to_str(step))
-
- def from_dsl(self, line):
- if match := re.match(self.parse_regex, line, re.IGNORECASE):
- return self.step_cls(**self.dict_to_kwargs(match.groupdict()))
-
-
-@dataclasses.dataclass
-class NestedDsl:
- step_cls: type[Any]
- starting: str
- ending: str
-
- def to_dsl(self, step):
- dsl = [self.starting.format(step=step)]
- dsl.extend(_steps_to_dsl_lines(step.get_children(), depth=1))
- dsl.append(self.ending.format(step=step))
- return dsl
-
- def from_dsl(self, line):
- if re.match(self.starting, line, re.IGNORECASE):
- return self.step_cls()
-
- def match_end(self, line):
- return re.match(self.ending, line, re.IGNORECASE)
-
-
-def _format_kv_pairs(field_name):
- """Factory function to produce a CSV string formatter for a field in a step class.
-
- Inverse of `_get_key_value_pairs`
- """
-
- def _formatter(step):
- return ", ".join(f'{k}="{v}"' for k, v in getattr(step, field_name).items())
-
- return _formatter
-
-
-def _get_key_value_pairs(line):
- """Extract key-value pairs from a string in the format 'key="value", key2="value2"'
- and return them as a dict.
-
- Inverse of `_format_kv_pairs`
- """
- inputs = {}
- for match in re.finditer(r'([\w-]+)="(.+?)"', line):
- inputs[match.group(1)] = match.group(2)
- return inputs
-
-
-@dataclasses.dataclass
-class KeyValueDsl(SimpleDsl):
- """DSL class for steps that contain key-value pairs i.e. a dictionary of inputs
- """
-
- field_name: str = "inputs"
- """The name of the dict field in the step class"""
-
- def to_dsl(self, step):
- self.value_to_str = _format_kv_pairs(self.field_name)
- return super().to_dsl(step)
-
- def from_dsl(self, line):
- if re.match(self.parse_regex, line, re.IGNORECASE):
- return self.step_cls(inputs=_get_key_value_pairs(line))
-
-
-def join_values(field_name):
- """Factory function to produce a CSV string joiner for a field in a step class.
- Inverse of `split_values`
-
- Args:
- field_name (str): The name of the field in the step class
- """
-
- def _joiner(step):
- value = getattr(step, field_name)
- return ', '.join(map(str, value))
-
- return _joiner
-
-
-def split_values(field_name, groupdict_field='value', cast=str):
- """Factory function to produce a value splitter for a regex groupdict.
- Inverse of `join_values`
-
- Args:
- field_name (str): The name of the field in the step class
- groupdict_field (str): The name of the field in the regex match groupdict
- cast (callable): A function to cast the value to the desired type
- """
-
- def _splitter(groupdict):
- values = []
- for val in groupdict[groupdict_field].split(','):
- if val_clean := val.strip():
- try:
- values.append(cast(val_clean))
- except Exception:
- raise AppExecutionError(f"Invalid value: {val_clean}")
- return {field_name: values}
-
- return _splitter
-
-
-def cast_value(field_name, cast):
- """Factory function to produce a value caster for a regex groupdict.
-
- Args:
- cast (callable): A function to cast the value to the desired type
- """
-
- def _caster(groupdict):
- return {field_name: cast(groupdict[field_name])}
-
- return _caster
-
-
-VALUE_ENDING = r'\s+"(?P.*?)"$'
-
-DSL = [
- SimpleDsl(
- data_model.steps.CommandStep,
- 'Select menu "{value}"',
- fr'^Select menu{VALUE_ENDING}'
- ),
- SimpleDsl(
- data_model.steps.CommandIdStep,
- 'Select menu with ID "{value}"',
- fr'^Select menu with ID{VALUE_ENDING}'
- ),
- SimpleDsl(
- data_model.steps.EntitySelectStep,
- 'Select entity with ID "{value}"',
- fr'^select entity with id{VALUE_ENDING}'
- ),
- SimpleDsl(
- data_model.steps.MultipleEntitySelectStep,
- 'Select entities with IDs "{value}"',
- fr'^Select entities with IDs{VALUE_ENDING}',
- value_to_str=join_values("values"),
- dict_to_kwargs=split_values("values")
- ),
- SimpleDsl(
- data_model.steps.EntitySelectIndexStep,
- 'Select entity at index {value}',
- r'^select entity at index\s+(?P\d+)$',
- dict_to_kwargs=cast_value("value", int)
- ),
- SimpleDsl(
- data_model.steps.MultipleEntitySelectByIndexStep,
- 'Select entities at indexes {value}',
- r'^Select entities at indexes\s+(?P[\d,\s]+)$',
- value_to_str=join_values("values"),
- dict_to_kwargs=split_values("values", groupdict_field="values", cast=int)
- ),
- KeyValueDsl(
- data_model.steps.QueryInputValidationStep,
- 'Update search parameters {value}',
- r'^Update search parameters',
- field_name="inputs",
- ),
- KeyValueDsl(
- data_model.steps.QueryStep,
- 'Search with parameters {value}',
- r'^Search with parameters',
- field_name="inputs",
- ),
- SimpleDsl(
- data_model.steps.ClearQueryStep,
- 'Clear search',
- r'^clear search$',
- value_to_str=identity
- ),
- SimpleDsl(
- data_model.steps.AnswerQuestionStep,
- 'Answer question "{step.question_text}" with "{value}"',
- fr'^Answer question\s+"(?P.+?)"\s+with{VALUE_ENDING}'
- ),
- SimpleDsl(
- data_model.steps.AnswerQuestionIdStep,
- 'Answer question with ID "{step.question_id}" with "{value}"',
- fr'^Answer question with ID\s+"(?P.+?)"\s+with{VALUE_ENDING}'
- ),
- SimpleDsl(
- data_model.steps.SubmitFormStep,
- 'Submit form',
- r'^submit form$',
- value_to_str=identity,
- ),
- SimpleDsl(
- data_model.steps.RawNavigationStep,
- 'Raw navigation request data {value}',
- r'^Raw navigation request data\s+(?P.*)$',
- value_to_str=lambda step: json.dumps(step.request_data),
- dict_to_kwargs=lambda groupdict: {"request_data": json.loads(groupdict["value"])},
- ),
- NestedDsl(
- data_model.steps.FormStep,
- starting='Start form',
- ending="End form"
- ),
-
- # Expectations
- SimpleDsl(
- data_model.expectations.XpathExpectation,
- 'Expect xpath {value}',
- r'^Expect xpath\s+(?P.+)$',
- value_to_str=attrgetter('xpath'),
- ),
- SimpleDsl(
- data_model.expectations.CasePresent,
- 'Expect case present {value}',
- r'^Expect case present\s+(?P.+)$',
- value_to_str=attrgetter('xpath_filter'),
- ),
- SimpleDsl(
- data_model.expectations.CaseAbsent,
- 'Expect case absent {value}',
- r'^Expect case absent\s+(?P.+)$',
- value_to_str=attrgetter('xpath_filter'),
- ),
- SimpleDsl(
- data_model.expectations.QuestionValue,
- 'Expect question "{step.question_path}" with "{value}"',
- rf'^Expect question\s+"(?P.+?)"\s+with{VALUE_ENDING}',
- ),
-]
-
-DSL_MAP = {dsl.step_cls.type: dsl for dsl in DSL}
diff --git a/corehq/apps/cleanup/migrations/0020_delete_app_execution_models.py b/corehq/apps/cleanup/migrations/0020_delete_app_execution_models.py
new file mode 100644
index 000000000000..69fadd77699f
--- /dev/null
+++ b/corehq/apps/cleanup/migrations/0020_delete_app_execution_models.py
@@ -0,0 +1,15 @@
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('cleanup', '0019_alter_deletedsqldoc_table'),
+ ]
+
+ operations = [
+ migrations.RunSQL("""
+ DROP TABLE IF EXISTS "app_execution_appexecutionlog" CASCADE;
+ DROP TABLE IF EXISTS "app_execution_appworkflowconfig" CASCADE;
+ """),
+ ]
diff --git a/corehq/apps/domain/tests/test_deletion_models.py b/corehq/apps/domain/tests/test_deletion_models.py
index 36d7220a93f9..f1bac7b45835 100644
--- a/corehq/apps/domain/tests/test_deletion_models.py
+++ b/corehq/apps/domain/tests/test_deletion_models.py
@@ -44,8 +44,6 @@
IGNORE_MODELS = {
'api.ApiUser',
- 'app_execution.AppWorkflowConfig',
- 'app_execution.AppExecutionLog',
'app_manager.ExchangeApplication',
'auth.Group',
'auth.Permission',
diff --git a/corehq/apps/dump_reload/tests/test_dump_models.py b/corehq/apps/dump_reload/tests/test_dump_models.py
index 5f7f6a410b15..8dd021865b02 100644
--- a/corehq/apps/dump_reload/tests/test_dump_models.py
+++ b/corehq/apps/dump_reload/tests/test_dump_models.py
@@ -44,8 +44,6 @@
"analytics.PartnerAnalyticsDataPoint",
"analytics.PartnerAnalyticsReport",
"api.ApiUser",
- "app_execution.AppWorkflowConfig",
- "app_execution.AppExecutionLog",
"app_manager.ExchangeApplication",
"auth.Group",
"auth.Permission",
diff --git a/corehq/tabs/tabclasses.py b/corehq/tabs/tabclasses.py
index 6d58fa84bacf..8947dae88ae3 100644
--- a/corehq/tabs/tabclasses.py
+++ b/corehq/tabs/tabclasses.py
@@ -1200,40 +1200,6 @@ def dropdown_items(self):
))
return submenu_context
- @property
- @memoized
- def sidebar_items(self):
- return [
- (_("Application Test Flows"), [
- {
- 'title': "Workflow List",
- 'url': reverse("app_execution:workflow_list", args=[self.domain]),
- 'subpages': [
- {
- 'title': _("New"),
- 'urlname': "new_workflow",
- },
- {
- 'title': _("Edit"),
- 'urlname': "edit_workflow",
- },
- {
- 'title': _("Run"),
- 'urlname': "test_workflow",
- },
- {
- 'title': _("Logs"),
- 'urlname': "workflow_logs",
- },
- {
- 'title': _("Log Details"),
- 'urlname': "workflow_log",
- },
- ],
- },
- ]),
- ]
-
@property
def _is_viewable(self):
couch_user = self.couch_user
diff --git a/migrations.lock b/migrations.lock
index 9e9cc20f4351..e82ad68602fa 100644
--- a/migrations.lock
+++ b/migrations.lock
@@ -150,11 +150,6 @@ api
0002_alter_permissions
0003_populate_apiuser
0004_rename_apiuser
-app_execution
- 0001_initial
- 0002_alter_appworkflowconfig_unique_together
- 0003_appexecutionlog_updated
- 0004_alter_appworkflowconfig_notification_emails_and_more
app_manager
0001_linked_app_domain
0002_latestenabledbuildprofiles
@@ -292,6 +287,7 @@ cleanup
0017_delete_oauth_integrations_models
0018_delete_ewsghana_models
0019_alter_deletedsqldoc_table
+ 0020_delete_app_execution_models
cloudcare
0001_initial
0002_sqlapplicationaccess
diff --git a/settings.py b/settings.py
index 54d42427d402..d23989349e6e 100755
--- a/settings.py
+++ b/settings.py
@@ -391,7 +391,6 @@
'corehq.apps.case_search',
'corehq.apps.zapier.apps.ZapierConfig',
'corehq.apps.translations',
- 'corehq.apps.app_execution',
'corehq.apps.tombstones',
# custom reports
diff --git a/urls.py b/urls.py
index 5b1830a6b90f..9e97d56f295c 100644
--- a/urls.py
+++ b/urls.py
@@ -93,7 +93,6 @@
url(r'^submit_feedback/$', submit_feedback, name='submit_feedback'),
url(r'^integration/', include('corehq.apps.integration.urls')),
url(r'^registries/', include('corehq.apps.registry.urls')),
- url(r'^apps/testing/', include("corehq.apps.app_execution.urls")),
]
for url_module in extension_points.domain_specific_urls():