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 }}

-
-
-
- {% if show_edit|default_if_none:True %} - {% translate 'Edit' %} - {% endif %} - {% if show_logs|default_if_none:True %} - {% translate 'Logs' %} - {% endif %} - {% if show_run|default_if_none:True %} - {% translate 'Run' %} - {% endif %} -
- -
-
-
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 %} -
- Create New -
-
-
-

{% trans "Average Timings" %}

- -
-
-

{% trans "Status" %}

- -
-
- - - - - - - - - - - - - {% for workflow in workflows %} - - - - - - - - - {% endfor %} - -
{% translate 'Name' %}{% translate 'App Name' %}{% translate 'User' %}{% translate 'Last Run' %}{% translate 'Last 10 Runs' %}
{{ 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 %} -
-
- -
- -{{ 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' %}

-

-

-

- {% 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 %} -
- {% csrf_token %} - -
-{% 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\"},\"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():