diff --git a/corehq/apps/accounting/models.py b/corehq/apps/accounting/models.py
index 29aa35b2c0a1..f68812c9086b 100644
--- a/corehq/apps/accounting/models.py
+++ b/corehq/apps/accounting/models.py
@@ -22,6 +22,7 @@
from corehq.apps.accounting.utils.stripe import charge_through_stripe
from corehq.apps.domain.shortcuts import publish_domain_saved
+from corehq.apps.users.dbaccessors import get_active_web_usernames_by_domain, get_web_user_count
from dimagi.ext.couchdbkit import (
BooleanProperty,
DateTimeProperty,
@@ -603,6 +604,22 @@ def _send_autopay_card_added_email(self, domain):
use_domain_gateway=True,
)
+ def get_web_user_usernames(self):
+ domains = self.get_domains()
+ web_users = set()
+
+ for domain in domains:
+ web_users.update(get_active_web_usernames_by_domain(domain))
+
+ return web_users
+
+ def get_web_user_count(self):
+ domains = self.get_domains()
+ count = 0
+ for domain in domains:
+ count += get_web_user_count(domain, include_inactive=False)
+ return count
+
@staticmethod
def should_show_sms_billable_report(domain):
account = BillingAccount.get_account_by_domain(domain)
diff --git a/corehq/apps/accounting/tasks.py b/corehq/apps/accounting/tasks.py
index 760ec194564a..64377ef974ff 100644
--- a/corehq/apps/accounting/tasks.py
+++ b/corehq/apps/accounting/tasks.py
@@ -69,7 +69,6 @@
from corehq.apps.accounting.utils.subscription import (
assign_explicit_unpaid_subscription,
)
-from corehq.apps.users.models import WebUser
from corehq.apps.app_manager.dbaccessors import get_all_apps
from corehq.apps.celery import periodic_task, task
from corehq.apps.domain.models import Domain
@@ -554,13 +553,13 @@ def weekly_digest():
in_forty_days = today + datetime.timedelta(days=40)
ending_in_forty_days = [sub for sub in Subscription.visible_objects.filter(
- date_end__lte=in_forty_days,
- date_end__gte=today,
- is_active=True,
- is_trial=False,
- ).exclude(
- account__dimagi_contact='',
- ) if not sub.is_renewed]
+ date_end__lte=in_forty_days,
+ date_end__gte=today,
+ is_active=True,
+ is_trial=False,
+ ).exclude(
+ account__dimagi_contact='',
+ ) if not sub.is_renewed]
if not ending_in_forty_days:
log_accounting_info(
@@ -833,11 +832,7 @@ def calculate_web_users_in_all_billing_accounts(today=None):
today = today or datetime.date.today()
for account in BillingAccount.objects.all():
record_date = today - relativedelta(days=1)
- domains = account.get_domains()
- web_user_in_account = set()
- for domain in domains:
- [web_user_in_account.add(id) for id in WebUser.ids_by_domain(domain)]
- num_users = len(web_user_in_account)
+ num_users = account.get_web_user_count()
try:
BillingAccountWebUserHistory.objects.create(
billing_account=account,
diff --git a/corehq/apps/app_execution/__init__.py b/corehq/apps/app_execution/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/app_execution/admin.py b/corehq/apps/app_execution/admin.py
new file mode 100644
index 000000000000..8d1ecb0a0fec
--- /dev/null
+++ b/corehq/apps/app_execution/admin.py
@@ -0,0 +1,8 @@
+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
new file mode 100644
index 000000000000..c0802bea4566
--- /dev/null
+++ b/corehq/apps/app_execution/api.py
@@ -0,0 +1,349 @@
+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, data_model
+from corehq.apps.app_execution.exceptions import AppExecutionError
+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 FormplayerException(Exception):
+ pass
+
+
+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"
+ 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")
+ print(f"Using app '{app.name}' ({app._id} - {build_on})", file=self.log)
+ return app._id
+
+ def sync(self):
+ if not self.sync_first:
+ return
+ print(f"Syncing user data for {self.client.username}", file=self.log)
+ sync_db(self.client.domain, self.client.username)
+
+ @property
+ def current_screen(self):
+ return self.get_screen_and_data()[0]
+
+ def get_screen_and_data(self):
+ return self._get_screen_and_data(self.data)
+
+ def _get_screen_and_data(self, current_data):
+ if not current_data:
+ return ScreenType.START, None
+
+ type_ = current_data.get("type")
+ if type_ == "commands":
+ return ScreenType.MENU, current_data["commands"]
+ if type_ == "entities":
+ return ScreenType.CASE_LIST, current_data["entities"]
+ if type_ == "query":
+ return ScreenType.SEARCH, current_data.get("displays")
+ data = current_data.get("details")
+ if data:
+ return ScreenType.DETAIL, data
+ data = current_data.get("tree")
+ if data:
+ return ScreenType.FORM, data
+ if current_data.get("submitResponseMessage"):
+ return self._get_screen_and_data(current_data["nextScreen"])
+
+ if current_data.get("errors"):
+ raise AppExecutionError(current_data["errors"])
+ raise AppExecutionError(f"Unknown screen type: {current_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, data_model.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",
+ "geo_location": None,
+ "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 execute_step(self, step):
+ is_form_step = isinstance(step, (data_model.AnswerQuestionStep, data_model.SubmitFormStep))
+ if is_form_step 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, data_model.SubmitFormStep):
+ self.log_step(step, skipped=True)
+ return
+ data = self.get_request_data(step) if step else self.get_session_start_data()
+ self.data = self.client.make_request(data, self.request_url(step))
+ self.log_step(step)
+
+ def log_step(self, step, indent=" ", skipped=False):
+ if not step:
+ print("Starting app session:\n", file=self.log)
+ skipped_log = " (ignored)" if skipped else ""
+ print(f"Execute step: {step or 'START'} {skipped_log}", file=self.log)
+ if skipped:
+ return
+ double_indent = indent * 2
+ screen, data = self.get_screen_and_data()
+ print(f"{indent}New Screen: {screen}", file=self.log)
+ if data:
+ if screen == ScreenType.START:
+ print("", file=self.log)
+ elif screen == ScreenType.MENU:
+ for command in data:
+ print(f"{double_indent}Command: {command['displayText']}", file=self.log)
+ elif screen == ScreenType.CASE_LIST:
+ for row in data:
+ print(f"{double_indent}Case: {row['id']}", file=self.log)
+ elif screen == ScreenType.SEARCH:
+ for display in data:
+ print(f"{double_indent}Search field: {display['text']}", file=self.log)
+ elif screen == ScreenType.FORM:
+ for item in data:
+ if item["type"] == "question":
+ answer = item.get('answer', None) or '""'
+ print(f"{double_indent}Question: {item['caption']}={answer}", file=self.log)
+
+
+def execute_workflow(session: FormplayerSession, workflow):
+ with session:
+ session.sync()
+ execute_step(session, None)
+ for step in workflow.steps:
+ execute_step(session, step)
+
+
+def execute_step(session, step):
+ if step and (children := step.get_children()):
+ for child in children:
+ session.execute_step(child)
+ else:
+ session.execute_step(step)
diff --git a/corehq/apps/app_execution/apps.py b/corehq/apps/app_execution/apps.py
new file mode 100644
index 000000000000..535cd42c7b8c
--- /dev/null
+++ b/corehq/apps/app_execution/apps.py
@@ -0,0 +1,6 @@
+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
new file mode 100644
index 000000000000..6570e78a811f
--- /dev/null
+++ b/corehq/apps/app_execution/const.py
@@ -0,0 +1,3 @@
+FORM_MODE_HUMAN = "human"
+FORM_MODE_NO_SUBMIT = "no_submit"
+FORM_MODE_IGNORE = "ignore"
diff --git a/corehq/apps/app_execution/data_model.py b/corehq/apps/app_execution/data_model.py
new file mode 100644
index 000000000000..3c3f9c16dfa2
--- /dev/null
+++ b/corehq/apps/app_execution/data_model.py
@@ -0,0 +1,226 @@
+from __future__ import annotations
+
+import dataclasses
+from typing import ClassVar
+
+from attr import define
+from attrs import asdict
+
+from corehq.apps.app_execution.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 AppWorkflow:
+ steps: list[Step] = 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)
+
+
+@define
+class CommandStep(Step):
+ type: ClassVar[str] = "command"
+ is_form_step: ClassVar[bool] = False
+ value: str
+
+ 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 EntitySelectStep(Step):
+ type: ClassVar[str] = "entity_select"
+ is_form_step: ClassVar[bool] = False
+ value: str
+
+ def get_request_data(self, session, data):
+ entities = {entity["id"] for entity in session.data.get("entities", [])}
+ if not entities:
+ raise AppExecutionError("No entities found")
+ if self.value not in entities:
+ raise AppExecutionError(f"Entity not found: {self.value}: {list(entities)}")
+ return _append_selection(data, self.value)
+
+ def __str__(self):
+ return f"Entity Select: {self.value}"
+
+
+@define
+class EntitySelectIndexStep(Step):
+ type: ClassVar[str] = "entity_select_index"
+ is_form_step: ClassVar[bool] = False
+ value: int
+
+ def get_request_data(self, session, data):
+ entities = [entity["id"] for entity in session.data.get("entities", [])]
+ if not entities:
+ raise AppExecutionError("No entities found")
+ if self.value >= len(entities):
+ raise AppExecutionError(f"Entity index out of range: {self.value}: {list(entities)}")
+ return _append_selection(data, entities[self.value])
+
+ def __str__(self):
+ return f"Entity Select: {self.value}"
+
+
+@define
+class QueryStep(Step):
+ type: ClassVar[str] = "query"
+ is_form_step: ClassVar[bool] = False
+ inputs: dict
+
+ 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 AnswerQuestionStep(Step):
+ type: ClassVar[str] = "answer_question"
+ is_form_step: ClassVar[bool] = True
+ question_text: str
+ question_id: str
+ value: str
+
+ def get_request_data(self, session, data):
+ try:
+ question = [
+ node for node in session.data["tree"]
+ if (
+ (self.question_text and node["caption"] == self.question_text)
+ or (self.question_id and node["question_id"] == self.question_id)
+ )
+ ][0]
+ except IndexError:
+ raise AppExecutionError(f"Question not found: {self.question_text or self.question_id}")
+
+ return {
+ **data,
+ "action": "answer",
+ "answersToValidate": {},
+ "answer": self.value,
+ "ix": question["ix"],
+ "session_id": session.data["session_id"]
+ }
+
+ def __str__(self):
+ return f"Answer Question: {self.question_text or self.question_id} = {self.value}"
+
+
+@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"
+ children: list[AnswerQuestionStep | SubmitFormStep]
+ is_form_step: ClassVar[bool] = True
+
+ 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):
+ return cls(children=_steps_from_json(data["children"]))
+
+
+def _append_selection(data, selection):
+ selections = data.get("selections", [])
+ selections.append(selection)
+ return {**data, "selections": selections}
+
+
+STEP_MAP = {
+ "command": CommandStep,
+ "entity_select": EntitySelectStep,
+ "entity_select_index": EntitySelectIndexStep,
+ "query": QueryStep,
+ "answer_question": AnswerQuestionStep,
+ "submit_form": SubmitFormStep,
+ "form": FormStep,
+}
+
+
+def _steps_from_json(data):
+ return [STEP_MAP[child.pop("type")].from_json(child) for child in data]
+
+
+EXAMPLE_WORKFLOW = AppWorkflow(steps=[
+ CommandStep(value="My Module"),
+ EntitySelectStep(value="clinic_123"),
+ QueryStep(inputs={"name": "John Doe"}),
+ EntitySelectIndexStep(value=0),
+ FormStep(children=[
+ AnswerQuestionStep(question_text="Name", question_id="name", value="John Doe"),
+ AnswerQuestionStep(question_text="Age", question_id="age", value="30"),
+ SubmitFormStep(),
+ ]),
+])
diff --git a/corehq/apps/app_execution/exceptions.py b/corehq/apps/app_execution/exceptions.py
new file mode 100644
index 000000000000..6e8f3928b032
--- /dev/null
+++ b/corehq/apps/app_execution/exceptions.py
@@ -0,0 +1,2 @@
+class AppExecutionError(Exception):
+ pass
diff --git a/corehq/apps/app_execution/forms.py b/corehq/apps/app_execution/forms.py
new file mode 100644
index 000000000000..7b4baed3d1ac
--- /dev/null
+++ b/corehq/apps/app_execution/forms.py
@@ -0,0 +1,65 @@
+from couchdbkit import NoResultFound, ResourceNotFound
+from crispy_forms import bootstrap as twbscrispy
+from crispy_forms import layout as crispy
+from django import forms
+
+from corehq.apps.app_execution.models import AppWorkflowConfig
+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
+
+
+class AppWorkflowConfigForm(forms.ModelForm):
+ run_every = forms.IntegerField(min_value=1)
+
+ class Meta:
+ model = AppWorkflowConfig
+ fields = (
+ "name",
+ "domain",
+ "app_id",
+ "user_id",
+ "workflow",
+ "sync_before_run",
+ "form_mode",
+ "run_every",
+ "notification_emails"
+ )
+ widgets = {
+ "form_mode": forms.RadioSelect(),
+ }
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.helper = hqcrispy.HQFormHelper()
+
+ self.helper.layout = crispy.Layout(
+ *self.fields.keys(),
+ hqcrispy.FormActions(
+ twbscrispy.StrictButton("Save", type='submit', css_class='btn-primary')
+ ),
+ )
+
+ def clean(self):
+ self.final_clean_app_id()
+ self.final_clean_user_id()
+ return self.cleaned_data
+
+ def final_clean_user_id(self):
+ domain = self.cleaned_data.get("domain")
+ try:
+ self.commcare_user = CommCareUser.get_by_user_id(self.cleaned_data.get("user_id"), domain)
+ except ResourceNotFound:
+ raise forms.ValidationError(f"User not found in domain: {domain}:{self.cleaned_data.get('user_id')}")
+
+ def final_clean_app_id(self):
+ domain = self.cleaned_data.get("domain")
+ app_id = self.cleaned_data.get("app_id")
+ try:
+ get_brief_app(domain, app_id)
+ except NoResultFound:
+ raise forms.ValidationError(f"App not found in domain: {domain}:{app_id}")
+
+ def save(self, commit=True):
+ self.instance.django_user = self.commcare_user.get_django_user()
+ return super().save(commit=commit)
diff --git a/corehq/apps/app_execution/migrations/0001_initial.py b/corehq/apps/app_execution/migrations/0001_initial.py
new file mode 100644
index 000000000000..040525ca7c73
--- /dev/null
+++ b/corehq/apps/app_execution/migrations/0001_initial.py
@@ -0,0 +1,38 @@
+# 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/__init__.py b/corehq/apps/app_execution/migrations/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/app_execution/models.py b/corehq/apps/app_execution/models.py
new file mode 100644
index 000000000000..d7fa493df5a0
--- /dev/null
+++ b/corehq/apps/app_execution/models.py
@@ -0,0 +1,58 @@
+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
+
+
+class AppWorkflowManager(models.Manager):
+ def get_due(self):
+ cutoff = functions.Now() - MakeInterval("mins", models.F("run_every"))
+ return self.filter(last_run__isnull=True) | self.filter(
+ last_run__lt=cutoff
+ )
+
+
+class AppWorkflowConfig(models.Model):
+ FORM_MODE_CHOICES = [
+ (const.FORM_MODE_HUMAN, "Human: Answer each question individually and submit form"),
+ (const.FORM_MODE_NO_SUBMIT, "No Submit: Answer all questions but don't submit the form"),
+ (const.FORM_MODE_IGNORE, "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="Sync user data before running")
+ run_every = models.IntegerField(default=0, help_text="Number of minutes between runs")
+ last_run = models.DateTimeField(null=True, blank=True)
+ notification_emails = ArrayField(models.EmailField(), default=list, help_text="Emails to notify on failure")
+
+ objects = AppWorkflowManager()
+
+ class Meta:
+ unique_together = ("domain", "user_id")
+
+ @cached_property
+ def app_name(self):
+ app = get_brief_app(self.domain, self.app_id)
+ return app.name
+
+ 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)
diff --git a/corehq/apps/app_execution/tasks.py b/corehq/apps/app_execution/tasks.py
new file mode 100644
index 000000000000..667231f2d029
--- /dev/null
+++ b/corehq/apps/app_execution/tasks.py
@@ -0,0 +1,34 @@
+import traceback
+
+from celery.schedules import crontab
+from django.utils import timezone
+
+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
+
+
+@periodic_task(run_every=crontab(minute=0, hour=0))
+def run_app_workflows():
+
+ for config in AppWorkflowConfig.objects.get_due():
+ try:
+ session = config.get_formplayer_session()
+ execute_workflow(session, config.workflow)
+ except Exception as e:
+ url = reverse('app_execution:edit_workflow', args=[config.pk], absolute=True)
+ message = f"""Error executing workflow: {config.name}
+
+
Error: {e}
+ {traceback.format_exc()}
+ """
+ send_HTML_email(
+ f"App Execution Workflow Failure: {config.name}",
+ config.notification_emails,
+ message,
+ )
+ finally:
+ config.last_run = timezone.now()
+ config.save()
diff --git a/corehq/apps/app_execution/templates/app_execution/workflow_form.html b/corehq/apps/app_execution/templates/app_execution/workflow_form.html
new file mode 100644
index 000000000000..22cd3b46526a
--- /dev/null
+++ b/corehq/apps/app_execution/templates/app_execution/workflow_form.html
@@ -0,0 +1,7 @@
+{% extends "hqwebapp/bootstrap5/base_section.html" %}
+{% load crispy_forms_tags %}
+{% load hq_shared_tags %}
+{% load i18n %}
+{% block page_content %}
+ {% 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
new file mode 100644
index 000000000000..605124f30c09
--- /dev/null
+++ b/corehq/apps/app_execution/templates/app_execution/workflow_list.html
@@ -0,0 +1,32 @@
+{% extends "hqwebapp/bootstrap5/base_section.html" %}
+{% load hq_shared_tags %}
+{% load i18n %}
+{% block page_content %}
+
+
+
+
+ Name
+ Domain
+ App
+ User
+ Last Run
+
+
+
+
+ {% for workflow in workflows %}
+
+ {{ workflow.name }}
+ {{ workflow.domain }}
+ {{ workflow.app_name }}
+ {{ workflow.django_user.username }}
+ {{ workflow.last_run|default:"" }}
+ Test
+
+ {% endfor %}
+
+
+{% endblock %}
diff --git a/corehq/apps/app_execution/templates/app_execution/workflow_test.html b/corehq/apps/app_execution/templates/app_execution/workflow_test.html
new file mode 100644
index 000000000000..9fc097295d8a
--- /dev/null
+++ b/corehq/apps/app_execution/templates/app_execution/workflow_test.html
@@ -0,0 +1,38 @@
+{% extends "hqwebapp/bootstrap5/base_section.html" %}
+{% load crispy_forms_tags %}
+{% load hq_shared_tags %}
+{% load i18n %}
+{% block js-inline %} {{ block.super }}
+
+{% endblock %}
+{% block page_content %}
+
+
Testing {{ workflow.name }}
+
Edit
+
+ {% if result %}
+ Logs
+ {{ log }}
+ {% if error %}
+
+ Error: {{ error }}
+
+ {% else %}
+
+ Success
+
+ {% endif %}
+ {% endif %}
+
+{% endblock %}
diff --git a/corehq/apps/app_execution/tests/__init__.py b/corehq/apps/app_execution/tests/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/app_execution/tests/mock_formplayer.py b/corehq/apps/app_execution/tests/mock_formplayer.py
new file mode 100644
index 000000000000..6fdff5dff678
--- /dev/null
+++ b/corehq/apps/app_execution/tests/mock_formplayer.py
@@ -0,0 +1,108 @@
+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
new file mode 100644
index 000000000000..ccfb844504e3
--- /dev/null
+++ b/corehq/apps/app_execution/tests/response_factory.py
@@ -0,0 +1,100 @@
+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_data_model.py b/corehq/apps/app_execution/tests/test_data_model.py
new file mode 100644
index 000000000000..029fa1355289
--- /dev/null
+++ b/corehq/apps/app_execution/tests/test_data_model.py
@@ -0,0 +1,53 @@
+from django.test import SimpleTestCase
+from testil import eq
+
+from corehq.apps.app_execution.data_model import (
+ AnswerQuestionStep, CommandStep, EntitySelectStep, FormStep, QueryStep,
+ SubmitFormStep, AppWorkflow,
+)
+
+
+class DataModelTest(SimpleTestCase):
+
+ def test_to_json(self):
+ eq(_get_workflow().__jsonattrs_to_json__(), _get_workflow_json())
+
+ def test_from_json(self):
+ workflow = AppWorkflow.__jsonattrs_from_json__(_get_workflow_json())
+ eq(workflow, _get_workflow())
+
+
+def _get_workflow():
+ return AppWorkflow(steps=[
+ CommandStep("Case Search"),
+ QueryStep({"first_name": "query value", "last_name": "query value"}),
+ EntitySelectStep("123"),
+ CommandStep("Followup Case"),
+ FormStep(children=[
+ AnswerQuestionStep(question_text='Name', question_id='name', value='str'),
+ SubmitFormStep()
+ ]),
+ ])
+
+
+def _get_workflow_json():
+ return {
+ "steps": [
+ {"type": "command", "value": "Case Search"},
+ {"type": "query", "inputs": {"first_name": "query value", "last_name": "query value"}},
+ {"type": "entity_select", "value": "123"},
+ {"type": "command", "value": "Followup Case"},
+ {
+ "type": "form",
+ "children": [
+ {
+ "type": "answer_question",
+ "question_text": "Name",
+ "question_id": "name",
+ "value": "str",
+ },
+ {"type": "submit_form"}
+ ]
+ }
+ ]
+ }
diff --git a/corehq/apps/app_execution/tests/test_execution.py b/corehq/apps/app_execution/tests/test_execution.py
new file mode 100644
index 000000000000..a776358a716e
--- /dev/null
+++ b/corehq/apps/app_execution/tests/test_execution.py
@@ -0,0 +1,36 @@
+from django.test import SimpleTestCase
+
+from . import response_factory as factory
+from .mock_formplayer import CaseList, Form, Menu, MockFormplayerClient
+from ..api import FormplayerSession, execute_workflow
+from ..data_model import AnswerQuestionStep, CommandStep, EntitySelectStep, FormStep, SubmitFormStep, AppWorkflow
+
+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=[
+ CommandStep("Case List"),
+ CommandStep("Followup"),
+ EntitySelectStep("123"),
+ CommandStep("Followup Case"),
+ FormStep(children=[
+ AnswerQuestionStep(question_text='Name', question_id='name', value='str'), SubmitFormStep()
+ ])
+ ])
+ session = FormplayerSession(MockFormplayerClient(APP), app_id="app_id")
+ session.__dict__["app_build_id"] = "app_build_id" # prime cache to avoid DB hit
+ execute_workflow(session, workflow)
diff --git a/corehq/apps/app_execution/urls.py b/corehq/apps/app_execution/urls.py
new file mode 100644
index 000000000000..535b9726f2d2
--- /dev/null
+++ b/corehq/apps/app_execution/urls.py
@@ -0,0 +1,12 @@
+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('test/', views.test_workflow, name="test_workflow"),
+]
diff --git a/corehq/apps/app_execution/views.py b/corehq/apps/app_execution/views.py
new file mode 100644
index 000000000000..a753c9aee0ec
--- /dev/null
+++ b/corehq/apps/app_execution/views.py
@@ -0,0 +1,100 @@
+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.api import execute_workflow
+from corehq.apps.app_execution.data_model import EXAMPLE_WORKFLOW
+from corehq.apps.app_execution.exceptions import AppExecutionError
+from corehq.apps.app_execution.forms import AppWorkflowConfigForm
+from corehq.apps.app_execution.models import AppWorkflowConfig
+from corehq.apps.domain.decorators import require_superuser_or_contractor
+from corehq.apps.hqadmin.views import get_hqadmin_base_context
+from corehq.apps.hqwebapp.decorators import use_bootstrap5
+
+
+@require_superuser_or_contractor
+@use_bootstrap5
+def workflow_list(request):
+ context = _get_context(
+ request, "Automatically Executed App Workflows", reverse("app_execution:workflow_list"),
+ workflows=AppWorkflowConfig.objects.all()
+ )
+ return render(request, "app_execution/workflow_list.html", context)
+
+
+@require_superuser_or_contractor
+@use_bootstrap5
+def new_workflow(request):
+ form = AppWorkflowConfigForm(initial={
+ "workflow": EXAMPLE_WORKFLOW, "run_every": 1, "form_mode": const.FORM_MODE_HUMAN
+ })
+ if request.method == "POST":
+ form = AppWorkflowConfigForm(request.POST)
+ if form.is_valid():
+ form.save()
+ return redirect("app_execution:workflow_list")
+
+ context = _get_context(
+ request, "New App Workflow", reverse("app_execution:new_workflow"),
+ add_parent=True, form=form
+ )
+ return render(request, "app_execution/workflow_form.html", context)
+
+
+@require_superuser_or_contractor
+@use_bootstrap5
+def edit_workflow(request, pk):
+ config = get_object_or_404(AppWorkflowConfig, pk=pk)
+ form = AppWorkflowConfigForm(instance=config)
+ if request.method == "POST":
+ form = AppWorkflowConfigForm(request.POST, instance=config)
+ if form.is_valid():
+ form.save()
+ return redirect("app_execution:workflow_list")
+
+ context = _get_context(
+ request, f"Edit App Workflow: {config.name}", reverse("app_execution:edit_workflow", args=[pk]),
+ add_parent=True, form=form
+ )
+ return render(request, "app_execution/workflow_form.html", context)
+
+
+@require_superuser_or_contractor
+@use_bootstrap5
+def test_workflow(request, pk):
+ config = get_object_or_404(AppWorkflowConfig, pk=pk)
+
+ context = _get_context(
+ request, f"Test App Workflow: {config.name}", reverse("app_execution:test_workflow", args=[pk]),
+ add_parent=True, workflow=config
+ )
+
+ if request.method == "POST":
+ session = config.get_formplayer_session()
+ try:
+ execute_workflow(session, config.workflow)
+ except AppExecutionError as e:
+ context["error"] = str(e)
+
+ context["result"] = True
+ context["log"] = session.log.getvalue()
+
+ return render(request, "app_execution/workflow_test.html", context)
+
+
+def _get_context(request, title, url, add_parent=False, **kwargs):
+ parents = [{
+ "title": "Auto App Execution",
+ "url": reverse("app_execution:workflow_list"),
+ }]
+ context = get_hqadmin_base_context(request)
+ context.update({
+ "current_page": {
+ "page_name": title,
+ "title": title,
+ "url": url,
+ "parents": parents if add_parent else [],
+ },
+ "section": {"page_name": "Admin", "url": reverse("default_admin_report")},
+ })
+ return {**context, **kwargs}
diff --git a/corehq/apps/app_manager/static/app_manager/js/preview_app.js b/corehq/apps/app_manager/static/app_manager/js/preview_app.js
index d82d127ad5f1..86d65691af55 100644
--- a/corehq/apps/app_manager/static/app_manager/js/preview_app.js
+++ b/corehq/apps/app_manager/static/app_manager/js/preview_app.js
@@ -1,6 +1,17 @@
"use strict";
-hqDefine('app_manager/js/preview_app', function () {
- 'use strict';
+hqDefine('app_manager/js/preview_app', [
+ 'jquery',
+ 'analytix/js/google',
+ 'analytix/js/kissmetrix',
+ 'app_manager/js/app_manager_utils',
+ 'hqwebapp/js/layout',
+], function (
+ $,
+ googleAnalytics,
+ kissAnalytics,
+ appManagerUtils,
+ layoutController
+) {
var module = {};
var _private = {};
@@ -38,8 +49,8 @@ hqDefine('app_manager/js/preview_app', function () {
$(module.SELECTORS.PREVIEW_ACTION_TEXT_HIDE).removeClass('hide');
if (triggerAnalytics) {
- hqImport('analytix/js/kissmetrix').track.event("[app-preview] Clicked Show App Preview");
- hqImport('analytix/js/google').track.event("App Preview", "Clicked Show App Preview");
+ kissAnalytics.track.event("[app-preview] Clicked Show App Preview");
+ googleAnalytics.track.event("App Preview", "Clicked Show App Preview");
}
var $offsetContainer = (_private.isFormdesigner) ? $(module.SELECTORS.FORMDESIGNER) : $(module.SELECTORS.APP_MANAGER_BODY);
@@ -60,8 +71,8 @@ hqDefine('app_manager/js/preview_app', function () {
}
if (triggerAnalytics) {
- hqImport('analytix/js/kissmetrix').track.event("[app-preview] Clicked Hide App Preview");
- hqImport('analytix/js/google').track.event("App Preview", "Clicked Hide App Preview");
+ kissAnalytics.track.event("[app-preview] Clicked Hide App Preview");
+ googleAnalytics.track.event("App Preview", "Clicked Hide App Preview");
}
};
@@ -72,7 +83,7 @@ hqDefine('app_manager/js/preview_app', function () {
_private.triggerPreviewEvent('tablet-view');
if (triggerAnalytics) {
- hqImport('analytix/js/kissmetrix').track.event('[app-preview] User turned on tablet mode');
+ kissAnalytics.track.event('[app-preview] User turned on tablet mode');
}
};
@@ -83,7 +94,7 @@ hqDefine('app_manager/js/preview_app', function () {
_private.triggerPreviewEvent('phone-view');
if (triggerAnalytics) {
- hqImport('analytix/js/kissmetrix').track.event('[app-preview] User turned off tablet mode');
+ kissAnalytics.track.event('[app-preview] User turned off tablet mode');
}
};
@@ -150,8 +161,7 @@ hqDefine('app_manager/js/preview_app', function () {
module.initPreviewWindow = function () {
- var layoutController = hqImport("hqwebapp/js/layout"),
- $appPreview = $(module.SELECTORS.PREVIEW_WINDOW),
+ var $appPreview = $(module.SELECTORS.PREVIEW_WINDOW),
$appBody = $(module.SELECTORS.APP_MANAGER_BODY),
$togglePreviewBtn = $(module.SELECTORS.BTN_TOGGLE_PREVIEW),
$iframe = $(module.SELECTORS.PREVIEW_WINDOW_IFRAME),
@@ -220,10 +230,10 @@ hqDefine('app_manager/js/preview_app', function () {
$('.js-preview-refresh').click(function () {
$(module.SELECTORS.BTN_REFRESH).removeClass('app-out-of-date');
_private.triggerPreviewEvent('refresh');
- hqImport('analytix/js/kissmetrix').track.event("[app-preview] Clicked Refresh App Preview");
- hqImport('analytix/js/google').track.event("App Preview", "Clicked Refresh App Preview");
+ kissAnalytics.track.event("[app-preview] Clicked Refresh App Preview");
+ googleAnalytics.track.event("App Preview", "Clicked Refresh App Preview");
});
- hqImport("app_manager/js/app_manager_utils").handleAjaxAppChange(function () {
+ appManagerUtils.handleAjaxAppChange(function () {
$(module.SELECTORS.BTN_REFRESH).addClass('app-out-of-date');
});
var onload = function () {
diff --git a/corehq/apps/case_search/xpath_functions/ancestor_functions.py b/corehq/apps/case_search/xpath_functions/ancestor_functions.py
index db748492b2c7..d18db2272baf 100644
--- a/corehq/apps/case_search/xpath_functions/ancestor_functions.py
+++ b/corehq/apps/case_search/xpath_functions/ancestor_functions.py
@@ -7,6 +7,18 @@
from corehq.apps.case_search.xpath_functions.utils import confirm_args_count
from corehq.apps.case_search.const import MAX_RELATED_CASES
from corehq.apps.es.case_search import CaseSearchES, reverse_index_case_query
+from corehq.toggles import NO_SCROLL_IN_CASE_SEARCH
+
+
+def _should_scroll(context):
+ if not isinstance(context.domain, list):
+ domains = [context.domain]
+ else:
+ domains = context.domain
+ for domain in domains:
+ if NO_SCROLL_IN_CASE_SEARCH.enabled(domain):
+ return False
+ return True
def is_ancestor_comparison(node):
@@ -91,7 +103,10 @@ def _child_case_lookup(context, case_ids, identifier):
"""
es_query = CaseSearchES().domain(context.domain).get_child_cases(case_ids, identifier)
context.profiler.add_query('_child_case_lookup', es_query)
- return es_query.scroll_ids()
+ if _should_scroll(context):
+ return es_query.scroll_ids()
+ else:
+ return es_query.get_ids()
def ancestor_exists(node, context):
@@ -154,4 +169,7 @@ def _get_case_ids_from_ast_filter(context, filter_node):
new_query
)
- return es_query.scroll_ids()
+ if _should_scroll(context):
+ return es_query.scroll_ids()
+ else:
+ return es_query.get_ids()
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/debugger/debugger.js b/corehq/apps/cloudcare/static/cloudcare/js/debugger/debugger.js
index a32894d3706f..dfe9cdb26b8d 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/debugger/debugger.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/debugger/debugger.js
@@ -1,9 +1,25 @@
'use strict';
-/* globals ace, Clipboard */
-hqDefine('cloudcare/js/debugger/debugger', function () {
- var kissmetrics = hqImport("analytix/js/kissmetrix"),
- readableForm = hqImport("reports/js/readable_form");
-
+hqDefine('cloudcare/js/debugger/debugger', [
+ 'jquery',
+ 'knockout',
+ 'underscore',
+ 'clipboard/dist/clipboard',
+ 'ace-builds/src-min-noconflict/ace',
+ 'analytix/js/kissmetrix',
+ 'reports/js/readable_form',
+ 'hqwebapp/js/atwho', // $.atwho
+ 'ace-builds/src-min-noconflict/mode-json',
+ 'ace-builds/src-min-noconflict/mode-xml',
+ 'ace-builds/src-min-noconflict/ext-searchbox',
+], function (
+ $,
+ ko,
+ _,
+ Clipboard,
+ ace,
+ kissmetrics,
+ readableForm
+) {
/**
* These define tabs that are availabe in the debugger.
* {
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/const.js b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/const.js
index 55ab0a953e1c..c3cad898f493 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/const.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/const.js
@@ -1,5 +1,5 @@
'use strict';
-hqDefine("cloudcare/js/form_entry/const", function () {
+hqDefine("cloudcare/js/form_entry/const", [], function () {
return {
GROUP_TYPE: 'sub-group',
REPEAT_TYPE: 'repeat-juncture',
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/entries.js b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/entries.js
index 267fec09c3c1..ae19f12b2f35 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/entries.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/entries.js
@@ -1,13 +1,37 @@
'use strict';
-/* globals moment, SignaturePad, DOMPurify */
-hqDefine("cloudcare/js/form_entry/entries", function () {
- var kissmetrics = hqImport("analytix/js/kissmetrix"),
- cloudcareUtils = hqImport("cloudcare/js/utils"),
- constants = hqImport("cloudcare/js/form_entry/const"),
- formEntryUtils = hqImport("cloudcare/js/form_entry/utils"),
- initialPageData = hqImport("hqwebapp/js/initial_page_data"),
- toggles = hqImport("hqwebapp/js/toggles");
-
+hqDefine("cloudcare/js/form_entry/entries", [
+ 'jquery',
+ 'knockout',
+ 'underscore',
+ 'DOMPurify/dist/purify.min',
+ 'moment',
+ 'fast-levenshtein/levenshtein',
+ 'hqwebapp/js/initial_page_data',
+ 'hqwebapp/js/toggles',
+ 'analytix/js/kissmetrix',
+ 'cloudcare/js/utils',
+ 'cloudcare/js/form_entry/const',
+ 'cloudcare/js/form_entry/utils',
+ 'signature_pad/dist/signature_pad.umd.min',
+ 'mapbox.js/dist/mapbox.uncompressed',
+ 'cloudcare/js/formplayer/utils/calendar-picker-translations', // EthiopianDateEntry
+ 'select2/dist/js/select2.full.min',
+], function (
+ $,
+ ko,
+ _,
+ DOMPurify,
+ moment,
+ Levenshtein,
+ initialPageData,
+ toggles,
+ kissmetrics,
+ cloudcareUtils,
+ constants,
+ formEntryUtils,
+ SignaturePad,
+ L
+) {
/**
* The base Object for all entries. Each entry takes a question object
* @param {Object} question - A question object
@@ -677,7 +701,7 @@ hqDefine("cloudcare/js/form_entry/entries", function () {
var isFuzzyMatch = function (haystack, query, distanceThreshold) {
return (
haystack === query ||
- (query.length > 3 && window.Levenshtein.get(haystack, query) <= distanceThreshold)
+ (query.length > 3 && Levenshtein.get(haystack, query) <= distanceThreshold)
);
};
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/errors.js b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/errors.js
index cb98b78074cf..694750532c28 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/errors.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/errors.js
@@ -1,5 +1,5 @@
'use strict';
-hqDefine("cloudcare/js/form_entry/errors", function () {
+hqDefine("cloudcare/js/form_entry/errors", [], function () {
return {
GENERIC_ERROR: gettext("Something unexpected went wrong on that request. " +
"If you have problems filling in the rest of your form please submit an issue. " +
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/form_ui.js b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/form_ui.js
index 9e6d086a9294..e3659c3476c4 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/form_ui.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/form_ui.js
@@ -1,10 +1,28 @@
'use strict';
-/* global DOMPurify */
-hqDefine("cloudcare/js/form_entry/form_ui", function () {
- var markdown = hqImport("cloudcare/js/markdown"),
- constants = hqImport("cloudcare/js/form_entry/const"),
- entries = hqImport("cloudcare/js/form_entry/entries"),
- formEntryUtils = hqImport("cloudcare/js/form_entry/utils");
+hqDefine("cloudcare/js/form_entry/form_ui", [
+ 'jquery',
+ 'knockout',
+ 'underscore',
+ 'DOMPurify/dist/purify.min',
+ 'hqwebapp/js/toggles',
+ 'cloudcare/js/markdown',
+ 'cloudcare/js/utils',
+ 'cloudcare/js/form_entry/const',
+ 'cloudcare/js/form_entry/entries',
+ 'cloudcare/js/form_entry/utils',
+ 'jquery-tiny-pubsub/dist/ba-tiny-pubsub', // $.pubsub
+], function (
+ $,
+ ko,
+ _,
+ DOMPurify,
+ toggles,
+ markdown,
+ cloudcareUtils,
+ constants,
+ entries,
+ formEntryUtils
+) {
var groupNum = 0;
_.delay(function () {
@@ -391,7 +409,7 @@ hqDefine("cloudcare/js/form_entry/form_ui", function () {
self.blockSubmit = ko.observable(false);
self.hasSubmitAttempted = ko.observable(false);
self.isSubmitting = ko.observable(false);
- self.isAnchoredSubmitStyle = hqImport('hqwebapp/js/toggles').toggleEnabled('WEB_APPS_ANCHORED_SUBMIT');
+ self.isAnchoredSubmitStyle = toggles.toggleEnabled('WEB_APPS_ANCHORED_SUBMIT');
self.currentIndex = ko.observable("0");
self.atLastIndex = ko.observable(false);
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/entries_spec.js b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/entries_spec.js
index 75b58eaef33a..94aeea199dc1 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/entries_spec.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/entries_spec.js
@@ -1,21 +1,34 @@
'use strict';
/* eslint-env mocha */
-/* globals moment */
-hqDefine("cloudcare/js/form_entry/spec/entries_spec", function () {
+hqDefine("cloudcare/js/form_entry/spec/entries_spec", [
+ "underscore",
+ "sinon/pkg/sinon",
+ "moment",
+ "hqwebapp/js/initial_page_data",
+ "cloudcare/js/form_entry/const",
+ "cloudcare/js/form_entry/entries",
+ "cloudcare/js/form_entry/form_ui",
+ "cloudcare/js/utils",
+], function (
+ _,
+ sinon,
+ moment,
+ initialPageData,
+ constants,
+ entries,
+ formUI,
+ utils
+) {
describe('Entries', function () {
- var constants = hqImport("cloudcare/js/form_entry/const"),
- entries = hqImport("cloudcare/js/form_entry/entries"),
- formUI = hqImport("cloudcare/js/form_entry/form_ui"),
- utils = hqImport("cloudcare/js/utils"),
- questionJSON,
+ var questionJSON,
spy;
before(function () {
- hqImport("hqwebapp/js/initial_page_data").register(
+ initialPageData.register(
"has_geocoder_privs",
true
);
- hqImport("hqwebapp/js/initial_page_data").register(
+ initialPageData.register(
"toggles_dict",
{
WEB_APPS_UPLOAD_QUESTIONS: true,
@@ -25,7 +38,7 @@ hqDefine("cloudcare/js/form_entry/spec/entries_spec", function () {
});
after(function () {
- hqImport("hqwebapp/js/initial_page_data").unregister("toggles_dict");
+ initialPageData.unregister("toggles_dict");
});
beforeEach(function () {
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/fixtures.js b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/fixtures.js
index f76283f466c3..b2b952def540 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/fixtures.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/fixtures.js
@@ -1,5 +1,9 @@
'use strict';
-hqDefine("cloudcare/js/form_entry/spec/fixtures", function () {
+hqDefine("cloudcare/js/form_entry/spec/fixtures", [
+ "underscore",
+], function (
+ _
+) {
return {
textJSON: (options = {}) => (_.defaults(options, {
"caption_audio": null,
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/form_ui_spec.js b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/form_ui_spec.js
index 8073477e821d..1fb8bdb07737 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/form_ui_spec.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/form_ui_spec.js
@@ -1,11 +1,22 @@
'use strict';
/* eslint-env mocha */
-hqDefine("cloudcare/js/form_entry/spec/form_ui_spec", function () {
+hqDefine("cloudcare/js/form_entry/spec/form_ui_spec", [
+ "underscore",
+ "sinon/pkg/sinon",
+ "hqwebapp/js/initial_page_data",
+ "cloudcare/js/form_entry/const",
+ "cloudcare/js/form_entry/form_ui",
+ "cloudcare/js/form_entry/spec/fixtures",
+], function (
+ _,
+ sinon,
+ initialPageData,
+ constants,
+ formUI,
+ fixtures
+) {
describe('Fullform formUI', function () {
- var constants = hqImport("cloudcare/js/form_entry/const"),
- formUI = hqImport("cloudcare/js/form_entry/form_ui"),
- fixtures = hqImport("cloudcare/js/form_entry/spec/fixtures"),
- questionJSON,
+ var questionJSON,
formJSON,
groupJSON,
noQuestionGroupJSON,
@@ -15,7 +26,7 @@ hqDefine("cloudcare/js/form_entry/spec/form_ui_spec", function () {
repeatNestJSON;
before(function () {
- hqImport("hqwebapp/js/initial_page_data").register(
+ initialPageData.register(
"toggles_dict",
{
WEB_APPS_UPLOAD_QUESTIONS: true,
@@ -25,7 +36,7 @@ hqDefine("cloudcare/js/form_entry/spec/form_ui_spec", function () {
});
after(function () {
- hqImport("hqwebapp/js/initial_page_data").unregister("toggles_dict");
+ initialPageData.unregister("toggles_dict");
});
beforeEach(function () {
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/integration_spec.js b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/integration_spec.js
index da59f607090e..10dec1f4d230 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/integration_spec.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/integration_spec.js
@@ -1,19 +1,29 @@
'use strict';
/* eslint-env mocha */
-hqDefine("cloudcare/js/form_entry/spec/integration_spec", function () {
+hqDefine("cloudcare/js/form_entry/spec/integration_spec", [
+ "underscore",
+ "hqwebapp/js/initial_page_data",
+ "sinon/pkg/sinon",
+ "cloudcare/js/form_entry/const",
+ "cloudcare/js/form_entry/form_ui",
+], function (
+ _,
+ initialPageData,
+ sinon,
+ constants,
+ formUI
+) {
describe('Integration', function () {
- var constants = hqImport("cloudcare/js/form_entry/const"),
- formUI = hqImport("cloudcare/js/form_entry/form_ui"),
- formJSON,
+ var formJSON,
questionJSONMulti,
questionJSONString;
before(function () {
- hqImport("hqwebapp/js/initial_page_data").register("toggles_dict", { WEB_APPS_ANCHORED_SUBMIT: false });
+ initialPageData.register("toggles_dict", { WEB_APPS_ANCHORED_SUBMIT: false });
});
after(function () {
- hqImport("hqwebapp/js/initial_page_data").unregister("toggles_dict");
+ initialPageData.unregister("toggles_dict");
});
beforeEach(function () {
@@ -74,7 +84,6 @@ hqDefine("cloudcare/js/form_entry/spec/integration_spec", function () {
this.clock.restore();
});
-
it('Should reconcile questions answered at the same time for strings', function () {
var questionJSONString2 = {};
$.extend(questionJSONString2, questionJSONString);
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/main.js b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/main.js
new file mode 100644
index 000000000000..7528fe52fd82
--- /dev/null
+++ b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/main.js
@@ -0,0 +1,18 @@
+'use strict';
+hqDefine("cloudcare/js/form_entry/spec/main", [
+ "mocha/js/main",
+], function (
+ hqMocha
+) {
+ hqRequire([
+ "cloudcare/js/form_entry/spec/entries_spec",
+ "cloudcare/js/form_entry/spec/form_ui_spec",
+ "cloudcare/js/form_entry/spec/integration_spec",
+ "cloudcare/js/form_entry/spec/utils_spec",
+ "cloudcare/js/form_entry/spec/web_form_session_spec",
+ ], function () {
+ hqMocha.run();
+ });
+
+ return 1;
+});
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/utils_spec.js b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/utils_spec.js
index f86180c3d6d6..77824b60d1b6 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/utils_spec.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/utils_spec.js
@@ -1,10 +1,16 @@
'use strict';
-hqDefine("cloudcare/js/form_entry/spec/utils_spec", function () {
+hqDefine("cloudcare/js/form_entry/spec/utils_spec", [
+ "hqwebapp/js/initial_page_data",
+ "cloudcare/js/form_entry/spec/fixtures",
+ "cloudcare/js/form_entry/form_ui",
+ "cloudcare/js/form_entry/utils",
+], function (
+ initialPageData,
+ fixtures,
+ formUI,
+ utils
+) {
describe('Formplayer utils', function () {
- var fixtures = hqImport("cloudcare/js/form_entry/spec/fixtures"),
- formUI = hqImport("cloudcare/js/form_entry/form_ui"),
- utils = hqImport("cloudcare/js/form_entry/utils");
-
it('Should determine if two answers are equal', function () {
var answersEqual = utils.answersEqual,
result;
@@ -38,7 +44,7 @@ hqDefine("cloudcare/js/form_entry/spec/utils_spec", function () {
* grouped-element-tile-row
* textInRepeat
*/
- hqImport("hqwebapp/js/initial_page_data").register("toggles_dict", { WEB_APPS_ANCHORED_SUBMIT: false });
+ initialPageData.register("toggles_dict", { WEB_APPS_ANCHORED_SUBMIT: false });
var text = fixtures.textJSON({ix: "0"}),
textInGroup = fixtures.textJSON({ix: "1,0"}),
group = fixtures.groupJSON({ix: "1", children: [textInGroup]}),
@@ -60,7 +66,7 @@ hqDefine("cloudcare/js/form_entry/spec/utils_spec", function () {
assert.equal(utils.getBroadcastContainer(text), form);
assert.equal(utils.getBroadcastContainer(textInRepeat), groupInRepeat);
- hqImport("hqwebapp/js/initial_page_data").unregister("toggles_dict");
+ initialPageData.unregister("toggles_dict");
});
});
});
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/web_form_session_spec.js b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/web_form_session_spec.js
index 3f127a340605..48522cc9538b 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/web_form_session_spec.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/spec/web_form_session_spec.js
@@ -1,25 +1,43 @@
'use strict';
/* eslint-env mocha */
-hqDefine("cloudcare/js/form_entry/spec/web_form_session_spec", function () {
+hqDefine("cloudcare/js/form_entry/spec/web_form_session_spec", [
+ "sinon/pkg/sinon",
+ "hqwebapp/js/initial_page_data",
+ "cloudcare/js/form_entry/const",
+ "cloudcare/js/form_entry/errors",
+ "cloudcare/js/form_entry/form_ui",
+ "cloudcare/js/form_entry/spec/fixtures",
+ "cloudcare/js/form_entry/task_queue",
+ "cloudcare/js/form_entry/utils",
+ "cloudcare/js/form_entry/web_form_session",
+ //"jasmine-fixture/dist/jasmine-fixture", // affix - TODO: this errors in a try
+], function (
+ sinon,
+ initialPageData,
+ constants,
+ errors,
+ formUI,
+ Fixtures,
+ taskQueue,
+ Utils,
+ webFormSession
+) {
describe('WebForm', function () {
- var constants = hqImport("cloudcare/js/form_entry/const"),
- formUI = hqImport("cloudcare/js/form_entry/form_ui");
-
before(function () {
- hqImport("hqwebapp/js/initial_page_data").register("toggles_dict", {
+ initialPageData.register("toggles_dict", {
WEB_APPS_ANCHORED_SUBMIT: false,
USE_PROMINENT_PROGRESS_BAR: false,
});
});
after(function () {
- hqImport("hqwebapp/js/initial_page_data").unregister("toggles_dict");
+ initialPageData.unregister("toggles_dict");
});
describe('TaskQueue', function () {
var callCount,
flag,
- queue = hqImport("cloudcare/js/form_entry/task_queue").TaskQueue(),
+ queue = taskQueue.TaskQueue(),
promise1,
promise2,
updateFlag = function (newValue, promise) {
@@ -73,10 +91,9 @@ hqDefine("cloudcare/js/form_entry/spec/web_form_session_spec", function () {
describe('WebFormSession', function () {
var server,
params,
- Utils = hqImport("cloudcare/js/form_entry/utils"),
- WebFormSession = hqImport("cloudcare/js/form_entry/web_form_session").WebFormSession;
+ WebFormSession = webFormSession.WebFormSession;
- hqImport("hqwebapp/js/initial_page_data").registerUrl(
+ initialPageData.registerUrl(
"report_formplayer_error",
"/a/domain/cloudcare/apps/report_formplayer_error"
);
@@ -241,7 +258,7 @@ hqDefine("cloudcare/js/form_entry/spec/web_form_session_spec", function () {
assert.isTrue(sess.onerror.calledOnce);
assert.isTrue(sess.onerror.calledWith({
- human_readable_message: hqImport("cloudcare/js/form_entry/errors").TIMEOUT_ERROR,
+ human_readable_message: errors.TIMEOUT_ERROR,
is_html: false,
reportToHq: false,
}));
@@ -260,11 +277,9 @@ hqDefine("cloudcare/js/form_entry/spec/web_form_session_spec", function () {
describe('Question Validation', function () {
let server,
formJSON,
- Utils = hqImport("cloudcare/js/form_entry/utils"),
- WebFormSession = hqImport("cloudcare/js/form_entry/web_form_session").WebFormSession,
- Fixtures = hqImport("cloudcare/js/form_entry/spec/fixtures");
+ WebFormSession = webFormSession.WebFormSession;
- hqImport("hqwebapp/js/initial_page_data").registerUrl(
+ initialPageData.registerUrl(
"report_formplayer_error",
"/a/domain/cloudcare/apps/report_formplayer_error"
);
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/task_queue.js b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/task_queue.js
index 47f57709f581..2a8671529006 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/task_queue.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/task_queue.js
@@ -5,7 +5,11 @@
*
* All task functions are expected to return a promise.
*/
-hqDefine("cloudcare/js/form_entry/task_queue", function () {
+hqDefine("cloudcare/js/form_entry/task_queue", [
+ 'underscore',
+], function (
+ _
+) {
var TaskQueue = function () {
var self = {};
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/utils.js b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/utils.js
index 1fb3c1efe336..219f806c4b95 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/utils.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/utils.js
@@ -1,11 +1,24 @@
'use strict';
-/*global MapboxGeocoder*/
-hqDefine("cloudcare/js/form_entry/utils", function () {
- var errors = hqImport("cloudcare/js/form_entry/errors"),
- formEntryConst = hqImport("cloudcare/js/form_entry/const"),
- toggles = hqImport("hqwebapp/js/toggles"),
- initialPageData = hqImport("hqwebapp/js/initial_page_data");
-
+hqDefine("cloudcare/js/form_entry/utils", [
+ 'jquery',
+ 'knockout',
+ 'underscore',
+ '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.min',
+ 'hqwebapp/js/initial_page_data',
+ 'hqwebapp/js/toggles',
+ 'cloudcare/js/form_entry/const',
+ 'cloudcare/js/form_entry/errors',
+ 'cloudcare/js/formplayer/constants',
+], function (
+ $,
+ ko,
+ _,
+ MapboxGeocoder,
+ initialPageData,
+ toggles,
+ formEntryConst,
+ errors
+) {
var module = {
resourceMap: undefined,
};
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/web_form_session.js b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/web_form_session.js
index f267ba027603..c182a247deb2 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/form_entry/web_form_session.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/form_entry/web_form_session.js
@@ -1,12 +1,25 @@
'use strict';
-hqDefine("cloudcare/js/form_entry/web_form_session", function () {
- var cloudcareUtils = hqImport("cloudcare/js/utils"),
- constants = hqImport("cloudcare/js/form_entry/const"),
- errors = hqImport("cloudcare/js/form_entry/errors"),
- taskQueue = hqImport("cloudcare/js/form_entry/task_queue"),
- formEntryUtils = hqImport("cloudcare/js/form_entry/utils"),
- formUI = hqImport("cloudcare/js/form_entry/form_ui");
-
+hqDefine("cloudcare/js/form_entry/web_form_session", [
+ 'jquery',
+ 'knockout',
+ 'underscore',
+ 'cloudcare/js/utils',
+ 'cloudcare/js/form_entry/const',
+ 'cloudcare/js/form_entry/errors',
+ 'cloudcare/js/form_entry/task_queue',
+ 'cloudcare/js/form_entry/utils',
+ 'cloudcare/js/form_entry/form_ui',
+], function (
+ $,
+ ko,
+ _,
+ cloudcareUtils,
+ constants,
+ errors,
+ taskQueue,
+ formEntryUtils,
+ formUI
+) {
function WebFormSession(params) {
var self = {};
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/app.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/app.js
index 02d9dacd235f..a43fec95608d 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/app.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/app.js
@@ -1,22 +1,49 @@
'use strict';
-/*global Marionette, Backbone */
-
/**
* The primary Marionette application managing menu navigation and launching form entry
*/
-hqDefine("cloudcare/js/formplayer/app", function () {
- var appcues = hqImport('analytix/js/appcues'),
- initialPageData = hqImport("hqwebapp/js/initial_page_data"),
- CloudcareUtils = hqImport("cloudcare/js/utils"),
- Const = hqImport("cloudcare/js/formplayer/constants"),
- FormplayerUtils = hqImport("cloudcare/js/formplayer/utils/utils"),
- GGAnalytics = hqImport("analytix/js/google"),
- Kissmetrics = hqImport("analytix/js/kissmetrix"),
- ProgressBar = hqImport("cloudcare/js/formplayer/layout/views/progress_bar"),
- UsersModels = hqImport("cloudcare/js/formplayer/users/models"),
- WebFormSession = hqImport('cloudcare/js/form_entry/web_form_session');
-
- Marionette.setRenderer(Marionette.TemplateCache.render);
+hqDefine("cloudcare/js/formplayer/app", [
+ 'jquery',
+ 'knockout',
+ 'underscore',
+ 'backbone',
+ 'backbone.marionette',
+ 'markdown-it/dist/markdown-it',
+ 'hqwebapp/js/initial_page_data',
+ 'analytix/js/appcues',
+ 'analytix/js/google',
+ 'analytix/js/kissmetrix',
+ 'cloudcare/js/utils',
+ 'cloudcare/js/formplayer/apps/api',
+ 'cloudcare/js/formplayer/constants',
+ 'cloudcare/js/formplayer/utils/utils',
+ 'cloudcare/js/formplayer/layout/views/progress_bar',
+ 'cloudcare/js/formplayer/users/models',
+ 'cloudcare/js/form_entry/web_form_session',
+ 'marionette.templatecache/lib/marionette.templatecache.min', // needed for Marionette.TemplateCache
+ 'backbone.radio',
+ 'jquery.cookie/jquery.cookie', // $.cookie
+], function (
+ $,
+ ko,
+ _,
+ Backbone,
+ Marionette,
+ markdowner,
+ initialPageData,
+ appcues,
+ GGAnalytics,
+ Kissmetrics,
+ CloudcareUtils,
+ AppsAPI,
+ Const,
+ FormplayerUtils,
+ ProgressBar,
+ UsersModels,
+ WebFormSession,
+ TemplateCache
+) {
+ Marionette.setRenderer(TemplateCache.render);
var FormplayerFrontend = new Marionette.Application();
FormplayerFrontend.on("before:start", function (app, options) {
@@ -50,11 +77,6 @@ hqDefine("cloudcare/js/formplayer/app", function () {
});
});
- FormplayerFrontend.navigate = function (route, options) {
- options || (options = {});
- Backbone.history.navigate(route, options);
- };
-
FormplayerFrontend.getCurrentRoute = function () {
return Backbone.history.fragment;
};
@@ -69,7 +91,7 @@ hqDefine("cloudcare/js/formplayer/app", function () {
* The actual mapping is contained in the app Couch document
*/
FormplayerFrontend.getChannel().reply('resourceMap', function (resourcePath, appId) {
- var currentApp = FormplayerFrontend.getChannel().request("appselect:getApp", appId);
+ var currentApp = AppsAPI.getAppEntity(appId);
if (!currentApp) {
console.warn('App is undefined for app_id: ' + appId);
console.warn('Not processing resource: ' + resourcePath);
@@ -80,7 +102,7 @@ hqDefine("cloudcare/js/formplayer/app", function () {
} else if (!_.isEmpty(currentApp.get("multimedia_map"))) {
var resource = currentApp.get('multimedia_map')[resourcePath];
if (!resource) {
- console.warn('Unable to find resource ' + resourcePath + ' in multimedia map');
+ console.warn('Unable to find resource ' + resourcePath + 'in multimedia map');
return;
}
var id = resource.multimedia_id;
@@ -97,13 +119,6 @@ hqDefine("cloudcare/js/formplayer/app", function () {
}
});
- FormplayerFrontend.getChannel().reply('currentUser', function () {
- if (!FormplayerFrontend.currentUser) {
- FormplayerFrontend.currentUser = UsersModels.CurrentUser();
- }
- return FormplayerFrontend.currentUser;
- });
-
FormplayerFrontend.getChannel().reply('lastRecordedLocation', function () {
if (!sessionStorage.locationLat) {
return null;
@@ -180,7 +195,7 @@ hqDefine("cloudcare/js/formplayer/app", function () {
data.onLoading = CloudcareUtils.formplayerLoading;
data.onLoadingComplete = CloudcareUtils.formplayerLoadingComplete;
- var user = FormplayerFrontend.getChannel().request('currentUser');
+ var user = UsersModels.getCurrentUser();
data.xform_url = user.formplayer_url;
data.domain = user.domain;
data.username = user.username;
@@ -206,8 +221,7 @@ hqDefine("cloudcare/js/formplayer/app", function () {
if (resp.status === "success") {
var $alert;
if (resp.submitResponseMessage) {
- var markdowner = window.markdownit(),
- analyticsLinks = [
+ var analyticsLinks = [
{ url: initialPageData.reverse('list_case_exports'), text: '[Data Feedback Loop Test] Clicked on Export Cases Link' },
{ url: initialPageData.reverse('list_form_exports'), text: '[Data Feedback Loop Test] Clicked on Export Forms Link' },
{ url: initialPageData.reverse('case_data', '.*'), text: '[Data Feedback Loop Test] Clicked on Case Data Link' },
@@ -226,7 +240,7 @@ hqDefine("cloudcare/js/formplayer/app", function () {
}
};
$("#cloudcare-notifications").off('click').on('click', dataFeedbackLoopAnalytics);
- $alert = CloudcareUtils.showSuccess(markdowner.render(resp.submitResponseMessage), $("#cloudcare-notifications"), undefined, true);
+ $alert = CloudcareUtils.showSuccess(markdowner().render(resp.submitResponseMessage), $("#cloudcare-notifications"), undefined, true);
} else {
$alert = CloudcareUtils.showSuccess(gettext("Form successfully saved!"), $("#cloudcare-notifications"));
}
@@ -268,7 +282,7 @@ hqDefine("cloudcare/js/formplayer/app", function () {
} else if (urlObject.appId !== null && urlObject.appId !== undefined) {
FormplayerFrontend.trigger("apps:currentApp");
} else {
- FormplayerFrontend.navigate('/apps', { trigger: true });
+ FormplayerUtils.navigate('/apps', { trigger: true });
}
} else {
if (user.environment === Const.PREVIEW_APP_ENVIRONMENT) {
@@ -285,47 +299,24 @@ hqDefine("cloudcare/js/formplayer/app", function () {
};
var sess = WebFormSession.WebFormSession(data);
sess.renderFormXml(data, $('#webforms'));
- if (user.environment === Const.WEB_APPS_ENVIRONMENT) {
- // This isn't a circular import, but importing it at the top level would
- // mean it would need to be faked for tests
- hqRequire(["notifications/js/bootstrap3/notifications_service_main"], function (Notifications) {
- Notifications.initNotifications();
- });
- }
$('.menu-scrollable-container').addClass('hide');
});
FormplayerFrontend.on("start", function (model, options) {
- var user = FormplayerFrontend.getChannel().request('currentUser'),
- self = this;
- user.username = options.username;
- user.domain = options.domain;
- user.formplayer_url = options.formplayer_url;
- user.debuggerEnabled = options.debuggerEnabled;
- user.environment = options.environment;
- user.restoreAs = FormplayerFrontend.getChannel().request('restoreAsUser', user.domain, user.username);
-
- hqRequire(["cloudcare/js/formplayer/apps/api"], function (AppsAPI) {
+ var self = this,
+ user = UsersModels.setCurrentUser(options);
+
+ hqRequire([
+ "cloudcare/js/formplayer/users/utils", // restoreAsUser
+ ], function () {
+ user.restoreAs = FormplayerFrontend.getChannel().request('restoreAsUser', user.domain, user.username);
AppsAPI.primeApps(user.restoreAs, options.apps);
});
- $.when(FormplayerUtils.getSavedDisplayOptions()).done(function (savedDisplayOptions) {
- savedDisplayOptions = _.pick(
- savedDisplayOptions,
- Const.ALLOWED_SAVED_OPTIONS
- );
- user.displayOptions = _.defaults(savedDisplayOptions, {
- singleAppMode: options.singleAppMode,
- landingPageAppMode: options.landingPageAppMode,
- phoneMode: options.phoneMode,
- oneQuestionPerScreen: options.oneQuestionPerScreen,
- language: options.language,
- });
- FormplayerFrontend.getChannel().request('gridPolyfillPath', options.gridPolyfillPath);
- $.when(
- FormplayerFrontend.getChannel().request("appselect:apps"),
- FormplayerFrontend.xsrfRequest
- ).done(function (appCollection) {
+ FormplayerFrontend.getChannel().request('gridPolyfillPath', options.gridPolyfillPath);
+ hqRequire(["cloudcare/js/formplayer/router"], function (Router) {
+ FormplayerFrontend.router = Router.start();
+ $.when(AppsAPI.getAppEntities()).done(function (appCollection) {
var appId;
var apps = appCollection.toJSON();
if (Backbone.history) {
@@ -415,7 +406,7 @@ hqDefine("cloudcare/js/formplayer/app", function () {
hqRequire(["cloudcare/js/debugger/debugger"], function (Debugger) {
var CloudCareDebugger = Debugger.CloudCareDebuggerMenu,
TabIDs = Debugger.TabIDs,
- user = FormplayerFrontend.getChannel().request('currentUser'),
+ user = UsersModels.getCurrentUser(),
cloudCareDebugger,
$debug = $('#cloudcare-debugger');
@@ -446,7 +437,7 @@ hqDefine("cloudcare/js/formplayer/app", function () {
FormplayerFrontend.getChannel().reply('getCurrentAppId', function () {
// First attempt to grab app id from URL
var urlObject = FormplayerUtils.currentUrlToObject(),
- user = FormplayerFrontend.getChannel().request('currentUser'),
+ user = UsersModels.getCurrentUser(),
appId;
appId = urlObject.appId;
@@ -509,7 +500,7 @@ hqDefine("cloudcare/js/formplayer/app", function () {
function makeSyncRequest(route, requestData) {
var options,
complete,
- user = FormplayerFrontend.getChannel().request('currentUser'),
+ user = UsersModels.getCurrentUser(),
formplayerUrl = user.formplayer_url,
data = {
"username": user.username,
@@ -617,7 +608,7 @@ hqDefine("cloudcare/js/formplayer/app", function () {
FormplayerFrontend.on('setVersionInfo', function (versionInfo) {
- var user = FormplayerFrontend.getChannel().request('currentUser');
+ var user = UsersModels.getCurrentUser();
$("#version-info").text(versionInfo || '');
if (versionInfo) {
user.set('versionInfo', versionInfo);
@@ -637,7 +628,7 @@ hqDefine("cloudcare/js/formplayer/app", function () {
if (!appId) {
throw new Error('Attempt to refresh application for null appId');
}
- var user = FormplayerFrontend.getChannel().request('currentUser'),
+ var user = UsersModels.getCurrentUser(),
formplayerUrl = user.formplayer_url,
resp,
options = {
@@ -673,7 +664,7 @@ hqDefine("cloudcare/js/formplayer/app", function () {
* current user. Returns the ajax promise.
*/
FormplayerFrontend.getChannel().reply('breakLocks', function () {
- var user = FormplayerFrontend.getChannel().request('currentUser'),
+ var user = UsersModels.getCurrentUser(),
formplayerUrl = user.formplayer_url,
resp,
options = {
@@ -702,7 +693,7 @@ hqDefine("cloudcare/js/formplayer/app", function () {
* current user. Returns the ajax promise.
*/
FormplayerFrontend.getChannel().reply('clearUserData', function () {
- var user = FormplayerFrontend.getChannel().request('currentUser'),
+ var user = UsersModels.getCurrentUser(),
formplayerUrl = user.formplayer_url,
resp,
options = {
@@ -730,7 +721,7 @@ hqDefine("cloudcare/js/formplayer/app", function () {
var urlObject = FormplayerUtils.currentUrlToObject(),
appId,
- currentUser = FormplayerFrontend.getChannel().request('currentUser');
+ currentUser = UsersModels.getCurrentUser();
urlObject.clearExceptApp();
FormplayerFrontend.regions.getRegion('sidebar').empty();
FormplayerFrontend.regions.getRegion('breadcrumb').empty();
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/apps/api.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/apps/api.js
index 8e512f1a201a..2bbe4aac018f 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/apps/api.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/apps/api.js
@@ -2,11 +2,15 @@
/**
* Backbone model and functions for listing and selecting CommCare apps
*/
-
-hqDefine("cloudcare/js/formplayer/apps/api", function () {
- var Collections = hqImport("cloudcare/js/formplayer/apps/collections"),
- FormplayerFrontend = hqImport("cloudcare/js/formplayer/app");
-
+hqDefine("cloudcare/js/formplayer/apps/api", [
+ 'jquery',
+ 'cloudcare/js/formplayer/apps/collections',
+ 'cloudcare/js/formplayer/users/models',
+], function (
+ $,
+ Collections,
+ UsersModels
+) {
var appsPromiseByRestoreAs = {};
var appsByRestoreAs = {};
var predefinedAppsPromise;
@@ -34,8 +38,8 @@ hqDefine("cloudcare/js/formplayer/apps/api", function () {
},
getAppEntities: function () {
var appsPromise,
- restoreAs = FormplayerFrontend.getChannel().request('currentUser').restoreAs,
- singleAppMode = FormplayerFrontend.getChannel().request('currentUser').displayOptions.singleAppMode;
+ restoreAs = UsersModels.getCurrentUser().restoreAs,
+ singleAppMode = UsersModels.getCurrentUser().displayOptions.singleAppMode;
if (singleAppMode) {
appsPromise = fetchPredefinedApps();
} else {
@@ -47,7 +51,7 @@ hqDefine("cloudcare/js/formplayer/apps/api", function () {
});
},
getAppEntity: function (id) {
- var restoreAs = FormplayerFrontend.getChannel().request('currentUser').restoreAs;
+ var restoreAs = UsersModels.getCurrentUser().restoreAs;
var apps = appsByRestoreAs[restoreAs];
if (!apps) {
console.warn("getAppEntity is returning null. If the app_id is correct, " +
@@ -59,13 +63,5 @@ hqDefine("cloudcare/js/formplayer/apps/api", function () {
},
};
- FormplayerFrontend.getChannel().reply("appselect:apps", function () {
- return API.getAppEntities();
- });
-
- FormplayerFrontend.getChannel().reply("appselect:getApp", function (id) {
- return API.getAppEntity(id);
- });
-
return API;
});
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/apps/collections.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/apps/collections.js
index 47c4e0ddf501..759db1b007e1 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/apps/collections.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/apps/collections.js
@@ -1,9 +1,11 @@
'use strict';
-/*global Backbone */
-
-hqDefine("cloudcare/js/formplayer/apps/collections", function () {
- var Models = hqImport("cloudcare/js/formplayer/apps/models");
-
+hqDefine("cloudcare/js/formplayer/apps/collections", [
+ 'backbone',
+ 'cloudcare/js/formplayer/apps/models',
+], function (
+ Backbone,
+ Models
+) {
var self = Backbone.Collection.extend({
url: "appSelects",
model: Models,
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/apps/controller.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/apps/controller.js
index 504d2b6ed1ce..2f1b04a224b0 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/apps/controller.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/apps/controller.js
@@ -1,16 +1,28 @@
'use strict';
-/*global Backbone */
-
-hqDefine("cloudcare/js/formplayer/apps/controller", function () {
- var constants = hqImport("cloudcare/js/formplayer/constants"),
- FormplayerFrontend = hqImport("cloudcare/js/formplayer/app"),
- settingsViews = hqImport("cloudcare/js/formplayer/layout/views/settings"),
- Toggles = hqImport("hqwebapp/js/toggles"),
- views = hqImport("cloudcare/js/formplayer/apps/views");
-
+hqDefine("cloudcare/js/formplayer/apps/controller", [
+ 'jquery',
+ 'backbone',
+ 'hqwebapp/js/toggles',
+ 'cloudcare/js/formplayer/constants',
+ 'cloudcare/js/formplayer/app',
+ 'cloudcare/js/formplayer/layout/views/settings',
+ 'cloudcare/js/formplayer/apps/api',
+ 'cloudcare/js/formplayer/apps/views',
+ 'cloudcare/js/formplayer/users/models',
+], function (
+ $,
+ Backbone,
+ Toggles,
+ constants,
+ FormplayerFrontend,
+ settingsViews,
+ AppsAPI,
+ views,
+ UsersModels
+) {
return {
listApps: function () {
- $.when(FormplayerFrontend.getChannel().request("appselect:apps")).done(function (appCollection) {
+ $.when(AppsAPI.getAppEntities()).done(function (appCollection) {
let apps = appCollection.toJSON();
let isIncompleteFormsDisabled = (app) => (app.profile.properties || {})['cc-show-incomplete'] === 'no';
let isAllIncompleteFormsDisabled = apps.every(isIncompleteFormsDisabled);
@@ -28,7 +40,7 @@ hqDefine("cloudcare/js/formplayer/apps/controller", function () {
* Renders a SingleAppView.
*/
singleApp: function (appId) {
- $.when(FormplayerFrontend.getChannel().request("appselect:apps")).done(function () {
+ $.when(AppsAPI.getAppEntities()).done(function () {
var singleAppView = views.SingleAppView({
appId: appId,
});
@@ -36,7 +48,7 @@ hqDefine("cloudcare/js/formplayer/apps/controller", function () {
});
},
landingPageApp: function (appId) {
- $.when(FormplayerFrontend.getChannel().request("appselect:apps")).done(function () {
+ $.when(AppsAPI.getAppEntities()).done(function () {
var landingPageAppView = views.LandingPageAppView({
appId: appId,
});
@@ -44,7 +56,7 @@ hqDefine("cloudcare/js/formplayer/apps/controller", function () {
});
},
listSettings: function () {
- var currentUser = FormplayerFrontend.getChannel().request('currentUser'),
+ var currentUser = UsersModels.getCurrentUser(),
slugs = settingsViews.slugs,
settings = [],
collection,
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/apps/models.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/apps/models.js
index 9c5a10322b79..21a91ab904aa 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/apps/models.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/apps/models.js
@@ -1,7 +1,9 @@
'use strict';
-/*global Backbone */
-
-hqDefine("cloudcare/js/formplayer/apps/models", function () {
+hqDefine("cloudcare/js/formplayer/apps/models", [
+ 'backbone',
+], function (
+ Backbone
+) {
return Backbone.Model.extend({
urlRoot: "appSelects",
idAttribute: "_id",
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/apps/views.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/apps/views.js
index 1b278a05713d..1a3a3173851b 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/apps/views.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/apps/views.js
@@ -1,12 +1,23 @@
'use strict';
-/*global Marionette */
-
-hqDefine("cloudcare/js/formplayer/apps/views", function () {
- var constants = hqImport("cloudcare/js/formplayer/constants"),
- FormplayerFrontend = hqImport("cloudcare/js/formplayer/app"),
- googleAnalytics = hqImport("analytix/js/google"),
- kissmetrics = hqImport("analytix/js/kissmetrix");
-
+hqDefine("cloudcare/js/formplayer/apps/views", [
+ 'jquery',
+ 'underscore',
+ 'backbone.marionette',
+ 'analytix/js/google',
+ 'analytix/js/kissmetrix',
+ 'cloudcare/js/formplayer/constants',
+ 'cloudcare/js/formplayer/app',
+ 'cloudcare/js/formplayer/apps/api',
+], function (
+ $,
+ _,
+ Marionette,
+ googleAnalytics,
+ kissmetrics,
+ constants,
+ FormplayerFrontend,
+ AppsAPI
+) {
var GridItem = Marionette.View.extend({
template: _.template($("#row-template").html() || ""),
tagName: "div",
@@ -141,7 +152,7 @@ hqDefine("cloudcare/js/formplayer/apps/views", function () {
this.appId = options.appId;
},
templateContext: function () {
- var currentApp = FormplayerFrontend.getChannel().request("appselect:getApp", this.appId),
+ var currentApp = AppsAPI.getAppEntity(this.appId),
appName;
appName = currentApp.get('name');
return {
@@ -186,7 +197,7 @@ hqDefine("cloudcare/js/formplayer/apps/views", function () {
this.appId = options.appId;
},
templateContext: function () {
- var currentApp = FormplayerFrontend.getChannel().request("appselect:getApp", this.appId),
+ var currentApp = AppsAPI.getAppEntity(this.appId),
appName = currentApp.get('name'),
imageUri = currentApp.get('imageUri');
return {
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/constants.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/constants.js
index dfc5201fd23a..bc0c79406460 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/constants.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/constants.js
@@ -1,5 +1,5 @@
'use strict';
-hqDefine("cloudcare/js/formplayer/constants", function () {
+hqDefine("cloudcare/js/formplayer/constants", [], function () {
return {
ALLOWED_SAVED_OPTIONS: ['oneQuestionPerScreen', 'language'],
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/hq_events.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/hq_events.js
index 1aba2f967f37..c1665c40db89 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/hq_events.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/hq_events.js
@@ -4,8 +4,13 @@
*
* This is framework for allowing messages from HQ
*/
-hqDefine("cloudcare/js/formplayer/hq_events", function () {
- var FormplayerFrontend = hqImport("cloudcare/js/formplayer/app");
+hqDefine("cloudcare/js/formplayer/hq_events", [
+ 'underscore',
+ 'cloudcare/js/formplayer/app',
+], function (
+ _,
+ FormplayerFrontend
+) {
var self = {};
self.Receiver = function (allowedHost) {
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/layout/views/progress_bar.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/layout/views/progress_bar.js
index 17db708af21a..088900faf60e 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/layout/views/progress_bar.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/layout/views/progress_bar.js
@@ -1,7 +1,15 @@
'use strict';
-/*global Marionette */
-
-hqDefine("cloudcare/js/formplayer/layout/views/progress_bar", function () {
+hqDefine("cloudcare/js/formplayer/layout/views/progress_bar", [
+ 'jquery',
+ 'underscore',
+ 'backbone.marionette',
+ 'hqwebapp/js/toggles',
+], function (
+ $,
+ _,
+ Marionette,
+ toggles
+) {
var ProgressView = Marionette.View.extend({
template: _.template($("#progress-view-template").html() || ""),
@@ -33,7 +41,7 @@ hqDefine("cloudcare/js/formplayer/layout/views/progress_bar", function () {
// Due to jQuery bug, can't use .animate() with % until jQuery 3.0
this.progressEl.find('.js-progress-bar').css('transition', duration + 'ms');
this.progressEl.find('.js-progress-bar').width(progress * 100 + '%');
- if (total > 0 && !(hqImport('hqwebapp/js/toggles').toggleEnabled('USE_PROMINENT_PROGRESS_BAR'))) {
+ if (total > 0 && !(toggles.toggleEnabled('USE_PROMINENT_PROGRESS_BAR'))) {
this.progressEl.find('.js-subtext small').text(
gettext('Completed: ') + done + '/' + total
);
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/layout/views/settings.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/layout/views/settings.js
index aec8998676e8..f4bad5ccb33f 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/layout/views/settings.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/layout/views/settings.js
@@ -1,10 +1,21 @@
'use strict';
-/*global Marionette */
-
-hqDefine("cloudcare/js/formplayer/layout/views/settings", function () {
- var FormplayerFrontend = hqImport("cloudcare/js/formplayer/app"),
- Utils = hqImport("cloudcare/js/formplayer/utils/utils");
-
+hqDefine("cloudcare/js/formplayer/layout/views/settings", [
+ 'jquery',
+ 'underscore',
+ 'backbone.marionette',
+ 'cloudcare/js/formplayer/app',
+ 'cloudcare/js/formplayer/apps/api',
+ 'cloudcare/js/formplayer/users/models',
+ 'cloudcare/js/formplayer/utils/utils',
+ 'bootstrap-switch/dist/js/bootstrap-switch', // bootstrapSwitch
+], function (
+ $,
+ _,
+ Marionette,
+ FormplayerFrontend,
+ AppsAPI,
+ UsersModels
+) {
var slugs = {
SET_LANG: 'lang',
SET_DISPLAY: 'display',
@@ -20,7 +31,7 @@ hqDefine("cloudcare/js/formplayer/layout/views/settings", function () {
template: _.template($("#lang-setting-template").html() || ""),
tagName: 'tr',
initialize: function () {
- this.currentUser = FormplayerFrontend.getChannel().request('currentUser');
+ this.currentUser = UsersModels.getCurrentUser();
},
ui: {
language: '.js-lang',
@@ -30,11 +41,11 @@ hqDefine("cloudcare/js/formplayer/layout/views/settings", function () {
},
onLanguageChange: function (e) {
this.currentUser.displayOptions.language = $(e.currentTarget).val();
- Utils.saveDisplayOptions(this.currentUser.displayOptions);
+ UsersModels.saveDisplayOptions(this.currentUser.displayOptions);
},
templateContext: function () {
var appId = FormplayerFrontend.getChannel().request('getCurrentAppId');
- var currentApp = FormplayerFrontend.getChannel().request("appselect:getApp", appId);
+ var currentApp = AppsAPI.getAppEntity(appId);
return {
langs: currentApp.get('langs'),
currentLang: this.currentUser.displayOptions.language,
@@ -50,7 +61,7 @@ hqDefine("cloudcare/js/formplayer/layout/views/settings", function () {
template: _.template($("#display-setting-template").html() || ""),
tagName: 'tr',
initialize: function () {
- this.currentUser = FormplayerFrontend.getChannel().request('currentUser');
+ this.currentUser = UsersModels.getCurrentUser();
},
ui: {
oneQuestionPerScreen: '.js-one-question-per-screen',
@@ -66,7 +77,7 @@ hqDefine("cloudcare/js/formplayer/layout/views/settings", function () {
},
onChangeOneQuestionPerScreen: function (e, switchValue) {
this.currentUser.displayOptions.oneQuestionPerScreen = switchValue;
- Utils.saveDisplayOptions(this.currentUser.displayOptions);
+ UsersModels.saveDisplayOptions(this.currentUser.displayOptions);
},
});
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/main.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/main.js
index 510ef6b3420b..ddcaade36b1a 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/main.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/main.js
@@ -1,11 +1,16 @@
'use strict';
-hqDefine("cloudcare/js/formplayer/main", function () {
-
+hqDefine("cloudcare/js/formplayer/main", [
+ 'jquery',
+ 'hqwebapp/js/initial_page_data',
+ 'cloudcare/js/formplayer/app',
+ 'cloudcare/js/sentry',
+], function (
+ $,
+ initialPageData,
+ FormplayerFrontEnd,
+ sentry
+) {
$(function () {
- var initialPageData = hqImport("hqwebapp/js/initial_page_data"),
- FormplayerFrontEnd = hqImport("cloudcare/js/formplayer/app"),
- sentry = hqImport("cloudcare/js/sentry");
-
sentry.initSentry();
window.MAPBOX_ACCESS_TOKEN = initialPageData.get('mapbox_access_token'); // maps api is loaded on-demand
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/api.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/api.js
index 46e4e84a8fd6..f97b116c5a87 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/api.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/api.js
@@ -1,26 +1,42 @@
'use strict';
-/* global Sentry */
/**
* Backbone model for listing and selecting CommCare menus (modules, forms, and cases)
*/
-
-hqDefine("cloudcare/js/formplayer/menus/api", function () {
- 'use strict';
-
- var Collections = hqImport("cloudcare/js/formplayer/menus/collections"),
- constants = hqImport("cloudcare/js/formplayer/constants"),
- errors = hqImport("cloudcare/js/form_entry/errors"),
- formEntryUtils = hqImport("cloudcare/js/form_entry/utils"),
- FormplayerFrontend = hqImport("cloudcare/js/formplayer/app"),
- formplayerUtils = hqImport("cloudcare/js/formplayer/utils/utils"),
- ProgressBar = hqImport("cloudcare/js/formplayer/layout/views/progress_bar"),
- initialPageData = hqImport("hqwebapp/js/initial_page_data"),
- currentSelections = null,
+hqDefine("cloudcare/js/formplayer/menus/api", [
+ 'jquery',
+ 'underscore',
+ 'sentry_browser',
+ 'hqwebapp/js/initial_page_data',
+ 'cloudcare/js/formplayer/menus/collections',
+ 'cloudcare/js/formplayer/constants',
+ 'cloudcare/js/form_entry/errors',
+ 'cloudcare/js/form_entry/utils',
+ 'cloudcare/js/formplayer/app',
+ 'cloudcare/js/formplayer/apps/api',
+ 'cloudcare/js/formplayer/users/models',
+ 'cloudcare/js/formplayer/utils/utils',
+ 'cloudcare/js/formplayer/layout/views/progress_bar',
+], function (
+ $,
+ _,
+ Sentry,
+ initialPageData,
+ Collections,
+ constants,
+ errors,
+ formEntryUtils,
+ FormplayerFrontend,
+ AppsAPI,
+ UsersModels,
+ formplayerUtils,
+ ProgressBar
+) {
+ let currentSelections = null,
ongoingRequests = [];
var API = {
queryFormplayer: function (params, route) {
- var user = FormplayerFrontend.getChannel().request('currentUser'),
+ var user = UsersModels.getCurrentUser(),
lastRecordedLocation = FormplayerFrontend.getChannel().request('lastRecordedLocation'),
timezoneOffsetMillis = (new Date()).getTimezoneOffset() * 60 * 1000 * -1,
tzFromBrowser = Intl.DateTimeFormat().resolvedOptions().timeZone,
@@ -29,7 +45,8 @@ hqDefine("cloudcare/js/formplayer/menus/api", function () {
defer = $.Deferred(),
options,
menus;
- $.when(FormplayerFrontend.getChannel().request("appselect:apps")).done(function (appCollection) {
+
+ $.when(AppsAPI.getAppEntities()).done(function (appCollection) {
if (!params.preview) {
// Make sure the user has access to the app
if (!appCollection.find(function (app) {
@@ -233,7 +250,7 @@ hqDefine("cloudcare/js/formplayer/menus/api", function () {
});
FormplayerFrontend.regions.getRegion('loadingProgress').show(progressView);
- var user = FormplayerFrontend.getChannel().request('currentUser');
+ var user = UsersModels.getCurrentUser();
if (options.forceLoginAs && !user.restoreAs) {
// Workflow requires a mobile user, likely because we're trying to access
// a session endpoint as a web user. If user isn't logged in as, send them
@@ -255,7 +272,7 @@ hqDefine("cloudcare/js/formplayer/menus/api", function () {
FormplayerFrontend.getChannel().reply("entity:get:details", function (options, isPersistent, isShortDetail, isRefreshCaseSearch) {
options.isPersistent = isPersistent;
- options.preview = FormplayerFrontend.currentUser.displayOptions.singleAppMode;
+ options.preview = UsersModels.getCurrentUser().displayOptions.singleAppMode;
options.isShortDetail = isShortDetail;
options.isRefreshCaseSearch = isRefreshCaseSearch;
return API.queryFormplayer(options, 'get_details');
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/collections.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/collections.js
index c700c9b1e74b..b5e79cba6b23 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/collections.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/collections.js
@@ -1,15 +1,22 @@
'use strict';
-/*global Backbone, Sentry */
-
/**
* A menu is implemented as a collection of items. Typically, the user
* selects one of these items. The query screen is also implemented as
* a menu, where each search field is an item.
*/
-hqDefine("cloudcare/js/formplayer/menus/collections", function () {
- var FormplayerFrontend = hqImport("cloudcare/js/formplayer/app"),
- Utils = hqImport("cloudcare/js/formplayer/utils/utils");
-
+hqDefine("cloudcare/js/formplayer/menus/collections", [
+ 'underscore',
+ 'backbone',
+ 'sentry_browser',
+ 'cloudcare/js/formplayer/app',
+ 'cloudcare/js/formplayer/utils/utils',
+], function (
+ _,
+ Backbone,
+ Sentry,
+ FormplayerFrontend,
+ Utils
+) {
function addBreadcrumb(collection, type, data) {
Sentry.addBreadcrumb({
category: "formplayer",
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/controller.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/controller.js
index 9ec9ba325342..a4b3cc33f274 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/controller.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/controller.js
@@ -1,19 +1,41 @@
'use strict';
-/*global Backbone */
-
-hqDefine("cloudcare/js/formplayer/menus/controller", function () {
- var constants = hqImport("cloudcare/js/formplayer/constants"),
- markdown = hqImport("cloudcare/js/markdown"),
- FormplayerFrontend = hqImport("cloudcare/js/formplayer/app"),
- formplayerUtils = hqImport("cloudcare/js/formplayer/utils/utils"),
- menusUtils = hqImport("cloudcare/js/formplayer/menus/utils"),
- views = hqImport("cloudcare/js/formplayer/menus/views"),
- queryView = hqImport("cloudcare/js/formplayer/menus/views/query"),
- initialPageData = hqImport("hqwebapp/js/initial_page_data"),
- Collection = hqImport("cloudcare/js/formplayer/menus/collections");
+hqDefine("cloudcare/js/formplayer/menus/controller", [
+ 'jquery',
+ 'underscore',
+ 'backbone',
+ 'DOMPurify/dist/purify.min',
+ 'hqwebapp/js/initial_page_data',
+ 'hqwebapp/js/toggles',
+ 'cloudcare/js/markdown',
+ 'cloudcare/js/formplayer/constants',
+ 'cloudcare/js/formplayer/app',
+ 'cloudcare/js/formplayer/users/models',
+ 'cloudcare/js/formplayer/utils/utils',
+ 'cloudcare/js/formplayer/menus/collections',
+ 'cloudcare/js/formplayer/menus/utils',
+ 'cloudcare/js/formplayer/menus/views/query',
+ 'cloudcare/js/formplayer/menus/views',
+ 'cloudcare/js/formplayer/menus/api', // app:select:menus and entity:get:details
+], function (
+ $,
+ _,
+ Backbone,
+ DOMPurify,
+ initialPageData,
+ toggles,
+ markdown,
+ constants,
+ FormplayerFrontend,
+ UsersModels,
+ formplayerUtils,
+ Collection,
+ menusUtils,
+ queryView,
+ views
+) {
var selectMenu = function (options) {
- options.preview = FormplayerFrontend.currentUser.displayOptions.singleAppMode;
+ options.preview = UsersModels.getCurrentUser().displayOptions.singleAppMode;
var fetchingNextMenu = FormplayerFrontend.getChannel().request("app:select:menus", options);
@@ -107,7 +129,7 @@ hqDefine("cloudcare/js/formplayer/menus/controller", function () {
var showMenu = function (menuResponse) {
var menuListView = menusUtils.getMenuView(menuResponse);
- var appPreview = FormplayerFrontend.currentUser.displayOptions.singleAppMode;
+ var appPreview = UsersModels.getCurrentUser().displayOptions.singleAppMode;
var sidebarEnabled = !appPreview && menusUtils.isSidebarEnabled(menuResponse);
if (menuListView && !sidebarEnabled) {
FormplayerFrontend.regions.getRegion('main').show(menuListView);
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/utils.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/utils.js
index 822f541c7809..435624b4febf 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/utils.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/utils.js
@@ -1,16 +1,29 @@
'use strict';
-/*global Backbone */
-
-hqDefine("cloudcare/js/formplayer/menus/utils", function () {
- var FormplayerFrontend = hqImport("cloudcare/js/formplayer/app"),
- kissmetrics = hqImport("analytix/js/kissmetrix"),
- ProgressBar = hqImport("cloudcare/js/formplayer/layout/views/progress_bar"),
- view = hqImport("cloudcare/js/formplayer/menus/views/query"),
- toggles = hqImport("hqwebapp/js/toggles"),
- utils = hqImport("cloudcare/js/formplayer/utils/utils"),
- views = hqImport("cloudcare/js/formplayer/menus/views"),
- constants = hqImport("cloudcare/js/formplayer/constants");
-
+hqDefine("cloudcare/js/formplayer/menus/utils", [
+ 'underscore',
+ 'backbone',
+ 'hqwebapp/js/toggles',
+ 'analytix/js/kissmetrix',
+ 'cloudcare/js/formplayer/app',
+ 'cloudcare/js/formplayer/constants',
+ 'cloudcare/js/formplayer/layout/views/progress_bar',
+ 'cloudcare/js/formplayer/menus/views/query',
+ 'cloudcare/js/formplayer/users/models',
+ 'cloudcare/js/formplayer/utils/utils',
+ 'cloudcare/js/formplayer/menus/views',
+], function (
+ _,
+ Backbone,
+ toggles,
+ kissmetrics,
+ FormplayerFrontend,
+ constants,
+ ProgressBar,
+ view,
+ UsersModels,
+ utils,
+ views
+) {
var recordPosition = function (position) {
sessionStorage.locationLat = position.coords.latitude;
sessionStorage.locationLon = position.coords.longitude;
@@ -179,7 +192,7 @@ hqDefine("cloudcare/js/formplayer/menus/utils", function () {
return views.MenuListView(menuData);
} else if (menuResponse.type === constants.QUERY) {
var props = {
- domain: FormplayerFrontend.getChannel().request('currentUser').domain,
+ domain: UsersModels.getCurrentUser().domain,
};
if (menuResponse.breadcrumbs && menuResponse.breadcrumbs.length) {
props.name = menuResponse.breadcrumbs[menuResponse.breadcrumbs.length - 1];
@@ -201,7 +214,7 @@ hqDefine("cloudcare/js/formplayer/menus/utils", function () {
menuData.sidebarEnabled = true;
}
var eventData = {
- domain: FormplayerFrontend.getChannel().request("currentUser").domain,
+ domain: UsersModels.getCurrentUser().domain,
name: menuResponse.title,
};
var fields = _.pick(utils.getCurrentQueryInputs(), function (v) { return !!v; });
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/views.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/views.js
index 4fff8d50eb14..e9f4b65ab0d2 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/views.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/views.js
@@ -1,16 +1,35 @@
'use strict';
-/*globals Marionette */
-
-hqDefine("cloudcare/js/formplayer/menus/views", function () {
- const kissmetrics = hqImport("analytix/js/kissmetrix"),
- constants = hqImport("cloudcare/js/formplayer/constants"),
- FormplayerFrontend = hqImport("cloudcare/js/formplayer/app"),
- initialPageData = hqImport("hqwebapp/js/initial_page_data"),
- toggles = hqImport("hqwebapp/js/toggles"),
- formplayerUtils = hqImport("cloudcare/js/formplayer/utils/utils"),
- cloudcareUtils = hqImport("cloudcare/js/utils"),
- markdown = hqImport("cloudcare/js/markdown");
-
+hqDefine("cloudcare/js/formplayer/menus/views", [
+ 'jquery',
+ 'underscore',
+ 'backbone.marionette',
+ 'DOMPurify/dist/purify.min',
+ 'hqwebapp/js/initial_page_data',
+ 'hqwebapp/js/toggles',
+ 'analytix/js/kissmetrix',
+ 'cloudcare/js/formplayer/constants',
+ 'cloudcare/js/formplayer/app',
+ 'cloudcare/js/formplayer/users/models',
+ 'cloudcare/js/formplayer/utils/utils',
+ 'cloudcare/js/markdown',
+ 'cloudcare/js/utils',
+ 'leaflet-fullscreen/dist/Leaflet.fullscreen.min', // adds L.control.fullscreen to L
+], function (
+ $,
+ _,
+ Marionette,
+ DOMPurify,
+ initialPageData,
+ toggles,
+ kissmetrics,
+ constants,
+ FormplayerFrontend,
+ UsersModels,
+ formplayerUtils,
+ markdown,
+ cloudcareUtils,
+ L
+) {
const MenuView = Marionette.View.extend({
tagName: function () {
if (this.model.collection.layoutStyle === 'grid') {
@@ -114,9 +133,10 @@ hqDefine("cloudcare/js/formplayer/menus/views", function () {
return _.template($(id).html() || "");
},
templateContext: function () {
+ const environment = UsersModels.getCurrentUser().environment;
return {
title: this.options.title,
- environment: FormplayerFrontend.getChannel().request('currentUser').environment,
+ isAppPreview: environment === constants.PREVIEW_APP_ENVIRONMENT,
};
},
childViewOptions: function (model) {
@@ -651,7 +671,7 @@ hqDefine("cloudcare/js/formplayer/menus/views", function () {
} else {
self.selectedCaseIds = [];
}
- const user = FormplayerFrontend.currentUser;
+ const user = UsersModels.getCurrentUser();
const displayOptions = user.displayOptions;
const appPreview = displayOptions.singleAppMode;
const addressFieldPresent = !!_.find(this.styles, function (style) { return style.displayFormat === constants.FORMAT_ADDRESS; });
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/views/query.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/views/query.js
index e7f3e68632ba..5a8c5626badb 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/views/query.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/views/query.js
@@ -1,21 +1,45 @@
'use strict';
-/*global Backbone, DOMPurify, Marionette */
-
-hqDefine("cloudcare/js/formplayer/menus/views/query", function () {
- // 'hqwebapp/js/bootstrap3/hq.helpers' is a dependency. It needs to be added
- // explicitly when webapps is migrated to requirejs
- var kissmetrics = hqImport("analytix/js/kissmetrix"),
- cloudcareUtils = hqImport("cloudcare/js/utils"),
- markdown = hqImport("cloudcare/js/markdown"),
- formEntryConstants = hqImport("cloudcare/js/form_entry/const"),
- formplayerConstants = hqImport("cloudcare/js/formplayer/constants"),
- formEntryUtils = hqImport("cloudcare/js/form_entry/utils"),
- FormplayerFrontend = hqImport("cloudcare/js/formplayer/app"),
- formplayerUtils = hqImport("cloudcare/js/formplayer/utils/utils"),
- initialPageData = hqImport("hqwebapp/js/initial_page_data"),
- toggles = hqImport("hqwebapp/js/toggles"),
- Collection = hqImport("cloudcare/js/formplayer/menus/collections");
-
+hqDefine("cloudcare/js/formplayer/menus/views/query", [
+ 'jquery',
+ 'underscore',
+ 'backbone',
+ 'DOMPurify/dist/purify.min',
+ 'backbone.marionette',
+ 'moment',
+ 'hqwebapp/js/initial_page_data',
+ 'hqwebapp/js/toggles',
+ 'analytix/js/kissmetrix',
+ 'cloudcare/js/markdown',
+ 'cloudcare/js/utils',
+ 'cloudcare/js/form_entry/const',
+ 'cloudcare/js/form_entry/utils',
+ 'cloudcare/js/formplayer/app',
+ 'cloudcare/js/formplayer/constants',
+ 'cloudcare/js/formplayer/menus/collections',
+ 'cloudcare/js/formplayer/utils/utils',
+ 'hqwebapp/js/bootstrap3/hq.helpers', // needed for hqHelp
+ 'bootstrap-daterangepicker/daterangepicker', // needed for $.daterangepicker
+ 'cloudcare/js/formplayer/menus/api', // needed for app:select:menus
+ 'select2/dist/js/select2.full.min',
+], function (
+ $,
+ _,
+ Backbone,
+ DOMPurify,
+ Marionette,
+ moment,
+ initialPageData,
+ toggles,
+ kissmetrics,
+ markdown,
+ cloudcareUtils,
+ formEntryConstants,
+ formEntryUtils,
+ FormplayerFrontend,
+ formplayerConstants,
+ Collection,
+ formplayerUtils
+) {
var separator = " to ",
serverSeparator = "__",
serverPrefix = "__range__",
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/middleware.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/middleware.js
index 30be995d26ad..f06635616fff 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/middleware.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/middleware.js
@@ -1,7 +1,15 @@
'use strict';
-hqDefine("cloudcare/js/formplayer/middleware", function () {
- var FormplayerFrontend = hqImport("cloudcare/js/formplayer/app");
-
+hqDefine("cloudcare/js/formplayer/middleware", [
+ 'jquery',
+ 'underscore',
+ 'cloudcare/js/formplayer/app',
+ 'cloudcare/js/formplayer/users/models',
+], function (
+ $,
+ _,
+ FormplayerFrontend,
+ UsersModels
+) {
var clearFormMiddleware = function () {
FormplayerFrontend.trigger("clearForm");
};
@@ -17,7 +25,7 @@ hqDefine("cloudcare/js/formplayer/middleware", function () {
};
var setScrollableMaxHeight = function () {
var maxHeight,
- user = FormplayerFrontend.getChannel().request('currentUser'),
+ user = UsersModels.getCurrentUser(),
restoreAsBannerHeight = 0;
if (user.restoreAs) {
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/router.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/router.js
index c1fc50ec6ce8..709ad42eba86 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/router.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/router.js
@@ -1,8 +1,37 @@
'use strict';
-/* global Backbone, Marionette */
-hqDefine("cloudcare/js/formplayer/router", function () {
- var utils = hqImport("cloudcare/js/formplayer/utils/utils");
- var Router = Marionette.AppRouter.extend({
+hqDefine("cloudcare/js/formplayer/router", [
+ 'underscore',
+ 'backbone',
+ 'backbone.marionette',
+ 'cloudcare/js/formplayer/utils/utils',
+ 'cloudcare/js/formplayer/app',
+ 'cloudcare/js/formplayer/constants',
+ 'cloudcare/js/formplayer/middleware',
+ 'cloudcare/js/formplayer/apps/controller',
+ 'cloudcare/js/formplayer/menus/collections',
+ 'cloudcare/js/formplayer/menus/controller',
+ 'cloudcare/js/formplayer/sessions/controller',
+ 'cloudcare/js/formplayer/users/controller',
+ 'cloudcare/js/formplayer/users/models',
+ 'marionette.approuter/lib/marionette.approuter.min', // for Marionette.AppRouter
+ 'cloudcare/js/formplayer/sessions/api', // for getSession
+], function (
+ _,
+ Backbone,
+ Marionette,
+ utils,
+ FormplayerFrontend,
+ formplayerConstants,
+ Middleware,
+ appsController,
+ menusCollections,
+ menusController,
+ sessionsController,
+ usersController,
+ usersModels,
+ AppRouter
+) {
+ var params = {
appRoutes: {
"apps": "listApps", // list all apps available to this user
"single_app/:id": "singleApp", // Show app in phone mode (SingleAppView)
@@ -15,17 +44,9 @@ hqDefine("cloudcare/js/formplayer/router", function () {
"settings": "listSettings",
":session": "listMenus", // Default route
},
- });
-
+ };
+ var Router = AppRouter.extend(params);
- var FormplayerFrontend = hqImport("cloudcare/js/formplayer/app"),
- Middleware = hqImport("cloudcare/js/formplayer/middleware"),
- appsController = hqImport("cloudcare/js/formplayer/apps/controller"),
- menusCollections = hqImport("cloudcare/js/formplayer/menus/collections"),
- menusController = hqImport("cloudcare/js/formplayer/menus/controller"),
- sessionsController = hqImport("cloudcare/js/formplayer/sessions/controller"),
- usersController = hqImport("cloudcare/js/formplayer/users/controller"),
- formplayerConstants = hqImport("cloudcare/js/formplayer/constants");
var API = {
listApps: function () {
FormplayerFrontend.regions.getRegion('breadcrumb').empty();
@@ -33,7 +54,7 @@ hqDefine("cloudcare/js/formplayer/router", function () {
appsController.listApps();
},
singleApp: function (appId) {
- var user = FormplayerFrontend.getChannel().request('currentUser');
+ var user = usersModels.getCurrentUser();
FormplayerFrontend.regions.getRegion('breadcrumb').empty();
user.previewAppId = appId;
appsController.singleApp(appId);
@@ -114,7 +135,7 @@ hqDefine("cloudcare/js/formplayer/router", function () {
currentFragment = Backbone.history.getFragment();
urlObject = utils.CloudcareUrl.fromJson(utils.encodedUrlToObject(currentFragment));
encodedUrl = utils.objectToEncodedUrl(urlObject.toJson());
- FormplayerFrontend.navigate(encodedUrl);
+ utils.navigate(encodedUrl);
menusController.showMenu(menuCollection);
},
@@ -129,7 +150,7 @@ hqDefine("cloudcare/js/formplayer/router", function () {
});
FormplayerFrontend.on("apps:list", function () {
- FormplayerFrontend.navigate("apps");
+ utils.navigate("apps");
API.listApps();
});
@@ -140,12 +161,12 @@ hqDefine("cloudcare/js/formplayer/router", function () {
});
FormplayerFrontend.on('app:singleApp', function (appId) {
- FormplayerFrontend.navigate("/single_app/" + appId);
+ utils.navigate("/single_app/" + appId);
API.singleApp(appId);
});
FormplayerFrontend.on('app:landingPageApp', function (appId) {
- FormplayerFrontend.navigate("/home/" + appId);
+ utils.navigate("/home/" + appId);
API.landingPageApp(appId);
});
@@ -225,12 +246,12 @@ hqDefine("cloudcare/js/formplayer/router", function () {
});
FormplayerFrontend.on('restore_as:list', function () {
- FormplayerFrontend.navigate("/restore_as");
+ utils.navigate("/restore_as");
API.listUsers();
});
FormplayerFrontend.on('settings:list', function () {
- FormplayerFrontend.navigate("/settings");
+ utils.navigate("/settings");
API.listSettings();
});
@@ -239,12 +260,12 @@ hqDefine("cloudcare/js/formplayer/router", function () {
});
FormplayerFrontend.on("sessions", function (pageNumber, pageSize) {
- FormplayerFrontend.navigate("/sessions", pageNumber, pageSize);
+ utils.navigate("/sessions", pageNumber, pageSize);
API.listSessions(pageNumber, pageSize);
});
FormplayerFrontend.on("getSession", function (sessionId) {
- FormplayerFrontend.navigate("/sessions/" + sessionId);
+ utils.navigate("/sessions/" + sessionId);
API.getSession(sessionId);
});
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/sessions/api.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/sessions/api.js
index d36cd815e5da..eaa7f78ca264 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/sessions/api.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/sessions/api.js
@@ -2,17 +2,25 @@
/**
* Backbone model for listing and selecting FormEntrySessions
*/
-
-hqDefine("cloudcare/js/formplayer/sessions/api", function () {
- var Collections = hqImport("cloudcare/js/formplayer/sessions/collections"),
- FormplayerFrontend = hqImport("cloudcare/js/formplayer/app"),
- MenuCollections = hqImport("cloudcare/js/formplayer/menus/collections");
-
+hqDefine("cloudcare/js/formplayer/sessions/api", [
+ 'jquery',
+ 'underscore',
+ 'cloudcare/js/formplayer/sessions/collections',
+ 'cloudcare/js/formplayer/app',
+ 'cloudcare/js/formplayer/menus/collections',
+ 'cloudcare/js/formplayer/users/models',
+], function (
+ $,
+ _,
+ Collections,
+ FormplayerFrontend,
+ MenuCollections,
+ UsersModels
+) {
var API = {
getSessions: function (pageNumber, pageSize) {
-
- var user = FormplayerFrontend.getChannel().request('currentUser');
+ var user = UsersModels.getCurrentUser();
var domain = user.domain;
var formplayerUrl = user.formplayer_url;
var options = {
@@ -48,7 +56,7 @@ hqDefine("cloudcare/js/formplayer/sessions/api", function () {
getSession: function (sessionId) {
- var user = FormplayerFrontend.getChannel().request('currentUser');
+ var user = UsersModels.getCurrentUser();
var formplayerUrl = user.formplayer_url;
var menus = MenuCollections();
var defer = $.Deferred();
@@ -71,7 +79,7 @@ hqDefine("cloudcare/js/formplayer/sessions/api", function () {
},
deleteSession: function (session) {
- var user = FormplayerFrontend.getChannel().request('currentUser');
+ var user = UsersModels.getCurrentUser();
var options = {
data: JSON.stringify({
"sessionId": session.get('sessionId'),
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/sessions/collections.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/sessions/collections.js
index 3a7a0f0ccb39..53c74514aa3f 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/sessions/collections.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/sessions/collections.js
@@ -1,10 +1,13 @@
'use strict';
-/*global Backbone */
-
-hqDefine("cloudcare/js/formplayer/sessions/collections", function () {
- var Models = hqImport("cloudcare/js/formplayer/sessions/models"),
- utils = hqImport("cloudcare/js/formplayer/utils/utils");
-
+hqDefine("cloudcare/js/formplayer/sessions/collections", [
+ 'backbone',
+ 'cloudcare/js/formplayer/sessions/models',
+ 'cloudcare/js/formplayer/utils/utils',
+], function (
+ Backbone,
+ Models,
+ utils
+) {
var session = Backbone.Collection.extend({
model: Models,
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/sessions/controller.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/sessions/controller.js
index 906a0f36610f..07efbc4e8dca 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/sessions/controller.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/sessions/controller.js
@@ -1,9 +1,16 @@
'use strict';
-hqDefine("cloudcare/js/formplayer/sessions/controller", function () {
- var constants = hqImport("cloudcare/js/formplayer/constants"),
- FormplayerFrontend = hqImport("cloudcare/js/formplayer/app"),
- Views = hqImport("cloudcare/js/formplayer/sessions/views");
-
+hqDefine("cloudcare/js/formplayer/sessions/controller", [
+ 'jquery',
+ 'cloudcare/js/formplayer/constants',
+ 'cloudcare/js/formplayer/app',
+ 'cloudcare/js/formplayer/sessions/views',
+ 'cloudcare/js/formplayer/sessions/api', // for sessions
+], function (
+ $,
+ constants,
+ FormplayerFrontend,
+ Views
+) {
return {
listSessions: function listSessions(pageNumber, pageSize) {
/* eslint-disable */
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/sessions/models.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/sessions/models.js
index 976f0f47744a..e309488ff069 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/sessions/models.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/sessions/models.js
@@ -1,9 +1,11 @@
'use strict';
-/*global Backbone */
-
-hqDefine("cloudcare/js/formplayer/sessions/models", function () {
- var utils = hqImport("cloudcare/js/formplayer/utils/utils");
-
+hqDefine("cloudcare/js/formplayer/sessions/models", [
+ 'backbone',
+ 'cloudcare/js/formplayer/utils/utils',
+], function (
+ Backbone,
+ utils
+) {
return Backbone.Model.extend({
isNew: function () {
return !this.get('sessionId');
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/sessions/views.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/sessions/views.js
index 0458c30740d3..b6938e304453 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/sessions/views.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/sessions/views.js
@@ -1,12 +1,26 @@
'use strict';
-/*global Backbone, Marionette, moment */
-
-hqDefine("cloudcare/js/formplayer/sessions/views", function () {
- var constants = hqImport("cloudcare/js/formplayer/constants"),
- FormplayerFrontend = hqImport("cloudcare/js/formplayer/app"),
- FormplayerUtils = hqImport("cloudcare/js/formplayer/utils/utils"),
- utils = hqImport("cloudcare/js/formplayer/utils/utils");
-
+hqDefine("cloudcare/js/formplayer/sessions/views", [
+ 'jquery',
+ 'underscore',
+ 'backbone',
+ 'backbone.marionette',
+ 'moment',
+ 'cloudcare/js/formplayer/constants',
+ 'cloudcare/js/formplayer/app',
+ 'cloudcare/js/formplayer/users/models',
+ 'cloudcare/js/formplayer/utils/utils',
+ 'cloudcare/js/formplayer/sessions/api', // deleteSession
+], function (
+ $,
+ _,
+ Backbone,
+ Marionette,
+ moment,
+ constants,
+ FormplayerFrontend,
+ UsersModels,
+ utils
+) {
var SessionView = Marionette.View.extend({
tagName: "tr",
className: "formplayer-request",
@@ -102,7 +116,7 @@ hqDefine("cloudcare/js/formplayer/sessions/views", function () {
var sessionsPerPage = this.ui.sessionsPerPageLimit.val();
this.model.set("limit", Number(sessionsPerPage));
this.model.set("page", 1);
- FormplayerUtils.savePerPageLimitCookie("sessions", this.model.get("limit"));
+ utils.savePerPageLimitCookie("sessions", this.model.get("limit"));
},
paginationGoAction: function (e) {
e.preventDefault();
@@ -125,12 +139,12 @@ hqDefine("cloudcare/js/formplayer/sessions/views", function () {
}
},
templateContext: function () {
- var user = FormplayerFrontend.getChannel().request('currentUser');
- var paginationConfig = utils.paginateOptions(
- this.options.pageNumber,
- this.options.totalPages,
- this.collection.totalSessions
- );
+ var user = UsersModels.getCurrentUser(),
+ paginationConfig = utils.paginateOptions(
+ this.options.pageNumber,
+ this.options.totalPages,
+ this.collection.totalSessions
+ );
return _.extend(paginationConfig, {
total: this.collection.totalSessions,
totalPages: this.options.totalPages,
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/case_list_pagination_spec.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/case_list_pagination_spec.js
index 48e231992374..e778771631a9 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/case_list_pagination_spec.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/case_list_pagination_spec.js
@@ -1,7 +1,10 @@
'use strict';
-hqDefine("cloudcare/js/formplayer/spec/case_list_pagination_spec", function () {
+hqDefine("cloudcare/js/formplayer/spec/case_list_pagination_spec", [
+ "cloudcare/js/formplayer/utils/utils",
+], function (
+ paginateItems
+) {
describe('#paginateOptions', function () {
- var paginateItems = hqImport("cloudcare/js/formplayer/utils/utils");
it('Should return paginateOptions', function () {
var case1 = paginateItems.paginateOptions(0, 15, 3);
/**
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/debugger_spec.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/debugger_spec.js
index 8cf02056bc1f..9b98239b91bb 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/debugger_spec.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/debugger_spec.js
@@ -1,10 +1,16 @@
'use strict';
/* eslint-env mocha */
-hqDefine("cloudcare/js/formplayer/spec/debugger_spec", function () {
+hqDefine("cloudcare/js/formplayer/spec/debugger_spec", [
+ "sinon/pkg/sinon",
+ "cloudcare/js/debugger/debugger",
+], function (
+ sinon,
+ Debugger
+) {
describe('Debugger', function () {
- let EvaluateXPath = hqImport('cloudcare/js/debugger/debugger').EvaluateXPath,
- API = hqImport('cloudcare/js/debugger/debugger').API,
- CloudCareDebugger = hqImport('cloudcare/js/debugger/debugger').CloudCareDebuggerFormEntry;
+ let EvaluateXPath = Debugger.EvaluateXPath,
+ API = Debugger.API,
+ CloudCareDebugger = Debugger.CloudCareDebuggerFormEntry;
describe('EvaluateXPath', function () {
it('should correctly match xpath input', function () {
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fake_formplayer.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fake_formplayer.js
index 272ed778e9da..b1c0f30becdc 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fake_formplayer.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fake_formplayer.js
@@ -12,9 +12,14 @@
* m1-f0: a form that updates a case
* No menus use display-only forms.
*/
-hqDefine("cloudcare/js/formplayer/spec/fake_formplayer", function () {
- let AssertProperties = hqImport("hqwebapp/js/assert_properties"),
- module = {},
+hqDefine("cloudcare/js/formplayer/spec/fake_formplayer", [
+ "underscore",
+ "hqwebapp/js/assert_properties",
+], function (
+ _,
+ AssertProperties
+) {
+ let module = {},
apps = {
'abc123': {
title: "My App",
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fixtures/case_grid_list.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fixtures/case_grid_list.js
index 6a4fd29bf2a1..5a88de9f24c1 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fixtures/case_grid_list.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fixtures/case_grid_list.js
@@ -1,7 +1,9 @@
'use strict';
-hqDefine("cloudcare/js/formplayer/spec/fixtures/case_grid_list", function () {
- let FakeFormplayer = hqImport("cloudcare/js/formplayer/spec/fake_formplayer");
-
+hqDefine("cloudcare/js/formplayer/spec/fixtures/case_grid_list", [
+ "cloudcare/js/formplayer/spec/fake_formplayer",
+], function (
+ FakeFormplayer
+) {
return FakeFormplayer.makeEntitiesResponse({
"title": "New Adherence Data",
"breadcrumbs": [
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fixtures/case_list.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fixtures/case_list.js
index de55f2e1729e..0ee3025f133e 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fixtures/case_list.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fixtures/case_list.js
@@ -1,7 +1,9 @@
'use strict';
-hqDefine("cloudcare/js/formplayer/spec/fixtures/case_list", function () {
- let FakeFormplayer = hqImport("cloudcare/js/formplayer/spec/fake_formplayer");
-
+hqDefine("cloudcare/js/formplayer/spec/fixtures/case_list", [
+ "cloudcare/js/formplayer/spec/fake_formplayer",
+], function (
+ FakeFormplayer
+) {
return FakeFormplayer.makeEntitiesResponse({
"title": "Update a Case",
"breadcrumbs": [
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fixtures/case_tile_list.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fixtures/case_tile_list.js
index 2ebaf2e68827..5a9a925bd47c 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fixtures/case_tile_list.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fixtures/case_tile_list.js
@@ -1,7 +1,9 @@
'use strict';
-hqDefine("cloudcare/js/formplayer/spec/fixtures/case_tile_list", function () {
- let FakeFormplayer = hqImport("cloudcare/js/formplayer/spec/fake_formplayer");
-
+hqDefine("cloudcare/js/formplayer/spec/fixtures/case_tile_list", [
+ "cloudcare/js/formplayer/spec/fake_formplayer",
+], function (
+ FakeFormplayer
+) {
return FakeFormplayer.makeEntitiesResponse({
"title": "Active Patients",
"breadcrumbs": [
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fixtures/menu_list.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fixtures/menu_list.js
index 6f8137dbed45..a3b8c0719cdb 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fixtures/menu_list.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fixtures/menu_list.js
@@ -1,7 +1,9 @@
'use strict';
-hqDefine("cloudcare/js/formplayer/spec/fixtures/menu_list", function () {
- let FakeFormplayer = hqImport("cloudcare/js/formplayer/spec/fake_formplayer");
-
+hqDefine("cloudcare/js/formplayer/spec/fixtures/menu_list", [
+ "cloudcare/js/formplayer/spec/fake_formplayer",
+], function (
+ FakeFormplayer
+) {
return FakeFormplayer.makeCommandsResponse({
"title": "Case Tests",
"breadcrumbs": [
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fixtures/split_screen_case_list.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fixtures/split_screen_case_list.js
index 52581591bdee..4c62da26a99f 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fixtures/split_screen_case_list.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/fixtures/split_screen_case_list.js
@@ -1,7 +1,9 @@
'use strict';
-hqDefine("cloudcare/js/formplayer/spec/fixtures/split_screen_case_list", function () {
- const FakeFormplayer = hqImport("cloudcare/js/formplayer/spec/fake_formplayer");
-
+hqDefine("cloudcare/js/formplayer/spec/fixtures/split_screen_case_list", [
+ "cloudcare/js/formplayer/spec/fake_formplayer",
+], function (
+ FakeFormplayer
+) {
return FakeFormplayer.makeEntitiesResponse({
"title": "Search All Cases",
"description": "",
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/hq_events_spec.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/hq_events_spec.js
index 91f87f78f521..491e8eb67ec7 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/hq_events_spec.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/hq_events_spec.js
@@ -1,12 +1,18 @@
'use strict';
/* eslint-env mocha */
-hqDefine("cloudcare/js/formplayer/spec/hq_events_spec", function () {
+hqDefine("cloudcare/js/formplayer/spec/hq_events_spec", [
+ "sinon/pkg/sinon",
+ "cloudcare/js/formplayer/app",
+ "cloudcare/js/formplayer/hq_events",
+], function (
+ sinon,
+ FormplayerFrontend,
+ hqEvents
+) {
describe('HQ Events', function () {
- let FormplayerFrontend = hqImport("cloudcare/js/formplayer/app");
-
describe('Receiver', function () {
- let Receiver = hqImport("cloudcare/js/formplayer/hq_events").Receiver,
- Actions = hqImport("cloudcare/js/formplayer/hq_events").Actions,
+ let Receiver = hqEvents.Receiver,
+ Actions = hqEvents.Actions,
origin = 'myorigin',
triggerSpy,
requestSpy,
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/integration_spec.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/integration_spec.js
deleted file mode 100644
index 3841f41c3015..000000000000
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/integration_spec.js
+++ /dev/null
@@ -1,72 +0,0 @@
-'use strict';
-/* global Backbone */
-/* eslint-env mocha */
-hqDefine("cloudcare/js/formplayer/spec/integration_spec", function () {
- describe('FormplayerFrontend Integration', function () {
- let FormplayerFrontend = hqImport("cloudcare/js/formplayer/app");
-
- describe('Start up', function () {
- let options,
- server;
- beforeEach(function () {
- server = sinon.useFakeXMLHttpRequest();
- options = {
- username: 'batman',
- domain: 'domain',
- apps: [],
- };
- sinon.stub(Backbone.history, 'start').callsFake(sinon.spy());
-
- // Prevent showing views, which doesn't work properly in tests
- FormplayerFrontend.off("before:start");
- FormplayerFrontend.regions = {
- getRegion: function () {
- return {
- show: function () {
- return;
- },
- };
- },
- };
- });
-
- afterEach(function () {
- server.restore();
- Backbone.history.start.restore();
- });
-
- it('should start the formplayer frontend app', function () {
- FormplayerFrontend.start(options);
-
- let user = FormplayerFrontend.getChannel().request('currentUser');
- assert.equal(user.username, options.username);
- assert.equal(user.domain, options.domain);
- });
-
- it('should correctly restore display options', function () {
- let newOptions = _.clone(options),
- user;
- newOptions.phoneMode = true;
- newOptions.oneQuestionPerScreen = true;
- newOptions.language = 'sindarin';
-
- FormplayerFrontend.start(newOptions);
-
- user = FormplayerFrontend.getChannel().request('currentUser');
- hqImport("cloudcare/js/formplayer/utils/utils").saveDisplayOptions(user.displayOptions);
-
- // New session, but old options
- FormplayerFrontend.start(options);
- user = FormplayerFrontend.getChannel().request('currentUser');
-
- assert.deepEqual(user.displayOptions, {
- phoneMode: undefined, // we don't store this option
- singleAppMode: undefined,
- landingPageAppMode: undefined,
- oneQuestionPerScreen: true,
- language: 'sindarin',
- });
- });
- });
- });
-});
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/main.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/main.js
new file mode 100644
index 000000000000..842823225548
--- /dev/null
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/main.js
@@ -0,0 +1,24 @@
+'use strict';
+hqDefine("cloudcare/js/formplayer/spec/main", [
+ "mocha/js/main",
+], function (
+ hqMocha
+) {
+ hqRequire([
+ "cloudcare/js/formplayer/spec/case_list_pagination_spec",
+ "cloudcare/js/formplayer/spec/debugger_spec",
+ "cloudcare/js/formplayer/spec/hq_events_spec",
+ "cloudcare/js/spec/markdown_spec",
+ "cloudcare/js/formplayer/spec/menu_list_spec",
+ "cloudcare/js/formplayer/spec/query_spec",
+ "cloudcare/js/formplayer/spec/session_middleware_spec",
+ "cloudcare/js/formplayer/spec/split_screen_case_search_spec",
+ "cloudcare/js/formplayer/spec/user_spec",
+ "cloudcare/js/formplayer/spec/utils_spec",
+ "cloudcare/js/spec/utils_spec",
+ ], function () {
+ hqMocha.run();
+ });
+
+ return 1;
+});
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/menu_list_spec.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/menu_list_spec.js
index b030dc669d1f..f80b885b2e43 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/menu_list_spec.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/menu_list_spec.js
@@ -1,13 +1,35 @@
'use strict';
-/* global Backbone */
/* eslint-env mocha */
-hqDefine("cloudcare/js/formplayer/spec/menu_list_spec", function () {
+hqDefine("cloudcare/js/formplayer/spec/menu_list_spec", [
+ "sinon/pkg/sinon",
+ "backbone",
+ "hqwebapp/js/initial_page_data",
+ "cloudcare/js/formplayer/app",
+ "cloudcare/js/formplayer/apps/api",
+ "cloudcare/js/formplayer/menus/utils",
+ "cloudcare/js/formplayer/spec/fixtures/case_list",
+ "cloudcare/js/formplayer/spec/fixtures/case_grid_list",
+ "cloudcare/js/formplayer/spec/fixtures/case_tile_list",
+ "cloudcare/js/formplayer/spec/fixtures/menu_list",
+ "cloudcare/js/formplayer/utils/utils",
+ "cloudcare/js/formplayer/users/models",
+], function (
+ sinon,
+ Backbone,
+ initialPageData,
+ FormplayerFrontend,
+ AppsAPI,
+ MenusUtils,
+ CaseListFixture,
+ CaseGridListFixture,
+ CaseTileListFixture,
+ MenuListFixture,
+ Utils,
+ UsersModels
+) {
describe('Render a case list', function () {
- let MenuListFixture = hqImport("cloudcare/js/formplayer/spec/fixtures/menu_list"),
- Utils = hqImport("cloudcare/js/formplayer/utils/utils");
-
before(function () {
- hqImport("hqwebapp/js/initial_page_data").register(
+ initialPageData.register(
"toggles_dict",
{
SPLIT_SCREEN_CASE_SEARCH: false,
@@ -19,18 +41,17 @@ hqDefine("cloudcare/js/formplayer/spec/menu_list_spec", function () {
});
after(function () {
- hqImport("hqwebapp/js/initial_page_data").unregister("toggles_dict");
+ initialPageData.unregister("toggles_dict");
});
describe('#getMenuView', function () {
- let FormplayerFrontend = hqImport("cloudcare/js/formplayer/app"),
- server,
+ let server,
user;
beforeEach(function () {
server = sinon.useFakeXMLHttpRequest();
sinon.stub(Backbone.history, 'getFragment').callsFake(sinon.spy());
- user = FormplayerFrontend.getChannel().request('currentUser');
+ user = UsersModels.getCurrentUser();
user.displayOptions = {
singleAppMode: false,
};
@@ -41,9 +62,9 @@ hqDefine("cloudcare/js/formplayer/spec/menu_list_spec", function () {
Backbone.history.getFragment.restore();
});
- let getMenuView = hqImport("cloudcare/js/formplayer/menus/utils").getMenuView;
+ let getMenuView = MenusUtils.getMenuView;
it('Should parse a case list response to a CaseListView', function () {
- let view = getMenuView(hqImport("cloudcare/js/formplayer/spec/fixtures/case_list"));
+ let view = getMenuView(CaseListFixture);
assert.isFalse(view.templateContext().useTiles);
});
@@ -53,26 +74,25 @@ hqDefine("cloudcare/js/formplayer/spec/menu_list_spec", function () {
});
it('Should parse a case list response with tiles to a CaseTileListView', function () {
- let view = getMenuView(hqImport("cloudcare/js/formplayer/spec/fixtures/case_tile_list"));
+ let view = getMenuView(CaseTileListFixture);
assert.isTrue(view.templateContext().useTiles);
});
it('Should parse a case grid response with tiles to a GridCaseTileListView', function () {
- let view = getMenuView(hqImport("cloudcare/js/formplayer/spec/fixtures/case_grid_list"));
+ let view = getMenuView(CaseGridListFixture);
assert.isTrue(view.templateContext().useTiles);
});
});
describe('#getMenus', function () {
- let FormplayerFrontend = hqImport("cloudcare/js/formplayer/app"),
- server,
+ let server,
clock,
user,
requests,
currentView;
before(function () {
- hqImport("hqwebapp/js/initial_page_data").register("apps", [{
+ initialPageData.register("apps", [{
"_id": "abc123",
}]);
});
@@ -101,14 +121,14 @@ hqDefine("cloudcare/js/formplayer/spec/menu_list_spec", function () {
server.onCreate = function (xhr) {
requests.push(xhr);
};
- user = FormplayerFrontend.getChannel().request('currentUser');
+ user = UsersModels.getCurrentUser();
user.domain = 'test-domain';
user.username = 'test-username';
user.formplayer_url = 'url';
user.restoreAs = '';
user.displayOptions = {};
- hqImport("cloudcare/js/formplayer/apps/api").primeApps(user.restoreAs, []);
+ AppsAPI.primeApps(user.restoreAs, []);
});
afterEach(function () {
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/menu_utils_spec.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/menu_utils_spec.js
index 51fa9e4b896f..8fc3867f0c6a 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/menu_utils_spec.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/menu_utils_spec.js
@@ -1,11 +1,12 @@
'use strict';
-/* eslint-env mocha */
-hqDefine("cloudcare/js/formplayer/spec/menu_utils_spec", function () {
+hqDefine("cloudcare/js/formplayer/spec/menu_utils_spec", [
+ "cloudcare/js/formplayer/menus/views/query",
+], function (
+ view
+) {
describe('Menu Utils', function () {
describe('groupDisplays', function () {
- const view = hqImport("cloudcare/js/formplayer/menus/views/query");
-
it('should return the displays grouped by their groupKey', function () {
const displays = [
{
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/query_spec.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/query_spec.js
index 6c8b110dc4d7..ab9077909682 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/query_spec.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/query_spec.js
@@ -1,19 +1,25 @@
'use strict';
/* eslint-env mocha */
-/* global Backbone */
-hqDefine("cloudcare/js/formplayer/spec/query_spec", function () {
+hqDefine("cloudcare/js/formplayer/spec/query_spec", [
+ "backbone",
+ "sinon/pkg/sinon",
+ "hqwebapp/js/initial_page_data",
+ "cloudcare/js/formplayer/utils/utils",
+ "cloudcare/js/formplayer/menus/views/query",
+], function (
+ Backbone,
+ sinon,
+ initialPageData,
+ Utils,
+ QueryListView
+) {
describe('Query', function () {
-
describe('itemset', function () {
let keyQueryView;
before(function () {
- const QueryListView = hqImport("cloudcare/js/formplayer/menus/views/query");
- const Utils = hqImport("cloudcare/js/formplayer/utils/utils");
-
- hqImport("hqwebapp/js/initial_page_data").register("toggles_dict", { DYNAMICALLY_UPDATE_SEARCH_RESULTS: false });
-
+ initialPageData.register("toggles_dict", { DYNAMICALLY_UPDATE_SEARCH_RESULTS: false });
const QueryViewModel = Backbone.Model.extend();
const QueryViewCollection = Backbone.Collection.extend();
const keyModel = new QueryViewModel({
@@ -38,7 +44,7 @@ hqDefine("cloudcare/js/formplayer/spec/query_spec", function () {
});
after(function () {
- hqImport("hqwebapp/js/initial_page_data").unregister("toggles_dict");
+ initialPageData.unregister("toggles_dict");
});
it('should create dictionary with either keys', function () {
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/session_middleware_spec.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/session_middleware_spec.js
index 7fe9935ebcca..8d30802790ab 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/session_middleware_spec.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/session_middleware_spec.js
@@ -1,9 +1,13 @@
'use strict';
/* eslint-env mocha */
-hqDefine("cloudcare/js/formplayer/spec/session_middleware_spec", function () {
+hqDefine("cloudcare/js/formplayer/spec/session_middleware_spec", [
+ "sinon/pkg/sinon",
+ "cloudcare/js/formplayer/middleware",
+], function (
+ sinon,
+ Middleware
+) {
describe('SessionMiddle', function () {
- let Middleware = hqImport("cloudcare/js/formplayer/middleware");
-
it('Should call middleware and apis with same arguments', function () {
let middlewareSpy = sinon.spy(),
result,
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/split_screen_case_search_spec.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/split_screen_case_search_spec.js
index acb34f69b956..94ec2910e765 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/split_screen_case_search_spec.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/split_screen_case_search_spec.js
@@ -1,16 +1,33 @@
'use strict';
/* eslint-env mocha */
-/* global Backbone, Marionette */
-hqDefine("cloudcare/js/formplayer/spec/split_screen_case_search_spec", function () {
+hqDefine("cloudcare/js/formplayer/spec/split_screen_case_search_spec", [
+ "underscore",
+ "backbone",
+ "backbone.marionette",
+ "sinon/pkg/sinon",
+ "hqwebapp/js/toggles",
+ "cloudcare/js/formplayer/app",
+ "cloudcare/js/formplayer/menus/api",
+ "cloudcare/js/formplayer/menus/controller",
+ "cloudcare/js/formplayer/spec/fake_formplayer",
+ "cloudcare/js/formplayer/spec/fixtures/split_screen_case_list",
+ "cloudcare/js/formplayer/utils/utils",
+ "cloudcare/js/formplayer/users/models",
+], function (
+ _,
+ Backbone,
+ Marionette,
+ sinon,
+ Toggles,
+ FormplayerFrontend,
+ API,
+ Controller,
+ FakeFormplayer,
+ splitScreenCaseListResponse,
+ Utils,
+ UsersModels
+) {
describe('Split Screen Case Search', function () {
- const API = hqImport("cloudcare/js/formplayer/menus/api"),
- Controller = hqImport('cloudcare/js/formplayer/menus/controller'),
- FakeFormplayer = hqImport('cloudcare/js/formplayer/spec/fake_formplayer'),
- FormplayerFrontend = hqImport('cloudcare/js/formplayer/app'),
- splitScreenCaseListResponse = hqImport('cloudcare/js/formplayer/spec/fixtures/split_screen_case_list'),
- Toggles = hqImport('hqwebapp/js/toggles'),
- Utils = hqImport('cloudcare/js/formplayer/utils/utils');
-
const currentUrl = new Utils.CloudcareUrl({ appId: 'abc123' }),
sandbox = sinon.sandbox.create(),
stubs = {};
@@ -45,7 +62,10 @@ hqDefine("cloudcare/js/formplayer/spec/split_screen_case_search_spec", function
});
beforeEach(function () {
- FormplayerFrontend.currentUser.displayOptions.singleAppMode = false;
+ var user = UsersModels.getCurrentUser();
+ user.displayOptions = {
+ singleAppMode: false,
+ };
stubs.splitScreenToggleEnabled.returns(true);
});
@@ -113,7 +133,10 @@ hqDefine("cloudcare/js/formplayer/spec/split_screen_case_search_spec", function
});
it('should empty sidebar if in app preview', function () {
- FormplayerFrontend.currentUser.displayOptions.singleAppMode = true;
+ var user = UsersModels.getCurrentUser();
+ user.displayOptions = {
+ singleAppMode: true,
+ };
Controller.showMenu(splitScreenCaseListResponse);
assert.isTrue(stubs.regions['sidebar'].empty.called);
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/user_spec.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/user_spec.js
index 0e4bc2603e9a..b99a16d1766f 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/user_spec.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/user_spec.js
@@ -1,31 +1,89 @@
'use strict';
/* eslint-env mocha */
-hqDefine("cloudcare/js/formplayer/spec/user_spec", function () {
+hqDefine("cloudcare/js/formplayer/spec/user_spec", [
+ "underscore",
+ "sinon/pkg/sinon",
+ "cloudcare/js/formplayer/app",
+ "cloudcare/js/formplayer/users/collections",
+ "cloudcare/js/formplayer/users/models",
+ "cloudcare/js/formplayer/users/utils",
+], function (
+ _,
+ sinon,
+ FormplayerFrontend,
+ UsersCollections,
+ UsersModels,
+ UsersUtils
+) {
describe('User', function () {
describe('Collection', function () {
it('should instantiate a user collection', function () {
- let collection = hqImport("cloudcare/js/formplayer/users/collections")([], { domain: 'mydomain' });
+ let collection = UsersCollections([], { domain: 'mydomain' });
assert.equal(collection.domain, 'mydomain');
});
it('should error on fetch a user collection', function () {
let instantiate = function () {
- let collection = hqImport("cloudcare/js/formplayer/users/collections")();
+ let collection = UsersCollections();
collection.fetch();
};
assert.throws(instantiate, /without domain/);
});
});
+ describe('Display Options', function () {
+ let options;
+ beforeEach(function () {
+ options = {
+ username: 'batman',
+ domain: 'domain',
+ apps: [],
+ };
+ });
+
+ it('should initialize user', function () {
+ UsersModels.setCurrentUser(options);
+
+ let user = UsersModels.getCurrentUser();
+ assert.equal(user.username, options.username);
+ assert.equal(user.domain, options.domain);
+ });
+
+ it('should correctly restore display options', function () {
+ let newOptions = _.clone(options),
+ user;
+ newOptions.phoneMode = true;
+ newOptions.oneQuestionPerScreen = true;
+ newOptions.language = 'sindarin';
+
+ UsersModels.setCurrentUser(newOptions);
+
+ user = UsersModels.getCurrentUser();
+ UsersModels.saveDisplayOptions(user.displayOptions);
+
+ // New session, but old options
+ UsersModels.setCurrentUser(options);
+ user = UsersModels.getCurrentUser();
+
+ assert.deepEqual(user.displayOptions, {
+ phoneMode: undefined, // we don't store this option
+ singleAppMode: undefined,
+ landingPageAppMode: undefined,
+ oneQuestionPerScreen: true,
+ language: 'sindarin',
+ });
+ });
+ });
+
describe('CurrentUser Model', function () {
it('should get the display name of a mobile worker', function () {
- let model = hqImport("cloudcare/js/formplayer/users/models").CurrentUser();
+ let model = UsersModels.getCurrentUser();
model.username = 'worker@domain.commcarehq.org';
assert.equal(model.getDisplayUsername(), 'worker');
});
it('should get the display name of a web user', function () {
- let model = hqImport("cloudcare/js/formplayer/users/models").CurrentUser();
+ let model = UsersModels.getCurrentUser();
model.username = 'web@gmail.com';
assert.equal(model.getDisplayUsername(), 'web@gmail.com');
});
@@ -33,26 +91,23 @@ hqDefine("cloudcare/js/formplayer/spec/user_spec", function () {
});
describe('Utils', function () {
- let Utils = hqImport("cloudcare/js/formplayer/users/utils").Users,
- FormplayerFrontend = hqImport("cloudcare/js/formplayer/app"),
+ let Utils = UsersUtils.Users,
username = 'clark@kent.com',
restoreAsUsername = 'worker@kent.com',
domain = 'preview-domain',
- dummyChannel,
dummyUser;
beforeEach(function () {
dummyUser = {
domain: domain,
username: username,
};
- dummyChannel = FormplayerFrontend.getChannel();
window.localStorage.clear();
- sinon.stub(dummyChannel, 'request').callsFake(function () { return dummyUser; });
+ sinon.stub(UsersModels, 'getCurrentUser').callsFake(function () { return dummyUser; });
});
afterEach(function () {
window.localStorage.clear();
- dummyChannel.request.restore();
+ UsersModels.getCurrentUser.restore();
});
it('should store and clear a restore as user', function () {
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/utils_spec.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/utils_spec.js
index d104f09f6b91..dca2054e97f4 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/utils_spec.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/spec/utils_spec.js
@@ -1,36 +1,47 @@
'use strict';
/* eslint-env mocha */
-/* global Backbone */
-hqDefine("cloudcare/js/formplayer/spec/utils_spec", function () {
+hqDefine("cloudcare/js/formplayer/spec/utils_spec", [
+ "underscore",
+ "backbone",
+ "sinon/pkg/sinon",
+ "hqwebapp/js/initial_page_data",
+ "cloudcare/js/formplayer/app",
+ "cloudcare/js/formplayer/menus/api",
+ "cloudcare/js/formplayer/spec/fake_formplayer",
+ "cloudcare/js/formplayer/users/models",
+ "cloudcare/js/formplayer/utils/utils",
+ "cloudcare/js/formplayer/router", // needed for navigation events, like menu:select
+], function (
+ _,
+ Backbone,
+ sinon,
+ initialPageData,
+ FormplayerFrontend,
+ API,
+ FakeFormplayer,
+ UsersModels,
+ Utils
+) {
describe('Utils', function () {
- let API = hqImport("cloudcare/js/formplayer/menus/api"),
- FakeFormplayer = hqImport("cloudcare/js/formplayer/spec/fake_formplayer"),
- FormplayerFrontend = hqImport("cloudcare/js/formplayer/app"),
- Utils = hqImport("cloudcare/js/formplayer/utils/utils");
-
describe('#displayOptions', function () {
beforeEach(function () {
- sinon.stub(Utils, 'getDisplayOptionsKey').callsFake(function () { return 'mykey'; });
+ sinon.stub(UsersModels, 'getDisplayOptionsKey').callsFake(function () { return 'mykey'; });
window.localStorage.clear();
});
afterEach(function () {
- Utils.getDisplayOptionsKey.restore();
+ UsersModels.getDisplayOptionsKey.restore();
});
it('should retrieve saved display options', function () {
let options = { option: 'yes' };
- Utils.saveDisplayOptions(options);
- $.when(Utils.getSavedDisplayOptions(), function (response) {
- assert.deepEqual(response, options);
- });
+ UsersModels.saveDisplayOptions(options);
+ assert.deepEqual(UsersModels.getSavedDisplayOptions(), options);
});
it('should not fail on bad json saved', function () {
- localStorage.setItem(Utils.getDisplayOptionsKey(), 'bad json');
- $.when(Utils.getSavedDisplayOptions(), function (response) {
- assert.deepEqual(response, {});
- });
+ localStorage.setItem(UsersModels.getDisplayOptionsKey(), 'bad json');
+ assert.deepEqual(UsersModels.getSavedDisplayOptions(), {});
});
});
@@ -39,14 +50,14 @@ hqDefine("cloudcare/js/formplayer/spec/utils_spec", function () {
let stubs = {};
before(function () {
- hqImport("hqwebapp/js/initial_page_data").register("toggles_dict", {
+ initialPageData.register("toggles_dict", {
SPLIT_SCREEN_CASE_SEARCH: false,
DYNAMICALLY_UPDATE_SEARCH_RESULTS: false,
});
});
after(function () {
- hqImport("hqwebapp/js/initial_page_data").unregister("toggles_dict");
+ initialPageData.unregister("toggles_dict");
});
beforeEach(function () {
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/users/collections.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/users/collections.js
index 333f3c5c9b5c..95d9fe119e1c 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/users/collections.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/users/collections.js
@@ -1,9 +1,11 @@
'use strict';
-/*global Backbone */
-
-hqDefine("cloudcare/js/formplayer/users/collections", function () {
- var Models = hqImport("cloudcare/js/formplayer/users/models");
-
+hqDefine("cloudcare/js/formplayer/users/collections", [
+ 'backbone',
+ 'cloudcare/js/formplayer/users/models',
+], function (
+ Backbone,
+ Models
+) {
/**
* This collection represents a mobile worker user
*/
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/users/controller.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/users/controller.js
index e0a97f2c8076..453e4622f6bd 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/users/controller.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/users/controller.js
@@ -1,12 +1,18 @@
'use strict';
-hqDefine("cloudcare/js/formplayer/users/controller", function () {
- var Collections = hqImport("cloudcare/js/formplayer/users/collections"),
- FormplayerFrontend = hqImport("cloudcare/js/formplayer/app"),
- views = hqImport("cloudcare/js/formplayer/users/views");
-
+hqDefine("cloudcare/js/formplayer/users/controller", [
+ 'cloudcare/js/formplayer/users/collections',
+ 'cloudcare/js/formplayer/app',
+ 'cloudcare/js/formplayer/users/models',
+ 'cloudcare/js/formplayer/users/views',
+], function (
+ Collections,
+ FormplayerFrontend,
+ models,
+ views
+) {
return {
listUsers: function (page, query) {
- var currentUser = FormplayerFrontend.getChannel().request('currentUser'),
+ var currentUser = models.getCurrentUser(),
users;
users = Collections([], { domain: currentUser.domain });
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/users/models.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/users/models.js
index 2bffe8074fa0..8d12a5f8a583 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/users/models.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/users/models.js
@@ -1,11 +1,19 @@
'use strict';
-/*global Backbone */
+hqDefine("cloudcare/js/formplayer/users/models", [
+ "underscore",
+ "backbone",
+ "analytix/js/kissmetrix",
+ "cloudcare/js/formplayer/constants",
+], function (
+ _,
+ Backbone,
+ kissmetrics,
+ Const
+) {
+ var self = {};
-hqDefine("cloudcare/js/formplayer/users/models", function () {
- var kissmetrics = hqImport("analytix/js/kissmetrix");
-
- var User = Backbone.Model.extend();
- var CurrentUser = Backbone.Model.extend({
+ self.User = Backbone.Model.extend();
+ self.CurrentUser = Backbone.Model.extend({
initialize: function () {
this.on('change:versionInfo', function (model) {
if (model.previous('versionInfo') && model.get('versionInfo')) {
@@ -36,10 +44,63 @@ hqDefine("cloudcare/js/formplayer/users/models", function () {
},
});
- return {
- User: User,
- CurrentUser: function () {
- return new CurrentUser();
- },
+ self.saveDisplayOptions = function (displayOptions) {
+ var displayOptionsKey = self.getDisplayOptionsKey();
+ localStorage.setItem(displayOptionsKey, JSON.stringify(displayOptions));
+ };
+
+ self.getSavedDisplayOptions = function () {
+ var displayOptionsKey = self.getDisplayOptionsKey();
+ try {
+ return JSON.parse(localStorage.getItem(displayOptionsKey));
+ } catch (e) {
+ window.console.warn('Unable to parse saved display options');
+ return {};
+ }
+ };
+
+ self.getDisplayOptionsKey = function () {
+ var user = self.getCurrentUser();
+ return [
+ user.environment,
+ user.domain,
+ user.username,
+ 'displayOptions',
+ ].join(':');
+ };
+
+ var userInstance;
+ self.getCurrentUser = function () {
+ if (!userInstance) {
+ userInstance = new self.CurrentUser();
+ }
+ return userInstance;
};
+
+ self.setCurrentUser = function (options) {
+ self.getCurrentUser(); // ensure userInstance is populated
+
+ userInstance.username = options.username;
+ userInstance.domain = options.domain;
+ userInstance.formplayer_url = options.formplayer_url;
+ userInstance.debuggerEnabled = options.debuggerEnabled;
+ userInstance.environment = options.environment;
+ userInstance.changeFormLanguage = options.changeFormLanguage;
+
+ var savedDisplayOptions = _.pick(
+ self.getSavedDisplayOptions(),
+ Const.ALLOWED_SAVED_OPTIONS
+ );
+ userInstance.displayOptions = _.defaults(savedDisplayOptions, {
+ singleAppMode: options.singleAppMode,
+ landingPageAppMode: options.landingPageAppMode,
+ phoneMode: options.phoneMode,
+ oneQuestionPerScreen: options.oneQuestionPerScreen,
+ language: options.language,
+ });
+
+ return userInstance;
+ };
+
+ return self;
});
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/users/utils.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/users/utils.js
index d59a7605be3a..35cf9ba5c3d0 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/users/utils.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/users/utils.js
@@ -1,9 +1,17 @@
'use strict';
-/* global Sentry */
-hqDefine("cloudcare/js/formplayer/users/utils", function () {
- var FormplayerFrontend = hqImport("cloudcare/js/formplayer/app"),
- initialPageData = hqImport("hqwebapp/js/initial_page_data");
-
+hqDefine("cloudcare/js/formplayer/users/utils", [
+ 'jquery',
+ 'sentry_browser',
+ 'hqwebapp/js/initial_page_data',
+ 'cloudcare/js/formplayer/app',
+ 'cloudcare/js/formplayer/users/models',
+], function (
+ $,
+ Sentry,
+ initialPageData,
+ FormplayerFrontend,
+ UsersModels
+) {
var self = {};
self.Users = {
/**
@@ -14,7 +22,7 @@ hqDefine("cloudcare/js/formplayer/users/utils", function () {
* setting it in a cookie
*/
logInAsUser: function (restoreAsUsername) {
- var currentUser = FormplayerFrontend.getChannel().request('currentUser');
+ var currentUser = UsersModels.getCurrentUser();
currentUser.restoreAs = restoreAsUsername;
Sentry.setTag("loginAsUser", restoreAsUsername);
@@ -71,7 +79,7 @@ hqDefine("cloudcare/js/formplayer/users/utils", function () {
* navigates you to the main page.
*/
FormplayerFrontend.on('clearRestoreAsUser', function () {
- var user = FormplayerFrontend.getChannel().request('currentUser');
+ var user = UsersModels.getCurrentUser();
self.Users.clearRestoreAsUser(
user.domain,
user.username
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/users/views.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/users/views.js
index 3fe65df9f93f..2635edf7cc1a 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/users/views.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/users/views.js
@@ -1,12 +1,25 @@
'use strict';
-/*global Backbone, Marionette */
-
-hqDefine("cloudcare/js/formplayer/users/views", function () {
- var FormplayerFrontend = hqImport("cloudcare/js/formplayer/app"),
- formplayerUtils = hqImport("cloudcare/js/formplayer/utils/utils"),
- Toggles = hqImport("hqwebapp/js/toggles"),
- usersUtils = hqImport("cloudcare/js/formplayer/users/utils");
-
+hqDefine("cloudcare/js/formplayer/users/views", [
+ 'jquery',
+ 'underscore',
+ 'backbone',
+ 'backbone.marionette',
+ 'hqwebapp/js/toggles',
+ 'cloudcare/js/formplayer/app',
+ 'cloudcare/js/formplayer/utils/utils',
+ 'cloudcare/js/formplayer/users/models',
+ 'cloudcare/js/formplayer/users/utils',
+], function (
+ $,
+ _,
+ Backbone,
+ Marionette,
+ toggles,
+ FormplayerFrontend,
+ formplayerUtils,
+ usersModels,
+ usersUtils
+) {
/**
* RestoreAsBanner
*
@@ -24,7 +37,7 @@ hqDefine("cloudcare/js/formplayer/users/views", function () {
},
templateContext: function () {
var template = "";
- if (Toggles.toggleEnabled('WEB_APPS_DOMAIN_BANNER')) {
+ if (toggles.toggleEnabled('WEB_APPS_DOMAIN_BANNER')) {
template = gettext("Working as <%- restoreAs %> in <%- domain %> .");
} else {
template = gettext("Working as <%- restoreAs %> .");
@@ -34,7 +47,7 @@ hqDefine("cloudcare/js/formplayer/users/views", function () {
var message = _.template(template)({
restoreAs: this.model.restoreAs,
username: this.model.getDisplayUsername(),
- domain: FormplayerFrontend.getChannel().request('currentUser').domain,
+ domain: usersModels.getCurrentUser().domain,
});
return {
message: message,
@@ -77,7 +90,7 @@ hqDefine("cloudcare/js/formplayer/users/views", function () {
usersUtils.Users.logInAsUser(this.model.get('username'));
FormplayerFrontend.regions.getRegion('restoreAsBanner').show(
new RestoreAsBanner({
- model: FormplayerFrontend.getChannel().request('currentUser'),
+ model: usersModels.getCurrentUser(),
})
);
var loginAsNextOptions = FormplayerFrontend.getChannel().request('getLoginAsNextOptions');
@@ -153,7 +166,7 @@ hqDefine("cloudcare/js/formplayer/users/views", function () {
});
},
navigate: function () {
- FormplayerFrontend.navigate(
+ formplayerUtils.navigate(
'/restore_as/' +
this.model.get('page') + '/' +
this.model.get('query')
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/utils/calendar-picker-translations.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/utils/calendar-picker-translations.js
index 47601865fac0..15f4a5c0c99b 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/utils/calendar-picker-translations.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/utils/calendar-picker-translations.js
@@ -1,5 +1,15 @@
'use strict';
-(function ($) {
+hqDefine('cloudcare/js/formplayer/utils/calendar-picker-translations', [
+ 'jquery',
+ 'calendars/dist/js/jquery.calendars.picker',
+ 'calendars/dist/js/jquery.calendars.ethiopian',
+ 'calendars/dist/js/jquery.calendars.plus',
+ 'calendars/dist/js/jquery.calendars-am',
+ 'calendars/dist/js/jquery.calendars.picker-am',
+ 'calendars/dist/js/jquery.calendars.ethiopian-am',
+], function (
+ $
+) {
// English
$.calendarsPicker.regionalOptions[''] = { // Default regional settings - English/US
renderer: $.calendarsPicker.regionalOptions[''].renderer, // this.defaultRenderer
@@ -331,4 +341,4 @@
firstDay: 0,
isRTL: false,
};
-})(window.jQuery);
+});
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/utils/utils.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/utils/utils.js
index 60467d233b2b..387d5a764e98 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/utils/utils.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/utils/utils.js
@@ -1,10 +1,21 @@
'use strict';
-/*global Backbone, DOMPurify */
-hqDefine("cloudcare/js/formplayer/utils/utils", function () {
- var initialPageData = hqImport("hqwebapp/js/initial_page_data"),
- toggles = hqImport("hqwebapp/js/toggles"),
- constants = hqImport("cloudcare/js/formplayer/constants");
-
+hqDefine("cloudcare/js/formplayer/utils/utils", [
+ 'jquery',
+ 'underscore',
+ 'backbone',
+ 'DOMPurify/dist/purify.min',
+ 'hqwebapp/js/initial_page_data',
+ 'hqwebapp/js/toggles',
+ "cloudcare/js/formplayer/constants"
+], function (
+ $,
+ _,
+ Backbone,
+ DOMPurify,
+ initialPageData,
+ toggles,
+ constants
+) {
var Utils = {};
/**
@@ -69,9 +80,12 @@ hqDefine("cloudcare/js/formplayer/utils/utils", function () {
}
var encodedUrl = Utils.objectToEncodedUrl(urlObject.toJson());
- hqRequire(["cloudcare/js/formplayer/app"], function (FormplayerFrontend) {
- FormplayerFrontend.navigate(encodedUrl, { replace: replace });
- });
+ Utils.navigate(encodedUrl, { replace: replace });
+ };
+
+ Utils.navigate = function (route, options) {
+ options || (options = {});
+ Backbone.history.navigate(route, options);
};
/**
@@ -101,39 +115,6 @@ hqDefine("cloudcare/js/formplayer/utils/utils", function () {
options.contentType = "application/json;charset=UTF-8";
};
- Utils.saveDisplayOptions = function (displayOptions) {
- $.when(Utils.getDisplayOptionsKey()).done(function (displayOptionsKey) {
- localStorage.setItem(displayOptionsKey, JSON.stringify(displayOptions));
- });
- };
-
- Utils.getSavedDisplayOptions = function () {
- var defer = $.Deferred();
- $.when(Utils.getDisplayOptionsKey()).done(function (displayOptionsKey) {
- try {
- defer.resolve(JSON.parse(localStorage.getItem(displayOptionsKey)));
- } catch (e) {
- window.console.warn('Unabled to parse saved display options');
- defer.resolve({});
- }
- });
- return defer.promise();
- };
-
- Utils.getDisplayOptionsKey = function () {
- var defer = $.Deferred();
- hqRequire(["cloudcare/js/formplayer/app"], function (FormplayerFrontend) {
- var user = FormplayerFrontend.getChannel().request('currentUser');
- defer.resolve([
- user.environment,
- user.domain,
- user.username,
- 'displayOptions',
- ].join(':'));
- });
- return defer.promise();
- };
-
// This method takes current page number on which user has clicked and total possible pages
// and calculate the range of page numbers (start and end) that has to be shown on pagination widget.
// totalItems can be either the total on the current page or across all pages.
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/markdown.js b/corehq/apps/cloudcare/static/cloudcare/js/markdown.js
index f58e42aa775b..b685124a0ad3 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/markdown.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/markdown.js
@@ -1,11 +1,14 @@
'use strict';
-/* global DOMPurify */
hqDefine('cloudcare/js/markdown', [
'jquery',
+ 'DOMPurify/dist/purify.min',
+ 'markdown-it/dist/markdown-it',
'hqwebapp/js/initial_page_data',
'integration/js/hmac_callout',
], function (
$,
+ DOMPurify,
+ markdowner,
initialPageData,
HMACCallout
) {
@@ -101,7 +104,7 @@ hqDefine('cloudcare/js/markdown', [
}
function initMd() {
- let md = window.markdownit({breaks: true}),
+ let md = markdowner({breaks: true}),
// https://github.com/markdown-it/markdown-it/blob/6db517357af5bb42398b474efd3755ad33245877/docs/architecture.md#renderer
defaultLinkOpen = md.renderer.rules.link_open || function (tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/preview_app/dragscroll.js b/corehq/apps/cloudcare/static/cloudcare/js/preview_app/dragscroll.js
index 4e526619e364..59d3d6b54dba 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/preview_app/dragscroll.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/preview_app/dragscroll.js
@@ -10,17 +10,7 @@
* @copyright 2015 asvd
*/
-/* globals exports */
-
-(function (root, factory) {
- if (typeof define === 'function' && define.amd) {
- define(['exports'], factory);
- } else if (typeof exports !== 'undefined') {
- factory(exports);
- } else {
- factory((root.dragscroll = {}));
- }
-}(this, function (exports) {
+hqDefine("cloudcare/js/preview_app/dragscroll", ["jquery"], function ($) {
var _window = window;
var _document = document;
var mousemove = 'mousemove';
@@ -85,13 +75,8 @@
}
};
-
- if (_document.readyState === 'complete') {
+ $(function () {
reset();
- } else {
- _window[addEventListener]('load', reset, 0);
- }
-
- exports.reset = reset;
-}));
+ });
+});
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/preview_app/main.js b/corehq/apps/cloudcare/static/cloudcare/js/preview_app/main.js
index 8994a97766e0..cfc21d5ccbce 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/preview_app/main.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/preview_app/main.js
@@ -1,9 +1,18 @@
'use strict';
-hqDefine("cloudcare/js/preview_app/main", function () {
- var initialPageData = hqImport("hqwebapp/js/initial_page_data"),
- previewApp = hqImport("cloudcare/js/preview_app/preview_app"),
- sentry = hqImport("cloudcare/js/sentry");
-
+hqDefine("cloudcare/js/preview_app/main", [
+ 'jquery',
+ 'underscore',
+ 'hqwebapp/js/initial_page_data',
+ 'cloudcare/js/sentry',
+ 'cloudcare/js/preview_app/preview_app',
+ 'cloudcare/js/preview_app/dragscroll', // for .dragscroll elements
+], function (
+ $,
+ _,
+ initialPageData,
+ sentry,
+ previewApp
+) {
$(function () {
sentry.initSentry();
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/preview_app/preview_app.js b/corehq/apps/cloudcare/static/cloudcare/js/preview_app/preview_app.js
index 39c7065fc66a..cac2e7e94641 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/preview_app/preview_app.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/preview_app/preview_app.js
@@ -1,7 +1,11 @@
'use strict';
-hqDefine('cloudcare/js/preview_app/preview_app', function () {
- var FormplayerFrontend = hqImport("cloudcare/js/formplayer/app");
-
+hqDefine('cloudcare/js/preview_app/preview_app', [
+ 'jquery',
+ 'cloudcare/js/formplayer/app',
+], function (
+ $,
+ FormplayerFrontend
+) {
var start = function (options) {
$('#cloudcare-notifications').on('click', 'a', function () {
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/sentry.js b/corehq/apps/cloudcare/static/cloudcare/js/sentry.js
index c99b5da81e77..fe842c2a84d1 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/sentry.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/sentry.js
@@ -1,9 +1,11 @@
'use strict';
-/* global Sentry */
hqDefine('cloudcare/js/sentry', [
'hqwebapp/js/initial_page_data',
+ 'sentry_browser',
+ 'sentry_captureconsole', // needed for Sentry.Integrations.CaptureConsole
], function (
- initialPageData
+ initialPageData,
+ Sentry
) {
let initSentry = function () {
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/spec/markdown_spec.js b/corehq/apps/cloudcare/static/cloudcare/js/spec/markdown_spec.js
index d498dcfe5e07..3e9d0037e393 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/spec/markdown_spec.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/spec/markdown_spec.js
@@ -1,11 +1,18 @@
'use strict';
/* eslint-env mocha */
-hqDefine("cloudcare/js/spec/markdown_spec", function () {
+hqDefine("cloudcare/js/spec/markdown_spec", [
+ "sinon/pkg/sinon",
+ "hqwebapp/js/initial_page_data",
+ "integration/js/hmac_callout",
+ "cloudcare/js/markdown",
+], function (
+ sinon,
+ initialPageData,
+ hmacCallout,
+ markdown
+) {
describe('Markdown', function () {
- let markdown = hqImport('cloudcare/js/markdown'),
- render = markdown.render,
- initialPageData = hqImport("hqwebapp/js/initial_page_data"),
- hmacCallout = hqImport("integration/js/hmac_callout");
+ let render = markdown.render;
let sandbox;
beforeEach(function () {
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/spec/utils_spec.js b/corehq/apps/cloudcare/static/cloudcare/js/spec/utils_spec.js
index 1a140e873a92..79c8a0038bda 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/spec/utils_spec.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/spec/utils_spec.js
@@ -1,10 +1,15 @@
'use strict';
/* eslint-env mocha */
-hqDefine("cloudcare/js/spec/utils_spec", function () {
+hqDefine("cloudcare/js/spec/utils_spec", [
+ "sinon/pkg/sinon",
+ "cloudcare/js/formplayer/constants",
+ "cloudcare/js/utils",
+], function (
+ sinon,
+ constants,
+ utils
+) {
describe("Cloudcare Utils", function () {
- const constants = hqImport("cloudcare/js/formplayer/constants"),
- utils = hqImport("cloudcare/js/utils");
-
describe('Small Screen Listener', function () {
const callback = sinon.stub().callsFake(smallScreenEnabled => smallScreenEnabled);
const smallScreenListener = utils.smallScreenListener(callback);
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/utils.js b/corehq/apps/cloudcare/static/cloudcare/js/utils.js
index 001392a97d5c..e100a8ccdf0d 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/utils.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/utils.js
@@ -1,13 +1,29 @@
'use strict';
-/* global Marionette, moment, NProgress, Sentry */
hqDefine('cloudcare/js/utils', [
'jquery',
+ 'underscore',
+ 'backbone.marionette',
+ 'moment',
'hqwebapp/js/initial_page_data',
+ "hqwebapp/js/toggles",
"cloudcare/js/formplayer/constants",
+ "cloudcare/js/formplayer/layout/views/progress_bar",
+ 'nprogress/nprogress',
+ 'sentry_browser',
+ "cloudcare/js/formplayer/users/models",
+ 'eonasdan-bootstrap-datetimepicker/build/js/bootstrap-datetimepicker.min', // for $.datetimepicker
], function (
$,
+ _,
+ Marionette,
+ moment,
initialPageData,
- constants
+ toggles,
+ constants,
+ ProgressBar,
+ NProgress,
+ Sentry,
+ UsersModels
) {
if (!String.prototype.startsWith) {
String.prototype.startsWith = function (searchString, position) {
@@ -183,13 +199,11 @@ hqDefine('cloudcare/js/utils', [
};
var showLoading = function () {
- hqRequire(["hqwebapp/js/toggles"], function (toggles) {
- if (toggles.toggleEnabled('USE_PROMINENT_PROGRESS_BAR')) {
- showProminentLoading();
- } else {
- NProgress.start();
- }
- });
+ if (toggles.toggleEnabled('USE_PROMINENT_PROGRESS_BAR')) {
+ showProminentLoading();
+ } else {
+ NProgress.start();
+ }
};
var formplayerLoading = function () {
@@ -249,10 +263,10 @@ hqDefine('cloudcare/js/utils', [
};
var hideLoading = function () {
- hqRequire(["cloudcare/js/formplayer/app", "hqwebapp/js/toggles"], function (FormplayerFrontend, toggles) {
- if (toggles.toggleEnabled('USE_PROMINENT_PROGRESS_BAR')) {
- $('#breadcrumb-region').css('z-index', '');
- clearInterval(sessionStorage.progressIncrementInterval);
+ if (toggles.toggleEnabled('USE_PROMINENT_PROGRESS_BAR')) {
+ $('#breadcrumb-region').css('z-index', '');
+ clearInterval(sessionStorage.progressIncrementInterval);
+ hqRequire(["cloudcare/js/formplayer/app"], function (FormplayerFrontend) {
const progressView = FormplayerFrontend.regions.getRegion('loadingProgress').currentView;
if (progressView) {
progressView.setProgress(100, 100, 200);
@@ -260,10 +274,10 @@ hqDefine('cloudcare/js/utils', [
FormplayerFrontend.regions.getRegion('loadingProgress').empty();
}, 250);
}
- } else {
- NProgress.done();
- }
- });
+ });
+ } else {
+ NProgress.done();
+ }
};
function getSentryMessage(data) {
@@ -278,43 +292,41 @@ hqDefine('cloudcare/js/utils', [
}
var reportFormplayerErrorToHQ = function (data) {
- hqRequire(["cloudcare/js/formplayer/app"], function (FormplayerFrontend) {
- try {
- var cloudcareEnv = FormplayerFrontend.getChannel().request('currentUser').environment;
- if (!data.cloudcareEnv) {
- data.cloudcareEnv = cloudcareEnv || 'unknown';
- }
-
- const sentryData = _.omit(data, "type", "htmlMessage");
- Sentry.captureMessage(getSentryMessage(data), {
- tags: {
- errorType: data.type,
- },
- extra: sentryData,
- level: "error",
- });
-
- $.ajax({
- type: 'POST',
- url: initialPageData.reverse('report_formplayer_error'),
- data: JSON.stringify(data),
- contentType: "application/json",
- dataType: "json",
- success: function () {
- window.console.info('Successfully reported error: ' + JSON.stringify(data));
- },
- error: function () {
- window.console.error('Failed to report error: ' + JSON.stringify(data));
- },
- });
- } catch (e) {
- window.console.error(
- "reportFormplayerErrorToHQ failed hard and there is nowhere " +
- "else to report this error: " + JSON.stringify(data),
- e
- );
+ try {
+ var cloudcareEnv = UsersModels.getCurrentUser().environment;
+ if (!data.cloudcareEnv) {
+ data.cloudcareEnv = cloudcareEnv || 'unknown';
}
- });
+
+ const sentryData = _.omit(data, "type", "htmlMessage");
+ Sentry.captureMessage(getSentryMessage(data), {
+ tags: {
+ errorType: data.type,
+ },
+ extra: sentryData,
+ level: "error",
+ });
+
+ $.ajax({
+ type: 'POST',
+ url: initialPageData.reverse('report_formplayer_error'),
+ data: JSON.stringify(data),
+ contentType: "application/json",
+ dataType: "json",
+ success: function () {
+ window.console.info('Successfully reported error: ' + JSON.stringify(data));
+ },
+ error: function () {
+ window.console.error('Failed to report error: ' + JSON.stringify(data));
+ },
+ });
+ } catch (e) {
+ window.console.error(
+ "reportFormplayerErrorToHQ failed hard and there is nowhere " +
+ "else to report this error: " + JSON.stringify(data),
+ e
+ );
+ }
};
var dateTimePickerTooltips = { // use default text, but enable translations
diff --git a/corehq/apps/cloudcare/templates/cloudcare/formplayer_home.html b/corehq/apps/cloudcare/templates/cloudcare/formplayer_home.html
index c58f01b3c4b0..cd2706f5cc50 100644
--- a/corehq/apps/cloudcare/templates/cloudcare/formplayer_home.html
+++ b/corehq/apps/cloudcare/templates/cloudcare/formplayer_home.html
@@ -52,9 +52,10 @@
{% endcompress %}
{% endblock %}
+{% requirejs_main "cloudcare/js/formplayer/main" %}
+
{% block js %} {{ block.super }}
{% include "cloudcare/partials/dependencies.html" %}
-
{% endblock %}
{% block content %}
@@ -130,18 +131,8 @@
{% if not request.session.secure_session %}
{% include 'hqwebapp/includes/inactivity_modal_data.html' %}
{% endif %}
- {% include 'cloudcare/partials/form_entry_templates.html' %}
- {% include 'cloudcare/partials/debugger.html' %}
- {% include 'cloudcare/partials/grid_view.html' %}
- {% include 'cloudcare/partials/settings_view.html' %}
- {% include 'cloudcare/partials/case_detail.html' %}
- {% include 'cloudcare/partials/case_list.html' %}
- {% include 'cloudcare/partials/menu_list.html' %}
- {% include 'cloudcare/partials/users.html' %}
- {% include 'cloudcare/partials/session_list.html' %}
- {% include 'cloudcare/partials/query_view.html' %}
{% include 'cloudcare/partials/confirmation_modal.html' %}
- {% include 'cloudcare/partials/progress.html' %}
{% include 'cloudcare/partials/new_app_version_modal.html' %}
+ {% include 'cloudcare/partials/all_templates.html' %}
{% endblock %}
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/all_templates.html b/corehq/apps/cloudcare/templates/cloudcare/partials/all_templates.html
new file mode 100644
index 000000000000..5958cb595181
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/all_templates.html
@@ -0,0 +1,44 @@
+{% include 'cloudcare/partials/case_detail.html' %}
+
+{% include 'cloudcare/partials/case_list/cell_container_style.html' %}
+{% include 'cloudcare/partials/case_list/cell_grid_style.html' %}
+{% include 'cloudcare/partials/case_list/cell_layout_style.html' %}
+{% include 'cloudcare/partials/case_list/detail.html' %}
+{% include 'cloudcare/partials/case_list/item.html' %}
+{% include 'cloudcare/partials/case_list/list.html' %}
+{% include 'cloudcare/partials/case_list/tile_grouped_item.html' %}
+{% include 'cloudcare/partials/case_list/tile_item.html' %}
+
+{% include 'cloudcare/partials/debugger.html' %}
+
+{% include 'cloudcare/partials/form_entry_templates.html' %}
+
+{% include 'cloudcare/partials/grid_view/grid.html' %}
+{% include 'cloudcare/partials/grid_view/landing_page_app.html' %}
+{% include 'cloudcare/partials/grid_view/row.html' %}
+{% include 'cloudcare/partials/grid_view/single_app.html' %}
+
+{% include 'cloudcare/partials/menu/audio.html' %}
+{% include 'cloudcare/partials/menu/badge.html' %}
+{% include 'cloudcare/partials/menu/breadcrumbs.html' %}
+{% include 'cloudcare/partials/menu/dropdown.html' %}
+{% include 'cloudcare/partials/menu/grid.html' %}
+{% include 'cloudcare/partials/menu/grid_item.html' %}
+{% include 'cloudcare/partials/menu/list.html' %}
+{% include 'cloudcare/partials/menu/row.html' %}
+
+{% include 'cloudcare/partials/progress.html' %}
+
+{% include 'cloudcare/partials/query/group.html' %}
+{% include 'cloudcare/partials/query/item.html' %}
+{% include 'cloudcare/partials/query/list.html' %}
+
+{% include 'cloudcare/partials/sessions/item.html' %}
+{% include 'cloudcare/partials/sessions/list.html' %}
+
+{% include 'cloudcare/partials/settings_view.html' %}
+
+{% include 'cloudcare/partials/users/restore_as.html' %}
+{% include 'cloudcare/partials/users/restore_as_banner.html' %}
+{% include 'cloudcare/partials/users/user_data.html' %}
+{% include 'cloudcare/partials/users/user_row.html' %}
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/case_list.html b/corehq/apps/cloudcare/templates/cloudcare/partials/case_list.html
deleted file mode 100644
index 5790c612f65c..000000000000
--- a/corehq/apps/cloudcare/templates/cloudcare/partials/case_list.html
+++ /dev/null
@@ -1,296 +0,0 @@
-{% load hq_shared_tags %}
-{% load i18n %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/cell_container_style.html b/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/cell_container_style.html
new file mode 100644
index 000000000000..0c5c48671864
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/cell_container_style.html
@@ -0,0 +1,9 @@
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/cell_grid_style.html b/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/cell_grid_style.html
new file mode 100644
index 000000000000..20ab7ec5dd00
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/cell_grid_style.html
@@ -0,0 +1,17 @@
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/cell_layout_style.html b/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/cell_layout_style.html
new file mode 100644
index 000000000000..575bfaa41c46
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/cell_layout_style.html
@@ -0,0 +1,37 @@
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/detail.html b/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/detail.html
new file mode 100644
index 000000000000..4f7546c28626
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/detail.html
@@ -0,0 +1,34 @@
+{% load i18n %}
+
+
+
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/item.html b/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/item.html
new file mode 100644
index 000000000000..5d2e3ccdf449
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/item.html
@@ -0,0 +1,25 @@
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/list.html b/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/list.html
new file mode 100644
index 000000000000..8d0d0c8bcfdc
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/list.html
@@ -0,0 +1,80 @@
+{% load i18n %}
+
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/tile_grouped_item.html b/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/tile_grouped_item.html
new file mode 100644
index 000000000000..4bbbb5b8d797
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/tile_grouped_item.html
@@ -0,0 +1,58 @@
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/tile_item.html b/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/tile_item.html
new file mode 100644
index 000000000000..ee7a88b5e5e8
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/case_list/tile_item.html
@@ -0,0 +1,30 @@
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/dependencies.html b/corehq/apps/cloudcare/templates/cloudcare/partials/dependencies.html
index ab5034b1f5e8..ad3a618a1dae 100644
--- a/corehq/apps/cloudcare/templates/cloudcare/partials/dependencies.html
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/dependencies.html
@@ -1,111 +1,11 @@
{% load compress %}
{% load hq_shared_tags %}
- {# serve sentry directly to prevent over zealous ad blockers from blocking it #}
-
-
-{% compress js %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-{% endcompress %}
-
-{% include 'hqwebapp/includes/ui_element_js.html' %}
-{% compress js %}
-
-
-
-
-
-
-
-
-
-
-
-{% endcompress %}
-
-{% compress js %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-{% endcompress %}
-
-
+
-
-
-
-
-
-
- <% if (shouldShowIncompleteForms) { %>
-
- <% } %>
-
- {% if not request|toggle_enabled:"HIDE_SYNC_BUTTON" %}
-
- {% endif %}
-
- {% if request|can_use_restore_as %}
-
- {% endif %}
-
-
-
-
-{# App Preview #}
-
-
-
-
-
-
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/grid_view/grid.html b/corehq/apps/cloudcare/templates/cloudcare/partials/grid_view/grid.html
new file mode 100644
index 000000000000..357330830112
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/grid_view/grid.html
@@ -0,0 +1,55 @@
+{% load hq_shared_tags %}
+{% load i18n %}
+
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/grid_view/landing_page_app.html b/corehq/apps/cloudcare/templates/cloudcare/partials/grid_view/landing_page_app.html
new file mode 100644
index 000000000000..9df4533c619d
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/grid_view/landing_page_app.html
@@ -0,0 +1,17 @@
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/grid_view/row.html b/corehq/apps/cloudcare/templates/cloudcare/partials/grid_view/row.html
new file mode 100644
index 000000000000..bbb6d86c0e7b
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/grid_view/row.html
@@ -0,0 +1,12 @@
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/grid_view/single_app.html b/corehq/apps/cloudcare/templates/cloudcare/partials/grid_view/single_app.html
new file mode 100644
index 000000000000..768adc91005b
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/grid_view/single_app.html
@@ -0,0 +1,58 @@
+{% load hq_shared_tags %}
+{% load i18n %}
+
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/menu/audio.html b/corehq/apps/cloudcare/templates/cloudcare/partials/menu/audio.html
new file mode 100644
index 000000000000..e7aaf9feda42
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/menu/audio.html
@@ -0,0 +1,24 @@
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/menu/badge.html b/corehq/apps/cloudcare/templates/cloudcare/partials/menu/badge.html
new file mode 100644
index 000000000000..5c9ed4b230c6
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/menu/badge.html
@@ -0,0 +1,11 @@
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/menu/breadcrumbs.html b/corehq/apps/cloudcare/templates/cloudcare/partials/menu/breadcrumbs.html
new file mode 100644
index 000000000000..4447ab2116cf
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/menu/breadcrumbs.html
@@ -0,0 +1,15 @@
+{% load i18n %}
+
+
+
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/menu/dropdown.html b/corehq/apps/cloudcare/templates/cloudcare/partials/menu/dropdown.html
new file mode 100644
index 000000000000..28be407acf86
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/menu/dropdown.html
@@ -0,0 +1,24 @@
+{% load i18n %}
+
+
+
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/menu/grid.html b/corehq/apps/cloudcare/templates/cloudcare/partials/menu/grid.html
new file mode 100644
index 000000000000..333474021597
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/menu/grid.html
@@ -0,0 +1,10 @@
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/menu/grid_item.html b/corehq/apps/cloudcare/templates/cloudcare/partials/menu/grid_item.html
new file mode 100644
index 000000000000..ed02feb25de9
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/menu/grid_item.html
@@ -0,0 +1,30 @@
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/menu/list.html b/corehq/apps/cloudcare/templates/cloudcare/partials/menu/list.html
new file mode 100644
index 000000000000..9a18181ff0b3
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/menu/list.html
@@ -0,0 +1,11 @@
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/menu/row.html b/corehq/apps/cloudcare/templates/cloudcare/partials/menu/row.html
new file mode 100644
index 000000000000..b8b7c5919b7d
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/menu/row.html
@@ -0,0 +1,21 @@
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/menu_list.html b/corehq/apps/cloudcare/templates/cloudcare/partials/menu_list.html
deleted file mode 100644
index 6160b9f4e7da..000000000000
--- a/corehq/apps/cloudcare/templates/cloudcare/partials/menu_list.html
+++ /dev/null
@@ -1,153 +0,0 @@
-{% load hq_shared_tags %}
-{% load i18n %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/query/group.html b/corehq/apps/cloudcare/templates/cloudcare/partials/query/group.html
new file mode 100644
index 000000000000..bfa9fad7b455
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/query/group.html
@@ -0,0 +1,16 @@
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/query_view.html b/corehq/apps/cloudcare/templates/cloudcare/partials/query/item.html
similarity index 65%
rename from corehq/apps/cloudcare/templates/cloudcare/partials/query_view.html
rename to corehq/apps/cloudcare/templates/cloudcare/partials/query/item.html
index 271b6971049c..3615d5eb7839 100644
--- a/corehq/apps/cloudcare/templates/cloudcare/partials/query_view.html
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/query/item.html
@@ -1,63 +1,6 @@
{% load hq_shared_tags %}
{% load i18n %}
-
-
-
-
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/sessions/item.html b/corehq/apps/cloudcare/templates/cloudcare/partials/sessions/item.html
new file mode 100644
index 000000000000..c88333db365e
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/sessions/item.html
@@ -0,0 +1,19 @@
+{% load hq_shared_tags %}
+
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/session_list.html b/corehq/apps/cloudcare/templates/cloudcare/partials/sessions/list.html
similarity index 59%
rename from corehq/apps/cloudcare/templates/cloudcare/partials/session_list.html
rename to corehq/apps/cloudcare/templates/cloudcare/partials/sessions/list.html
index d9f16c59b1cc..9922df8a008a 100644
--- a/corehq/apps/cloudcare/templates/cloudcare/partials/session_list.html
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/sessions/list.html
@@ -1,4 +1,3 @@
-{% load hq_shared_tags %}
{% load i18n %}
-
-
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/users.html b/corehq/apps/cloudcare/templates/cloudcare/partials/users.html
deleted file mode 100644
index 59b9158642b9..000000000000
--- a/corehq/apps/cloudcare/templates/cloudcare/partials/users.html
+++ /dev/null
@@ -1,91 +0,0 @@
-{% load hq_shared_tags %}
-{% load i18n %}
-
-
-
-
-
-
-
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/users/restore_as.html b/corehq/apps/cloudcare/templates/cloudcare/partials/users/restore_as.html
new file mode 100644
index 000000000000..a7100d207279
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/users/restore_as.html
@@ -0,0 +1,33 @@
+{% load hq_shared_tags %}
+{% load i18n %}
+
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/users/restore_as_banner.html b/corehq/apps/cloudcare/templates/cloudcare/partials/users/restore_as_banner.html
new file mode 100644
index 000000000000..723c895e3273
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/users/restore_as_banner.html
@@ -0,0 +1,11 @@
+{% load hq_shared_tags %}
+
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/users/user_data.html b/corehq/apps/cloudcare/templates/cloudcare/partials/users/user_data.html
new file mode 100644
index 000000000000..f490f1df49dd
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/users/user_data.html
@@ -0,0 +1,34 @@
+{% load i18n %}
+
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/users/user_row.html b/corehq/apps/cloudcare/templates/cloudcare/partials/users/user_row.html
new file mode 100644
index 000000000000..9856d20f4203
--- /dev/null
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/users/user_row.html
@@ -0,0 +1,14 @@
+
diff --git a/corehq/apps/cloudcare/templates/cloudcare/preview_app.html b/corehq/apps/cloudcare/templates/cloudcare/preview_app.html
index 08fcc0429e09..0a7fee53e023 100644
--- a/corehq/apps/cloudcare/templates/cloudcare/preview_app.html
+++ b/corehq/apps/cloudcare/templates/cloudcare/preview_app.html
@@ -81,12 +81,3 @@
{% endif %}
{% endblock %}
-
-{% block js %}{{ block.super }}
- {% compress js %}
-
-
-
-
- {% endcompress %}
-{% endblock %}
diff --git a/corehq/apps/cloudcare/templates/cloudcare/preview_app_base.html b/corehq/apps/cloudcare/templates/cloudcare/preview_app_base.html
index db3cc0f3abf5..0b5bcc023a69 100644
--- a/corehq/apps/cloudcare/templates/cloudcare/preview_app_base.html
+++ b/corehq/apps/cloudcare/templates/cloudcare/preview_app_base.html
@@ -1,6 +1,9 @@
{% load hq_shared_tags %}
{% load compress %}
{% load statici18n %}
+
+{% requirejs_main "cloudcare/js/preview_app/main" %}
+
@@ -43,42 +46,23 @@
- {% javascript_libraries use_bootstrap5=use_bootstrap5 underscore=True jquery_ui=True ko=True hq=True analytics=True %}
{# DO NOT COMPRESS #}
- {% compress js %}
-
-
- {% endcompress %}
-
- {% if request.use_daterangepicker and not requirejs_main %}
- {% compress js %}
-
-
-
- {% endcompress %}
- {% endif %}
+ {% include "hqwebapp/partials/requirejs.html" %}
+ {# This is fine as an inline script; it'll be removed once form designer is migrated to RequireJS #}
+
{% block body %}{% endblock %}
-{# HTML templates #}
-{% include 'cloudcare/partials/form_entry_templates.html' %}
-{% include 'cloudcare/partials/debugger.html' %}
-{% include 'cloudcare/partials/grid_view.html' %}
-{% include 'cloudcare/partials/settings_view.html' %}
-{% include 'cloudcare/partials/case_detail.html' %}
-{% include 'cloudcare/partials/case_list.html' %}
-{% include 'cloudcare/partials/menu_list.html' %}
-{% include 'cloudcare/partials/session_list.html' %}
{% include 'cloudcare/partials/confirmation_modal.html' %}
-{% include 'cloudcare/partials/users.html' %}
-{% include 'cloudcare/partials/progress.html' %}
-{% include 'cloudcare/partials/query_view.html' %}
+{% include 'cloudcare/partials/all_templates.html' %}
{% block js %}{{ block.super }}
{% include 'cloudcare/partials/dependencies.html' %}
diff --git a/corehq/apps/cloudcare/templates/cloudcare/spec/form_entry/mocha.html b/corehq/apps/cloudcare/templates/cloudcare/spec/form_entry/mocha.html
index 415d30018561..c99aa28c8c24 100644
--- a/corehq/apps/cloudcare/templates/cloudcare/spec/form_entry/mocha.html
+++ b/corehq/apps/cloudcare/templates/cloudcare/spec/form_entry/mocha.html
@@ -1,6 +1,8 @@
{% extends "mocha/base.html" %}
{% load hq_shared_tags %}
+{% requirejs_main "cloudcare/js/form_entry/spec/main" %}
+
{% block stylesheets %}{{ block.super }}
-{% endblock %}
-
-{% block mocha_tests %}
-
-
-
-
-
-
{% endblock %}
{% block fixtures %}
- {% include "cloudcare/partials/case_list.html" %}
{% endblock %}
diff --git a/corehq/apps/cloudcare/templates/cloudcare/spec/mocha.html b/corehq/apps/cloudcare/templates/cloudcare/spec/mocha.html
index f7318b7b9a02..547934d1b4f7 100644
--- a/corehq/apps/cloudcare/templates/cloudcare/spec/mocha.html
+++ b/corehq/apps/cloudcare/templates/cloudcare/spec/mocha.html
@@ -1,35 +1,13 @@
{% extends "mocha/base.html" %}
{% load hq_shared_tags %}
+{% requirejs_main "cloudcare/js/formplayer/spec/main" %}
+
{% block dependencies %}
{% include "cloudcare/partials/dependencies.html" %}
-
-
-
-
-
-
-{% endblock %}
-
-{% block mocha_tests %}
-
-
-
-
-
-
-
-
-
-
-
-
-
{% endblock %}
{% block fixtures %}
- {% include "cloudcare/partials/case_list.html" %}
-
@@ -37,17 +15,7 @@
- {% include 'cloudcare/partials/form_entry_templates.html' %}
- {% include 'cloudcare/partials/debugger.html' %}
- {% include 'cloudcare/partials/grid_view.html' %}
- {% include 'cloudcare/partials/settings_view.html' %}
- {% include 'cloudcare/partials/case_detail.html' %}
- {% include 'cloudcare/partials/case_list.html' %}
- {% include 'cloudcare/partials/menu_list.html' %}
- {% include 'cloudcare/partials/users.html' %}
- {% include 'cloudcare/partials/session_list.html' %}
- {% include 'cloudcare/partials/query_view.html' %}
{% include 'cloudcare/partials/confirmation_modal.html' %}
- {% include 'cloudcare/partials/progress.html' %}
+ {% include 'cloudcare/partials/all_templates.html' %}
{% endblock %}
diff --git a/corehq/apps/cloudcare/views.py b/corehq/apps/cloudcare/views.py
index 0aaa495cc47e..3f54c352a41a 100644
--- a/corehq/apps/cloudcare/views.py
+++ b/corehq/apps/cloudcare/views.py
@@ -71,7 +71,6 @@
from corehq.apps.hqwebapp.decorators import (
use_bootstrap5,
use_daterangepicker,
- use_jquery_ui,
waf_allow,
)
from corehq.apps.hqwebapp.templatetags.hq_shared_tags import can_use_restore_as
@@ -101,7 +100,6 @@ class FormplayerMain(View):
@xframe_options_sameorigin
@use_daterangepicker
- @use_jquery_ui
@method_decorator(require_cloudcare_access)
@method_decorator(requires_privilege_for_commcare_user(privileges.CLOUDCARE))
def dispatch(self, request, *args, **kwargs):
@@ -258,7 +256,6 @@ class FormplayerPreviewSingleApp(View):
urlname = 'formplayer_single_app'
- @use_jquery_ui
@method_decorator(require_cloudcare_access)
@method_decorator(requires_privilege_for_commcare_user(privileges.CLOUDCARE))
def dispatch(self, request, *args, **kwargs):
diff --git a/corehq/apps/domain/decorators.py b/corehq/apps/domain/decorators.py
index 9d360c2ff1b8..b09fbce9b77f 100644
--- a/corehq/apps/domain/decorators.py
+++ b/corehq/apps/domain/decorators.py
@@ -1,11 +1,12 @@
import logging
from functools import wraps
+from urllib.parse import urljoin
from django.conf import settings
from django.contrib import messages
-from django.http import (
+from django.http import HttpRequest
+from django.http.response import (
Http404,
- HttpRequest,
HttpResponse,
HttpResponseForbidden,
HttpResponseRedirect,
@@ -17,6 +18,7 @@
from django.utils.translation import gettext as _
from django.views import View
+from django_digest.decorators import httpdigest
from django_otp import match_token
from django_prbac.utils import has_privilege
from oauth2_provider.oauth2_backends import get_oauthlib_core
@@ -56,7 +58,6 @@
TWO_FACTOR_SUPERUSER_ROLLOUT,
)
from corehq.util.soft_assert import soft_assert
-from django_digest.decorators import httpdigest
auth_logger = logging.getLogger("commcare_auth")
@@ -85,7 +86,10 @@ def login_and_domain_required(view_func):
def _inner(req, domain, *args, **kwargs):
user = req.user
domain_name, domain_obj = load_domain(req, domain)
- def call_view(): return view_func(req, domain_name, *args, **kwargs)
+
+ def call_view():
+ return view_func(req, domain_name, *args, **kwargs)
+
if not domain_obj:
msg = _('The domain "{domain}" was not found.').format(domain=domain_name)
raise Http404(msg)
@@ -454,10 +458,10 @@ def _inner(request, domain, *args, **kwargs):
domain_obj = Domain.get_by_name(domain)
_ensure_request_couch_user(request)
if (
- not api_key and
- not getattr(request, 'skip_two_factor_check', False) and
- domain_obj and
- _two_factor_required(view_func, domain_obj, request)
+ not api_key
+ and not getattr(request, 'skip_two_factor_check', False)
+ and domain_obj
+ and _two_factor_required(view_func, domain_obj, request)
):
token = request.META.get('HTTP_X_COMMCAREHQ_OTP')
if not token and 'otp' in request.GET:
@@ -667,6 +671,13 @@ def _inner(request, *args, **kwargs):
def check_domain_migration(view_func):
def wrapped_view(request, domain, *args, **kwargs):
+ domain_obj = Domain.get_by_name(domain)
+ if domain_obj.redirect_url:
+ # IMPORTANT!
+ # We assume that the domain name is the same on both
+ # environments.
+ url = urljoin(domain_obj.redirect_url, request.path)
+ return HttpResponseRedirect(url)
if DATA_MIGRATION.enabled(domain):
auth_logger.info(
"Request rejected domain=%s reason=%s request=%s",
diff --git a/corehq/apps/domain/management/commands/redirect_url.py b/corehq/apps/domain/management/commands/redirect_url.py
new file mode 100644
index 000000000000..7178716bef21
--- /dev/null
+++ b/corehq/apps/domain/management/commands/redirect_url.py
@@ -0,0 +1,67 @@
+from django.core.exceptions import ValidationError
+from django.core.management.base import BaseCommand, CommandError
+from django.core.validators import URLValidator
+
+from corehq.apps.domain.models import Domain
+from corehq.toggles import DATA_MIGRATION
+
+
+class Command(BaseCommand):
+ help = (
+ 'Sets the redirect URL for a "308 Permanent Redirect" response to '
+ 'form submissions and syncs. Only valid for domains that have been '
+ 'migrated to new environments. Set the schema and hostname only '
+ '(e.g. "https://example.com/"). The rest of the path will be appended '
+ 'for redirecting different endpoints. THIS FEATURE ASSUMES THE DOMAIN '
+ 'NAME IS THE SAME ON BOTH ENVIRONMENTS.'
+ )
+
+ def add_arguments(self, parser):
+ parser.add_argument('domain')
+ parser.add_argument(
+ '--set',
+ help="The URL to redirect to",
+ )
+ parser.add_argument(
+ '--unset',
+ help="Remove the current redirect",
+ action='store_true',
+ )
+
+ def handle(self, domain, **options):
+ domain_obj = Domain.get_by_name(domain)
+
+ if options['set']:
+ _assert_data_migration(domain)
+ url = options['set']
+ _assert_valid_url(url)
+ domain_obj.redirect_url = url
+ domain_obj.save()
+
+ elif options['unset']:
+ domain_obj.redirect_url = ''
+ domain_obj.save()
+
+ if domain_obj.redirect_url:
+ self.stdout.write(
+ 'Form submissions and syncs are redirected to '
+ f'{domain_obj.redirect_url}'
+ )
+ else:
+ self.stdout.write('Redirect URL not set')
+
+
+def _assert_data_migration(domain):
+ if not DATA_MIGRATION.enabled(domain):
+ raise CommandError(f'Domain {domain} is not migrated.')
+
+
+def _assert_valid_url(url):
+ if not url.startswith('https'):
+ raise CommandError(f'{url} is not a secure URL.')
+
+ validate = URLValidator()
+ try:
+ validate(url)
+ except ValidationError:
+ raise CommandError(f'{url} is not a valid URL.')
diff --git a/corehq/apps/domain/models.py b/corehq/apps/domain/models.py
index 8db23f1bbc30..4aa6f541d84e 100644
--- a/corehq/apps/domain/models.py
+++ b/corehq/apps/domain/models.py
@@ -449,6 +449,9 @@ class Domain(QuickCachedDocumentMixin, BlobMixin, Document, SnapshotMixin):
ga_opt_out = BooleanProperty(default=False)
orphan_case_alerts_warning = BooleanProperty(default=False)
+ # For domains that have been migrated to a different environment
+ redirect_url = StringProperty()
+
@classmethod
def wrap(cls, data):
# for domains that still use original_doc
diff --git a/corehq/apps/domain/tests/test_deletion_models.py b/corehq/apps/domain/tests/test_deletion_models.py
index b6014d93d612..02cd1e1919e1 100644
--- a/corehq/apps/domain/tests/test_deletion_models.py
+++ b/corehq/apps/domain/tests/test_deletion_models.py
@@ -43,6 +43,7 @@
IGNORE_MODELS = {
'api.ApiUser',
+ 'app_execution.AppWorkflowConfig',
'app_manager.ExchangeApplication',
'auth.Group',
'auth.Permission',
diff --git a/corehq/apps/domain/tests/test_redirect_url.py b/corehq/apps/domain/tests/test_redirect_url.py
new file mode 100644
index 000000000000..d14cfa169ba9
--- /dev/null
+++ b/corehq/apps/domain/tests/test_redirect_url.py
@@ -0,0 +1,185 @@
+from contextlib import contextmanager
+from io import StringIO
+
+from django.core.management import call_command
+from django.core.management.base import CommandError
+from django.test import TestCase
+from django.test.client import Client
+from django.urls import reverse
+
+from corehq.apps.domain.models import Domain
+from corehq.apps.domain.shortcuts import create_domain
+from corehq.apps.users.models import WebUser
+from corehq.const import OPENROSA_VERSION_2
+from corehq.middleware import OPENROSA_VERSION_HEADER
+from corehq.util.test_utils import flag_enabled
+
+DOMAIN = 'test-redirect-url'
+
+
+class TestRedirectUrlCommand(TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.domain_obj = create_domain(DOMAIN)
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.domain_obj.delete()
+ super().tearDownClass()
+
+ def test_data_migration_not_enabled(self):
+ with self.assertRaisesRegex(
+ CommandError,
+ r'^Domain test\-redirect\-url is not migrated\.$'
+ ):
+ self._call_redirect_url('--set', 'https://example.com/')
+
+ @flag_enabled('DATA_MIGRATION')
+ def test_no_https(self):
+ with self.assertRaisesRegex(
+ CommandError,
+ r'^http://example.com/ is not a secure URL\.$'
+ ):
+ self._call_redirect_url('--set', 'http://example.com/')
+
+ @flag_enabled('DATA_MIGRATION')
+ def test_bad_url(self):
+ with self.assertRaisesRegex(
+ CommandError,
+ r'^https://example/ is not a valid URL\.$'
+ ):
+ self._call_redirect_url('--set', 'https://example/')
+
+ @flag_enabled('DATA_MIGRATION')
+ def test_set_url(self):
+ stdout = self._call_redirect_url('--set', 'https://example.com/')
+ self.assertEqual(
+ stdout,
+ 'Form submissions and syncs are redirected to '
+ 'https://example.com/\n'
+ )
+
+ def test_unset_url_data_migration_not_enabled(self):
+ with _set_redirect_url():
+ stdout = self._call_redirect_url('--unset')
+ self.assertEqual(stdout, 'Redirect URL not set\n')
+
+ @flag_enabled('DATA_MIGRATION')
+ def test_unset_url_migration_enabled(self):
+ with _set_redirect_url():
+ stdout = self._call_redirect_url('--unset')
+ self.assertEqual(stdout, 'Redirect URL not set\n')
+
+ def test_return_set_url_data_migration_not_enabled(self):
+ with _set_redirect_url():
+ stdout = self._call_redirect_url()
+ self.assertEqual(
+ stdout,
+ 'Form submissions and syncs are redirected to '
+ 'https://example.com/\n'
+ )
+
+ @flag_enabled('DATA_MIGRATION')
+ def test_return_set_url_migration_enabled(self):
+ with _set_redirect_url():
+ stdout = self._call_redirect_url()
+ self.assertEqual(
+ stdout,
+ 'Form submissions and syncs are redirected to '
+ 'https://example.com/\n'
+ )
+
+ def test_return_unset_url_data_migration_not_enabled(self):
+ stdout = self._call_redirect_url()
+ self.assertEqual(stdout, 'Redirect URL not set\n')
+
+ @flag_enabled('DATA_MIGRATION')
+ def test_return_unset_url_migration_enabled(self):
+ stdout = self._call_redirect_url()
+ self.assertEqual(stdout, 'Redirect URL not set\n')
+
+ @staticmethod
+ def _call_redirect_url(*args, **kwargs):
+ stdout = StringIO()
+ call_command(
+ 'redirect_url', DOMAIN, *args,
+ stdout=stdout, **kwargs,
+ )
+ return stdout.getvalue()
+
+
+class TestCheckDomainMigration(TestCase):
+ """
+ Tests the ``receiver_post`` view, which is wrapped with the
+ ``corehq.apps.domain.decorators.check_domain_migration`` decorator.
+
+ All relevant views are protected by that decorator during and after
+ data migration. These tests verify that the decorator returns a 302
+ redirect response when the domain's ``redirect_url`` is set, and a
+ 503 service unavailable response when it is not set.
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.domain_obj = create_domain(DOMAIN)
+ cls.user = WebUser.create(
+ None, 'admin', 'Passw0rd!',
+ None, None,
+ )
+ cls.user.add_domain_membership(DOMAIN, is_admin=True)
+ cls.user.save()
+ cls.client = Client()
+ cls.client.login(username='admin', password='Passw0rd!')
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.user.delete(DOMAIN, deleted_by=None)
+ cls.domain_obj.delete()
+ super().tearDownClass()
+
+ @flag_enabled('DATA_MIGRATION')
+ def test_redirect_response(self):
+ with _set_redirect_url():
+ response = self._submit_form()
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(
+ response.url,
+ 'https://example.com/a/test-redirect-url/receiver/'
+ )
+
+ @flag_enabled('DATA_MIGRATION')
+ def test_service_unavailable_response(self):
+ response = self._submit_form()
+ self.assertEqual(response.status_code, 503)
+ self.assertEqual(
+ response.content.decode('utf-8'),
+ 'Service Temporarily Unavailable',
+ )
+
+ def _submit_form(self):
+ form = """
+
+ """
+ with StringIO(form) as f:
+ response = self.client.post(
+ reverse("receiver_post", args=[DOMAIN]),
+ {"xml_submission_file": f},
+ **{OPENROSA_VERSION_HEADER: OPENROSA_VERSION_2}
+ )
+ return response
+
+
+@contextmanager
+def _set_redirect_url():
+ domain_obj = Domain.get_by_name(DOMAIN)
+ domain_obj.redirect_url = 'https://example.com/'
+ domain_obj.save()
+ try:
+ yield
+ finally:
+ domain_obj = Domain.get_by_name(DOMAIN)
+ domain_obj.redirect_url = ''
+ domain_obj.save()
diff --git a/corehq/apps/dump_reload/sql/serialization.py b/corehq/apps/dump_reload/sql/serialization.py
index 7547f1c03ffc..75da0516c1db 100644
--- a/corehq/apps/dump_reload/sql/serialization.py
+++ b/corehq/apps/dump_reload/sql/serialization.py
@@ -10,12 +10,19 @@ class JsonLinesSerializer(JsonSerializer):
"""
Convert a queryset to JSON outputting one object per line
"""
+
def start_serialization(self):
self._init_options()
def end_serialization(self):
pass
+ def get_dump_object(self, obj):
+ dumped_obj = super().get_dump_object(obj)
+ if hasattr(obj, 'encrypted_fields'):
+ dumped_obj['fields'].update(obj.encrypted_fields)
+ return dumped_obj
+
def end_object(self, obj):
# self._current has the field data
json_kwargs = copy(self.json_kwargs)
diff --git a/corehq/apps/dump_reload/tests/test_dump_models.py b/corehq/apps/dump_reload/tests/test_dump_models.py
index 12b819b95734..8b131610ef96 100644
--- a/corehq/apps/dump_reload/tests/test_dump_models.py
+++ b/corehq/apps/dump_reload/tests/test_dump_models.py
@@ -42,6 +42,7 @@
"analytics.PartnerAnalyticsDataPoint",
"analytics.PartnerAnalyticsReport",
"api.ApiUser",
+ "app_execution.AppWorkflowConfig",
"app_manager.ExchangeApplication",
"auth.Group",
"auth.Permission",
diff --git a/corehq/apps/dump_reload/tests/test_serialization.py b/corehq/apps/dump_reload/tests/test_serialization.py
index ea78c5b82db4..e408999f9f3d 100644
--- a/corehq/apps/dump_reload/tests/test_serialization.py
+++ b/corehq/apps/dump_reload/tests/test_serialization.py
@@ -12,6 +12,8 @@
from corehq.form_processor.models.cases import CaseTransaction, CommCareCase
from corehq.form_processor.models.forms import XFormInstance, XFormOperation
from corehq.apps.registry.models import DataRegistry, RegistryGrant
+from corehq.motech.models import ConnectionSettings
+from corehq.motech.const import PASSWORD_PLACEHOLDER
class TestJSONFieldSerialization(SimpleTestCase):
@@ -110,3 +112,19 @@ def test_natural_foreign_key_for_XFormInstance_returns_str_when_serialized(self)
deserialized_model = json.loads(output_stream.getvalue())
fk_field = deserialized_model['fields']['form']
self.assertEqual(fk_field, 'abc123')
+
+
+class TestEncryptedFieldSerialization(SimpleTestCase):
+
+ def test_encrypted_fields_are_reset_on_model_that_provides_encrypted_fields(self):
+ test_data = ConnectionSettings(
+ domain="test", name="test-connection-settings", url="http://www.example.com", password='abc123'
+ )
+
+ output_stream = StringIO()
+ with patch('corehq.apps.dump_reload.sql.dump.get_objects_to_dump', return_value=[test_data]):
+ SqlDataDumper('test', [], []).dump(output_stream)
+
+ deserialized_model = json.loads(output_stream.getvalue())
+ self.assertEqual(deserialized_model['fields']['password'], PASSWORD_PLACEHOLDER)
+ self.assertEqual(deserialized_model['fields']['name'], 'test-connection-settings')
diff --git a/corehq/apps/es/client.py b/corehq/apps/es/client.py
index f76e20ca5058..8b162b32f2ac 100644
--- a/corehq/apps/es/client.py
+++ b/corehq/apps/es/client.py
@@ -21,6 +21,7 @@
TransportError,
bulk,
)
+from corehq.toggles import ES_QUERY_PREFERENCE
from corehq.util.global_request import get_request_domain
from corehq.util.metrics import (
limit_domains,
@@ -644,12 +645,19 @@ def _search(self, query, **kw):
"""Perform a "low-level" search and return the raw result. This is
split into a separate method for ease of testing the result format.
"""
+ domain = get_request_domain()
+ if ES_QUERY_PREFERENCE.enabled(domain):
+ # Use domain as key to route to a consistent set of shards.kwargs
+ # See https://www.elastic.co/guide/en/elasticsearch/reference/5.6/search-request-preference.html
+ if 'preference' not in kw:
+ kw['preference'] = domain
+
with metrics_histogram_timer(
'commcare.elasticsearch.search.timing',
timing_buckets=(1, 10),
tags={
'index': self.canonical_name,
- 'domain': limit_domains(get_request_domain()),
+ 'domain': limit_domains(domain),
},
):
return self._es.search(self.index_name, self.type, query, **kw)
diff --git a/corehq/apps/hqadmin/urls.py b/corehq/apps/hqadmin/urls.py
index d9066017946f..546ada3c4b6f 100644
--- a/corehq/apps/hqadmin/urls.py
+++ b/corehq/apps/hqadmin/urls.py
@@ -68,5 +68,6 @@
url(r'^reprocess_messaging_case_updates/$', ReprocessMessagingCaseUpdatesView.as_view(),
name=ReprocessMessagingCaseUpdatesView.urlname),
url(r'^web_user_data', WebUserDataView.as_view(), name=WebUserDataView.urlname),
+ url(r'workflows/', include("corehq.apps.app_execution.urls")),
AdminReportDispatcher.url_pattern(),
]
diff --git a/corehq/apps/hqwebapp/management/commands/build_bootstrap5_diffs.py b/corehq/apps/hqwebapp/management/commands/build_bootstrap5_diffs.py
index 97de5909801f..13b4b8f7c229 100644
--- a/corehq/apps/hqwebapp/management/commands/build_bootstrap5_diffs.py
+++ b/corehq/apps/hqwebapp/management/commands/build_bootstrap5_diffs.py
@@ -1,6 +1,8 @@
import difflib
import json
+import os
from pathlib import Path
+import re
import shutil
from django.core.management import BaseCommand
@@ -79,10 +81,14 @@ def get_bootstrap5_filepaths(full_diff_config):
path_bootstrap5 = COREHQ_BASE_DIR / parent_path / directory_bootstrap5
if compare_all_files:
- migrated_files = [
- [x.name, x.name] for x in path_bootstrap3.glob('**/*')
- if x.is_file() and not x.name.startswith(".")
- ]
+ migrated_files = []
+ for path in path_bootstrap3.glob('**/*'):
+ if path.is_file() and not path.name.startswith("."):
+ path = os.path.relpath(path, path_bootstrap3)
+ pair = [path, path]
+ if file_type == "stylesheet":
+ pair[1] = re.sub(r'\.less$', '.scss', pair[1])
+ migrated_files.append(pair)
for filename_bootstrap3, filename_bootstrap5 in migrated_files:
diff_filename = get_diff_filename(filename_bootstrap3, filename_bootstrap5, file_type)
diff --git a/corehq/apps/hqwebapp/management/commands/build_requirejs.py b/corehq/apps/hqwebapp/management/commands/build_requirejs.py
index 964223e66f8a..ad047c7eba41 100644
--- a/corehq/apps/hqwebapp/management/commands/build_requirejs.py
+++ b/corehq/apps/hqwebapp/management/commands/build_requirejs.py
@@ -254,29 +254,28 @@ def _copy_modules_back_into_corehq(self, config, local_js_dirs):
else:
logger.warning("Could not copy {} to {}".format(os.path.relpath(src), os.path.relpath(dest)))
- def _update_resource_hash(self, name, filename):
- file_hash = self.get_hash(filename)
+ def _update_resource_hash(self, name, filename, file_hash=None):
+ if file_hash is None:
+ file_hash = self.get_hash(filename)
self.resource_versions[name] = file_hash
- return file_hash
# Overwrite source map references. Source maps are accessed on the CDN, so they need the version hash
def _update_source_maps(self, config):
- if not self.optimize:
- return
-
logger.info("Updating resource_versions with hashes from newly minified bundles")
for module in config['modules']:
filename = self._staticfiles_path(module['name'] + ".js")
- with open(filename, 'r') as fin:
- lines = fin.readlines()
- with open(filename, 'w') as fout:
- for line in lines:
- match = re.search(BUNDLE_SOURCE_MAP_PATTERN, line)
- if match:
- file_hash = self._update_resource_hash(module['name'] + ".js", filename)
- line += f'?version={file_hash}'
- fout.write(line)
+ file_hash = self.get_hash(filename)
+ if self.optimize:
+ with open(filename, 'r') as fin:
+ lines = fin.readlines()
+ with open(filename, 'w') as fout:
+ for line in lines:
+ match = re.search(BUNDLE_SOURCE_MAP_PATTERN, line)
+ if match:
+ line += f'?version={file_hash}'
+ fout.write(line)
+ self._update_resource_hash(module['name'] + ".js", filename, file_hash=file_hash)
def _write_resource_versions(self):
logger.info("Writing out resource_versions.js")
diff --git a/corehq/apps/hqwebapp/session_details_endpoint/tests.py b/corehq/apps/hqwebapp/session_details_endpoint/tests.py
index 32d3ec308efd..d3eeb35c1ed5 100644
--- a/corehq/apps/hqwebapp/session_details_endpoint/tests.py
+++ b/corehq/apps/hqwebapp/session_details_endpoint/tests.py
@@ -3,7 +3,7 @@
import pytz
from django.conf import settings
-from django.test import Client, TestCase, override_settings
+from django.test import Client, TestCase
from django.urls import reverse
from django.utils.dateparse import parse_datetime
@@ -19,9 +19,10 @@ class SessionDetailsViewTest(TestCase):
@classmethod
def setUpClass(cls):
super(SessionDetailsViewTest, cls).setUpClass()
- cls.domain = Domain(name="toyland", is_active=True)
- cls.domain.save()
+ cls.domain = Domain.get_or_create_with_name('toyland', is_active=True)
+ cls.addClassCleanup(cls.domain.delete)
cls.couch_user = CommCareUser.create(cls.domain.name, 'bunkey', '123', None, None)
+ cls.addClassCleanup(cls.couch_user.delete, cls.domain.name, deleted_by=None)
cls.sql_user = cls.couch_user.get_django_user()
cls.expected_response = {
@@ -44,12 +45,7 @@ def setUp(self):
self.session = self.client.session
self.session.save()
self.session_key = self.session.session_key
-
- @classmethod
- def tearDownClass(cls):
- cls.couch_user.delete(cls.domain.name, deleted_by=None)
- cls.domain.delete()
- super(SessionDetailsViewTest, cls).tearDownClass()
+ self.expected_response['authToken'] = self.session_key
def _assert_session_expiry_in_minutes(self, expected_minutes, actual_time_string):
delta = parse_datetime(actual_time_string) - datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
diff --git a/corehq/apps/hqwebapp/session_details_endpoint/views.py b/corehq/apps/hqwebapp/session_details_endpoint/views.py
index ddaa22fcfbce..683d3b79dfad 100644
--- a/corehq/apps/hqwebapp/session_details_endpoint/views.py
+++ b/corehq/apps/hqwebapp/session_details_endpoint/views.py
@@ -87,7 +87,7 @@ def post(self, request, *args, **kwargs):
'username': user.username,
'djangoUserId': user.pk,
'superUser': user.is_superuser,
- 'authToken': None,
+ 'authToken': session_id,
'domains': list(domains),
'anonymous': False,
'enabled_toggles': list(enabled_toggles),
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/debugger/debugger.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/debugger/debugger.scss
new file mode 100644
index 000000000000..731952bbeede
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/debugger/debugger.scss
@@ -0,0 +1,76 @@
+.debugger {
+ width: 800px;
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ z-index: @zindex-cloudcare-debugger;
+ background-color: white;
+ overflow: hidden;
+
+ -webkit-transition: height 0.5s;
+ transition: height 0.5s;
+}
+
+.debugger .debugger-content {
+ overflow-y: auto;
+ margin-top: 12px;
+ padding: 10px;
+ padding-bottom: 0;
+}
+
+.debugger-updating {
+ #debugger-form-data, #debugger-xml-instance {
+ opacity: 0.5;
+ }
+}
+
+
+.debugger.debugger-minimized {
+ height: 30px;
+ overflow-y: hidden;
+}
+
+.debugger.debugger-maximized {
+ height: 400px;
+}
+
+.debugger .debugger-state {
+ cursor: pointer;
+}
+
+.debugger-code {
+ font-family: monospace;
+}
+
+.debugger-navbar {
+ border-radius: 0px;
+ border: none;
+ background-color: @cc-bg;
+}
+
+.debugger-tab-title {
+ line-height: 18px;
+ color: white;
+ background-color: @cc-brand-low;
+ border-bottom: #ddd solid 4px;
+ border-top: 0;
+ border-left: 0;
+ border-right: 0;
+ padding: 6px;
+ cursor: pointer;
+ .debugger-title {
+ text-transform: uppercase;
+ font-size: 11px;
+ }
+
+ &:hover {
+ background-color: darken(@cc-brand-low, 5);
+ }
+
+ -webkit-transition: background-color 0.5s;
+ transition: background-color 0.5s;
+}
+
+.debugger .query-container:hover {
+ cursor: pointer;
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/font-formplayer.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/font-formplayer.scss
new file mode 100644
index 000000000000..233c5d03cc0e
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/font-formplayer.scss
@@ -0,0 +1,6 @@
+// Based on the Structure of FontAwesome
+
+@import "font-formplayer/variables.less";
+@import "font-formplayer/path.less";
+@import "font-formplayer/core.less";
+@import "font-formplayer/icons.less";
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/font-formplayer/core.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/font-formplayer/core.scss
new file mode 100644
index 000000000000..edb1454e83c4
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/font-formplayer/core.scss
@@ -0,0 +1,9 @@
+.@{css-prefix} {
+ display: inline-block;
+ font-family: @font-family;
+ font-style: normal;
+ font-weight: normal;
+ line-height: 1;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/font-formplayer/icons.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/font-formplayer/icons.scss
new file mode 100644
index 000000000000..f7af76a0f169
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/font-formplayer/icons.scss
@@ -0,0 +1,13 @@
+.@{css-prefix}-start-full:before { content: @var-start-full; }
+.@{css-prefix}-start-fg:before { content: @var-start-fg; }
+.@{css-prefix}-start-bg:before { content: @var-start-bg; }
+
+.@{css-prefix}-saved-full:before { content: @var-saved-full; }
+.@{css-prefix}-saved-fg:before { content: @var-saved-fg; }
+.@{css-prefix}-saved-bg:before { content: @var-saved-bg; }
+
+.@{css-prefix}-sync:before { content: @var-sync; }
+
+.@{css-prefix}-incomplete-full:before { content: @var-incomplete-full; }
+.@{css-prefix}-incomplete-fg:before { content: @var-incomplete-fg; }
+.@{css-prefix}-incomplete-bg:before { content: @var-incomplete-bg; }
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/font-formplayer/path.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/font-formplayer/path.scss
new file mode 100644
index 000000000000..eb0ccd278cfb
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/font-formplayer/path.scss
@@ -0,0 +1,13 @@
+/* FONT PATH
+ * -------------------------- */
+
+@font-face {
+ font-family: @font-family;
+ src: ~"url('@{font-path}/@{font-name}.eot?v=@{version}')";
+ src: ~"url('@{font-path}/@{font-name}.eot?#iefix&v=@{version}') format('embedded-opentype')",
+ ~"url('@{font-path}/@{font-name}.woff?v=@{version}') format('woff')",
+ ~"url('@{font-path}/@{font-name}.ttf?v=@{version}') format('truetype')",
+ ~"url('@{font-path}/@{font-name}.svg?v=@{version}#@{font-family}') format('svg')";
+ font-weight: normal;
+ font-style: normal;
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/font-formplayer/variables.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/font-formplayer/variables.scss
new file mode 100644
index 000000000000..7261dfefe1fc
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/font-formplayer/variables.scss
@@ -0,0 +1,17 @@
+@font-path: "/static/cloudcare/font";
+@css-prefix: ff;
+@inverse: #ffffff;
+@font-family: 'FormplayerFontRegular';
+@font-name: 'formplayerfont-regular';
+@version: '1.1';
+
+@var-start-full: "\f000";
+@var-start-fg: "\f001";
+@var-start-bg: "\f002";
+@var-saved-full: "\f003";
+@var-saved-fg: "\f004";
+@var-saved-bg: "\f005";
+@var-sync: "\f006";
+@var-incomplete-full: "\f007";
+@var-incomplete-fg: "\f008";
+@var-incomplete-bg: "\f009";
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common-main.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common-main.scss
new file mode 100644
index 000000000000..8cdfa45726ca
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common-main.scss
@@ -0,0 +1,27 @@
+@import '../../hqwebapp/less/_hq/includes/variables.less';
+@import '../../hqwebapp/less/_hq/includes/mixins.less';
+
+/**
+These are the styles shared across both PREVIEW and the FULL PAGE WEB APP
+version of FORMPLAYER
+**/
+
+@import "formplayer-common/mixins";
+
+@import "formplayer-common/navigation";
+@import "formplayer-common/notifications";
+@import "formplayer-common/grid";
+@import "formplayer-common/breadcrumbs";
+@import "formplayer-common/paginate";
+@import "formplayer-common/appicon";
+@import "formplayer-common/case";
+@import "formplayer-common/request";
+@import "formplayer-common/module";
+@import "formplayer-common/version";
+@import "formplayer-common/form";
+@import "formplayer-common/formnav";
+@import "formplayer-common/markdown-table";
+@import "formplayer-common/address";
+@import "formplayer-common/webforms";
+
+@import "debugger/debugger";
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common.scss
new file mode 100644
index 000000000000..fe43084c5bee
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common.scss
@@ -0,0 +1,4 @@
+@b3-import-variables: '../../../../../../../../node_modules/bootstrap/less/variables';
+@b3-import-mixins: '../../../../../../../../node_modules/bootstrap/less/mixins';
+
+@import "formplayer-common-main";
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/address.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/address.scss
new file mode 100644
index 000000000000..6e3986511261
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/address.scss
@@ -0,0 +1,6 @@
+// Overwrite mapboxgl geocoder library styling.
+.widget .mapboxgl-ctrl-geocoder {
+ max-width: 30000px;
+ width: 100%;
+ min-width: 30px;
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/appicon.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/appicon.scss
new file mode 100644
index 000000000000..1c573600b903
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/appicon.scss
@@ -0,0 +1,203 @@
+@appicon-height-xs: 150px;
+@appicon-icon-height-xs: @appicon-height-xs - 30px;
+
+
+.make-appicon-size(@container-height, @icon-height, @title-height, @title-size) {
+ .appicon {
+ min-height: @container-height;
+
+ .appicon-title {
+ height: @title-height;
+ bottom: @container-height * 0.05;
+ h3 {
+ font-size: @title-size;
+ padding: 0 @title-size/2;
+ }
+ }
+ .appicon-icon {
+ top: 0.05*@icon-height;
+ margin-left: -@icon-height/2;
+ width: @icon-height;
+ height: @icon-height;
+ line-height: @icon-height;
+ font-size: @icon-height;
+ &[class*='fa-'] {
+ font-size: @icon-height * .8;
+ }
+ }
+ .appicon-custom {
+ // the default icons have internal padding in the image itself.
+ // this prevents users from needing to replicate that
+ top: 0.05*@icon-height + 10;
+ height: @icon-height - 10;
+ }
+ }
+ .appicon-default .appicon-icon {
+ font-size: @icon-height * .9;
+ }
+}
+
+.make-gridicon-size(@icon-height) {
+ .gridicon {
+ width: @icon-height;
+ height: @icon-height;
+ margin: 10% auto;
+
+ .gridicon-icon {
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ max-width: 100%;
+ max-height: 100%;
+ line-height: @icon-height;
+ font-size: @icon-height / 2;
+ }
+ }
+ .gridicon-circle {
+ border-radius: 2 * @icon-height;
+ }
+}
+
+.appicon, .gridicon {
+ background-color: @cc-neutral-mid;
+ text-align: center;
+ color: white;
+ position: relative;
+ box-sizing: border-box;
+ margin-bottom: 15px;
+
+ .border-top-radius(5px);
+ .border-bottom-radius(5px);
+ .transition(background 1s);
+ .box-shadow(0 0 5px 0 rgba(0, 0, 0, 0.45));
+
+ .appicon-title {
+ width: 100%;
+ position: absolute;
+ text-overflow: ellipsis;
+ h3 {
+ text-overflow: ellipsis;
+ margin: 0;
+ height: 100%;
+ overflow: hidden;
+ font-weight: 300;
+ }
+ }
+
+ .appicon-custom {
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: center;
+ }
+
+ .appicon-icon, .gridicon-icon {
+ position: absolute;
+ box-sizing: border-box;
+ top: 0;
+ left: 50%;
+ .transition(color 1s);
+ }
+
+ &:hover {
+ .box-shadow(0 0 2x 3px rgba(0, 0, 0, 0.45));
+ }
+}
+
+.gridicon {
+ background-size: contain;
+ background-color: transparent;
+ background-repeat: no-repeat;
+ background-position: 50%;
+ // For icons with their own image, don't make assumptions about the shadowing
+ .box-shadow(0 0 0 0 rgba(0, 0, 0, 0));
+}
+
+.gridicon.gridicon-circle {
+ background-color: @cc-neutral-mid;
+ .box-shadow(0 0 5px 0 rgba(0, 0, 0, 0.45));
+}
+
+.make-appicon-size(170px, 115px, 35px, 16px);
+.make-gridicon-size(70px);
+
+@media(min-width: @screen-xs) {
+ .make-appicon-size(200px, 135px, 35px, 16px);
+ .make-gridicon-size(60px);
+}
+
+@media(min-width: @screen-sm) {
+ .make-appicon-size(210px, 140px, 40px, 18px);
+ .make-gridicon-size(70px);
+}
+
+@media(min-width: @screen-md) {
+ .make-appicon-size(230px, 150px, 45px, 20px);
+ .make-gridicon-size(130px);
+}
+
+.appicon-start {
+ background-color: @cc-att-pos-mid;
+ .appicon-icon-fg { color: @cc-att-pos-hi; }
+ .appicon-icon-bg { color: @cc-att-pos-low; }
+ &:hover {
+ background-color: darken(@cc-att-pos-mid, 5);
+ .appicon-icon-fg { color: lighten(@cc-att-pos-hi, 5); }
+ .appicon-icon-bg { color: darken(@cc-att-pos-low, 10); }
+ }
+}
+
+.appicon-incomplete {
+ background-color: @cc-dark-warm-accent-mid;
+ .appicon-icon-fg { color: @cc-dark-warm-accent-hi; }
+ .appicon-icon-bg { color: @cc-dark-warm-accent-low; }
+ &:hover {
+ background-color: darken(@cc-dark-warm-accent-mid, 7);
+ .appicon-icon-fg { color: lighten(@cc-dark-warm-accent-hi, 5); }
+ .appicon-icon-bg { color: darken(@cc-dark-warm-accent-low, 10); }
+ }
+}
+
+.appicon-sync {
+ background-color: @cc-brand-mid;
+ .appicon-icon {
+ top: 2px;
+ color: @cc-brand-hi;
+ }
+ &:hover {
+ background-color: darken(@cc-brand-mid, 10);
+ .appicon-icon { color: lighten(@cc-brand-hi, 5); }
+ }
+}
+
+.appicon-restore-as {
+ background-color: @cc-dark-cool-accent-mid;
+ .appicon-icon {
+ color: @cc-dark-cool-accent-hi;
+ }
+ &:hover {
+ background-color: darken(@cc-dark-cool-accent-mid, 5);
+ .appicon-icon { color: lighten(@cc-dark-cool-accent-hi, 5); }
+ }
+}
+
+.appicon-settings {
+ background-color: @cc-neutral-low;
+ .appicon-icon {
+ color: @cc-neutral-hi;
+ }
+ &:hover {
+ background-color: darken(@cc-neutral-low, 5);
+ .appicon-icon { color: lighten(@cc-neutral-hi, 5); }
+ }
+}
+
+.appicon-default {
+ background-color: white;
+ color: lighten(@cc-text, 30);
+ .box-shadow(0 0 10px 0 rgba(0, 0, 0, 0.2));
+ .appicon-icon { color: @cc-brand-mid; }
+ &:hover {
+ background-color: darken(white, 5);
+ }
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/breadcrumbs.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/breadcrumbs.scss
new file mode 100644
index 000000000000..52eb1bf27b33
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/breadcrumbs.scss
@@ -0,0 +1,37 @@
+#breadcrumb-region {
+ position: sticky;
+ position: -webkit-sticky;
+ top: 0;
+ z-index: @zindex-navbar-cloudcare;
+}
+
+#breadcrumb-region .breadcrumb-text, .single-app-view .breadcrumb-text {
+ cursor: pointer;
+}
+
+#breadcrumb-region .breadcrumb,
+.single-app-view .breadcrumb,
+.breadcrumb-form-container .breadcrumb {
+ background-color: @cc-brand-mid;
+ color: white;
+ .border-top-radius(0);
+ .border-bottom-radius(0);
+ .box-shadow(0 0 5px 2px rgba(0,0,0,.3));
+ border: none;
+
+ .breadcrumb-text {
+ &:before {
+ content: '\f054';
+ font-family: 'FontAwesome';
+ }
+ }
+ .breadcrumb-text:first-child {
+ &:before {
+ display: none;
+ }
+ }
+}
+
+#breadcrumb__menu-dropdown {
+ cursor: pointer;
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/case.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/case.scss
new file mode 100644
index 000000000000..b4af848cdff2
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/case.scss
@@ -0,0 +1,9 @@
+#case-crumbs > .breadcrumb > .active {
+ font-weight: 600;
+ color: inherit;
+}
+
+#case-details > div > .breadcrumb > .active {
+ font-weight: 600;
+ color: inherit;
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/form.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/form.scss
new file mode 100644
index 000000000000..2d3004ec149a
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/form.scss
@@ -0,0 +1,43 @@
+.hint-text {
+ font-size: 80%;
+ font-style: italic;
+}
+
+.help-popover, .help-popover:hover, .help-popover:focus {
+ text-decoration: none;
+ outline: 0;
+}
+
+.unsupported-question-type-trigger {
+ float: right;
+ margin-bottom: 5px;
+ margin-left: 5px;
+}
+
+.help-text-trigger {
+ padding: 0;
+}
+
+.atwho-view:after {
+ text-align: center;
+ content: attr(data-message);
+ display: block;
+ text-align: center;
+ line-height: 20px;
+}
+
+legend {
+ border: none;
+}
+
+.gr.repetition:not(:first-child) {
+ border-top: @cc-neutral-hi solid 1px;
+ padding-top: 20px;
+}
+
+.question-tile-row {
+ .gr {
+ padding-right: 20px !important;
+ padding-left: 20px !important;
+ }
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/formnav.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/formnav.scss
new file mode 100644
index 000000000000..4d14a7494a4b
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/formnav.scss
@@ -0,0 +1,61 @@
+.webforms-nav {
+ margin-bottom: 10px;
+ width: 100%;
+}
+
+.formnav-title {
+ position: absolute;
+ z-index: 1001;
+ color: white;
+ top: -41px;
+ font-size: 14px;
+ margin: 0;
+ font-weight: 400;
+ left: 50px;
+ line-height: 40px;
+ width: 144px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+.formnav-container {
+ transition: width 1s;
+ width: 100%;
+ background-color: @cc-bg;
+ box-sizing: border-box;
+ height: 33px;
+ padding: 0;
+}
+
+.btn-formnav {
+ background-color: transparent;
+ line-height: 12px;
+ padding: 12px 17px 9px;
+ border: none;
+ color: @cc-brand-mid;
+
+ .border-top-radius(0);
+ .border-bottom-radius(0);
+ .transition(background 1s);
+
+ &:hover {
+ background-color: darken(@cc-bg, 5);
+ color: @cc-brand-mid;
+ }
+}
+
+.btn-formnav:focus {
+ outline: none;
+}
+
+.btn-formnav.disabled {
+ color: lighten(@cc-text, 50);
+ &:hover {
+ background-color: transparent;
+ }
+}
+
+.btn-formnav-next {
+ float: right;
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/grid.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/grid.scss
new file mode 100644
index 000000000000..8d7848aa21b0
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/grid.scss
@@ -0,0 +1,5 @@
+.box {
+ border-color: #685c53;
+ color: #685c53;
+ font-size: 100%;
+}
\ No newline at end of file
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/markdown-table.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/markdown-table.scss
new file mode 100644
index 000000000000..1629bfee102f
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/markdown-table.scss
@@ -0,0 +1,38 @@
+.webapp-markdown-output {
+ table {
+ padding: 0;
+ tr {
+ background-color: white;
+ margin: 0;
+ padding: 0;
+
+ &:nth-child(2n) {
+ background-color: #f8f8f8;
+ }
+
+ th, td {
+ border: 1px solid #ccc;
+ text-align: left;
+ margin: 0;
+ padding: 6px 13px;
+ &:first-child {
+ margin-top: 0;
+ }
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ th {
+ font-weight: bold;
+ background-color: #ddd; // override tr background color
+ }
+ }
+ }
+}
+
+.text-center {
+ .webapp-markdown-output table {
+ margin: auto;
+ }
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/mixins.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/mixins.scss
new file mode 100644
index 000000000000..1a162354bba8
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/mixins.scss
@@ -0,0 +1,37 @@
+.make-module-size(@icon-size) {
+ .module-icon-image,
+ .module-icon-container {
+ width: @icon-size;
+ height: @icon-size;
+ }
+ .module-icon-container {
+ .border-top-radius(@icon-size/2);
+ .border-bottom-radius(@icon-size/2);
+ .module-icon {
+ font-size: @icon-size * 0.5;
+ line-height: @icon-size;
+ }
+ }
+ .module-column-icon {
+ width: @icon-size + 0.4*@icon-size;
+ height: @icon-size + 0.4*@icon-size;
+ }
+ @control-size: 0.8*@icon-size;
+ .module-audio-control, .module-delete-control {
+ width: @control-size;
+ height: @control-size;
+ right: (0.4*@icon-size)/2;
+ margin-top: -@control-size/2;
+
+ .border-top-radius(@control-size/2);
+ .border-bottom-radius(@control-size/2);
+ }
+ .module-audio-icon, .module-delete-icon {
+ font-size: @control-size * 0.5;
+ line-height: @control-size;
+ }
+ .module-column-name-audio h3,
+ .module-column-name-session h3 {
+ margin-right: @control-size + 0.4*@icon-size !important;
+ }
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/module.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/module.scss
new file mode 100644
index 000000000000..04437809e6d7
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/module.scss
@@ -0,0 +1,317 @@
+.module-table {
+ .module-column {
+ border: none;
+ text-align: left;
+ vertical-align: middle;
+ .transition(background 1s);
+ }
+ .module-column-name {
+ position: relative;
+ }
+ tr:hover {
+ .module-column {
+ background-color: @cc-bg;
+ }
+ }
+}
+
+.module-column-name {
+ margin: 0 10px 0 10px;
+}
+
+.badge-container {
+ position: relative;
+ > .badge {
+ position: absolute;
+ bottom: -5px;
+ right: -5px;
+ }
+}
+
+button.clickable-icon {
+ background: unset;
+ border: unset;
+ padding: unset;
+}
+
+.module-icon-image {
+ margin: 0 auto;
+ background: no-repeat center;
+ -webkit-background-size: cover;
+ -moz-background-size: cover;
+ -o-background-size: cover;
+ background-size: cover;
+}
+
+.module-icon-container {
+ background-color: @cc-neutral-mid;
+ text-align: center;
+ margin: 0 auto;
+
+ .module-icon {
+ color: white;
+ }
+}
+
+.module-icon-user {
+ background-color: @cc-dark-cool-accent-mid;
+ .module-icon {
+ color: @cc-dark-cool-accent-hi;
+ }
+}
+
+.module-audio-control, .module-delete-control {
+ text-align: center;
+ .transition(background 1s);
+ position: absolute;
+ top: 50%;
+
+}
+
+.module-audio-control {
+ .module-audio-icon {
+ color: @cc-light-cool-accent-mid;
+ }
+ &:hover {
+ background-color: @cc-light-cool-accent-hi;
+ }
+}
+
+.module-delete-control {
+ .module-delete-icon {
+ color: @cc-att-neg-mid;
+ }
+ &:hover {
+ background-color: @cc-att-neg-hi;
+ }
+}
+
+.make-module-size(45px);
+.module-column-name h3 {
+ font-size: 16px;
+}
+
+@media (min-width: @screen-md) {
+ .make-module-size(80px);
+ .module-column-name h3 {
+ font-size: 20px;
+ }
+}
+
+.query-button-container {
+ padding: 8px;
+ text-align: center;
+}
+.query-button-container .btn{
+ width: 48%;
+}
+
+.module-table .module-case-list-header {
+ background-color: @cc-brand-low;
+ color: white;
+ padding: 20px 10px;
+ cursor: default;
+ font-size: 14px;
+ text-transform: uppercase;
+ font-weight: bold;
+ border: none;
+}
+
+.module-table .module-case-list-column {
+ padding: 20px 10px;
+ font-size: 14px;
+ border: 0;
+ cursor: pointer;
+}
+
+.module-table .module-case-list-column-checkbox {
+ cursor: default;
+ width: 35px;
+ vertical-align: middle;
+}
+
+.module-table-case-list tbody tr td {
+ transition: background .6s;
+}
+
+.module-table-case-list tbody tr:nth-child(even) > td {
+ background-color: @cc-bg;
+}
+
+.module-table-case-list tbody tr:hover > td {
+ background-color: darken(@cc-bg, 5);
+}
+
+.module-search-container, .case-list-actions {
+ padding: 10px;
+}
+
+.module-go-container {
+ margin-top: 17px;
+}
+
+.pagination-container {
+ width: inherit;
+ padding-left: 17px;
+ padding-right: 17px;
+ min-height: 76px;
+}
+
+.module-per-page-container {
+ display: inline-block;
+ margin-top: 17px;
+}
+
+.module-table-case-list tbody tr:hover .module-case-list-column-empty {
+ background-color: white;
+ cursor: default;
+}
+
+.module-case-list-column-empty .alert {
+ margin-bottom: 0;
+}
+
+.module-table-case-detail {
+ margin-top: 10px;
+}
+
+.module-table-case-detail tbody tr {
+ &:hover {
+ cursor: default;
+ }
+ > td, > th {
+ border: none;
+ padding: 10px 15px;
+ &:hover {
+ cursor: default;
+ }
+ }
+
+ th, td {
+ background-color: @cc-brand-hi;
+ color: @cc-brand-low;
+ }
+}
+
+.module-banner {
+ background-color: @cc-light-warm-accent-hi;
+ color: black;
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+ border: none;
+ margin-top: -1px;
+ text-align: center;
+ font-size: 14px;
+}
+
+.module-table-case-detail tbody tr:nth-child(odd) {
+ th, td {
+ background-color: darken(@cc-brand-hi, 3);
+ }
+}
+
+.module-case-detail-tabs .nav-tabs li a {
+ text-transform: uppercase;
+ font-weight: bold;
+ .transition(1s all);
+ &:hover {
+ background-color: @cc-brand-hi;
+ border-top-color: transparent;
+ border-left-color: transparent;
+ border-right-color: transparent;
+ }
+}
+
+.module-case-detail-continue {
+ width: 100%;
+ .transition(.5s all);
+}
+
+.module-case-detail-btn {
+ width: 48%;
+ display: inline;
+}
+
+.module-case-detail-modal, .module-modal {
+ .modal-header {
+ border-bottom: none;
+ }
+ .modal-body {
+ padding-bottom: 0;
+ padding-top: 0;
+ }
+ .modal-footer {
+ border-top: none;
+ }
+}
+
+.module-modal {
+ .modal-body {
+ font-size: 16px;
+ }
+}
+
+.module-pagination-container {
+ text-align: center;
+ .pagination {
+ font-size: 1.8rem;
+ }
+}
+
+.menus-container {
+ margin-bottom: 20px;
+}
+
+#formplayer-progress {
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ position: fixed;
+ background-color: rgba(255, 255, 255, 0.9);
+ z-index: @zindex-formplayer-progress;
+ text-align: center;
+}
+
+#formplayer-progress .progress-container {
+ text-align: center;
+ left: 0;
+ right: 0;
+ position: absolute;
+ margin: 0 auto;
+ width: 80%;
+ top: 50%;
+
+ // Centers container vertically
+ transform: translateY(-50%);
+ -ms-transform: translateY(-50%);
+ -webkit-transform: translateY(-50%);
+
+ .progress-title {
+ display: inline-block;
+ h1 {
+ margin-bottom: 0px;
+ }
+ .subtext {
+ margin-top: 0px;
+ }
+ margin-bottom: 14px;
+ }
+}
+
+@media(max-width: @screen-sm-max) {
+ .module-case-list-table-container {
+ width: 100%;
+ overflow-x: scroll;
+ }
+ #formplayer-progress .progress-container {
+ .progress-title {
+ h1, h2 {
+ font-size: 1.3em;
+ }
+ h1 {
+ margin-bottom: 4px;
+ }
+ }
+ }
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/navigation.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/navigation.scss
new file mode 100644
index 000000000000..733071ede13c
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/navigation.scss
@@ -0,0 +1,24 @@
+.nav-list > li > a, .nav-list > li > a:hover {
+ cursor: pointer;
+}
+
+.nav-list > li.disabled > a, .nav-list > li.disabled > a:hover {
+ cursor: wait;
+}
+
+nav#sessions > ul > li > a.close{
+ z-index:1000;
+}
+
+nav#sessions > ul > li.nav-header {
+ color: #6c6c6c;
+}
+
+
+#hq-navigation .navbar {
+ margin-bottom: 0;
+}
+
+#hq-navigation {
+ transition: all 1s;
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/notifications.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/notifications.scss
new file mode 100644
index 000000000000..8e709ff49de2
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/notifications.scss
@@ -0,0 +1,11 @@
+.notifications-container {
+ background: transparent;
+
+ .alert {
+ .box-shadow(0 0 8px 1px rgba(0, 0, 0, 0.18));
+ }
+}
+
+#cloudcare-notifications {
+ overflow-wrap: break-word;
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/paginate.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/paginate.scss
new file mode 100644
index 000000000000..be663eedc724
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/paginate.scss
@@ -0,0 +1,6 @@
+.paginate-disabled {
+ cursor: not-allowed;
+ pointer-events: none;
+ opacity: .5;
+ box-shadow: none;
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/request.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/request.scss
new file mode 100644
index 000000000000..daac8d3f2908
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/request.scss
@@ -0,0 +1,8 @@
+.formplayer-requester-disabled {
+ pointer-events: none;
+ opacity: .5;
+}
+
+.formplayer-request {
+ cursor: pointer;
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/version.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/version.scss
new file mode 100644
index 000000000000..4f77c47d78c9
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/version.scss
@@ -0,0 +1,4 @@
+#version-info {
+ text-align: center;
+ display: block
+}
\ No newline at end of file
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/webforms.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/webforms.scss
new file mode 100644
index 000000000000..9b72d1fa8f62
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-common/webforms.scss
@@ -0,0 +1,124 @@
+.webforms {
+ max-width: 800px;
+ font-family: sans-serif;
+}
+
+#sync-container {
+ padding-top: 9px;
+
+ #sync-button {
+ margin-bottom: 3px;
+ margin-top: 3px;
+ }
+}
+
+/* Override default nprogress bar color to match CommCareHQ */
+#nprogress {
+ .bar {
+ background: #4aba32;
+ height: 4px;
+ }
+
+ .peg {
+ box-shadow: 0 0 10px #004ebc, 0 0 5px #004ebc;
+ }
+}
+
+video {
+ max-width: 400px;
+}
+
+.ix {
+ display: none;
+}
+
+.widget-container {
+ position: relative;
+}
+
+.widget {
+ margin-right: 20px;
+
+ button p {
+ margin-bottom: 0;
+ }
+}
+
+.dirty .widget {
+ background-color: lightblue;
+}
+
+.loading {
+ position: absolute;
+ right: 0px;
+ line-height: 30px;
+ font-size: 18px
+}
+
+.error-message {
+ font-weight: bold;
+}
+
+input:invalid {
+ color: #b94a48;
+ border-color: #b94a48;
+}
+
+.required .caption::before,
+.required-group .gr-header .caption::before {
+ content: '*';
+ font-weight: bold;
+ color: #c0392b;
+ margin: 0 3px;
+ float: left;
+}
+
+.caption {
+ font-weight: normal;
+}
+
+.map {
+ width: 100%;
+ height: 250px;
+ background: #eee;
+ max-width: none !important;
+ isolation: isolate;
+}
+
+.widget button p {
+ margin-bottom: 0;
+}
+
+.widget button h1 {
+ margin-top: 7px;
+ margin-bottom: 5px;
+}
+
+.widget button h2 {
+ margin-top: 6.5px;
+ margin-bottom: 4.5px;
+}
+
+.widget button h3 {
+ margin-top: 6px;
+ margin-bottom: 4px;
+}
+
+.widget button h4 {
+ margin-top: 5.5px;
+ margin-bottom: 3.5px;
+}
+
+.coordinate {
+ font-weight: bold;
+ width: 8em;
+}
+
+.wait {
+ margin: auto;
+ padding-top: 60px;
+ max-width: 200px;
+ color: #bbb;
+ font-size: 24pt;
+ line-height: 28pt;
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp-main.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp-main.scss
new file mode 100644
index 000000000000..834cd26cbeb9
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp-main.scss
@@ -0,0 +1,24 @@
+@import '../../hqwebapp/less/_hq/includes/variables.less';
+@import '../../hqwebapp/less/_hq/includes/mixins.less';
+
+/**
+These are all the styles relevant to the full-screen version of formplayer which
+might interfere with the app preview formplayer. Making changes here should not
+affect app preview.
+
+If you want to make changes relevant for BOTH app preview and the main
+formplayer, please make changes to the cloudcare styles.
+**/
+
+@import "formplayer-webapp/content";
+@import "formplayer-webapp/navbar";
+@import "formplayer-webapp/breadcrumbs";
+@import "formplayer-webapp/query";
+@import "formplayer-webapp/menu";
+@import "formplayer-webapp/module";
+@import "formplayer-webapp/form";
+@import "formplayer-webapp/formnav";
+@import "formplayer-webapp/version";
+@import "formplayer-webapp/print-general";
+@import "formplayer-webapp/case-tile";
+@import "formplayer-webapp/leaflet";
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp.scss
new file mode 100644
index 000000000000..a5c5ec05a317
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp.scss
@@ -0,0 +1,4 @@
+@b3-import-variables: '../../../../../../../../node_modules/bootstrap/less/variables';
+@b3-import-mixins: '../../../../../../../../node_modules/bootstrap/less/mixins';
+
+@import "formplayer-webapp-main";
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/breadcrumbs.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/breadcrumbs.scss
new file mode 100644
index 000000000000..1105d012434f
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/breadcrumbs.scss
@@ -0,0 +1,62 @@
+#breadcrumb-region .breadcrumb,
+.single-app-view .breadcrumb,
+.breadcrumb-form-container .breadcrumb {
+ font-size: 2rem;
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+ margin-top: -1px;
+ .breadcrumb-text {
+ &.js-home {
+ margin-right: -5px;
+ }
+ &:before {
+ padding: 0 6px 0 12px;
+ color: #ffffff;
+ font-size: 12px;
+ vertical-align: top;
+ line-height: 28px;
+ }
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden
+ }
+}
+
+#breadcrumb-region .breadcrumb-nav {
+ background-color: @cc-brand-mid;
+ box-shadow: 0 0 5px 2px rgba(0,0,0,.3);
+ display: flex;
+ align-items: center;
+ height: @breadcrumb-height-cloudcare;
+
+ .breadcrumb {
+ box-shadow: none;
+ background-color: unset;
+ padding: 10px 10px 0 10px;
+ margin-bottom: 0;
+ flex-grow: 1;
+ }
+
+ .btn-group {
+ padding-right: 10px;
+ }
+}
+
+@media print {
+ #breadcrumb-region {
+ position: static;
+ }
+
+ #breadcrumb-region .breadcrumb,
+ .breadcrumb-form-container .breadcrumb {
+ font-size: 12px;
+ margin-bottom: 0px;
+ padding-left: 33px;
+ .breadcrumb-text {
+ &:before {
+ font-size: 7px;
+ line-height: 17px;
+ }
+ }
+ }
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/case-tile.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/case-tile.scss
new file mode 100644
index 000000000000..f1e93a4c5388
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/case-tile.scss
@@ -0,0 +1,212 @@
+#case-list-menu-header{
+ padding-left: 1.5rem;
+ padding-right: 1.5rem;
+ background-color: white;
+ @media (max-width: @screen-sm-max) {
+ min-height: 70px;
+ }
+ h1 {
+ padding-left: 0px;
+ }
+
+ div button {
+ margin-left: 12px;
+ }
+}
+
+#case-list-search-controls {
+ background-color: @cc-bg;
+ margin: 0 5px 5px 5px;
+ display: flex;
+ justify-content: space-between;
+ #case-list-sort-by-btn {
+ background-color: transparent;
+ }
+}
+
+#select-all-tile-checkbox {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ margin-left: 10px;
+ margin-right: 10px;
+}
+
+#select-all-tile-checkbox-label {
+ font-weight: normal;
+}
+
+.select-row-checkbox-div {
+ margin: 10px 10px 10px 10px;
+ float: left;
+}
+
+.collapsed-tile-content {
+ height: 100px;
+ overflow-y: clip;
+ -webkit-mask-image: linear-gradient(180deg, #000 60%, transparent);
+}
+
+.show-more {
+ text-align: center;
+ font-size: large;
+}
+
+.sticky {
+ position: sticky !important;
+ z-index: 1; // keep sticky elements on top of case list
+ &-header {
+ top: 32px; // header scrolls excess padding under breadcrumbs
+ }
+ &-map {
+ top: 46px; // map sits directly against breadcrumbs
+ }
+}
+
+// todo should this be a re-usable class? only used for case list right now
+.btn-circle {
+ border-radius: 50%;
+ aspect-ratio: 1;
+}
+
+#scroll-to-bottom {
+ position: fixed;
+ z-index: @zindex-formplayer-scroll-to-bottom;
+ bottom: 45px;
+ left: calc(100vw - 80px);
+}
+
+.case-tile-container {
+ background: transparent !important;
+ > div {
+ background: white;
+ margin-bottom: 20px;
+ display: grid;
+ container-type: inline-size;
+ }
+
+ #persistent-case-tile .persistent-sticky .webapp-markdown-output {
+ img {
+ max-height: 100%;
+ max-width: 100%;
+ }
+
+ h1,h2,h3 {
+ margin-top: 0px
+ }
+ }
+}
+
+@media screen {
+ .case-tile-container {
+ position: sticky;
+ top: @breadcrumb-height-cloudcare;
+ z-index: @zindex-persistent-tile-cloudcare;
+
+ #persistent-case-tile .persistent-sticky {
+ box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);
+ border-bottom: 1px solid @cc-neutral-hi;
+ }
+ }
+}
+
+@media print {
+ .case-tile-container {
+ > div {
+ margin-bottom: 0px;
+ }
+ > div:not(:empty) {
+ border-bottom: 1px dashed black;
+ }
+ }
+}
+
+#menu-region .module-menu-container,
+#menu-region .module-case-list-container {
+
+ .white-border {
+ border-right-color: transparent;
+ border-right-style: solid;
+ border-right-width: 5px;
+ }
+
+ // todo: determine appropriate nesting
+ #module-case-list-map {
+ height: calc(~"100vh - 65px");
+
+ @media (max-width: @screen-sm-max) {
+ height: 25vh;
+ }
+
+ .marker-pin {
+ text-align: center;
+ /* Horizontally center the text (icon) */
+ line-height: 12px;
+ /* Vertically center the text (icon) */
+ color: @call-to-action-hi;
+ text-shadow: -1px 0 @call-to-action-low, 0 1px @call-to-action-low, 1px 0 @call-to-action-low, 0 -1px @call-to-action-low;
+ }
+ }
+
+ .list-cell-wrapper-style {
+ margin: 10px 5px 0 5px;
+ border-collapse: collapse;
+ vertical-align: top;
+ background-color: @cc-bg;
+ container-type: inline-size;
+
+ a {
+ color: @cc-brand-mid;
+ }
+
+ .module-icon,
+ .webapp-markdown-output img {
+ max-height: 100%;
+ max-width: 100%;
+ }
+
+ &:hover {
+ background-color: darken(@cc-bg, 5);
+ transition: background 0.6s;
+ }
+ }
+
+ .highlighted-case {
+ border-style: solid;
+ border-color: @cc-brand-mid;
+ }
+
+ .case-tile-group{
+ margin: 10px;
+ padding: 10px 0;
+
+ .group-data{
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ }
+ .group-rows{
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ align-items: center;
+ }
+ .group-row{
+ background-color: white;
+ width: 95%;
+ }
+ }
+}
+
+@media print {
+ #menu-region .module-menu-container,
+ #menu-region .module-case-list-container {
+ #module-case-list-container__results-container {
+ grid-template-columns: [tiles] 1fr !important;
+ }
+
+ .list-cell-wrapper-style {
+ border: 1px solid @cc-bg;
+ break-inside: avoid;
+ }
+ }
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/content.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/content.scss
new file mode 100644
index 000000000000..10cb62ae38cc
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/content.scss
@@ -0,0 +1,65 @@
+body {
+ overflow-x: hidden;
+ background-color: @cc-bg;
+}
+
+.cloudcare-home-content {
+ padding: 0 0 2rem;
+}
+
+
+#hq-footer {
+ display: none;
+}
+
+.page-header {
+ border-bottom: none;
+ padding: 14px 8px 7px;
+ margin: 0;
+ h1 {
+ font-size: 2rem;
+ text-transform: uppercase;
+ color: @cc-neutral-mid;
+ padding-left: 1.5rem;
+ font-weight: bold;
+ margin-top: 0px;
+ }
+}
+
+.page-footer {
+ padding: 14px 8px 7px;
+}
+
+.page-header-apps {
+ padding-left: 2px;
+}
+
+.page-header-apps h1 {
+ padding-left: 0;
+}
+
+#content-container {
+ background: transparent;
+}
+
+@media print {
+ * {
+ visibility: collapse;
+ }
+
+ .print-container, .print-container * {
+ visibility: visible;
+ }
+
+ .noprint-sub-container {
+ display: none !important;
+ }
+
+ #cloudcare-main {
+ padding: 0px;
+ }
+
+ video {
+ border: 1px solid #ddd;
+ }
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/form.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/form.scss
new file mode 100644
index 000000000000..fd5601f9957e
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/form.scss
@@ -0,0 +1,274 @@
+@form-text-indent: 23px;
+@form-text-size: 16px; // If updating, update .checkbox, .radio margin-top to fit
+@group-indent: 15px;
+
+.form-container {
+ background-color: white;
+ .box-shadow(0 0 10px 2px rgba(0,0,0,.1));
+ margin-bottom: 2rem;
+ font-size: @form-text-size; // Don't overshadow inputs
+
+ .page-header h1 {
+ padding-left: @form-text-indent - 8px;
+ }
+
+ .controls {
+ padding-right: 25px;
+ padding-top: 3px;
+ }
+
+ .form-control {
+ font-size: @form-text-size;
+ }
+
+ .form-actions {
+ margin: 30px 0 0 0;
+ &.form-group {
+ margin-left: -@form-text-indent;
+ margin-right: -@form-text-indent;
+ }
+
+ background-color: lighten(@cc-brand-hi, 10);
+ .border-bottom-radius(0);
+
+ .nonanchored-submit .btn {
+ font-size: 20px;
+ padding: 13px 24px;
+ .transition(all .5s);
+ }
+ }
+
+ .anchored-submit {
+ background-color: @call-to-action-low;
+ width: 100vw;
+ padding-top: 5px;
+ padding-bottom: 5px;
+ position: fixed;
+ bottom: 0px;
+ left: 0;
+ z-index: @zindex-formplayer-anchored-submit;
+ }
+
+ // Bootstrap introduces -10px left/right margin for row classes. This causes element to overflow parent.
+ .row {
+ margin-left: 0px;
+ margin-right: 0px;
+ }
+ .question-container {
+ padding-left: @form-text-indent;
+ padding-right: @form-text-indent;
+ }
+
+ .form-group {
+ margin-left: 0px;
+ margin-right: 0px;
+ .caption.control-label {
+ display: block;
+ }
+ }
+
+ .group-body {
+ margin-left: @group-indent;
+ margin-right: @group-indent;
+ }
+
+ .gr.panel {
+ border-radius: 0px;
+ }
+
+ .rep.panel {
+ border-radius: 0px;
+ }
+
+ .col-sm-12,
+ .col-sm-11,
+ .col-sm-10,
+ .col-sm-9,
+ .col-sm-8,
+ .col-sm-7,
+ .col-sm-6,
+ .col-sm-5,
+ .col-sm-4,
+ .col-sm-3,
+ .col-sm-2,
+ .col-sm-1 {
+ &:first-child {
+ padding-left: 0;
+ }
+ &:last-child {
+ padding-right: 0;
+ }
+ }
+
+ .panel-body {
+ @media (max-width: @screen-xs-max) {
+ padding-left: 0px;
+ padding-right: 0px;
+ }
+ }
+
+ .stripe-repeats {
+ > .row, .panel-body > .children > .row {
+ &:nth-of-type(odd) {
+ background-color: @table-bg-accent;
+ }
+ &:nth-of-type(even) {
+ background-color: white;
+ }
+ &:hover {
+ background-color: @table-bg-hover;
+ }
+ }
+ }
+
+ .group-border {
+ border: solid 1px @cc-neutral-mid;
+ border-radius: 8px;
+ margin: 2px;
+ padding-top: 5px;
+ padding-bottom: 5px;
+ }
+
+ .info {
+ overflow-x: auto;
+ }
+
+ .gr-header {
+ .collapsible-icon-container {
+ float: left;
+ margin-right: 10px;
+ }
+ .webapp-markdown-output img {
+ height: 1em;
+ vertical-align: baseline;
+ }
+ }
+
+ .panel-heading {
+ border-top-left-radius: 0px;
+ border-top-right-radius: 0px;
+ }
+
+}
+
+.form-group-required-label {
+ display: none;
+}
+
+.form-single-question {
+ padding-bottom: 20px;
+ padding-top: 20px;
+
+ .page-header {
+ display: none;
+ }
+
+ .form-group.required {
+ .transition(all .5s);
+ margin-bottom: 0;
+ label:before {
+ display: none;
+ }
+ }
+
+ .form-group.required.on {
+ background-color: @cc-att-neg-hi;
+ border: 10px solid @cc-att-neg-mid;
+ border-bottom: none;
+ padding-top: 10px;
+ padding-bottom: 10px;
+
+ label {
+ margin-left: 0;
+ margin-right: 0;
+ }
+ }
+
+ .form-group-required-label {
+ display: block;
+ opacity: 0;
+ .transition(all .5s);
+ }
+
+ .form-group-required-label.on {
+ opacity: 100;
+ font-size: 1.6rem;
+ background-color: @cc-att-neg-mid;
+ color: white;
+ width: auto;
+ line-height: 14px;
+ margin-left: -10px;
+ margin-right: -10px;
+ padding: 10px 10px 11px;
+ text-align: left;
+ margin-top: 0;
+ border: none;
+ }
+
+}
+
+@media print {
+ .form-container.print-container {
+ margin: 0px;
+ .page-header {
+ padding-top: 0px;
+ }
+ }
+
+ .q.form-group {
+ break-inside: avoid;
+ }
+
+ .panel.panel-default.last,
+ .panel.panel-default.last *,
+ .q.form-group.last,
+ .q.form-group.last * {
+ margin-bottom: 0px;
+ padding-bottom: 0px;
+ .widget-container {
+ margin-bottom: 0px;
+ padding-bottom: 0px;
+ }
+ }
+
+ .help-block {
+ color: #52616f !important;
+ font-size: 14px;
+ }
+}
+
+.question-tile-row:has(div.q) {
+ padding-top: 8px;
+ padding-bottom: 8px;
+}
+
+.question-tile-row {
+ display: flex;
+ align-items: start;
+ * .form-group, * p, * .control-label {
+ padding-top: 0px !important;
+ padding-bottom: 0px !important;
+ margin-top: 0px !important;
+ margin-bottom: 0px !important;
+ }
+}
+
+.question-tile-row {
+ .gr {
+ padding-right: 0px !important;
+ padding-left: 0px !important;
+ }
+}
+
+.gr-has-no-nested-questions {
+ display: none;
+}
+
+.checkbox, .radio {
+ input[type="checkbox"], input[type="radio"] {
+ margin-top: 3.15px;
+ }
+ // Overrides Bootstrap defaults
+ padding-top: 0px !important;
+ padding-bottom: 7px;
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/formnav.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/formnav.scss
new file mode 100644
index 000000000000..2b9bdc7c3dab
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/formnav.scss
@@ -0,0 +1,24 @@
+.formnav-container {
+ background-color: white;
+ height: 45px;
+ margin-top: -16px;
+}
+
+.btn-formnav {
+ font-size: 2.2rem;
+}
+
+.btn-formnav-submit {
+ transition: width 1s;
+ width: 80%;
+ text-transform: uppercase;
+ padding: 9px 10px;
+ border: none;
+ float: right;
+ font-size: 1.9rem;
+
+ .transition(all 1s);
+
+ .border-top-radius(0);
+ .border-bottom-radius(0);
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/leaflet.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/leaflet.scss
new file mode 100644
index 000000000000..30eba98af909
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/leaflet.scss
@@ -0,0 +1,9 @@
+// override leaflet-fullscreen styles to fix icon alignment in mapbox.js
+.leaflet-touch {
+ .leaflet-control-fullscreen a {
+ background-position: 0px 0px !important;
+ }
+ &.leaflet-fullscreen-on .leaflet-control-fullscreen a {
+ background-position: 0px -26px !important;
+ }
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/menu.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/menu.scss
new file mode 100644
index 000000000000..3f95e2298ad9
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/menu.scss
@@ -0,0 +1,14 @@
+#menu-region {
+ background: transparent;
+}
+
+#menu-region .module-menu-container,
+#menu-region .module-case-list-container {
+ background-color: white;
+ .box-shadow(0 0 10px 2px rgba(0,0,0,.1));
+ margin-bottom: 2rem;
+}
+
+#menu-region .module-menu-container {
+ padding-bottom: 6px;
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/module.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/module.scss
new file mode 100644
index 000000000000..e9d62ce41412
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/module.scss
@@ -0,0 +1,13 @@
+.module-table {
+ .module-column-icon {
+ padding-left: 20px;
+ }
+}
+
+
+@media print {
+ .module-banner {
+ padding: 0px;
+ height: 0px;
+ }
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/navbar.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/navbar.scss
new file mode 100644
index 000000000000..c69345f61152
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/navbar.scss
@@ -0,0 +1,28 @@
+@cloudcare-nav-height: 30px;
+
+.navbar-cloudcare {
+ margin-bottom: 0;
+ border: none;
+ background-color: @cc-brand-low;
+ min-height: @cloudcare-nav-height;
+ .transition(1s all);
+ z-index: @zindex-navbar-cloudcare;
+}
+
+.navbar-cloudcare .navbar-brand {
+ color: @cc-bg;
+ height: @cloudcare-nav-height;
+ padding: 6.5px 15px;
+ text-transform: uppercase;
+ font-size: 11px;
+}
+
+.navbar-cloudcare .navbar-nav {
+ float: right;
+ margin: 0;
+
+ > li > a {
+ padding-top: 6.5px;
+ padding-bottom: 6.5px;
+ }
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/print-general.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/print-general.scss
new file mode 100644
index 000000000000..56c3a1d3d2cf
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/print-general.scss
@@ -0,0 +1,194 @@
+// Renders Bootstrap as "small" layout when printing
+@media print {
+ .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {
+ float: left;
+ }
+ .col-sm-12 {
+ width: 100%;
+ }
+ .col-sm-11 {
+ width: 91.66666667%;
+ }
+ .col-sm-10 {
+ width: 83.33333333%;
+ }
+ .col-sm-9 {
+ width: 75%;
+ }
+ .col-sm-8 {
+ width: 66.66666667%;
+ }
+ .col-sm-7 {
+ width: 58.33333333%;
+ }
+ .col-sm-6 {
+ width: 50%;
+ }
+ .col-sm-5 {
+ width: 41.66666667%;
+ }
+ .col-sm-4 {
+ width: 33.33333333%;
+ }
+ .col-sm-3 {
+ width: 25%;
+ }
+ .col-sm-2 {
+ width: 16.66666667%;
+ }
+ .col-sm-1 {
+ width: 8.33333333%;
+ }
+ .col-sm-pull-12 {
+ right: 100%;
+ }
+ .col-sm-pull-11 {
+ right: 91.66666667%;
+ }
+ .col-sm-pull-10 {
+ right: 83.33333333%;
+ }
+ .col-sm-pull-9 {
+ right: 75%;
+ }
+ .col-sm-pull-8 {
+ right: 66.66666667%;
+ }
+ .col-sm-pull-7 {
+ right: 58.33333333%;
+ }
+ .col-sm-pull-6 {
+ right: 50%;
+ }
+ .col-sm-pull-5 {
+ right: 41.66666667%;
+ }
+ .col-sm-pull-4 {
+ right: 33.33333333%;
+ }
+ .col-sm-pull-3 {
+ right: 25%;
+ }
+ .col-sm-pull-2 {
+ right: 16.66666667%;
+ }
+ .col-sm-pull-1 {
+ right: 8.33333333%;
+ }
+ .col-sm-pull-0 {
+ right: auto;
+ }
+ .col-sm-push-12 {
+ left: 100%;
+ }
+ .col-sm-push-11 {
+ left: 91.66666667%;
+ }
+ .col-sm-push-10 {
+ left: 83.33333333%;
+ }
+ .col-sm-push-9 {
+ left: 75%;
+ }
+ .col-sm-push-8 {
+ left: 66.66666667%;
+ }
+ .col-sm-push-7 {
+ left: 58.33333333%;
+ }
+ .col-sm-push-6 {
+ left: 50%;
+ }
+ .col-sm-push-5 {
+ left: 41.66666667%;
+ }
+ .col-sm-push-4 {
+ left: 33.33333333%;
+ }
+ .col-sm-push-3 {
+ left: 25%;
+ }
+ .col-sm-push-2 {
+ left: 16.66666667%;
+ }
+ .col-sm-push-1 {
+ left: 8.33333333%;
+ }
+ .col-sm-push-0 {
+ left: auto;
+ }
+ .col-sm-offset-12 {
+ margin-left: 100%;
+ }
+ .col-sm-offset-11 {
+ margin-left: 91.66666667%;
+ }
+ .col-sm-offset-10 {
+ margin-left: 83.33333333%;
+ }
+ .col-sm-offset-9 {
+ margin-left: 75%;
+ }
+ .col-sm-offset-8 {
+ margin-left: 66.66666667%;
+ }
+ .col-sm-offset-7 {
+ margin-left: 58.33333333%;
+ }
+ .col-sm-offset-6 {
+ margin-left: 50%;
+ }
+ .col-sm-offset-5 {
+ margin-left: 41.66666667%;
+ }
+ .col-sm-offset-4 {
+ margin-left: 33.33333333%;
+ }
+ .col-sm-offset-3 {
+ margin-left: 25%;
+ }
+ .col-sm-offset-2 {
+ margin-left: 16.66666667%;
+ }
+ .col-sm-offset-1 {
+ margin-left: 8.33333333%;
+ }
+ .col-sm-offset-0 {
+ margin-left: 0%;
+ }
+ .visible-xs {
+ display: none !important;
+ }
+ .hidden-xs {
+ display: block !important;
+ }
+ table.hidden-xs {
+ display: table;
+ }
+ tr.hidden-xs {
+ display: table-row !important;
+ }
+ th.hidden-xs,
+ td.hidden-xs {
+ display: table-cell !important;
+ }
+ .hidden-xs.hidden-print {
+ display: none !important;
+ }
+ .hidden-sm {
+ display: none !important;
+ }
+ .visible-sm {
+ display: block !important;
+ }
+ table.visible-sm {
+ display: table;
+ }
+ tr.visible-sm {
+ display: table-row !important;
+ }
+ th.visible-sm,
+ td.visible-sm {
+ display: table-cell !important;
+ }
+ }
\ No newline at end of file
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/query.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/query.scss
new file mode 100644
index 000000000000..9b73443dfb4f
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/query.scss
@@ -0,0 +1,102 @@
+.full-width {
+ width: 100%;
+}
+
+.sidebar-push {
+ @media (min-width: @screen-md-min) {
+ margin-left: 310px;
+ }
+}
+
+#sidebar-region {
+ background: transparent;
+ @media (min-width: @screen-md-min) {
+ width: 300px;
+ position: absolute;
+ }
+ @media (max-width: @screen-sm-max) {
+ max-width: 600px;
+ margin: auto;
+ }
+
+ table {
+ table-layout: fixed;
+ background: white;
+ }
+
+ .query-input-group {
+ margin-right: 18px;
+ margin-bottom: 10px;
+ margin-left: 18px;
+ }
+ .query-caption {
+ margin-top: 8px;
+ margin-right: 10px;
+ margin-left: 8px;
+ }
+
+ .query-caption > div {
+ margin-left: 10px;
+ margin-right: 10px;
+ }
+
+ .mapboxgl-ctrl-geocoder {
+ min-width: 100%;
+ font-size: 1em;
+ > .mapboxgl-ctrl-geocoder--input {
+ height: inherit;
+ }
+ }
+ .select2-container .select2-selection--multiple {
+ min-height: inherit;
+ }
+}
+
+.query-field > .checkbox {
+ &:first-of-type {
+ margin-top: 0;
+ }
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+ input[type="checkbox"] {
+ margin-top: 2.5px;
+ }
+}
+
+.query-description {
+ a {
+ color: @cc-brand-mid;
+ }
+ a:hover {
+ color: @cc-brand-low;
+ }
+}
+
+.query-caption.required .control-label::before, .search-query-group-header.required .search-query-group-header-label::before {
+ content: '*';
+ font-weight: bold;
+ color: #c0392b;
+ margin: 0 3px;
+ float: left;
+}
+
+.search-query-group-header-label {
+ font-size: larger;
+}
+
+.search-query-group {
+ background-color: @cc-bg;
+}
+
+#query-group-content > tr {
+ background-color: white;
+}
+
+#query-properties td {
+ border-top: 0px;
+}
+
+.search-query-group .table {
+ border-collapse: separate; margin-bottom: 0px;
+}
diff --git a/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/version.scss b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/version.scss
new file mode 100644
index 000000000000..95a2c809c9dd
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/cloudcare/scss/formplayer-webapp/version.scss
@@ -0,0 +1,5 @@
+@media print {
+ #version-info {
+ display: none;
+ }
+}
diff --git a/corehq/apps/hqwebapp/static/hqwebapp/js/atwho.js b/corehq/apps/hqwebapp/static/hqwebapp/js/atwho.js
index 5b99f4baf507..e1f60e605e38 100644
--- a/corehq/apps/hqwebapp/static/hqwebapp/js/atwho.js
+++ b/corehq/apps/hqwebapp/static/hqwebapp/js/atwho.js
@@ -1,6 +1,8 @@
hqDefine('hqwebapp/js/atwho', [
"knockout",
"underscore",
+ "Caret.js/dist/jquery.caret",
+ "At.js/dist/js/jquery.atwho",
],
function (
ko,
diff --git a/corehq/apps/hqwebapp/static/hqwebapp/js/bootstrap3/requirejs_config.js b/corehq/apps/hqwebapp/static/hqwebapp/js/bootstrap3/requirejs_config.js
index 08f998cb67eb..e223aadc0696 100644
--- a/corehq/apps/hqwebapp/static/hqwebapp/js/bootstrap3/requirejs_config.js
+++ b/corehq/apps/hqwebapp/static/hqwebapp/js/bootstrap3/requirejs_config.js
@@ -2,6 +2,9 @@
requirejs.config({
baseUrl: '/static/',
paths: {
+ "backbone": "backbone/backbone-min",
+ "backbone.radio": "backbone.radio/build/backbone.radio.min",
+ "backbone.marionette": "backbone.marionette/lib/backbone.marionette.min",
"bootstrap": "bootstrap/dist/js/bootstrap.min",
"datatables": "datatables.net/js/jquery.dataTables.min",
"datatables.bootstrap": "datatables-bootstrap3/BS3/assets/js/datatables",
@@ -16,7 +19,46 @@ requirejs.config({
shim: {
"accounting/js/lib/stripe": { exports: 'Stripe' },
"ace-builds/src-min-noconflict/ace": { exports: "ace" },
+ "ace-builds/src-min-noconflict/mode-json": { deps: ["ace-builds/src-min-noconflict/ace"] },
+ "ace-builds/src-min-noconflict/mode-xml": { deps: ["ace-builds/src-min-noconflict/ace"] },
+ "ace-builds/src-min-noconflict/ext-searchbox": { deps: ["ace-builds/src-min-noconflict/ace"] },
+ "At.js/dist/js/jquery.atwho": { deps: ['jquery', 'Caret.js/dist/jquery.caret'] },
+ "backbone": { exports: "backbone" },
"bootstrap": { deps: ['jquery'] },
+ "calendars/dist/js/jquery.calendars.picker": {
+ deps: [
+ "calendars/dist/js/jquery.plugin",
+ "calendars/dist/js/jquery.calendars",
+ ],
+ },
+ "calendars/dist/js/jquery.calendars.ethiopian": {
+ deps: [
+ "calendars/dist/js/jquery.calendars",
+ ],
+ },
+ "calendars/dist/js/jquery.calendars.plus": {
+ deps: [
+ "calendars/dist/js/jquery.calendars",
+ ],
+ },
+ "calendars/dist/js/jquery.calendars-am": {
+ deps: [
+ "calendars/dist/js/jquery.calendars.picker",
+ "calendars/dist/js/jquery.calendars",
+ ],
+ },
+ "calendars/dist/js/jquery.calendars.picker-am": {
+ deps: [
+ "calendars/dist/js/jquery.calendars.picker",
+ "calendars/dist/js/jquery.calendars",
+ ],
+ },
+ "calendars/dist/js/jquery.calendars.ethiopian-am": {
+ deps: [
+ "calendars/dist/js/jquery.calendars.picker",
+ "calendars/dist/js/jquery.calendars.ethiopian",
+ ],
+ },
"datatables.bootstrap": { deps: ['datatables'] },
"d3/d3.min": {
"exports": "d3",
@@ -30,12 +72,21 @@ requirejs.config({
exports: 'RMI',
},
"ko.mapping": { deps: ['knockout'] },
+ // Use the uncompressed version of mapbox, because the compressed version lacks an ending semicolon,
+ // which can interfere with whatever modules is included after it in bundle files.
+ // During deploy, build_requirejs will minify it.
+ "leaflet-fullscreen/dist/Leaflet.fullscreen.min": {
+ deps: ["mapbox.js/dist/mapbox.uncompressed"],
+ exports: "L",
+ },
+ "mapbox.js/dist/mapbox.uncompressed": { exports: "L" },
"nvd3/nv.d3.min": {
deps: ['d3/d3.min'],
exports: 'nv',
},
"sentry_browser": { exports: "Sentry" },
},
+ wrapShim: true,
packages: [{
name: 'moment',
location: 'moment',
diff --git a/corehq/apps/hqwebapp/static/hqwebapp/js/bootstrap5/requirejs_config.js b/corehq/apps/hqwebapp/static/hqwebapp/js/bootstrap5/requirejs_config.js
index 3b01c845be03..6de25323353f 100644
--- a/corehq/apps/hqwebapp/static/hqwebapp/js/bootstrap5/requirejs_config.js
+++ b/corehq/apps/hqwebapp/static/hqwebapp/js/bootstrap5/requirejs_config.js
@@ -4,6 +4,9 @@ requirejs.config({
paths: {
"babel": "@babel/standalone/babel.min",
"babel-plugin-transform-modules-requirejs-babel": "babel-plugin-transform-modules-requirejs-babel/index",
+ "backbone": "backbone/backbone-min",
+ "backbone.radio": "backbone.radio/build/backbone.radio.min",
+ "backbone.marionette": "backbone.marionette/lib/backbone.marionette.min",
"bootstrap": "bootstrap/dist/js/bootstrap.min",
"bootstrap5": "bootstrap5/dist/js/bootstrap.bundle.min",
"datatables": "datatables.net/js/jquery.dataTables.min",
@@ -23,6 +26,45 @@ requirejs.config({
shim: {
"accounting/js/lib/stripe": { exports: 'Stripe' },
"ace-builds/src-min-noconflict/ace": { exports: "ace" },
+ "ace-builds/src-min-noconflict/mode-json": { deps: ["ace-builds/src-min-noconflict/ace"] },
+ "ace-builds/src-min-noconflict/mode-xml": { deps: ["ace-builds/src-min-noconflict/ace"] },
+ "ace-builds/src-min-noconflict/ext-searchbox": { deps: ["ace-builds/src-min-noconflict/ace"] },
+ "At.js/dist/js/jquery.atwho": { deps: ['jquery', 'Caret.js/dist/jquery.caret'] },
+ "backbone": { exports: "backbone" },
+ "calendars/dist/js/jquery.calendars.picker": {
+ deps: [
+ "calendars/dist/js/jquery.plugin",
+ "calendars/dist/js/jquery.calendars",
+ ],
+ },
+ "calendars/dist/js/jquery.calendars.ethiopian": {
+ deps: [
+ "calendars/dist/js/jquery.calendars",
+ ],
+ },
+ "calendars/dist/js/jquery.calendars.plus": {
+ deps: [
+ "calendars/dist/js/jquery.calendars",
+ ],
+ },
+ "calendars/dist/js/jquery.calendars-am": {
+ deps: [
+ "calendars/dist/js/jquery.calendars.picker",
+ "calendars/dist/js/jquery.calendars",
+ ],
+ },
+ "calendars/dist/js/jquery.calendars.picker-am": {
+ deps: [
+ "calendars/dist/js/jquery.calendars.picker",
+ "calendars/dist/js/jquery.calendars",
+ ],
+ },
+ "calendars/dist/js/jquery.calendars.ethiopian-am": {
+ deps: [
+ "calendars/dist/js/jquery.calendars.picker",
+ "calendars/dist/js/jquery.calendars.ethiopian",
+ ],
+ },
"datatables.bootstrap": { deps: ['datatables'] },
"datatables.fixedColumns.bootstrap": { deps: ['datatables.fixedColumns'] },
"tempusDominus": {
@@ -40,12 +82,21 @@ requirejs.config({
exports: 'RMI',
},
"ko.mapping": { deps: ['knockout'] },
+ // Use the uncompressed version of mapbox, because the compressed version lacks an ending semicolon,
+ // which can interfere with whatever modules is included after it in bundle files.
+ // During deploy, build_requirejs will minify it.
+ "leaflet-fullscreen/dist/Leaflet.fullscreen.min": {
+ deps: ["mapbox.js/dist/mapbox.uncompressed"],
+ exports: "L",
+ },
+ "mapbox.js/dist/mapbox.uncompressed": { exports: "L" },
"nvd3/nv.d3.min": {
deps: ['d3/d3.min'],
exports: 'nv',
},
"sentry_browser": { exports: "Sentry" },
},
+ wrapShim: true,
packages: [{
name: 'moment',
location: 'moment',
diff --git a/corehq/apps/hqwebapp/static/preview_app/scss/preview_app-main.scss b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app-main.scss
new file mode 100644
index 000000000000..00d00adeddce
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app-main.scss
@@ -0,0 +1,25 @@
+@import '../../hqwebapp/less/_hq/includes/variables.less';
+@import '../../hqwebapp/less/_hq/includes/mixins.less';
+
+/* this is included in cloudcare/preview_app.html */
+
+@import "preview_app/mixins";
+
+@transition-speed: .5s;
+
+@import "preview_app/base";
+@import "preview_app/variables";
+@import "preview_app/notifications";
+@import "preview_app/scrollable";
+@import "preview_app/navigation";
+@import "preview_app/grid";
+@import "preview_app/appicon";
+@import "preview_app/menu";
+@import "preview_app/breadcrumb";
+@import "preview_app/module";
+@import "preview_app/form";
+@import "preview_app/formnav";
+@import "preview_app/case";
+@import "preview_app/datepicker";
+@import "preview_app/panels";
+@import "preview_app/debugger";
diff --git a/corehq/apps/hqwebapp/static/preview_app/scss/preview_app.scss b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app.scss
new file mode 100644
index 000000000000..d1be7e539ea1
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app.scss
@@ -0,0 +1,4 @@
+@b3-import-variables: '../../../../../../../../node_modules/bootstrap/less/variables';
+@b3-import-mixins: '../../../../../../../../node_modules/bootstrap/less/mixins';
+
+@import "preview_app-main";
diff --git a/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/appicon.scss b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/appicon.scss
new file mode 100644
index 000000000000..ff8db5618be6
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/appicon.scss
@@ -0,0 +1,42 @@
+@appicon-height-preview: 130px;
+@appicon-icon-height-preview: @appicon-height-preview - 40px;
+
+.appicon {
+ min-height: @appicon-height-preview;
+ margin-bottom: 10px;
+
+ .appicon-title {
+ .transition(height @transition-speed);
+ height: 30px;
+ bottom: 7px;
+ h3 {
+ font-size: 14px;
+ font-weight: 300;
+ }
+ }
+
+ .appicon-icon {
+ margin-left: -@appicon-icon-height-preview/2;
+ width: @appicon-icon-height-preview;
+ height: @appicon-icon-height-preview;
+ line-height: @appicon-icon-height-preview;
+ font-size: @appicon-icon-height-preview;
+ &[class*='fa-'] {
+ font-size: 70px;
+ }
+ }
+}
+
+.appicon-start,
+.appicon-sync,
+.appicon-settings,
+.appicon-restore-as,
+.preview-tablet-mode .appicon-incomplete {
+ .appicon-title {
+ height: 21px;
+ }
+}
+
+.container-appicons .page-header-apps {
+ display: none;
+}
diff --git a/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/base.scss b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/base.scss
new file mode 100644
index 000000000000..12e116579044
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/base.scss
@@ -0,0 +1,8 @@
+body {
+ background-color: @cc-bg;
+}
+
+#version-info {
+ margin-top: 10px;
+ margin-bottom: 10px;
+}
diff --git a/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/breadcrumb.scss b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/breadcrumb.scss
new file mode 100644
index 000000000000..a2c829dff9eb
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/breadcrumb.scss
@@ -0,0 +1,55 @@
+#breadcrumb-region .breadcrumb,
+.single-app-view .breadcrumb,
+.breadcrumb-form-container .breadcrumb {
+ text-transform: uppercase;
+ margin: 0;
+ padding: 0 10px;
+ line-height: 30px;
+ font-size: 11px;
+
+ li {
+ display: none;
+ &:before {
+ color: white;
+ vertical-align: middle;
+ font-size: 8px;
+ padding: 0 4px;
+ }
+ }
+ li:first-child {
+ display: inline-block;
+ white-space: nowrap;
+ }
+ li:last-child {
+ display: inline-block;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+}
+
+#breadcrumb-region {
+ overflow: hidden;
+ max-height: 30px;
+ width: 100%;
+}
+
+.breadcrumb-form {
+ width: 100%;
+ .box-shadow(none);
+
+ li {
+ padding-top: 1px;
+ }
+}
+
+.breadcrumb-form-container {
+ position: fixed;
+ z-index: 10;
+ width: 100%;
+ height: @webforms-breadcrumb-height;
+ .box-shadow(0 0 5px 2px rgba(0,0,0,.3));
+}
+
+.webforms-nav-single-question {
+ height: @webforms-nav-height;
+}
diff --git a/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/case.scss b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/case.scss
new file mode 100644
index 000000000000..88f5b20f8b1b
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/case.scss
@@ -0,0 +1,9 @@
+.case-list-action-button {
+ width: 100%;
+ .btn {
+ width: 100%;
+ font-size: 1.2rem;
+ .border-top-radius(0);
+ .border-bottom-radius(0);
+ }
+}
diff --git a/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/datepicker.scss b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/datepicker.scss
new file mode 100644
index 000000000000..865a9b159006
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/datepicker.scss
@@ -0,0 +1,26 @@
+@datepicker-width: 247px;
+@datepicker-width-tablet: 447px;
+@datepicker-height: 410px;
+@datepicker-height-tablet: 565px;
+
+.bootstrap-datetimepicker-widget.dropdown-menu {
+ position: fixed;
+ top: 30px !important;
+ left: 0px !important;
+ padding-bottom: 15px !important;
+ min-height: @datepicker-height !important;
+
+ padding: 5px !important;
+ width: @datepicker-width !important;
+ margin-left: 2px !important;
+ margin-right: 2px !important;
+
+ .datepicker {
+ margin: auto;
+ }
+}
+
+.preview-tablet-mode .bootstrap-datetimepicker-widget.dropdown-menu {
+ width: @datepicker-width-tablet !important;
+ min-height: @datepicker-height-tablet !important;
+}
diff --git a/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/debugger.scss b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/debugger.scss
new file mode 100644
index 000000000000..44de58a71ce6
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/debugger.scss
@@ -0,0 +1,42 @@
+.debugger {
+ -webkit-transition: all 0.5s;
+ transition: all 0.5s;
+}
+
+.debugger.debugger-maximized {
+ height: @preview-phone-height;
+ max-height: 100vh;
+}
+
+.debugger.debugger-minimized {
+ height: 30px;
+ // Javascript auto sets the width for normal web apps so we need to override that with !important
+ // This way form entry javascript doesn't need to know whether its in web apps or preview
+ width: 30px !important;
+ border-radius: 15px;
+
+ // Space the debugger button so it's not flush against the phone
+ bottom: 3px;
+ right: 3px;
+ left: initial;
+
+ .box-shadow(0 0 5px 0 rgba(0, 0, 0, 0.45));
+
+ .debugger-tab-title {
+ text-align: center;
+ .debugger-title, .fa-xmark {
+ display: none;
+ }
+ }
+}
+
+.debugger .debugger-content {
+ font-size: 1rem;
+}
+
+
+.preview-tablet-mode {
+ .debugger.debugger-maximized {
+ height: @preview-tablet-height;
+ }
+}
diff --git a/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/form.scss b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/form.scss
new file mode 100644
index 000000000000..709e88260259
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/form.scss
@@ -0,0 +1,133 @@
+.form-container {
+ background-color: white;
+ min-height: -webkit-calc(~"100vh - " (@webforms-nav-height + @webforms-breadcrumb-height));
+ min-height: -moz-calc(~"100vh - " (@webforms-nav-height + @webforms-breadcrumb-height));
+ min-height: calc(~"100vh - " (@webforms-nav-height + @webforms-breadcrumb-height));
+ box-sizing: border-box;
+ padding: 0 10px;
+
+ .page-header {
+ border-bottom: none;
+ margin: 0;
+ padding: 0;
+ }
+ .page-header h1 {
+ padding: 10px 0 15px;
+ margin-bottom: 0;
+ font-size: 10px;
+ font-weight: 600;
+ text-transform: uppercase;
+ color: @cc-neutral-mid;
+ margin-top: 0;
+ }
+}
+
+.form-container .gr {
+ background-color: desaturate(lighten(@cc-brand-hi, 8), 40);
+ margin-left: -10px;
+ margin-right: -10px;
+ padding: 10px;
+ margin-bottom: 10px;
+ border-bottom: 3px solid desaturate(@cc-brand-hi, 30);
+ border-top: 3px solid desaturate(@cc-brand-hi, 30);
+}
+
+.form-container .gr-has-no-nested-questions {
+ display: none;
+}
+
+.form-container .gr.gr-no-children {
+ background-color: transparent;
+ border-bottom: none;
+ border-top: none;
+}
+
+.form-container .gr-header legend {
+ font-size: 1.3rem;
+ text-transform: uppercase;
+}
+
+.page-footer {
+ padding: 14px 8px 7px;
+}
+
+.form-single-question {
+ padding-top: 0px;
+}
+
+.form-container .form-actions {
+ background-color: transparent;
+ border-top: none;
+ margin-top: 0;
+ padding-top: 0;
+
+ .btn {
+ width: 100%;
+ font-size: 16px;
+ .transition(all .5s);
+ }
+}
+
+.form-container {
+ .loading {
+ right: 12px;
+ font-size: 14px;
+ top: 2px;
+ }
+}
+
+.form-group {
+ padding: 10px 0;
+ label {
+ margin-right: 10px;
+ margin-bottom: 10px;
+ }
+}
+
+.form-group.required {
+ .transition(all .5s);
+ margin-bottom: 0;
+ label:before {
+ display: none;
+ }
+}
+
+.form-group.required.on {
+ background-color: @cc-att-neg-hi;
+ border: 10px solid @cc-att-neg-mid;
+ border-bottom: none;
+ label {
+ margin-left: 0;
+ margin-right: 0;
+ }
+}
+
+.form-group-required-label {
+ opacity: 0;
+ .transition(all .5s);
+}
+
+.form-group-required-label.on {
+ opacity: 100;
+ font-size: 10px;
+ background-color: @cc-att-neg-mid;
+ color: white;
+ width: auto;
+ line-height: 14px;
+ margin-left: -10px;
+ margin-right: -10px;
+ padding: 6px 10px 7px;
+ text-align: left;
+ margin-top: 0;
+ border: none;
+}
+
+.form-group .help-block {
+ font-size: 11px;
+ &:before {
+ content: "\f05a";
+ font-family: 'FontAwesome';
+ display: inline-block;
+ padding-right: 5px;
+ }
+}
diff --git a/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/formnav.scss b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/formnav.scss
new file mode 100644
index 000000000000..df04c507756c
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/formnav.scss
@@ -0,0 +1,20 @@
+.btn-formnav-submit {
+ transition: width @transition-speed;
+ width: @preview-phone-width - 46px;
+ text-transform: uppercase;
+ padding: 8px 10px;
+ border: none;
+ .transition(all @transition-speed);
+
+ .border-top-radius(0);
+ .border-bottom-radius(0);
+}
+
+.preview-tablet-mode {
+ .formnav-container {
+ width: @preview-tablet-width;
+ }
+ .btn-formnav-submit {
+ width: @preview-tablet-width - 46px;
+ }
+}
diff --git a/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/grid.scss b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/grid.scss
new file mode 100644
index 000000000000..e61fd65d6dc5
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/grid.scss
@@ -0,0 +1,11 @@
+
+.container-appicons {
+ padding-left: 15px;
+ padding-right: 15px;
+ padding-top: 10px;
+
+ .grid-item {
+ padding-left: 5px;
+ padding-right: 5px;
+ }
+}
diff --git a/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/menu.scss b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/menu.scss
new file mode 100644
index 000000000000..fbb85d411822
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/menu.scss
@@ -0,0 +1,3 @@
+.menu-header {
+ display: none;
+}
diff --git a/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/mixins.scss b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/mixins.scss
new file mode 100644
index 000000000000..1a162354bba8
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/mixins.scss
@@ -0,0 +1,37 @@
+.make-module-size(@icon-size) {
+ .module-icon-image,
+ .module-icon-container {
+ width: @icon-size;
+ height: @icon-size;
+ }
+ .module-icon-container {
+ .border-top-radius(@icon-size/2);
+ .border-bottom-radius(@icon-size/2);
+ .module-icon {
+ font-size: @icon-size * 0.5;
+ line-height: @icon-size;
+ }
+ }
+ .module-column-icon {
+ width: @icon-size + 0.4*@icon-size;
+ height: @icon-size + 0.4*@icon-size;
+ }
+ @control-size: 0.8*@icon-size;
+ .module-audio-control, .module-delete-control {
+ width: @control-size;
+ height: @control-size;
+ right: (0.4*@icon-size)/2;
+ margin-top: -@control-size/2;
+
+ .border-top-radius(@control-size/2);
+ .border-bottom-radius(@control-size/2);
+ }
+ .module-audio-icon, .module-delete-icon {
+ font-size: @control-size * 0.5;
+ line-height: @control-size;
+ }
+ .module-column-name-audio h3,
+ .module-column-name-session h3 {
+ margin-right: @control-size + 0.4*@icon-size !important;
+ }
+}
diff --git a/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/module.scss b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/module.scss
new file mode 100644
index 000000000000..5fe7b26259af
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/module.scss
@@ -0,0 +1,155 @@
+.make-module-size(30px);
+
+.module-column-name h3 {
+ font-size: 12px;
+ margin: 0;
+}
+
+.module-table {
+ margin-bottom: 0;
+}
+
+.module-table .module-column {
+ vertical-align: middle;
+ padding: 0;
+}
+
+.module-table .module-column-name {
+ padding: 4px;
+ padding-right: 8px;
+}
+
+.module-table .module-column-icon {
+ padding: 8px;
+}
+
+.module-menu-container {
+ padding-bottom: 5px;
+ padding-top: 5px;
+ background-color: white;
+}
+
+.module-case-list-container {
+ background-color: white;
+}
+
+.module-search-container, .module-go-container{
+ .input-group-lg .form-control,
+ .input-group-lg .btn{
+ font-size: 11px;
+ height: 28px;
+ padding: 8px;
+ }
+ .input-group-lg .btn [class*='fa-'] {
+ font-size: 11px;
+ }
+}
+
+.module-go-container {
+ width: 140px;
+}
+
+.module-per-page-container {
+ .input-group-lg .form-control,
+ .input-group-lg .btn{
+ font-size: 11px;
+ height: 30px;
+ padding: 5px 10px;
+ }
+ .input-group-lg .btn [class*='fa-'] {
+ font-size: 11px;
+ }
+}
+
+.module-table .module-case-list-header {
+ font-size: 10px;
+ padding: 13px 11px;
+}
+
+.module-table .module-case-list-column {
+ padding: 13px 11px;
+ font-size: 11px;
+ vertical-align: middle;
+
+ .module-icon {
+ max-height: 30px;
+ }
+}
+
+.module-pagination-container .pagination {
+ font-size: 10px;
+ [class*='fa-'] {
+ font-size: 14px;
+ }
+}
+
+.module-case-detail-modal {
+ padding: 0 !important;
+ width: 251px;
+ height: 100%;
+ .modal-dialog {
+ margin: 1px;
+ }
+ .modal-body {
+ height: 318px;
+ width: 263px;
+ overflow-y: scroll;
+
+ }
+ .modal-content {
+ height: 442px;
+ overflow-x: hidden;
+ .border-top-radius(0);
+ .border-bottom-radius(0);
+ }
+}
+
+.module-modal {
+ .modal-title {
+ font-size: 1.3rem;
+ }
+
+ .modal-content {
+ .border-top-radius(0);
+ .border-bottom-radius(0);
+ }
+
+ .btn-lg {
+ font-size: 1.3rem;
+ padding: 8px 13px;
+ }
+}
+
+
+.preview-tablet-mode .module-case-detail-modal {
+ width: 450px;
+ .modal-body {
+ width: 446px;
+ }
+}
+
+.module-table-case-detail {
+ th, td {
+ font-size: 11px;
+ padding: 8px;
+ }
+}
+
+.module-case-detail-tabs .nav-tabs li a {
+ font-size: 10px;
+ padding: 8px;
+}
+
+.module-banner {
+ font-size: 11px;
+ padding-left: 6px;
+ padding-right: 6px;
+}
+
+.module-menu-bar-offset {
+ padding-top: 40px;
+ .table > tbody > tr > th,
+ .table > tbody > tr > td {
+ border-top: none;
+ }
+}
diff --git a/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/navigation.scss b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/navigation.scss
new file mode 100644
index 000000000000..d7d5b2b2a846
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/navigation.scss
@@ -0,0 +1,36 @@
+@preview-navbar-height: 40px;
+
+.navbar-formplayer {
+ background-color: @cc-brand-mid;
+ min-height: @preview-navbar-height;
+ color: white;
+ margin-bottom: 0;
+
+ .navbar-nav {
+ margin-top: 0;
+ margin-bottom: 0;
+ float: left;
+ }
+ .navbar-right {
+ float: right;
+ }
+ .navbar-nav > li {
+ display: inline-block;
+ font-size: 2rem;
+ }
+ .navbar-nav > li > a {
+ color: white;
+ line-height: @preview-navbar-height;
+ padding-right: 19px;
+ .transition(background @transition-speed);
+
+ &:link, &:focus, &:visited, &:hover {
+ color: white;
+ text-decoration: none;
+ }
+
+ &:hover {
+ background-color: @cc-brand-low;
+ }
+ }
+}
diff --git a/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/notifications.scss b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/notifications.scss
new file mode 100644
index 000000000000..06714571ca5c
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/notifications.scss
@@ -0,0 +1,22 @@
+.notifications-container {
+ .alert {
+ .box-shadow(none);
+ margin-bottom: 0;
+ .border-top-radius(0);
+ .border-bottom-radius(0);
+ padding: 9px;
+ .close {
+ vertical-align: top;
+ margin-top: -4px;
+ }
+ }
+}
+
+.alert-build {
+ #build-errors > li {
+ font-size: 11px;
+ margin-bottom: 5px;
+ padding-bottom: 3px;
+ border-bottom: 1px solid fade(@cc-dark-warm-accent-low, 20);
+ }
+}
diff --git a/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/panels.scss b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/panels.scss
new file mode 100644
index 000000000000..fdfbc80cb67c
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/panels.scss
@@ -0,0 +1,3 @@
+.question-container .panel {
+ margin-top: 15px;
+}
diff --git a/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/scrollable.scss b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/scrollable.scss
new file mode 100644
index 000000000000..dc59e34871a6
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/scrollable.scss
@@ -0,0 +1,30 @@
+.scrollable-container {
+ transition: width @transition-speed;
+ overflow-x: hidden;
+ overflow-y: scroll;
+ // 15px is the approximate width of the scrollbar in OSX.
+ // javascript is the most reliable way to hide it when pure css options fail
+ width: @preview-phone-width;
+ &.has-scrollbar-15 { width: @preview-phone-width + 15px; }
+ box-sizing: content-box;
+
+ &:hover {
+ cursor: -webkit-grab;
+ cursor: -moz-grab;
+ cursor: grab;
+ }
+}
+
+.preview-tablet-mode {
+ .scrollable-container {
+ width: @preview-tablet-width;
+ &.has-scrollbar-15 { width: @preview-tablet-width + 15px; }
+ }
+}
+
+// Hiding scrollbars for the different browsers, generally only works on windows.
+.scrollable-container::-webkit-scrollbar { width: 0 !important }
+.scrollable-container { -ms-overflow-style: none; }
+@-moz-document url-prefix() {
+ .scrollable-container { overflow-y: auto; }
+}
diff --git a/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/variables.scss b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/variables.scss
new file mode 100644
index 000000000000..ed6ccd9b96fb
--- /dev/null
+++ b/corehq/apps/hqwebapp/static/preview_app/scss/preview_app/variables.scss
@@ -0,0 +1,7 @@
+@preview-phone-width: 250px;
+@preview-tablet-width: 450px;
+@preview-tablet-height: 644px;
+@preview-phone-height: 444px;
+
+@webforms-nav-height: 33px;
+@webforms-breadcrumb-height: 30px;
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diff_config.json b/corehq/apps/hqwebapp/tests/data/bootstrap5_diff_config.json
index d1333130c410..0960e0872008 100644
--- a/corehq/apps/hqwebapp/tests/data/bootstrap5_diff_config.json
+++ b/corehq/apps/hqwebapp/tests/data/bootstrap5_diff_config.json
@@ -315,6 +315,28 @@
"compare_all_files": true
}
],
+ "apps/hqwebapp/static/cloudcare": [
+ {
+ "directories": [
+ "less/",
+ "scss/"
+ ],
+ "file_type": "stylesheet",
+ "label": "stylesheets",
+ "compare_all_files": true
+ }
+ ],
+ "apps/hqwebapp/static/preview_app": [
+ {
+ "directories": [
+ "less/",
+ "scss/"
+ ],
+ "file_type": "stylesheet",
+ "label": "stylesheets",
+ "compare_all_files": true
+ }
+ ],
"apps/settings/templates/settings": [
{
"directories": [
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/javascript/hqwebapp/js/requirejs_config.js.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/javascript/hqwebapp/js/requirejs_config.js.diff.txt
index 58d82a198f0a..8d3fe27335c1 100644
--- a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/javascript/hqwebapp/js/requirejs_config.js.diff.txt
+++ b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/javascript/hqwebapp/js/requirejs_config.js.diff.txt
@@ -1,11 +1,14 @@
---
+++
-@@ -2,26 +2,36 @@
+@@ -2,18 +2,25 @@
requirejs.config({
baseUrl: '/static/',
paths: {
+ "babel": "@babel/standalone/babel.min",
+ "babel-plugin-transform-modules-requirejs-babel": "babel-plugin-transform-modules-requirejs-babel/index",
+ "backbone": "backbone/backbone-min",
+ "backbone.radio": "backbone.radio/build/backbone.radio.min",
+ "backbone.marionette": "backbone.marionette/lib/backbone.marionette.min",
"bootstrap": "bootstrap/dist/js/bootstrap.min",
+ "bootstrap5": "bootstrap5/dist/js/bootstrap.bundle.min",
"datatables": "datatables.net/js/jquery.dataTables.min",
@@ -25,9 +28,17 @@
"underscore": "underscore/underscore",
},
shim: {
- "accounting/js/lib/stripe": { exports: 'Stripe' },
- "ace-builds/src-min-noconflict/ace": { exports: "ace" },
+@@ -24,7 +31,6 @@
+ "ace-builds/src-min-noconflict/ext-searchbox": { deps: ["ace-builds/src-min-noconflict/ace"] },
+ "At.js/dist/js/jquery.atwho": { deps: ['jquery', 'Caret.js/dist/jquery.caret'] },
+ "backbone": { exports: "backbone" },
- "bootstrap": { deps: ['jquery'] },
+ "calendars/dist/js/jquery.calendars.picker": {
+ deps: [
+ "calendars/dist/js/jquery.plugin",
+@@ -60,10 +66,14 @@
+ ],
+ },
"datatables.bootstrap": { deps: ['datatables'] },
+ "datatables.fixedColumns.bootstrap": { deps: ['datatables.fixedColumns'] },
+ "tempusDominus": {
@@ -41,7 +52,7 @@
"hqwebapp/js/lib/modernizr": {
exports: 'Modernizr',
},
-@@ -47,7 +57,7 @@
+@@ -98,7 +108,7 @@
},
},
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/debugger_debugger.debugger_debugger.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/debugger_debugger.debugger_debugger.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/font-formplayer.font-formplayer.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/font-formplayer.font-formplayer.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/font-formplayer_core.font-formplayer_core.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/font-formplayer_core.font-formplayer_core.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/font-formplayer_icons.font-formplayer_icons.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/font-formplayer_icons.font-formplayer_icons.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/font-formplayer_path.font-formplayer_path.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/font-formplayer_path.font-formplayer_path.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/font-formplayer_variables.font-formplayer_variables.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/font-formplayer_variables.font-formplayer_variables.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common-main.formplayer-common-main.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common-main.formplayer-common-main.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common.formplayer-common.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common.formplayer-common.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_address.formplayer-common_address.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_address.formplayer-common_address.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_appicon.formplayer-common_appicon.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_appicon.formplayer-common_appicon.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_breadcrumbs.formplayer-common_breadcrumbs.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_breadcrumbs.formplayer-common_breadcrumbs.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_case.formplayer-common_case.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_case.formplayer-common_case.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_form.formplayer-common_form.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_form.formplayer-common_form.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_formnav.formplayer-common_formnav.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_formnav.formplayer-common_formnav.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_grid.formplayer-common_grid.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_grid.formplayer-common_grid.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_markdown-table.formplayer-common_markdown-table.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_markdown-table.formplayer-common_markdown-table.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_mixins.formplayer-common_mixins.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_mixins.formplayer-common_mixins.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_module.formplayer-common_module.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_module.formplayer-common_module.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_navigation.formplayer-common_navigation.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_navigation.formplayer-common_navigation.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_notifications.formplayer-common_notifications.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_notifications.formplayer-common_notifications.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_paginate.formplayer-common_paginate.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_paginate.formplayer-common_paginate.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_request.formplayer-common_request.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_request.formplayer-common_request.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_version.formplayer-common_version.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_version.formplayer-common_version.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_webforms.formplayer-common_webforms.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-common_webforms.formplayer-common_webforms.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp-main.formplayer-webapp-main.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp-main.formplayer-webapp-main.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp.formplayer-webapp.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp.formplayer-webapp.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_breadcrumbs.formplayer-webapp_breadcrumbs.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_breadcrumbs.formplayer-webapp_breadcrumbs.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_case-tile.formplayer-webapp_case-tile.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_case-tile.formplayer-webapp_case-tile.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_content.formplayer-webapp_content.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_content.formplayer-webapp_content.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_form.formplayer-webapp_form.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_form.formplayer-webapp_form.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_formnav.formplayer-webapp_formnav.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_formnav.formplayer-webapp_formnav.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_leaflet.formplayer-webapp_leaflet.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_leaflet.formplayer-webapp_leaflet.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_menu.formplayer-webapp_menu.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_menu.formplayer-webapp_menu.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_module.formplayer-webapp_module.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_module.formplayer-webapp_module.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_navbar.formplayer-webapp_navbar.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_navbar.formplayer-webapp_navbar.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_print-general.formplayer-webapp_print-general.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_print-general.formplayer-webapp_print-general.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_query.formplayer-webapp_query.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_query.formplayer-webapp_query.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_version.formplayer-webapp_version.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/formplayer-webapp_version.formplayer-webapp_version.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app-main.preview_app-main.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app-main.preview_app-main.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app.preview_app.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app.preview_app.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_appicon.preview_app_appicon.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_appicon.preview_app_appicon.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_base.preview_app_base.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_base.preview_app_base.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_breadcrumb.preview_app_breadcrumb.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_breadcrumb.preview_app_breadcrumb.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_case.preview_app_case.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_case.preview_app_case.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_datepicker.preview_app_datepicker.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_datepicker.preview_app_datepicker.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_debugger.preview_app_debugger.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_debugger.preview_app_debugger.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_form.preview_app_form.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_form.preview_app_form.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_formnav.preview_app_formnav.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_formnav.preview_app_formnav.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_grid.preview_app_grid.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_grid.preview_app_grid.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_menu.preview_app_menu.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_menu.preview_app_menu.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_mixins.preview_app_mixins.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_mixins.preview_app_mixins.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_module.preview_app_module.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_module.preview_app_module.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_navigation.preview_app_navigation.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_navigation.preview_app_navigation.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_notifications.preview_app_notifications.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_notifications.preview_app_notifications.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_panels.preview_app_panels.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_panels.preview_app_panels.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_scrollable.preview_app_scrollable.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_scrollable.preview_app_scrollable.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_variables.preview_app_variables.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/preview_app_variables.preview_app_variables.style.diff.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/corehq/apps/hqwebapp/utils/bootstrap/status/bootstrap3_to_5_completed.json b/corehq/apps/hqwebapp/utils/bootstrap/status/bootstrap3_to_5_completed.json
index 110f75ad8dd2..5a6b2db01970 100644
--- a/corehq/apps/hqwebapp/utils/bootstrap/status/bootstrap3_to_5_completed.json
+++ b/corehq/apps/hqwebapp/utils/bootstrap/status/bootstrap3_to_5_completed.json
@@ -37,6 +37,7 @@
"javascript": [
"js/profile_case_search.js",
"js/case_search.js"
- ]
+ ],
+ "is_complete": true
}
}
diff --git a/corehq/apps/integration/static/integration/js/hmac_callout.js b/corehq/apps/integration/static/integration/js/hmac_callout.js
index 4b2511b82fe6..79458f51c424 100644
--- a/corehq/apps/integration/static/integration/js/hmac_callout.js
+++ b/corehq/apps/integration/static/integration/js/hmac_callout.js
@@ -2,6 +2,7 @@
hqDefine("integration/js/hmac_callout", [
"hqwebapp/js/initial_page_data",
"crypto-js/crypto-js",
+ "url-polyfill/url-polyfill", // for URL.searchParams in signedCallout
], function (
initialPageData,
CryptoJS
diff --git a/corehq/apps/reports_core/static/reports_core/js/base_template_new.js b/corehq/apps/reports_core/static/reports_core/js/base_template_new.js
index 45d1609fce17..ee78ad1c6599 100644
--- a/corehq/apps/reports_core/static/reports_core/js/base_template_new.js
+++ b/corehq/apps/reports_core/static/reports_core/js/base_template_new.js
@@ -50,8 +50,9 @@ hqDefine('reports_core/js/base_template_new', function () {
};
var successCallback = function (data) {
- if (data.error) {
- $('#error-message').html(data.error);
+ if (data.error || data.error_message) {
+ const message = data.error || data.error_message;
+ $('#error-message').html(message);
$('#report-error').removeClass('hide');
} else {
$('#report-error').addClass('hide');
diff --git a/corehq/apps/sso/exceptions.py b/corehq/apps/sso/exceptions.py
index 2708ec7b1274..77a347e55c91 100644
--- a/corehq/apps/sso/exceptions.py
+++ b/corehq/apps/sso/exceptions.py
@@ -20,3 +20,13 @@ def __init__(self, error_code, message=None):
super().__init__()
self.code = error_code
self.message = message
+
+
+class EntraVerificationFailed(Exception):
+ def __init__(self, error, message):
+ super().__init__(f"{error}: {message}")
+ self.error = error
+ self.message = message
+
+ def __str__(self):
+ return f"EntraVerificationFailed({self.error}, {self.message})"
diff --git a/corehq/apps/sso/forms.py b/corehq/apps/sso/forms.py
index cee168163e79..f53264709f67 100644
--- a/corehq/apps/sso/forms.py
+++ b/corehq/apps/sso/forms.py
@@ -674,59 +674,54 @@ class BaseSsoEnterpriseSettingsForm(forms.Form):
'"Update Configuration" below. This will apply only to SSO users associated with this Identity Provider.'
))
- def __init__(self, identity_provider, *args, uses_api_key_management=False, **kwargs):
+ def __init__(self, identity_provider, *args, **kwargs):
if 'show_remote_user_management' in kwargs:
self.show_remote_user_management = kwargs.pop('show_remote_user_management')
else:
self.show_remote_user_management = False
self.idp = identity_provider
- self.uses_api_key_management = uses_api_key_management
initial = kwargs['initial'] = kwargs.get('initial', {}).copy()
initial.setdefault('enable_user_deactivation', identity_provider.enable_user_deactivation)
initial.setdefault('api_host', identity_provider.api_host)
initial.setdefault('api_id', identity_provider.api_id)
initial.setdefault('date_api_secret_expiration', identity_provider.date_api_secret_expiration)
- if self.uses_api_key_management:
- initial.setdefault('always_show_user_api_keys', identity_provider.always_show_user_api_keys)
- if identity_provider.max_days_until_user_api_key_expiration is not None:
- initial.setdefault('enforce_user_api_key_expiration', True)
- initial.setdefault(
- 'max_days_until_user_api_key_expiration',
- identity_provider.max_days_until_user_api_key_expiration
- )
+ initial.setdefault('always_show_user_api_keys', identity_provider.always_show_user_api_keys)
+ if identity_provider.max_days_until_user_api_key_expiration is not None:
+ initial.setdefault('enforce_user_api_key_expiration', True)
+ initial.setdefault(
+ 'max_days_until_user_api_key_expiration',
+ identity_provider.max_days_until_user_api_key_expiration
+ )
super().__init__(*args, **kwargs)
def get_primary_fields(self):
- if self.uses_api_key_management:
- warning_message_div = render_to_string(
- 'sso/enterprise_admin/sso_api_expiration_warning.html',
- {'warning_message': self.expiration_warning_message}
- )
- fieldset = crispy.Fieldset(
- _('API Key Management'),
- hqcrispy.CheckboxField('always_show_user_api_keys'),
- hqcrispy.CheckboxField('enforce_user_api_key_expiration', data_bind='checked: enforceExpiration'),
+ warning_message_div = render_to_string(
+ 'sso/enterprise_admin/sso_api_expiration_warning.html',
+ {'warning_message': self.expiration_warning_message}
+ )
+ fieldset = crispy.Fieldset(
+ _('API Key Management'),
+ hqcrispy.CheckboxField('always_show_user_api_keys'),
+ hqcrispy.CheckboxField('enforce_user_api_key_expiration', data_bind='checked: enforceExpiration'),
+ crispy.Div(
+ crispy.Field(
+ 'max_days_until_user_api_key_expiration', data_bind="value: expirationLengthValue"),
+ crispy.HTML(warning_message_div),
+ data_bind='visible: enforceExpiration'
+ ),
+ )
+
+ api_key_management = \
+ crispy.Div(
crispy.Div(
- crispy.Field(
- 'max_days_until_user_api_key_expiration', data_bind="value: expirationLengthValue"),
- crispy.HTML(warning_message_div),
- data_bind='visible: enforceExpiration'
+ fieldset,
+ css_class="panel-body"
),
+ css_class="panel panel-modern-gray panel-form-only"
)
- api_key_management = \
- crispy.Div(
- crispy.Div(
- fieldset,
- css_class="panel-body"
- ),
- css_class="panel panel-modern-gray panel-form-only"
- )
- else:
- api_key_management = None
-
return [
crispy.Div(
crispy.Div(
@@ -805,10 +800,9 @@ def clean(self):
def update_identity_provider(self, admin_user):
self.idp.last_modified_by = admin_user.username
- if self.uses_api_key_management:
- self.idp.always_show_user_api_keys = self.cleaned_data['always_show_user_api_keys']
- self.idp.max_days_until_user_api_key_expiration = \
- self.cleaned_data['max_days_until_user_api_key_expiration']
+ self.idp.always_show_user_api_keys = self.cleaned_data['always_show_user_api_keys']
+ self.idp.max_days_until_user_api_key_expiration = \
+ self.cleaned_data['max_days_until_user_api_key_expiration']
self.idp.save()
return self.idp
diff --git a/corehq/apps/sso/models.py b/corehq/apps/sso/models.py
index 1f0cdbcf0a32..c9a25bc9f7ec 100644
--- a/corehq/apps/sso/models.py
+++ b/corehq/apps/sso/models.py
@@ -1,3 +1,5 @@
+import logging
+
from django.db import models
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
@@ -7,9 +9,12 @@
from corehq.apps.accounting.models import BillingAccount, Subscription
from corehq.apps.sso import certificates
from corehq.apps.sso.exceptions import ServiceProviderCertificateError
+from corehq.apps.sso.utils.entra import get_all_members_of_the_idp_from_entra
from corehq.apps.sso.utils.user_helpers import get_email_domain_from_username
from corehq.util.quickcache import quickcache
+log = logging.getLogger(__name__)
+
class IdentityProviderType:
ENTRA_ID = 'azure_ad' # Microsoft renamed Entra ID to Azure ID after implementing this feature
@@ -434,6 +439,12 @@ def get_required_identity_provider(cls, username):
return idp
return None
+ def get_all_members_of_the_idp(self):
+ if self.idp_type == IdentityProviderType.ENTRA_ID:
+ return get_all_members_of_the_idp_from_entra(self)
+ else:
+ raise NotImplementedError("Not implemented")
+
@receiver(post_save, sender=Subscription)
@receiver(post_delete, sender=Subscription)
diff --git a/corehq/apps/sso/static/sso/js/enterprise_edit_identity_provider.js b/corehq/apps/sso/static/sso/js/enterprise_edit_identity_provider.js
index f1879d3bbcb3..b2711656de08 100644
--- a/corehq/apps/sso/static/sso/js/enterprise_edit_identity_provider.js
+++ b/corehq/apps/sso/static/sso/js/enterprise_edit_identity_provider.js
@@ -37,7 +37,7 @@ hqDefine('sso/js/enterprise_edit_identity_provider', [
$('#sso-test-user-manager').koApplyBindings(ssoTestUserManager);
ssoTestUserManager.init();
- let editEnterpriseIdPFormManager = function (showAPIFields) {
+ let editEnterpriseIdPFormManager = function () {
let self = {};
if (initialPageData.get('is_oidc')) {
@@ -81,35 +81,32 @@ hqDefine('sso/js/enterprise_edit_identity_provider', [
};
}
- if (showAPIFields) {
- const initialEnforce = $('#id_enforce_user_api_key_expiration').is(':checked');
- self.initialExpirationLength =
- $('#id_max_days_until_user_api_key_expiration').val();
- if (self.initialExpirationLength) {
- self.initialExpirationLength = parseInt(self.initialExpirationLength, 10);
- }
- self.enforceExpiration = ko.observable(initialEnforce);
- self.expirationLengthValue = ko.observable(self.initialExpirationLength);
- self.expirationLength = ko.observable(null);
- self.expirationLengthValue.subscribe(function (newValue) {
- if (newValue) {
- const selValue = $('#id_max_days_until_user_api_key_expiration option:selected').text();
- self.expirationLength(selValue);
- }
- });
- self.showExpirationWarning = ko.pureComputed(function () {
- return (
- (self.initialExpirationLength === '' && self.expirationLengthValue() !== '') ||
- (self.expirationLengthValue() < self.initialExpirationLength)
- );
- });
+ const initialEnforce = $('#id_enforce_user_api_key_expiration').is(':checked');
+ self.initialExpirationLength =
+ $('#id_max_days_until_user_api_key_expiration').val();
+ if (self.initialExpirationLength) {
+ self.initialExpirationLength = parseInt(self.initialExpirationLength, 10);
}
+ self.enforceExpiration = ko.observable(initialEnforce);
+ self.expirationLengthValue = ko.observable(self.initialExpirationLength);
+ self.expirationLength = ko.observable(null);
+ self.expirationLengthValue.subscribe(function (newValue) {
+ if (newValue) {
+ const selValue = $('#id_max_days_until_user_api_key_expiration option:selected').text();
+ self.expirationLength(selValue);
+ }
+ });
+ self.showExpirationWarning = ko.pureComputed(function () {
+ return (
+ (self.initialExpirationLength === '' && self.expirationLengthValue() !== '') ||
+ (self.expirationLengthValue() < self.initialExpirationLength)
+ );
+ });
return self;
};
- const showAPIFields = initialPageData.get('show_api_fields');
- let formManager = new editEnterpriseIdPFormManager(showAPIFields);
+ let formManager = new editEnterpriseIdPFormManager();
$('#idp form').koApplyBindings(formManager);
});
});
diff --git a/corehq/apps/sso/tasks.py b/corehq/apps/sso/tasks.py
index 9ea3aa86b031..4ca1b0f50305 100644
--- a/corehq/apps/sso/tasks.py
+++ b/corehq/apps/sso/tasks.py
@@ -1,20 +1,33 @@
import datetime
import logging
+import requests
from celery.schedules import crontab
from corehq.apps.celery import periodic_task, task
from corehq.apps.hqwebapp.tasks import send_html_email_async
-from corehq.apps.sso.models import IdentityProvider, IdentityProviderProtocol
+from corehq.apps.sso.exceptions import EntraVerificationFailed
+from corehq.apps.sso.models import (
+ AuthenticatedEmailDomain,
+ IdentityProvider,
+ IdentityProviderProtocol,
+ IdentityProviderType,
+ UserExemptFromSingleSignOn
+)
from corehq.apps.sso.utils.context_helpers import (
get_api_secret_expiration_email_context,
get_idp_cert_expiration_email_context,
+ get_sso_deactivation_skip_email_context,
)
+from corehq.apps.sso.utils.entra import MSGraphIssue
+from corehq.apps.sso.utils.user_helpers import get_email_domain_from_username
+from corehq.apps.users.models import WebUser
from corehq.apps.users.models import HQApiKey
from django.contrib.auth.models import User
from corehq.sql_db.util import paginate_query
from django.db import router
from django.db.models import Q
+from django.utils.translation import gettext as _
from dimagi.utils.chunked import chunked
from dimagi.utils.logging import notify_exception
@@ -110,6 +123,89 @@ def send_idp_cert_expires_reminder_emails(num_days):
)
+@periodic_task(run_every=crontab(minute=0, hour=2), acks_late=True)
+def auto_deactivate_removed_sso_users():
+ for idp in IdentityProvider.objects.filter(
+ enable_user_deactivation=True,
+ idp_type=IdentityProviderType.ENTRA_ID
+ ).all():
+ try:
+ idp_users = idp.get_all_members_of_the_idp()
+ except EntraVerificationFailed as e:
+ notify_exception(None, f"Failed to get members of the IdP. {str(e)}")
+ send_deactivation_skipped_email(idp=idp, failure_code=MSGraphIssue.VERIFICATION_ERROR,
+ error=EntraVerificationFailed.error,
+ error_description=EntraVerificationFailed.message)
+ continue
+ except requests.exceptions.HTTPError as e:
+ notify_exception(None, f"Failed to get members of the IdP. {str(e)}")
+ send_deactivation_skipped_email(idp=idp, failure_code=MSGraphIssue.HTTP_ERROR)
+ continue
+ except Exception as e:
+ notify_exception(None, f"Failed to get members of the IdP. {str(e)}")
+ send_deactivation_skipped_email(idp=idp, failure_code=MSGraphIssue.OTHER_ERROR)
+ continue
+
+ # if the Graph Users API returns an empty list of users we will skip auto deactivation
+ if len(idp_users) == 0:
+ send_deactivation_skipped_email(idp=idp, failure_code=MSGraphIssue.EMPTY_ERROR)
+ continue
+
+ usernames_in_account = idp.owner.get_web_user_usernames()
+
+ # Get criteria for exempting usernames and email domains from the deactivation list
+ authenticated_domains = AuthenticatedEmailDomain.objects.filter(identity_provider=idp)
+ exempt_usernames = UserExemptFromSingleSignOn.objects.filter(email_domain__in=authenticated_domains
+ ).values_list('username', flat=True)
+
+ usernames_to_deactivate = []
+ authenticated_email_domains = authenticated_domains.values_list('email_domain', flat=True)
+
+ for username in usernames_in_account:
+ if username not in idp_users and username not in exempt_usernames:
+ email_domain = get_email_domain_from_username(username)
+ if email_domain in authenticated_email_domains:
+ usernames_to_deactivate.append(username)
+
+ # Deactivate user that is not returned by Graph Users API
+ for username in usernames_to_deactivate:
+ user = WebUser.get_by_username(username)
+ if user and user.is_active:
+ user.is_active = False
+ user.save()
+
+
+def send_deactivation_skipped_email(idp, failure_code, error=None, error_description=None):
+ if failure_code == MSGraphIssue.VERIFICATION_ERROR:
+ failure_reason = _("There was an issue connecting to the Microsoft Graph API. "
+ f"Error: {error}. Error description: {error_description}")
+ elif failure_code == MSGraphIssue.HTTP_ERROR:
+ failure_reason = _("An HTTP error occured when connecting to the Microsoft Graph API, which usually"
+ "indicates an issue with Microsoft's servers.")
+ elif failure_code == MSGraphIssue.EMPTY_ERROR:
+ failure_reason = _("We received an empty list of users from your Microsoft Entra ID instance.")
+ elif failure_code == MSGraphIssue.OTHER_ERROR:
+ failure_reason = _("We encountered an unknown issue, please contact Commcare HQ Support.")
+
+ context = get_sso_deactivation_skip_email_context(idp, failure_reason)
+ for send_to in context["to"]:
+ send_html_email_async.delay(
+ context["subject"],
+ send_to,
+ context["html"],
+ text_content=context["plaintext"],
+ email_from=context["from"],
+ bcc=context["bcc"],
+ )
+ log.info(
+ "Sent sso user deactivation skipped notification"
+ "email for %(idp_name)s to %(send_to)s." % {
+ "idp_name": idp.name,
+ "send_to": send_to,
+ }
+ )
+
+
@task(bind=True, default_retry_delay=15 * 60, max_retries=10, acks_late=True)
def update_sso_user_api_key_expiration_dates(self, identity_provider_id):
idp = IdentityProvider.objects.get(id=identity_provider_id)
diff --git a/corehq/apps/sso/templates/sso/email/sso_deactivation_skip_notification.html b/corehq/apps/sso/templates/sso/email/sso_deactivation_skip_notification.html
new file mode 100644
index 000000000000..d51578bad96d
--- /dev/null
+++ b/corehq/apps/sso/templates/sso/email/sso_deactivation_skip_notification.html
@@ -0,0 +1,34 @@
+{% load i18n %}
+{% blocktrans %}Dear enterprise administrator,{% endblocktrans %}
+
+
+ {% blocktrans %}
+ We have temporarily skipped automatic deactivation of SSO-managed Web Users for the following reason:
+ {{ failure_reason }}
+ {% endblocktrans %}
+
+
+
+ {% blocktrans %}
+ Please check the configuration for Remote User Management in your Identity Provider settings.
+ {% endblocktrans %}
+
+
+
+ {% blocktrans %}
+ If you have any questions, or if this issue persists, please don’t hesitate to contact
+ {{ contact_email }}. Thank you for your use and support of CommCare.
+ {% endblocktrans %}
+
+
+
+
+ {% blocktrans %}
+ Best regards,
+ {% endblocktrans %}
+
+
+
+ {% blocktrans %}The CommCare HQ Team{% endblocktrans %}
+ {{ base_url }}
+
diff --git a/corehq/apps/sso/templates/sso/email/sso_deactivation_skip_notification.txt b/corehq/apps/sso/templates/sso/email/sso_deactivation_skip_notification.txt
new file mode 100644
index 000000000000..2b9c36ca835c
--- /dev/null
+++ b/corehq/apps/sso/templates/sso/email/sso_deactivation_skip_notification.txt
@@ -0,0 +1,17 @@
+{% load i18n %}
+{% blocktrans %}
+Dear enterprise administrator,
+
+We have temporarily skipped automatic deactivation of SSO-managed Web Users for the following reason:
+{{ failure_reason }}
+
+Please check the configuration for Remote User Management in your Identity Provider settings.
+
+If you have any questions, or if this issue persists, please don’t hesitate to contact {{ contact_email }}.
+Thank you for your use and support of CommCare.
+
+Best regards,
+
+The CommCare HQ Team
+{{ base_url }}
+{% endblocktrans %}
diff --git a/corehq/apps/sso/templates/sso/enterprise_admin/edit_identity_provider.html b/corehq/apps/sso/templates/sso/enterprise_admin/edit_identity_provider.html
index 4fce7e15927a..c5837320b8a4 100644
--- a/corehq/apps/sso/templates/sso/enterprise_admin/edit_identity_provider.html
+++ b/corehq/apps/sso/templates/sso/enterprise_admin/edit_identity_provider.html
@@ -9,7 +9,6 @@
{% initial_page_data 'idp_slug' idp_slug %}
{% initial_page_data 'is_oidc' is_oidc %}
{% initial_page_data 'show_remote_user_management' show_remote_user_management %}
- {% initial_page_data 'show_api_fields' show_api_fields %}
{% trans "Identity Provider" %}
diff --git a/corehq/apps/sso/tests/test_forms.py b/corehq/apps/sso/tests/test_forms.py
index 238ba8a85c31..6e4ab4bd3070 100644
--- a/corehq/apps/sso/tests/test_forms.py
+++ b/corehq/apps/sso/tests/test_forms.py
@@ -708,7 +708,6 @@ def test_expiration_window_is_coerced_to_int(self):
def test_form_default_expiration_values_are_empty(self):
form = SsoSamlEnterpriseSettingsForm(self.idp)
- self.assertIsNone(form['always_show_user_api_keys'].value())
self.assertIsNone(form['enforce_user_api_key_expiration'].value())
self.assertIsNone(form['max_days_until_user_api_key_expiration'].value())
@@ -720,7 +719,7 @@ def test_form_is_valid_without_api_expiration_values(self):
def test_always_show_key_management_is_saved(self):
self._update_identity_provider(always_show_user_api_keys=False)
post_data = self._get_post_data(always_show_user_api_keys='on')
- form = SsoSamlEnterpriseSettingsForm(self.idp, post_data, uses_api_key_management=True)
+ form = SsoSamlEnterpriseSettingsForm(self.idp, post_data)
form.update_identity_provider(self.accounting_admin)
@@ -733,7 +732,7 @@ def test_max_days_until_user_api_key_expiration_is_saved(self):
enforce_user_api_key_expiration=True,
max_days_until_user_api_key_expiration=60
)
- form = SsoSamlEnterpriseSettingsForm(self.idp, post_data, uses_api_key_management=True)
+ form = SsoSamlEnterpriseSettingsForm(self.idp, post_data)
form.update_identity_provider(self.accounting_admin)
@@ -743,7 +742,7 @@ def test_max_days_until_user_api_key_expiration_is_saved(self):
def test_expiration_enforcement_can_be_removed(self):
self._update_identity_provider(max_days_until_user_api_key_expiration=60)
post_data = self._get_post_data(enforce_user_api_key_expiration=False)
- form = SsoSamlEnterpriseSettingsForm(self.idp, post_data, uses_api_key_management=True)
+ form = SsoSamlEnterpriseSettingsForm(self.idp, post_data)
form.update_identity_provider(self.accounting_admin)
diff --git a/corehq/apps/sso/tests/test_tasks.py b/corehq/apps/sso/tests/test_tasks.py
index 65e7e2e98626..cb267bd62068 100644
--- a/corehq/apps/sso/tests/test_tasks.py
+++ b/corehq/apps/sso/tests/test_tasks.py
@@ -4,13 +4,17 @@
from django.test import TestCase
from freezegun import freeze_time
+from corehq.apps.accounting.models import SoftwarePlanEdition
+from corehq.apps.accounting.tests import generator as accounting_generator
+from corehq.apps.domain.models import Domain
from corehq.apps.sso.certificates import DEFAULT_EXPIRATION
from django.contrib.auth.models import User
from corehq.apps.users.models import WebUser
from corehq.apps.users.models import HQApiKey
-from corehq.apps.sso.models import AuthenticatedEmailDomain
+from corehq.apps.sso.models import AuthenticatedEmailDomain, IdentityProviderType, UserExemptFromSingleSignOn
from corehq.apps.sso.tasks import (
IDP_CERT_EXPIRES_REMINDER_DAYS,
+ auto_deactivate_removed_sso_users,
idp_cert_expires_reminder,
renew_service_provider_x509_certificates,
create_rollover_service_provider_x509_certificates,
@@ -298,3 +302,101 @@ def _create_user(self, username):
def _create_key_for_user(self, user, expiration_date=None):
return HQApiKey.objects.create(
user=user.get_django_user(), key='key', name='test-key', expiration_date=expiration_date)
+
+
+class TestAutoDeactivationTask(TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.account = generator.get_billing_account_for_idp()
+ cls.account.enterprise_admin_emails = ['test@vaultwax.com']
+ cls.account.save()
+ cls.domain = Domain.get_or_create_with_name("vaultwax-001", is_active=True)
+ cls.addClassCleanup(cls.domain.delete)
+
+ enterprise_plan = accounting_generator.subscribable_plan_version(edition=SoftwarePlanEdition.ENTERPRISE)
+ accounting_generator.generate_domain_subscription(
+ cls.account,
+ cls.domain,
+ date_start=datetime.date.today(),
+ date_end=None,
+ plan_version=enterprise_plan,
+ is_active=True,
+ )
+
+ cls.idp = generator.create_idp('vaultwax', cls.account)
+ cls.idp.enable_user_deactivation = True
+ cls.idp.idp_type = IdentityProviderType.ENTRA_ID
+ cls.idp.save()
+ cls.email_domain = AuthenticatedEmailDomain.objects.create(
+ email_domain='vaultwax.com',
+ identity_provider=cls.idp,
+ )
+ idp_patcher = patch('corehq.apps.sso.models.IdentityProvider.get_all_members_of_the_idp')
+ cls.mock_get_all_members_of_the_idp = idp_patcher.start()
+ cls.addClassCleanup(idp_patcher.stop)
+
+ def setUp(self):
+ super().setUp()
+ self.web_user_a = self._create_web_user('a@vaultwax.com')
+ self.web_user_b = self._create_web_user('b@vaultwax.com')
+ self.web_user_c = self._create_web_user('c@vaultwax.com')
+
+ def test_user_is_deactivated_if_not_member_of_idp(self):
+ self.assertTrue(self.web_user_c.is_active)
+ self.mock_get_all_members_of_the_idp.return_value = [self.web_user_a.username, self.web_user_b.username]
+
+ auto_deactivate_removed_sso_users()
+
+ # Refetch Web User
+ web_user = WebUser.get_by_username(self.web_user_c.username)
+ self.assertFalse(web_user.is_active)
+
+ def test_sso_exempt_users_are_not_deactivated(self):
+ sso_exempt = self._create_web_user('exempt@vaultwax.com')
+ UserExemptFromSingleSignOn.objects.create(
+ username=sso_exempt.username,
+ email_domain=self.email_domain,
+ )
+ self.mock_get_all_members_of_the_idp.return_value = [self.web_user_a.username, self.web_user_b.username]
+
+ auto_deactivate_removed_sso_users()
+
+ # Refetch Web User
+ web_user = WebUser.get_by_username(sso_exempt.username)
+ self.assertTrue(web_user.is_active)
+
+ @patch('corehq.apps.sso.tasks.send_html_email_async.delay')
+ def test_deactivation_skipped_if_entra_return_empty_sso_user(self, mock_send):
+ self.mock_get_all_members_of_the_idp.return_value = []
+
+ auto_deactivate_removed_sso_users()
+
+ # Refetch Web User
+ web_user_a = WebUser.get_by_username(self.web_user_a.username)
+ self.assertTrue(web_user_a.is_active)
+ web_user_b = WebUser.get_by_username(self.web_user_b.username)
+ self.assertTrue(web_user_b.is_active)
+ web_user_c = WebUser.get_by_username(self.web_user_c.username)
+ self.assertTrue(web_user_c.is_active)
+ mock_send.assert_called_once()
+
+ def test_deactivation_skip_members_of_the_domains_but_not_have_an_email_domain_controlled_by_the_idp(self):
+ dimagi_user = self._create_web_user('superuser@dimagi.com')
+ self.mock_get_all_members_of_the_idp.return_value = [self.web_user_a.username, self.web_user_b.username]
+
+ auto_deactivate_removed_sso_users()
+
+ # Refetch Web User
+ dimagi_user = WebUser.get_by_username(dimagi_user.username)
+ self.assertTrue(dimagi_user.is_active)
+ web_user_c = WebUser.get_by_username(self.web_user_c.username)
+ self.assertFalse(web_user_c.is_active)
+
+ def _create_web_user(self, username):
+ user = WebUser.create(
+ self.domain.name, username, 'testpwd', None, None
+ )
+ self.addCleanup(user.delete, self.domain.name, deleted_by=None)
+ return user
diff --git a/corehq/apps/sso/utils/context_helpers.py b/corehq/apps/sso/utils/context_helpers.py
index edb90ae8abd2..a417cbed1f39 100644
--- a/corehq/apps/sso/utils/context_helpers.py
+++ b/corehq/apps/sso/utils/context_helpers.py
@@ -66,6 +66,57 @@ def get_idp_cert_expiration_email_context(idp):
return email_context
+def get_graph_api_connection_issue_email_context(idp, error):
+ subject = _("CommCare HQ Alert: SSO Remote User Management - Issue Connecting to Microsoft Graph API")
+ template_context = {
+ "error": error,
+ "contact_email": settings.ACCOUNTS_EMAIL,
+ "base_url": get_site_domain(),
+ }
+ body_html, body_txt = render_multiple_to_strings(
+ template_context,
+ "sso/email/microsoft_graph_api_connection_issue_notification.html",
+ "sso/email/microsoft_graph_api_connection_issue_notification.txt",
+ )
+ email_context = {
+ "subject": subject,
+ "from": _(f"Dimagi CommCare Accounts <{settings.ACCOUNTS_EMAIL}>"),
+ "to": idp.owner.enterprise_admin_emails or [idp.owner.dimagi_contact] or [settings.ACCOUNT_EMAIL],
+ "bcc": [settings.ACCOUNTS_EMAIL],
+ "html": body_html,
+ "plaintext": body_txt,
+ }
+ if idp.owner.dimagi_contact:
+ email_context["bcc"].append(idp.owner.dimagi_contact)
+ return email_context
+
+
+def get_sso_deactivation_skip_email_context(idp, failure_reason):
+ subject = _("CommCare HQ Alert: Temporarily skipped automatic deactivation of SSO Web Users"
+ " (Remote User Management)")
+ template_context = {
+ "contact_email": settings.ACCOUNTS_EMAIL,
+ "base_url": get_site_domain(),
+ "failure_reason": failure_reason,
+ }
+ body_html, body_txt = render_multiple_to_strings(
+ template_context,
+ "sso/email/sso_deactivation_skip_notification.html",
+ "sso/email/sso_deactivation_skip_notification.txt",
+ )
+ email_context = {
+ "subject": subject,
+ "from": _(f"Dimagi CommCare Accounts <{settings.ACCOUNTS_EMAIL}>"),
+ "to": idp.owner.enterprise_admin_emails or [idp.owner.dimagi_contact] or [settings.ACCOUNT_EMAIL],
+ "bcc": [settings.ACCOUNTS_EMAIL],
+ "html": body_html,
+ "plaintext": body_txt,
+ }
+ if idp.owner.dimagi_contact:
+ email_context["bcc"].append(idp.owner.dimagi_contact)
+ return email_context
+
+
def get_api_secret_expiration_email_context(idp):
"""
Utility to generate metadata and render messages for an IdP api secret
diff --git a/corehq/apps/sso/utils/entra.py b/corehq/apps/sso/utils/entra.py
new file mode 100644
index 000000000000..847fd60eac3a
--- /dev/null
+++ b/corehq/apps/sso/utils/entra.py
@@ -0,0 +1,83 @@
+import json
+import requests
+
+from corehq.apps.sso.exceptions import EntraVerificationFailed
+
+
+class MSGraphIssue:
+ HTTP_ERROR = "http_error"
+ VERIFICATION_ERROR = "verification_error"
+ EMPTY_ERROR = "empty_error"
+ OTHER_ERROR = "other_error"
+
+
+def get_all_members_of_the_idp_from_entra(idp):
+ import msal
+ authority_base_url = "https://login.microsoftonline.com/"
+ authority = f"{authority_base_url}{idp.api_host}"
+
+ config = {
+ "authority": authority,
+ "client_id": idp.api_id,
+ "scope": ["https://graph.microsoft.com/.default"],
+ "secret": idp.api_secret,
+ "endpoint": f"https://graph.microsoft.com/v1.0/servicePrincipals(appId='{idp.api_id}')/"
+ "appRoleAssignedTo?$select=principalId, principalType"
+ }
+
+ # Create a preferably long-lived app instance which maintains a token cache.
+ app = msal.ConfidentialClientApplication(
+ config["client_id"], authority=config["authority"],
+ client_credential=config["secret"],
+ )
+ # looks up a token from cache
+ result = app.acquire_token_silent(config["scope"], account=None)
+ if not result:
+ result = app.acquire_token_for_client(scopes=config["scope"])
+ if "access_token" in result:
+ # Calling graph using the access token
+ response = requests.get(
+ config["endpoint"],
+ headers={'Authorization': 'Bearer ' + result['access_token']},
+ )
+ response.raise_for_status() # Raises an error for bad status
+ assignments = response.json()
+
+ # microsoft.graph.appRoleAssignment's property doesn't userPrincipalName
+ # Property principalType can either be User, Group or ServicePrincipal
+ principal_ids = {assignment["principalId"] for assignment in assignments["value"]
+ if assignment["principalType"] == "User"}
+
+ # Prepare batch request
+ batch_payload = {
+ "requests": [
+ {
+ "id": str(i),
+ "method": "GET",
+ "url": f"/users/{principal_id}?$select=userPrincipalName"
+ } for i, principal_id in enumerate(principal_ids)
+ ]
+ }
+
+ # Send batch request
+ batch_response = requests.post(
+ 'https://graph.microsoft.com/v1.0/$batch',
+ headers={'Authorization': 'Bearer ' + result['access_token'], 'Content-Type': 'application/json'},
+ data=json.dumps(batch_payload)
+ )
+ batch_response.raise_for_status()
+ batch_result = batch_response.json()
+
+ for resp in batch_result['responses']:
+ if 'body' in resp and 'error' in resp['body']:
+ raise EntraVerificationFailed(resp['body']['error']['code'], resp['body']['message'])
+
+ # Extract userPrincipalName from batch response
+ user_principal_names = [
+ resp['body']['userPrincipalName'] for resp in batch_result['responses']
+ if 'body' in resp and 'userPrincipalName' in resp['body']
+ ]
+ return user_principal_names
+ else:
+ raise EntraVerificationFailed(result.get('error', {}),
+ result.get('error_description', 'No error description provided'))
diff --git a/corehq/apps/sso/views/enterprise_admin.py b/corehq/apps/sso/views/enterprise_admin.py
index 89b4c3360fbe..66a8f38711bf 100644
--- a/corehq/apps/sso/views/enterprise_admin.py
+++ b/corehq/apps/sso/views/enterprise_admin.py
@@ -72,7 +72,6 @@ def page_context(self):
'idp_slug': self.idp_slug,
'is_oidc': self.identity_provider.protocol == IdentityProviderProtocol.OIDC,
'show_remote_user_management': self.show_remote_user_management,
- 'show_api_fields': self.uses_api_key_management,
}
@property
@@ -116,24 +115,18 @@ def edit_enterprise_idp_form(self):
self.identity_provider,
self.request.POST,
self.request.FILES,
- show_remote_user_management=self.show_remote_user_management,
- uses_api_key_management=self.uses_api_key_management
+ show_remote_user_management=self.show_remote_user_management
)
return form_class(
self.identity_provider,
- show_remote_user_management=self.show_remote_user_management,
- uses_api_key_management=self.uses_api_key_management
+ show_remote_user_management=self.show_remote_user_management
)
@property
def show_remote_user_management(self):
return toggles.SSO_REMOTE_USER_MANAGEMENT.enabled_for_request(self.request)
- @property
- def uses_api_key_management(self):
- return toggles.MULTI_VIEW_API_KEYS.enabled_for_request(self.request)
-
def post(self, request, *args, **kwargs):
if self.async_response is not None:
return self.async_response
diff --git a/corehq/apps/userreports/reports/view.py b/corehq/apps/userreports/reports/view.py
index e9fc68f3e693..5705aae8a951 100644
--- a/corehq/apps/userreports/reports/view.py
+++ b/corehq/apps/userreports/reports/view.py
@@ -454,7 +454,7 @@ def get_ajax(self, params):
if settings.DEBUG:
raise
return self.render_json_response({
- 'error': str(e),
+ 'error_message': str(e),
'aaData': [],
'iTotalRecords': 0,
'iTotalDisplayRecords': 0,
diff --git a/corehq/apps/users/dbaccessors.py b/corehq/apps/users/dbaccessors.py
index 8bf641462606..8d5d43077976 100644
--- a/corehq/apps/users/dbaccessors.py
+++ b/corehq/apps/users/dbaccessors.py
@@ -221,6 +221,10 @@ def get_all_user_id_username_pairs_by_domain(domain, include_web_users=True, inc
))
+def get_active_web_usernames_by_domain(domain):
+ return (row['key'][3] for row in get_all_user_rows(domain, include_mobile_users=False, include_inactive=False))
+
+
def get_web_user_count(domain, include_inactive=True):
return sum([
row['value']
diff --git a/corehq/apps/users/migrations/0061_auto_20240423_0802.py b/corehq/apps/users/migrations/0061_auto_20240423_0802.py
new file mode 100644
index 000000000000..1efc126bf0f4
--- /dev/null
+++ b/corehq/apps/users/migrations/0061_auto_20240423_0802.py
@@ -0,0 +1,22 @@
+# Generated by Django 3.2.25 on 2024-04-23 08:02
+
+from django.db import migrations
+
+from corehq.apps.users.models_role import Permission
+from corehq.util.django_migrations import skip_on_fresh_install
+
+
+@skip_on_fresh_install
+def create_commcare_analytics_permissions(*args, **kwargs):
+ Permission.create_all()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0060_invitations_addtableau_roles_and_groupids'),
+ ]
+
+ operations = [
+ migrations.RunPython(create_commcare_analytics_permissions, reverse_code=migrations.RunPython.noop)
+ ]
diff --git a/corehq/apps/users/models.py b/corehq/apps/users/models.py
index 37485b5b0aa5..d582cfd9dab1 100644
--- a/corehq/apps/users/models.py
+++ b/corehq/apps/users/models.py
@@ -161,6 +161,7 @@ def allowed_items(self):
'view_data_registry_contents': 'view_data_registry_contents_list',
'view_reports': 'view_report_list',
'view_tableau': 'view_tableau_list',
+ 'commcare_analytics_roles': 'commcare_analytics_roles_list',
}
@@ -225,6 +226,12 @@ class HqPermissions(DocumentSchema):
manage_domain_alerts = BooleanProperty(default=False)
+ view_commcare_analytics = BooleanProperty(default=False)
+ edit_commcare_analytics = BooleanProperty(default=False)
+
+ commcare_analytics_roles = BooleanProperty(default=False)
+ commcare_analytics_roles_list = StringListProperty(default=[])
+
@classmethod
def from_permission_list(cls, permission_list):
"""Converts a list of Permission objects into a Permissions object"""
diff --git a/corehq/apps/users/static/users/js/roles.js b/corehq/apps/users/static/users/js/roles.js
index 0799446bfedc..6aa30f6770b7 100644
--- a/corehq/apps/users/static/users/js/roles.js
+++ b/corehq/apps/users/static/users/js/roles.js
@@ -143,6 +143,17 @@ hqDefine('users/js/roles',[
}),
};
+ data.commcareAnalyticsRoles = {
+ all: data.permissions.commcare_analytics_roles,
+ specific: ko.utils.arrayMap(o.commcareAnalyticsRoles, function (role) {
+ return {
+ name: role.name,
+ slug: role.slug,
+ value: data.permissions.commcare_analytics_roles_list.indexOf(role.slug) !== -1,
+ };
+ }),
+ };
+
self = ko.mapping.fromJS(data);
let filterSpecific = (permissions) => {
return ko.computed(function () {
@@ -158,6 +169,7 @@ hqDefine('users/js/roles',[
self.tableauPermissions.filteredSpecific = filterSpecific(self.tableauPermissions);
self.manageRegistryPermission.filteredSpecific = filterSpecific(self.manageRegistryPermission);
self.viewRegistryContentsPermission.filteredSpecific = filterSpecific(self.viewRegistryContentsPermission);
+ self.commcareAnalyticsRoles.filteredSpecific = filterSpecific(self.commcareAnalyticsRoles);
self.canSeeAnyReports = ko.computed(function () {
return self.reportPermissions.all() || _.any(self.reportPermissions.specific(), (p) => p.value());
});
@@ -406,6 +418,22 @@ hqDefine('users/js/roles',[
allowCheckboxId: null,
allowCheckboxPermission: null,
},
+ {
+ showOption: toggles.toggleEnabled("SUPERSET_ANALYTICS"),
+ editPermission: self.permissions.edit_commcare_analytics,
+ viewPermission: self.permissions.view_commcare_analytics,
+ text: gettext("CommCare Analytics — manage CommCare Analytics associated with this project"),
+ showEditCheckbox: true,
+ editCheckboxLabel: "edit-commcare-analytics-checkbox",
+ showViewCheckbox: true,
+ viewCheckboxLabel: "view-commcare-analytics-checkbox",
+ screenReaderEditAndViewText: gettext("Edit & View CommCare Analytics"),
+ screenReaderViewOnlyText: gettext("View-Only CommCare Analytics"),
+ showAllowCheckbox: false,
+ allowCheckboxText: null,
+ allowCheckboxId: null,
+ allowCheckboxPermission: null,
+ },
];
var hasEmbeddedTableau = toggles.toggleEnabled("EMBEDDED_TABLEAU");
@@ -547,6 +575,10 @@ hqDefine('users/js/roles',[
data.permissions.view_data_registry_contents_list = unwrapItemList(
data.viewRegistryContentsPermission.specific);
+ data.permissions.commcare_analytics_roles = data.commcareAnalyticsRoles.all;
+ data.permissions.commcare_analytics_roles_list = unwrapItemList(
+ data.commcareAnalyticsRoles.specific);
+
data.is_non_admin_editable = data.manageRoleAssignments.all;
data.assignable_by = unwrapItemList(data.manageRoleAssignments.specific, 'path');
return data;
diff --git a/corehq/apps/users/static/users/js/roles_and_permissions.js b/corehq/apps/users/static/users/js/roles_and_permissions.js
index 333c46fb5afa..788aa7a53279 100644
--- a/corehq/apps/users/static/users/js/roles_and_permissions.js
+++ b/corehq/apps/users/static/users/js/roles_and_permissions.js
@@ -75,6 +75,7 @@ hqDefine("users/js/roles_and_permissions",[
ExportOwnershipEnabled: initialPageData.get("export_ownership_enabled"),
dataRegistryChoices: initialPageData.get("data_registry_choices"),
canEditLinkedData: initialPageData.get("can_edit_linked_data"),
+ commcareAnalyticsRoles: initialPageData.get('commcare_analytics_roles'),
});
});
});
diff --git a/corehq/apps/users/templates/users/partials/edit_role_modal.html b/corehq/apps/users/templates/users/partials/edit_role_modal.html
index cc6b3319b2b3..093726401672 100644
--- a/corehq/apps/users/templates/users/partials/edit_role_modal.html
+++ b/corehq/apps/users/templates/users/partials/edit_role_modal.html
@@ -251,7 +251,49 @@
{% endif %}
-
+ {% if request|toggle_enabled:"SUPERSET_ANALYTICS" %}
+
+ {% trans "CommCare Analytics" %}
+
+
+
+ {% endif %}
{% trans "Other Settings" %}
@@ -465,8 +507,7 @@
-
- {% if request|request_has_privilege:"CUSTOM_DOMAIN_ALERTS" %}
+ {% if request|request_has_privilege:"CUSTOM_DOMAIN_ALERTS" %}
{% trans "Manage Project Alerts" %}
@@ -482,7 +523,9 @@
- {% endif %}
+ {% endif %}
+
+