Skip to content

Commit

Permalink
Merge pull request #35437 from dimagi/mjr/sms-usage-tile
Browse files Browse the repository at this point in the history
Add Enterprise SMS Report
  • Loading branch information
mjriley authored Dec 4, 2024
2 parents e02f956 + 6400bd1 commit 4561106
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 26 deletions.
2 changes: 2 additions & 0 deletions corehq/apps/enterprise/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
MobileUserResource,
ODataFeedResource,
WebUserResource,
SMSResource,
)

v1_api = Api(api_name='v1')
Expand All @@ -14,3 +15,4 @@
v1_api.register(MobileUserResource())
v1_api.register(FormSubmissionResource())
v1_api.register(ODataFeedResource())
v1_api.register(SMSResource())
45 changes: 45 additions & 0 deletions corehq/apps/enterprise/api/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.utils.translation import gettext as _

from dateutil import tz
from datetime import timezone
from tastypie import fields, http
from tastypie.exceptions import ImmediateHttpResponse

Expand Down Expand Up @@ -314,6 +315,50 @@ def get_primary_keys(self):
return ('user_id',)


class SMSResource(ODataEnterpriseReportResource):
domain = fields.CharField()
num_sent = fields.IntegerField()
num_received = fields.IntegerField()
num_error = fields.IntegerField()

REPORT_SLUG = EnterpriseReport.SMS

def get_report_task(self, request):
start_date, end_date = get_date_range_from_request(request.GET)

account = BillingAccount.get_account_by_domain(request.domain)
return generate_enterprise_report.s(
self.REPORT_SLUG,
account.id,
request.couch_user.username,
start_date=start_date,
end_date=end_date
)

def dehydrate(self, bundle):
bundle.data['domain'] = bundle.obj[0]
bundle.data['num_sent'] = bundle.obj[1]
bundle.data['num_received'] = bundle.obj[2]
bundle.data['num_error'] = bundle.obj[3]

return bundle

def get_primary_keys(self):
return ('domain',)


def get_date_range_from_request(request_dict):
start_date = request_dict.get('startdate', None)
if start_date:
start_date = str(datetime.fromisoformat(start_date).astimezone(timezone.utc))

end_date = request_dict.get('enddate', None)
if end_date:
end_date = str(datetime.fromisoformat(end_date).astimezone(timezone.utc))

return (start_date, end_date,)


