- <%= _.template($('#maybe-custom-badge-template').text())(arguments[0]) %>{# maybe-custom-badge-template output is already escaped #}
+ <%- badgeText %>
diff --git a/corehq/apps/cloudcare/templates/cloudcare/partials/menu/row.html b/corehq/apps/cloudcare/templates/cloudcare/partials/menu/row.html
index b8b7c5919b7d..f2f3378e7c02 100644
--- a/corehq/apps/cloudcare/templates/cloudcare/partials/menu/row.html
+++ b/corehq/apps/cloudcare/templates/cloudcare/partials/menu/row.html
@@ -2,7 +2,7 @@
<% if (imageUrl) { %>
- <%= _.template($('#maybe-custom-badge-template').text())(arguments[0]) %>{# maybe-custom-badge-template output is already escaped #}
+ <%- badgeText %>
<% } else { %>
@@ -11,7 +11,7 @@
<% } else { %>
<% } %>
- <%= _.template($('#maybe-custom-badge-template').text())(arguments[0]) %>{# maybe-custom-badge-template output is already escaped #}
+ <%- badgeText %>
<% } %>
|
diff --git a/corehq/apps/cloudcare/templates/cloudcare/preview_app_base.html b/corehq/apps/cloudcare/templates/cloudcare/preview_app_base.html
index 62b81b425b60..ee5d579b4535 100644
--- a/corehq/apps/cloudcare/templates/cloudcare/preview_app_base.html
+++ b/corehq/apps/cloudcare/templates/cloudcare/preview_app_base.html
@@ -2,7 +2,7 @@
{% load compress %}
{% load statici18n %}
-{% requirejs_main_b5 "cloudcare/js/preview_app/main" %}
+{% js_entry "cloudcare/js/preview_app/main" %}
@@ -50,14 +50,14 @@
{# DO NOT COMPRESS #}
- {% include "hqwebapp/partials/requirejs.html" %}
+ {% include "hqwebapp/partials/webpack.html" %}
- {# This is fine as an inline script; it'll be removed once form designer is migrated to RequireJS #}
+ {# This is fine as an inline script; it'll be removed once all of HQ is using webpack #}
@@ -77,6 +77,7 @@
{# do not override this block, use initial_page_data template tag to populate #}
{% endblock %}
{% block registered_urls %}
{# do not override this block, use registerurl template tag to populate #}
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 45691d6db14a..f918e608c24b 100644
--- a/corehq/apps/cloudcare/templates/cloudcare/spec/form_entry/mocha.html
+++ b/corehq/apps/cloudcare/templates/cloudcare/spec/form_entry/mocha.html
@@ -1,18 +1,6 @@
{% extends "mocha/base.html" %}
{% load hq_shared_tags %}
-
-{% requirejs_main_b5 "cloudcare/js/form_entry/spec/main" %}
-
-{% block stylesheets %}{{ block.super }}
-
-{% endblock %}
-
-{% block dependencies %}
- {% include "cloudcare/partials/dependencies.html" %}
-{% endblock %}
+{% js_entry "cloudcare/js/form_entry/spec/main" %}
{% block fixtures %}
diff --git a/corehq/apps/cloudcare/templates/cloudcare/spec/mocha.html b/corehq/apps/cloudcare/templates/cloudcare/spec/mocha.html
index 3cedc4fe1f95..157d7cc77ddd 100644
--- a/corehq/apps/cloudcare/templates/cloudcare/spec/mocha.html
+++ b/corehq/apps/cloudcare/templates/cloudcare/spec/mocha.html
@@ -1,11 +1,6 @@
{% extends "mocha/base.html" %}
{% load hq_shared_tags %}
-
-{% requirejs_main_b5 "cloudcare/js/formplayer/spec/main" %}
-
-{% block dependencies %}
- {% include "cloudcare/partials/dependencies.html" %}
-{% endblock %}
+{% js_entry "cloudcare/js/formplayer/spec/main" %}
{% block fixtures %}
@@ -17,5 +12,4 @@
{% include 'cloudcare/partials/confirmation_modal.html' %}
{% include 'cloudcare/partials/all_templates.html' %}
-
{% endblock %}
diff --git a/corehq/apps/domain/deletion.py b/corehq/apps/domain/deletion.py
index 2714f2124206..8ca2791647c8 100644
--- a/corehq/apps/domain/deletion.py
+++ b/corehq/apps/domain/deletion.py
@@ -398,6 +398,7 @@ def _delete_demo_user_restores(domain_name):
ModelDeletion('integration', 'GaenOtpServerSettings', 'domain'),
ModelDeletion('integration', 'HmacCalloutSettings', 'domain'),
ModelDeletion('integration', 'SimprintsIntegration', 'domain'),
+ ModelDeletion('integration', 'KycConfig', 'domain'),
ModelDeletion('linked_domain', 'DomainLink', 'linked_domain', ['DomainLinkHistory']),
CustomDeletion('scheduling', _delete_sms_content_events_schedules, [
'SMSContent', 'EmailContent', 'SMSSurveyContent',
diff --git a/corehq/apps/domain/forms.py b/corehq/apps/domain/forms.py
index 68584dbbd973..124a01b7b0a7 100644
--- a/corehq/apps/domain/forms.py
+++ b/corehq/apps/domain/forms.py
@@ -952,7 +952,8 @@ def __init__(self, *args, **kwargs):
excluded_fields.append('secure_sessions_timeout')
for field in self.fields.values():
- if not isinstance(field.widget, BootstrapCheckboxInput):
+ has_custom_input = isinstance(field.widget, BootstrapCheckboxInput)
+ if isinstance(field, BooleanField) and not has_custom_input:
field.widget = BootstrapCheckboxInput()
fields = [hqcrispy.CheckboxField(field_name)
diff --git a/corehq/apps/dump_reload/sql/dump.py b/corehq/apps/dump_reload/sql/dump.py
index f683e2d94816..83bd46e2894b 100644
--- a/corehq/apps/dump_reload/sql/dump.py
+++ b/corehq/apps/dump_reload/sql/dump.py
@@ -135,6 +135,7 @@
FilteredModelIteratorBuilder('integration.GaenOtpServerSettings', SimpleFilter('domain')),
FilteredModelIteratorBuilder('integration.HmacCalloutSettings', SimpleFilter('domain')),
FilteredModelIteratorBuilder('integration.SimprintsIntegration', SimpleFilter('domain')),
+ FilteredModelIteratorBuilder('integration.KycConfig', SimpleFilter('domain')),
FilteredModelIteratorBuilder('phonelog.DeviceReportEntry', SimpleFilter('domain')),
FilteredModelIteratorBuilder('phonelog.ForceCloseEntry', SimpleFilter('domain')),
FilteredModelIteratorBuilder('phonelog.UserErrorEntry', SimpleFilter('domain')),
diff --git a/corehq/apps/enterprise/api/api.py b/corehq/apps/enterprise/api/api.py
index a0c8508b5e54..265991d2864b 100644
--- a/corehq/apps/enterprise/api/api.py
+++ b/corehq/apps/enterprise/api/api.py
@@ -10,8 +10,10 @@
WebUserResource,
DataExportReportResource,
SMSResource,
- APIUsageResource,
+ APIKeysResource,
TwoFactorAuthResource,
+ DataForwardingResource,
+ ApplicationVersionComplianceResource,
)
v1_api = Api(api_name='v1')
@@ -24,5 +26,7 @@
v1_api.register(DataExportReportResource())
v1_api.register(CommCareVersionComplianceResource())
v1_api.register(SMSResource())
-v1_api.register(APIUsageResource())
+v1_api.register(APIKeysResource())
v1_api.register(TwoFactorAuthResource())
+v1_api.register(DataForwardingResource())
+v1_api.register(ApplicationVersionComplianceResource())
diff --git a/corehq/apps/enterprise/api/resources.py b/corehq/apps/enterprise/api/resources.py
index 29cb9a22c5b6..49b40ba66335 100644
--- a/corehq/apps/enterprise/api/resources.py
+++ b/corehq/apps/enterprise/api/resources.py
@@ -14,6 +14,7 @@
get_account_or_404,
request_has_permissions_for_enterprise_admin,
)
+from corehq.apps.analytics.tasks import record_event
from corehq.apps.api.odata.utils import FieldMetadata
from corehq.apps.api.odata.views import add_odata_headers
from corehq.apps.api.resources import HqBaseResource
@@ -21,6 +22,7 @@
from corehq.apps.api.resources.meta import get_hq_throttle
from corehq.apps.api.keyset_paginator import KeysetPaginator
from corehq.apps.enterprise.enterprise import EnterpriseReport
+from corehq.apps.enterprise.metric_events import ENTERPRISE_API_ACCESS
from corehq.apps.enterprise.iterators import IterableEnterpriseFormQuery, EnterpriseFormReportConverter
from corehq.apps.enterprise.tasks import generate_enterprise_report, ReportTaskProgress
@@ -188,6 +190,9 @@ def get_object_list(self, request):
return data
elif status == ReportTaskProgress.STATUS_NEW:
progress.start_task(self.get_report_task(request))
+ record_event(ENTERPRISE_API_ACCESS, request.couch_user, {
+ 'api_type': self.REPORT_SLUG
+ })
# PowerBI respects delays with only two response codes:
# 429 (TooManyRequests) and 503 (ServiceUnavailable). Although 503 is likely more semantically
@@ -398,6 +403,11 @@ def get_object_list(self, request):
converter = EnterpriseFormReportConverter()
query_kwargs = converter.get_kwargs_from_map(request.GET)
+ if converter.is_initial_query(request.GET):
+ record_event(ENTERPRISE_API_ACCESS, request.couch_user, {
+ 'api_type': self.REPORT_SLUG
+ })
+
return IterableEnterpriseFormQuery(account, converter, start_date, end_date, **query_kwargs)
def dehydrate(self, bundle):
@@ -490,7 +500,7 @@ def get_primary_keys(self):
return ('mobile_worker', 'domain',)
-class APIUsageResource(ODataEnterpriseReportResource):
+class APIKeysResource(ODataEnterpriseReportResource):
web_user = fields.CharField()
api_key_name = fields.CharField()
scope = fields.CharField()
@@ -498,7 +508,7 @@ class APIUsageResource(ODataEnterpriseReportResource):
created_date = fields.DateTimeField()
last_used_date = fields.DateTimeField()
- REPORT_SLUG = EnterpriseReport.API_USAGE
+ REPORT_SLUG = EnterpriseReport.API_KEYS
def dehydrate(self, bundle):
bundle.data['web_user'] = bundle.obj[0]
@@ -511,3 +521,45 @@ def dehydrate(self, bundle):
def get_primary_keys(self):
return ('web_user', 'api_key_name',)
+
+
+class DataForwardingResource(ODataEnterpriseReportResource):
+ domain = fields.CharField()
+ service_name = fields.CharField()
+ service_type = fields.CharField()
+ last_modified = fields.DateTimeField()
+
+ REPORT_SLUG = EnterpriseReport.DATA_FORWARDING
+
+ def dehydrate(self, bundle):
+ bundle.data['domain'] = bundle.obj[0]
+ bundle.data['service_name'] = bundle.obj[1]
+ bundle.data['service_type'] = bundle.obj[2]
+ bundle.data['last_modified'] = self.convert_datetime(bundle.obj[3])
+ return bundle
+
+ def get_primary_keys(self):
+ return ('domain', 'service_name', 'service_type')
+
+
+class ApplicationVersionComplianceResource(ODataEnterpriseReportResource):
+ mobile_worker = fields.CharField()
+ domain = fields.CharField()
+ application = fields.CharField()
+ latest_version_available_when_last_used = fields.CharField()
+ version_in_use = fields.CharField()
+ last_used = fields.DateTimeField()
+
+ REPORT_SLUG = EnterpriseReport.APP_VERSION_COMPLIANCE
+
+ def dehydrate(self, bundle):
+ bundle.data['mobile_worker'] = bundle.obj[0]
+ bundle.data['domain'] = bundle.obj[1]
+ bundle.data['application'] = bundle.obj[2]
+ bundle.data['latest_version_available_when_last_used'] = bundle.obj[3]
+ bundle.data['version_in_use'] = bundle.obj[4]
+ bundle.data['last_used'] = self.convert_datetime(bundle.obj[5])
+ return bundle
+
+ def get_primary_keys(self):
+ return ('mobile_worker', 'application',)
diff --git a/corehq/apps/enterprise/enterprise.py b/corehq/apps/enterprise/enterprise.py
index 1864d2238a83..68ed6dcffdd4 100644
--- a/corehq/apps/enterprise/enterprise.py
+++ b/corehq/apps/enterprise/enterprise.py
@@ -5,6 +5,7 @@
from django.conf import settings
from django.contrib.auth.models import User
from django.db.models import Count, Subquery, Q
+from dimagi.ext.jsonobject import DateTimeProperty
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy
@@ -21,7 +22,8 @@
)
from corehq.apps.accounting.models import BillingAccount
from corehq.apps.accounting.utils import get_default_domain_url
-from corehq.apps.app_manager.dbaccessors import get_brief_apps_in_domain
+from corehq.apps.app_manager.dbaccessors import get_app_ids_in_domain, get_brief_apps_in_domain
+from corehq.apps.app_manager.models import Application
from corehq.apps.builds.utils import get_latest_version_at_time, is_out_of_date
from corehq.apps.builds.models import CommCareBuildConfig
from corehq.apps.domain.calculations import sms_in_last
@@ -31,9 +33,7 @@
TooMuchRequestedDataError,
)
from corehq.apps.enterprise.iterators import raise_after_max_elements
-from corehq.apps.es import forms as form_es
-from corehq.apps.es import filters
-from corehq.apps.es.apps import AppES
+from corehq.apps.es import AppES, filters, forms as form_es
from corehq.apps.es.users import UserES
from corehq.apps.export.dbaccessors import ODataExportFetcher
from corehq.apps.reports.util import (
@@ -47,6 +47,8 @@
)
from corehq.apps.users.models import CouchUser, HQApiKey, Invitation, WebUser
+from corehq.motech.repeaters.models import Repeater
+
class EnterpriseReport(ABC):
DOMAINS = 'domains'
@@ -58,8 +60,10 @@ class EnterpriseReport(ABC):
DATA_EXPORTS = 'data_exports'
COMMCARE_VERSION_COMPLIANCE = 'commcare_version_compliance'
SMS = 'sms'
- API_USAGE = 'api_usage'
+ API_KEYS = 'api_keys'
TWO_FACTOR_AUTH = '2fa'
+ DATA_FORWARDING = 'data_forwarding'
+ APP_VERSION_COMPLIANCE = 'app_version_compliance'
DATE_ROW_FORMAT = '%Y/%m/%d %H:%M:%S'
@@ -69,12 +73,11 @@ def title(self):
pass
@property
- @abstractmethod
def total_description(self):
"""
To provide a description of the total number we displayed in tile
"""
- pass
+ return ''
def __init__(self, account, couch_user, **kwargs):
self.account = account
@@ -111,10 +114,14 @@ def create(cls, slug, account_id, couch_user, **kwargs):
report = EnterpriseCommCareVersionReport(account, couch_user, **kwargs)
elif slug == cls.SMS:
report = EnterpriseSMSReport(account, couch_user, **kwargs)
- elif slug == cls.API_USAGE:
+ elif slug == cls.API_KEYS:
report = EnterpriseAPIReport(account, couch_user, **kwargs)
elif slug == cls.TWO_FACTOR_AUTH:
report = Enterprise2FAReport(account, couch_user, **kwargs)
+ elif slug == cls.DATA_FORWARDING:
+ report = EnterpriseDataForwardingReport(account, couch_user, **kwargs)
+ elif slug == cls.APP_VERSION_COMPLIANCE:
+ report = EnterpriseAppVersionComplianceReport(account, couch_user, **kwargs)
if report:
report.slug = slug
@@ -160,7 +167,6 @@ def total(self):
class EnterpriseDomainReport(EnterpriseReport):
title = gettext_lazy('Project Spaces')
- total_description = gettext_lazy('# of Project Spaces')
def __init__(self, account, couch_user):
super().__init__(account, couch_user)
@@ -192,7 +198,6 @@ def total_for_domain(self, domain_obj):
class EnterpriseWebUserReport(EnterpriseReport):
title = gettext_lazy('Web Users')
- total_description = gettext_lazy('# of Web Users')
@property
def headers(self):
@@ -239,7 +244,6 @@ def total_for_domain(self, domain_obj):
class EnterpriseMobileWorkerReport(EnterpriseReport):
title = gettext_lazy('Mobile Workers')
- total_description = gettext_lazy('# of Mobile Workers')
@property
def headers(self):
@@ -271,7 +275,6 @@ def total_for_domain(self, domain_obj):
class EnterpriseFormReport(EnterpriseReport):
title = gettext_lazy('Mobile Form Submissions')
- total_description = gettext_lazy('# of Mobile Form Submissions')
MAXIMUM_USERS_PER_DOMAIN = getattr(settings, 'ENTERPRISE_REPORT_DOMAIN_USER_LIMIT', 20_000)
MAXIMUM_ROWS_PER_REQUEST = getattr(settings, 'ENTERPRISE_REPORT_ROW_LIMIT', 1_000_000)
@@ -380,7 +383,6 @@ def total_for_domain(self, domain_obj):
class EnterpriseODataReport(EnterpriseReport):
title = gettext_lazy('OData Feeds')
- total_description = gettext_lazy('# of OData Feeds')
MAXIMUM_EXPECTED_EXPORTS = 150
@@ -476,7 +478,6 @@ def app_query(self, domain):
class EnterpriseDataExportReport(EnterpriseReport):
title = gettext_lazy('Data Exports')
- total_description = gettext_lazy('# of Exports')
@property
def headers(self):
@@ -617,12 +618,6 @@ def rows_for_domain(self, domain, config, cache):
return rows
-def _format_percentage_for_enterprise_tile(dividend, divisor):
- if not divisor:
- return '--'
- return f"{dividend / divisor * 100:.1f}%"
-
-
class EnterpriseSMSReport(EnterpriseReport):
title = gettext_lazy('SMS Usage')
total_description = gettext_lazy('# of SMS Sent')
@@ -685,8 +680,7 @@ def rows_for_domain(self, domain_obj):
class EnterpriseAPIReport(EnterpriseReport):
- title = gettext_lazy('API Usage')
- total_description = gettext_lazy('# of Active API Keys')
+ title = gettext_lazy('API Keys')
@property
def headers(self):
@@ -749,3 +743,163 @@ def rows_for_domain(self, domain_obj):
if domain_obj.two_factor_auth:
return []
return [(domain_obj.name,)]
+
+
+class EnterpriseDataForwardingReport(EnterpriseReport):
+ title = gettext_lazy('Data Forwarding')
+ total_description = gettext_lazy('# of Data Forwarders')
+
+ @property
+ def headers(self):
+ return [_('Project Space'), _('Service Name'), _('Type'), _('Last Modified [UTC]')]
+
+ def total_for_domain(self, domain_obj):
+ return Repeater.objects.filter(domain=domain_obj.name).count()
+
+ def rows_for_domain(self, domain_obj):
+ repeaters = Repeater.objects.filter(domain=domain_obj.name)
+ rows = []
+ for repeater in repeaters:
+ rows.append(
+ [
+ domain_obj.name,
+ repeater.name,
+ repeater.friendly_name,
+ self.format_date(repeater.last_modified)
+ ]
+ )
+
+ return rows
+
+
+class EnterpriseAppVersionComplianceReport(EnterpriseReport):
+ title = gettext_lazy('Application Version Compliance')
+ total_description = gettext_lazy('The statistic of this tile is not currently supported')
+
+ def __init__(self, account, couch_user):
+ super().__init__(account, couch_user)
+ self.builds_by_app_id = {}
+ self.build_info_cache = {}
+
+ @property
+ def headers(self):
+ return [
+ _('Mobile Worker'),
+ _('Project Space'),
+ _('Application'),
+ _('Latest Version Available When Last Used'),
+ _('Version in Use'),
+ _('Last Used [UTC]'),
+ ]
+
+ @property
+ def rows(self):
+ rows = []
+ for domain in self.account.get_domains():
+ rows.extend(self.rows_for_domain(domain))
+ return rows
+
+ @property
+ def total(self):
+ # Skip the stat for this report due to performance issue
+ return '--'
+
+ def rows_for_domain(self, domain):
+ rows = []
+ app_name_by_id = {}
+ app_ids = get_app_ids_in_domain(domain)
+
+ for build_and_latest_version in self.all_last_builds_with_latest_version(domain, app_ids):
+ version_in_use = str(build_and_latest_version['build']['build_version'])
+ latest_version = str(build_and_latest_version['latest_version'])
+ if is_out_of_date(version_in_use, latest_version):
+ app_id = build_and_latest_version['build']['app_id']
+ if app_id not in app_name_by_id:
+ app_name_by_id[app_id] = Application.get_db().get(app_id).get('name')
+ rows.append([
+ build_and_latest_version['username'],
+ domain,
+ app_name_by_id[app_id],
+ latest_version,
+ version_in_use,
+ self.format_date(
+ DateTimeProperty.deserialize(
+ build_and_latest_version['build']['build_version_date']
+ )
+ ),
+ ])
+
+ return rows
+
+ def all_last_builds_with_latest_version(self, domain, app_ids):
+ user_query = (UserES()
+ .domain(domain)
+ .mobile_users()
+ .source([
+ 'username',
+ 'reporting_metadata.last_builds',
+ ]))
+ for user in user_query.run().hits:
+ last_builds = user.get('reporting_metadata', {}).get('last_builds', [])
+ for build in last_builds:
+ app_id = build.get('app_id')
+ build_version = build.get('build_version')
+ if app_id not in app_ids or not build_version:
+ continue
+ build_version_date = DateTimeProperty.deserialize(build.get('build_version_date'))
+ latest_version = self.get_latest_build_version(domain, app_id, build_version_date)
+ yield {
+ 'username': user['username'],
+ 'build': build,
+ 'latest_version': latest_version,
+ }
+
+ def get_latest_build_version(self, domain, app_id, at_datetime):
+ builds = self.get_app_builds(domain, app_id)
+ latest_build = self._find_latest_build_version_from_builds(builds, at_datetime)
+
+ return latest_build
+
+ def get_app_builds(self, domain, app_id):
+ if app_id in self.builds_by_app_id:
+ return self.builds_by_app_id[app_id]
+
+ app_es = (
+ AppES()
+ .domain(domain)
+ .is_build()
+ .app_id(app_id)
+ .sort('version', desc=True)
+ .is_released()
+ .source(['_id', 'version', 'last_released', 'built_on'])
+ )
+ self.builds_by_app_id[app_id] = app_es.run().hits
+ return self.builds_by_app_id[app_id]
+
+ def _find_latest_build_version_from_builds(self, all_builds, at_datetime):
+ for build_doc in all_builds:
+ build_info = self._get_build_info(build_doc)
+ if build_info['last_released'] <= at_datetime:
+ return build_info['version']
+ return None
+
+ def _get_build_info(self, build_doc):
+ build_id = build_doc['_id']
+ build_info = self.build_info_cache.get(build_id)
+ if not build_info:
+ # last_released is added in 2019, build before 2019 don't have this field
+ # TODO: have a migration to populate last_released from built_on
+ # Then this code can be modified to use last_released only
+ released_date = build_doc.get('last_released') or build_doc['built_on']
+ build_info = {
+ 'version': build_doc['version'],
+ 'last_released': DateTimeProperty.deserialize(released_date)
+ }
+ self.build_info_cache[build_id] = build_info
+ return build_info
+
+
+def _format_percentage_for_enterprise_tile(dividend, divisor):
+ if not divisor:
+ return '--'
+ return f"{dividend / divisor * 100:.1f}%"
diff --git a/corehq/apps/enterprise/iterators.py b/corehq/apps/enterprise/iterators.py
index ff1a1cf941e2..ae147f1750b6 100644
--- a/corehq/apps/enterprise/iterators.py
+++ b/corehq/apps/enterprise/iterators.py
@@ -146,6 +146,14 @@ def get_kwargs_from_map(cls, map):
'last_id': last_id
}
+ @classmethod
+ def is_initial_query(cls, map):
+ last_domain = map.get('domain', None)
+ last_time = map.get('inserted_at', None)
+ last_id = map.get('id', None)
+
+ return not (last_domain or last_time or last_id)
+
class AppIdToNameResolver:
def __init__(self):
diff --git a/corehq/apps/enterprise/metric_events.py b/corehq/apps/enterprise/metric_events.py
new file mode 100644
index 000000000000..56344681ac26
--- /dev/null
+++ b/corehq/apps/enterprise/metric_events.py
@@ -0,0 +1,2 @@
+ENTERPRISE_API_ACCESS = 'enterprise_api_access'
+ENTERPRISE_REPORT_REQUEST = 'enterprise_report_request'
diff --git a/corehq/apps/enterprise/static/enterprise/js/project_dashboard.js b/corehq/apps/enterprise/static/enterprise/js/project_dashboard.js
index 048da6ca2c26..a12d27097066 100644
--- a/corehq/apps/enterprise/static/enterprise/js/project_dashboard.js
+++ b/corehq/apps/enterprise/static/enterprise/js/project_dashboard.js
@@ -225,11 +225,13 @@ hqDefine("enterprise/js/project_dashboard", [
function updateDisplayTotal($element, kwargs) {
const $display = $element.find(".js-total");
+ const $helpTotal = $element.find(".help-total");
const slug = $element.data("slug");
const requestParams = {
url: initialPageData.reverse("enterprise_dashboard_total", slug),
success: function (data) {
$display.html(localizeNumberlikeString(data.total));
+ $helpTotal.removeClass("d-none");
},
error: function (request) {
if (request.responseJSON) {
@@ -238,10 +240,12 @@ hqDefine("enterprise/js/project_dashboard", [
alertUser.alert_user(gettext("Error updating display total, please try again or report an issue if this persists."), "danger");
}
$display.html(gettext("??"));
+ $helpTotal.addClass("d-none");
},
data: kwargs,
};
$display.html('
');
+ $helpTotal.addClass("d-none");
$.ajax(requestParams);
}
diff --git a/corehq/apps/enterprise/templates/enterprise/partials/project_tile.html b/corehq/apps/enterprise/templates/enterprise/partials/project_tile.html
index 49b7e6f201d6..49fbde957902 100644
--- a/corehq/apps/enterprise/templates/enterprise/partials/project_tile.html
+++ b/corehq/apps/enterprise/templates/enterprise/partials/project_tile.html
@@ -1,34 +1,52 @@
{% load i18n %}
-
-
+
+
+
+
+
+ {% if report.total_description %}
+
+
+
+
+ {% endif %}
+
-
{{ report.total_description }}