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 %} +
+ Create New +
+ + + + + + + + + + + + + {% for workflow in workflows %} + + + + + + + + + {% endfor %} + +
NameDomainAppUserLast Run
{{ workflow.name }}{{ workflow.domain }}{{ workflow.app_name }}{{ workflow.django_user.username }}{{ workflow.last_run|default:"" }}Test
+{% 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 %} +
+ {% csrf_token %} + +
+{% 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) { %> -
-
- - -
-

{% trans "Incomplete Forms" %}

-
-
-
- <% } %> - - {% if not request|toggle_enabled:"HIDE_SYNC_BUTTON" %} -
-
- -
-

{% trans "Sync" %}

-
-
-
- {% endif %} - - {% if request|can_use_restore_as %} -
-
- -
-

{% trans "Log in as" %}

-
-
-
- {% endif %} -
-
- -
-

{% trans "Settings" %}

-
-
-
-
- - -{# 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 = """ +
Not a real 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 %}
{% endif %} - + {% if request|toggle_enabled:"SUPERSET_ANALYTICS" %} + + {% trans "CommCare Analytics" %} + +
+ +
+
+ + +
+
+
+
+ +
+
+
+ {% trans "Commcare Analytics Roles:" %} +
+
+
+ + +
+
+
+
+
+ {% endif %} {% trans "Other Settings" %} @@ -465,8 +507,7 @@ - - {% if request|request_has_privilege:"CUSTOM_DOMAIN_ALERTS" %} + {% if request|request_has_privilege:"CUSTOM_DOMAIN_ALERTS" %}
- {% endif %} + {% endif %} + +