Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions corehq/apps/enterprise/api/api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from tastypie.api import Api

from corehq.apps.enterprise.api.resources import (
CaseManagementResource,
DomainResource,
FormSubmissionResource,
MobileUserResource,
Expand All @@ -14,3 +15,4 @@
v1_api.register(MobileUserResource())
v1_api.register(FormSubmissionResource())
v1_api.register(ODataFeedResource())
v1_api.register(CaseManagementResource())
30 changes: 30 additions & 0 deletions corehq/apps/enterprise/api/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,3 +388,33 @@ def dehydrate(self, bundle):

def get_primary_keys(self):
return ('form_id', 'submitted',)


class CaseManagementResource(ODataEnterpriseReportResource):
domain = fields.CharField()
num_applications = fields.IntegerField()
num_surveys_only = fields.IntegerField()
num_cases_only = fields.IntegerField()
num_mixed = fields.IntegerField()

REPORT_SLUG = EnterpriseReport.CASE_MANAGEMENT

def get_report_task(self, request):
account = BillingAccount.get_account_by_domain(request.domain)
return generate_enterprise_report.s(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's .s() do here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.s is short for .signature.

From the docs:

...sometimes you may want to pass the signature of a task invocation to another process or as an argument to another function.

A signature() wraps the arguments, keyword arguments, and execution options of a single task invocation in a way such that it can be passed to functions or even serialized and sent across the wire.

Here, we're looking to return the task for some arbitrary use, so it seemed appropriate to pass the full task signature.

self.REPORT_SLUG,
account.id,
request.couch_user.username,
)

def dehydrate(self, bundle):
bundle.data['domain'] = bundle.obj[0]
bundle.data['num_applications'] = bundle.obj[1]
bundle.data['num_surveys_only'] = bundle.obj[2]
bundle.data['num_cases_only'] = bundle.obj[3]
bundle.data['num_mixed'] = bundle.obj[4]

return bundle

def get_primary_keys(self):
return ('domain',) # very odd report that makes coming up with an actual key challenging
68 changes: 68 additions & 0 deletions corehq/apps/enterprise/enterprise.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
from corehq.apps.domain.calculations import sms_in_last
from corehq.apps.domain.models import Domain
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.users import UserES
from corehq.apps.export.dbaccessors import ODataExportFetcher
from corehq.apps.users.dbaccessors import (
Expand All @@ -34,6 +36,7 @@ class EnterpriseReport:
MOBILE_USERS = 'mobile_users'
FORM_SUBMISSIONS = 'form_submissions'
ODATA_FEEDS = 'odata_feeds'
CASE_MANAGEMENT = 'case_management'

DATE_ROW_FORMAT = '%Y/%m/%d %H:%M:%S'

Expand Down Expand Up @@ -67,6 +70,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.CASE_MANAGEMENT:
report = EnterpriseCaseManagementReport(account, couch_user, **kwargs)

if report:
report.slug = slug
Expand Down Expand Up @@ -383,3 +388,66 @@ def _get_individual_export_rows(self, exports, export_line_counts):
)

return rows


class EnterpriseCaseManagementReport(EnterpriseReport):
title = gettext_lazy('Case Management')

@property
def headers(self):
return [_('Project Space'), _('# Applications'), _('# Surveys Only'), _('# Cases Only'), _('# Mixed')]

def rows_for_domain(self, domain_obj):
app_query = self.app_query(domain_obj.name)
app_count = app_query.count()

if app_count == 0:
survey_only_count = 0
case_only_count = 0
mixed_count = 0
else:
has_surveys = filters.nested('modules', filters.empty('modules.case_type.exact'))
has_cases = filters.nested('modules', filters.non_null('modules.case_type.exact'))

survey_only_count = app_query.filter(filters.AND(has_surveys, filters.NOT(has_cases))).count()
case_only_count = app_query.filter(filters.AND(has_cases, filters.NOT(has_surveys))).count()
mixed_count = app_query.filter(filters.AND(has_surveys, has_cases)).count()

return [[domain_obj.name, app_count, survey_only_count, case_only_count, mixed_count],]

@property
def total(self):
num_domains_with_apps = 0
num_domains_using_case_management = 0

for domain_obj in self.domains():
(app_count, uses_case_management) = self.total_for_domain(domain_obj)
if app_count > 0:
if uses_case_management:
num_domains_using_case_management += 1
num_domains_with_apps += 1

if num_domains_with_apps == 0:
return '--'

case_management_percent = num_domains_using_case_management / num_domains_with_apps * 100

return "{:.1f}%".format(case_management_percent)

def total_for_domain(self, domain_obj):
app_query = self.app_query(domain_obj.name)
app_count = app_query.count()
if app_count > 0:
has_cases = filters.nested('modules', filters.non_null('modules.case_type.exact'))
uses_case_management = app_query.filter(has_cases).count() > 0
else:
uses_case_management = False

return [app_count, uses_case_management]

def app_query(self, domain):
return (
AppES().domain(domain)
.filter(filters.term('doc_type', 'Application'))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this to make sure we don't get LinkedApplications or RemoteApps?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, I think this was probably influenced pretty heavily by talking with Ethan, so I'm not sure what my thinking was at the time (this was likely code I used directly from him). That said, When I run the query without the Application filter, I receive DeleteFormRecord, DeleteModuleRecord, and DeleteApplicationRecord in addition to Application -- so it seems AppES holds more than current application information

.is_build(False)
)
1 change: 1 addition & 0 deletions corehq/apps/enterprise/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ def platform_overview(request, domain):
EnterpriseReport.MOBILE_USERS,
EnterpriseReport.FORM_SUBMISSIONS,
EnterpriseReport.ODATA_FEEDS,
EnterpriseReport.CASE_MANAGEMENT,
)],
'metric_type': 'Platform Overview',
})
Expand Down