class ODataFeedResource(ODataEnterpriseReportResource):
'''
A Resource for listing all Domain-level OData feeds which belong to the Enterprise.
Expand Down
69 changes: 69 additions & 0 deletions corehq/apps/enterprise/enterprise.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import re
from django.db.models import Count
from datetime import datetime, timedelta

from django.utils.translation import gettext as _
Expand All @@ -20,6 +21,7 @@
from corehq.apps.es import forms as form_es
from corehq.apps.es.users import UserES
from corehq.apps.export.dbaccessors import ODataExportFetcher
from corehq.apps.sms.models import SMS, OUTGOING, INCOMING
from corehq.apps.users.dbaccessors import (
get_all_user_rows,
get_mobile_user_count,
Expand All @@ -34,6 +36,7 @@ class EnterpriseReport:
MOBILE_USERS = 'mobile_users'
FORM_SUBMISSIONS = 'form_submissions'
ODATA_FEEDS = 'odata_feeds'
SMS = 'sms'

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.SMS:
report = EnterpriseSMSReport(account, couch_user, **kwargs)

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

return rows


class EnterpriseSMSReport(EnterpriseReport):
title = gettext_lazy('SMS Usage')
MAX_DATE_RANGE_DAYS = 90

def __init__(self, account, couch_user, start_date=None, end_date=None, num_days=30):
super().__init__(account, couch_user)

if not end_date:
end_date = datetime.utcnow()
elif isinstance(end_date, str):
end_date = datetime.fromisoformat(end_date)

if start_date:
if isinstance(start_date, str):
start_date = datetime.fromisoformat(start_date)
self.datespan = DateSpan(start_date, end_date)
self.subtitle = _("{} to {}").format(
start_date.date(),
end_date.date(),
)
else:
self.datespan = DateSpan(end_date - timedelta(days=num_days), end_date)
self.subtitle = _("past {} days").format(num_days)

if self.datespan.enddate - self.datespan.startdate > timedelta(days=self.MAX_DATE_RANGE_DAYS):
raise TooMuchRequestedDataError(
_('Date ranges with more than {} days are not supported').format(self.MAX_DATE_RANGE_DAYS)
)

def total_for_domain(self, domain_obj):
query = SMS.objects.filter(
domain=domain_obj.name,
processed=True,
direction=OUTGOING,
error=False,
date__gte=self.datespan.startdate,
date__lt=self.datespan.enddate_adjusted
)

return query.count()

@property
def headers(self):
headers = [_('Project Space'), _('# Sent'), _('# Received'), _('# Errors')]

return headers

def rows_for_domain(self, domain_obj):
results = SMS.objects.filter(
domain=domain_obj.name,
processed=True,
date__gte=self.datespan.startdate,
date__lt=self.datespan.enddate_adjusted
).values('direction', 'error').annotate(direction_count=Count('pk'))

num_sent = sum([result['direction_count'] for result in results
if result['direction'] == OUTGOING and not result['error']])
num_received = sum([result['direction_count'] for result in results
if result['direction'] == INCOMING and not result['error']])
num_errors = sum([result['direction_count'] for result in results if result['error']])

return [(domain_obj.name, num_sent, num_received, num_errors), ]
87 changes: 67 additions & 20 deletions corehq/apps/enterprise/static/enterprise/js/project_dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,47 @@ hqDefine("enterprise/js/project_dashboard", [
return self;
};

var DateRangeModal = function (datePicker, presetOptions, maxDateRangeDays, tileDisplay) {
var SMSTile = function (datePicker) {
var self = {};
self.endDate = ko.observable(moment().utc());
self.startDate = ko.observable(self.endDate().clone().subtract(30, "days"));
self.presetType = ko.observable(PRESET_LAST_30);
self.customDateRangeDisplay = ko.observable(datePicker.optionsStore.input.value);

self.presetText = ko.pureComputed(function () {
if (self.presetType() !== PRESET_CUSTOM) {
return dateRangePresetOptions.find(ele => ele.id === self.presetType()).text;
} else {
return self.customDateRangeDisplay();
}
});

self.onApply = function (preset, startDate, endDate) {
self.startDate(startDate);
self.endDate(endDate);
self.presetType(preset);
self.customDateRangeDisplay(datePicker.optionsStore.input.value);

updateDisplayTotal($("#sms"), {
"start_date": startDate.toISOString(),
"end_date": endDate.toISOString(),
});
};

return self;
};

var DateRangeModal = function ($modal, datePicker, presetOptions, maxDateRangeDays, tileMap) {
let tileDisplay = null;
$modal.on('show.bs.modal', function (event) {
var button = $(event.relatedTarget);
tileDisplay = tileMap[button.data('sender')];

self.presetType(tileDisplay.presetType());
self.customStartDate(tileDisplay.startDate().clone());
self.customEndDate(tileDisplay.endDate().clone());
});

var self = {};
self.presetOptions = presetOptions;
self.presetType = ko.observable(PRESET_LAST_30);
Expand Down Expand Up @@ -193,24 +233,29 @@ hqDefine("enterprise/js/project_dashboard", [

$(function () {
const metricType = initialPageData.get('metric_type');
const $dateRangeDisplay = $("#dateRangeDisplay");
let dateRangeModal = null;
if ($dateRangeDisplay.length) {
const datePicker = tempusDominus.createDefaultDateRangePicker(
document.getElementById("id_date_range"),
moment().subtract(30, "days"),
moment()
);

const formSubmissionsDisplay = MobileFormSubmissionsTile(datePicker);
const maxDateRangeDays = initialPageData.get("max_date_range_days");
dateRangeModal = DateRangeModal(datePicker, dateRangePresetOptions, maxDateRangeDays, formSubmissionsDisplay);

$dateRangeDisplay.koApplyBindings(formSubmissionsDisplay);
$("#enterpriseFormsDaterange").koApplyBindings(
dateRangeModal
);
}
const datePicker = tempusDominus.createDefaultDateRangePicker(
document.getElementById("id_date_range"),
moment().subtract(30, "days"),
moment()
);

const $dateRangeModal = $('#enterpriseFormsDaterange');

const formSubmissionsDisplay = MobileFormSubmissionsTile(datePicker);
const smsDisplay = SMSTile(datePicker);
const maxDateRangeDays = initialPageData.get("max_date_range_days");

const displayMap = {
"form_submission": formSubmissionsDisplay,
"sms": smsDisplay,
};
const dateRangeModal = DateRangeModal($dateRangeModal, datePicker, dateRangePresetOptions, maxDateRangeDays, displayMap);

$("#form_submission_dateRangeDisplay").koApplyBindings(formSubmissionsDisplay);
$("#sms_dateRangeDisplay").koApplyBindings(smsDisplay);
$dateRangeModal.koApplyBindings(
dateRangeModal
);

kissmetrics.track.event(`[${metricType}] Visited page`);
$(".report-panel").each(function () {
Expand Down Expand Up @@ -238,7 +283,9 @@ hqDefine("enterprise/js/project_dashboard", [
$button.enableButton();
},
};
if (slug === "form_submissions") {

const dateRangeSlugs = ["form_submissions", "sms"];
if (dateRangeSlugs.includes(slug)) {
requestParams["data"] = {
"start_date": dateRangeModal.startDate().toISOString(),
"end_date": dateRangeModal.endDate().toISOString(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
<div class="card-header">
<div class="fs-4">{{ report.title }}</div>
{% if report.title == "Mobile Form Submissions" %}
<button id="dateRangeDisplay" type="button" data-bind="text: presetText" data-bs-toggle="modal" data-bs-target="#enterpriseFormsDaterange" class="btn btn-link fs-6">&nbsp;</button>
<button id="form_submission_dateRangeDisplay" type="button" data-bind="text: presetText" data-bs-toggle="modal" data-bs-target="#enterpriseFormsDaterange" data-sender="form_submission" class="btn btn-link fs-6">&nbsp;</button>
{% elif report.title == "SMS Usage" %}
<button id="sms_dateRangeDisplay" type="button" data-bind="text: presetText" data-bs-toggle="modal" data-bs-target="#enterpriseFormsDaterange" data-sender="sms" class="btn btn-link fs-6">&nbsp;</button>
{% else %}
<div class="form-control-plaintext fs-6">{{ report.subtitle|default:"&nbsp;" }}</div>
{% endif %}
Expand Down
13 changes: 8 additions & 5 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.SMS,
)],
'metric_type': 'Platform Overview',
})
Expand Down Expand Up @@ -131,8 +132,9 @@ def security_center(request, domain):
@login_and_domain_required
def enterprise_dashboard_total(request, domain, slug):
kwargs = {}
if slug == EnterpriseReport.FORM_SUBMISSIONS:
kwargs = get_form_submission_report_kwargs(request)
date_range_slugs = [EnterpriseReport.FORM_SUBMISSIONS, EnterpriseReport.SMS]
if slug in date_range_slugs:
kwargs = get_date_range_kwargs(request)
try:
report = EnterpriseReport.create(slug, request.account.id, request.couch_user, **kwargs)
except TooMuchRequestedDataError as e:
Expand Down Expand Up @@ -179,8 +181,9 @@ def _get_export_filename(request, slug):
@login_and_domain_required
def enterprise_dashboard_email(request, domain, slug):
kwargs = {}
if slug == EnterpriseReport.FORM_SUBMISSIONS:
kwargs = get_form_submission_report_kwargs(request)
date_range_slugs = [EnterpriseReport.FORM_SUBMISSIONS, EnterpriseReport.SMS]
if slug in date_range_slugs:
kwargs = get_date_range_kwargs(request)
try:
report = EnterpriseReport.create(slug, request.account.id, request.couch_user, **kwargs)
except TooMuchRequestedDataError as e:
Expand All @@ -196,7 +199,7 @@ def enterprise_dashboard_email(request, domain, slug):
return JsonResponse({'message': message})


def get_form_submission_report_kwargs(request):
def get_date_range_kwargs(request):
kwargs = {}
start_date = request.GET.get('start_date')
end_date = request.GET.get('end_date')
Expand Down

0 comments on commit 4561106

Please sign in to comment.