From 9fd706ec87b04ef8045d4cb8e933ec5898ef7a2c Mon Sep 17 00:00:00 2001 From: Matt Riley Date: Mon, 2 Dec 2024 15:08:02 -0500 Subject: [PATCH] Added Enterprise Data Exports Report --- corehq/apps/enterprise/api/api.py | 2 + corehq/apps/enterprise/api/resources.py | 30 ++++++++++ corehq/apps/enterprise/enterprise.py | 75 +++++++++++++++++++++++++ corehq/apps/enterprise/views.py | 1 + corehq/apps/export/dbaccessors.py | 27 ++++++++- 5 files changed, 134 insertions(+), 1 deletion(-) diff --git a/corehq/apps/enterprise/api/api.py b/corehq/apps/enterprise/api/api.py index e3703aae5edd..4b7a3ba2e5bd 100644 --- a/corehq/apps/enterprise/api/api.py +++ b/corehq/apps/enterprise/api/api.py @@ -6,6 +6,7 @@ MobileUserResource, ODataFeedResource, WebUserResource, + DataExportReportResource ) v1_api = Api(api_name='v1') @@ -14,3 +15,4 @@ v1_api.register(MobileUserResource()) v1_api.register(FormSubmissionResource()) v1_api.register(ODataFeedResource()) +v1_api.register(DataExportReportResource()) diff --git a/corehq/apps/enterprise/api/resources.py b/corehq/apps/enterprise/api/resources.py index 09d5b85d120d..07bd57ca5881 100644 --- a/corehq/apps/enterprise/api/resources.py +++ b/corehq/apps/enterprise/api/resources.py @@ -388,3 +388,33 @@ def dehydrate(self, bundle): def get_primary_keys(self): return ('form_id', 'submitted',) + + +class DataExportReportResource(ODataEnterpriseReportResource): + domain = fields.CharField() + name = fields.CharField() + export_type = fields.CharField() + export_subtype = fields.CharField() + owner = fields.CharField() + + REPORT_SLUG = EnterpriseReport.DATA_EXPORTS + + def get_report_task(self, request): + account = BillingAccount.get_account_by_domain(request.domain) + return generate_enterprise_report.s( + self.REPORT_SLUG, + account.id, + request.couch_user.username + ) + + def dehydrate(self, bundle): + bundle.data['domain'] = bundle.obj[0] + bundle.data['name'] = bundle.obj[1] + bundle.data['export_type'] = bundle.obj[2] + bundle.data['export_subtype'] = bundle.obj[3] + bundle.data['owner'] = bundle.obj[4] + + return bundle + + def get_primary_keys(self): + return ('domain', 'export_type', 'export_subtype', 'name') diff --git a/corehq/apps/enterprise/enterprise.py b/corehq/apps/enterprise/enterprise.py index 352119c6ea00..e776514d4a05 100644 --- a/corehq/apps/enterprise/enterprise.py +++ b/corehq/apps/enterprise/enterprise.py @@ -10,6 +10,13 @@ from couchforms.analytics import get_last_form_submission_received from dimagi.utils.dates import DateSpan +from corehq.apps.users.models import WebUser +from corehq.apps.export.dbaccessors import ( + get_brief_exports, + is_standard, + is_daily_saved_export, + is_excel_integration +) from corehq.apps.enterprise.exceptions import EnterpriseReportError, TooMuchRequestedDataError from corehq.apps.enterprise.iterators import raise_after_max_elements from corehq.apps.accounting.models import BillingAccount @@ -34,6 +41,7 @@ class EnterpriseReport: MOBILE_USERS = 'mobile_users' FORM_SUBMISSIONS = 'form_submissions' ODATA_FEEDS = 'odata_feeds' + DATA_EXPORTS = 'data_exports' DATE_ROW_FORMAT = '%Y/%m/%d %H:%M:%S' @@ -67,6 +75,8 @@ def create(cls, slug, account_id, couch_user, **kwargs): report = EnterpriseFormReport(account, couch_user, **kwargs) elif slug == cls.ODATA_FEEDS: report = EnterpriseODataReport(account, couch_user, **kwargs) + elif slug == cls.DATA_EXPORTS: + report = EnterpriseDataExportReport(account, couch_user, **kwargs) if report: report.slug = slug @@ -383,3 +393,68 @@ def _get_individual_export_rows(self, exports, export_line_counts): ) return rows + + +class EnterpriseDataExportReport(EnterpriseReport): + title = gettext_lazy('Data Exports') + + def __init__(self, account, couch_user): + super().__init__(account, couch_user) + + @property + def headers(self): + return [ + _('Project Space'), + _('Name'), + _('Type'), + _('SubType'), + _('Created By'), + ] + + def type_lookup(self, doc_type): + from corehq.apps.export.models.new import FormExportInstance, CaseExportInstance + if doc_type == FormExportInstance.__name__: + return _('Form') + elif doc_type == CaseExportInstance.__name__: + return _('Case') + else: + return _('Unknown') + + SUBTYPE_MAP = { + is_standard: gettext_lazy('Standard'), + is_daily_saved_export: gettext_lazy('Daily Saved Export'), + is_excel_integration: gettext_lazy('Excel Integration'), + } + + def subtype_lookup(self, export): + for (is_subtype_fn, subtype) in self.SUBTYPE_MAP.items(): + if is_subtype_fn(export): + return subtype + + return _('Unknown') + + def user_lookup(self, owner_id): + if not owner_id: + return _('Unknown') + + owner = WebUser.get_by_user_id(owner_id) + return owner.username + + def get_exports(self, domain_obj): + valid_subtypes = self.SUBTYPE_MAP.values() + return [ + export for export in get_brief_exports(domain_obj.name) + if self.subtype_lookup(export) in valid_subtypes + ] + + def rows_for_domain(self, domain_obj): + return [[ + domain_obj.name, + export['name'], + self.type_lookup(export['doc_type']), + self.subtype_lookup(export), + self.user_lookup(export['owner_id']), + ] for export in self.get_exports(domain_obj)] + + def total_for_domain(self, domain_obj): + return len(self.get_exports(domain_obj)) diff --git a/corehq/apps/enterprise/views.py b/corehq/apps/enterprise/views.py index 169d1ed15ef6..861ebee490e8 100644 --- a/corehq/apps/enterprise/views.py +++ b/corehq/apps/enterprise/views.py @@ -80,6 +80,7 @@ def enterprise_dashboard(request, domain): EnterpriseReport.MOBILE_USERS, EnterpriseReport.FORM_SUBMISSIONS, EnterpriseReport.ODATA_FEEDS, + EnterpriseReport.DATA_EXPORTS, )], 'current_page': { 'page_name': _('Enterprise Dashboard'), diff --git a/corehq/apps/export/dbaccessors.py b/corehq/apps/export/dbaccessors.py index f79b2c10a9ed..9d8cc0a091da 100644 --- a/corehq/apps/export/dbaccessors.py +++ b/corehq/apps/export/dbaccessors.py @@ -3,6 +3,7 @@ from dimagi.utils.parsing import json_format_datetime from corehq.util.test_utils import unit_testing_only +from couchexport.models import IntegrationFormat def get_latest_case_export_schema(domain, case_type): @@ -202,6 +203,30 @@ def delete_all_export_instances(): safe_delete(db, doc_id) +def is_standard(export): + return (not export['is_daily_saved_export'] + and not export['is_odata_config'] + and not IntegrationFormat.is_integration_format(export['export_format'])) + + +def is_daily_saved_export(export): + return (export['is_daily_saved_export'] + and not export['export_format'] == "html" + and not export['is_odata_config'] + and not IntegrationFormat.is_integration_format(export['export_format'])) + + +def is_excel_integration(export): + return (export['is_daily_saved_export'] + and export['export_format'] == "html" + and not export['is_odata_config'] + and not IntegrationFormat.is_integration_format(export['export_format'])) + + +def is_odata_export(export): + return export['is_odata_config'] + + class ODataExportFetcher: def get_export_count(self, domain): return len(self._get_odata_exports(domain)) @@ -220,4 +245,4 @@ def get_exports(self, domain): def _get_odata_exports(self, domain): all_domain_exports = get_brief_exports(domain) - return [export for export in all_domain_exports if export['is_odata_config']] + return [export for export in all_domain_exports if is_odata_export(export)]