diff --git a/cloud/endagaweb/models.py b/cloud/endagaweb/models.py index 65b0780f..2cc602a1 100644 --- a/cloud/endagaweb/models.py +++ b/cloud/endagaweb/models.py @@ -525,6 +525,7 @@ class Subscriber(models.Model): # When toggled, this will protect a subsriber from getting "vacuumed." You # can still delete subs with the usual "deactivate" button. prevent_automatic_deactivation = models.BooleanField(default=False) + role = models.TextField(default='retailer') @classmethod def update_balance(cls, imsi, other_bal): diff --git a/cloud/endagaweb/stats_app/stats_client.py b/cloud/endagaweb/stats_app/stats_client.py index 5ebe043f..791d0260 100644 --- a/cloud/endagaweb/stats_app/stats_client.py +++ b/cloud/endagaweb/stats_app/stats_client.py @@ -369,3 +369,32 @@ def timeseries(self, key=None, **kwargs): if 'aggregation' not in kwargs: kwargs['aggregation'] = 'average_value' return self.aggregate_timeseries(key, **kwargs) + + +class TransferStatsClient(StatsClientBase): + """ Gather retailer transfer and recharge report """ + + def __init__(self, *args, **kwargs): + super(TransferStatsClient, self).__init__(*args, **kwargs) + + def timeseries(self, kind=None, **kwargs): + # Set queryset from subscriber role as retailer + kwargs['query'] = Q(subscriber__role='retailer') + return self.aggregate_timeseries(kind, **kwargs) + + +class TopUpStatsClient(StatsClientBase): + def __init__(self, *args, **kwargs): + super(TopUpStatsClient, self).__init__(*args, **kwargs) + + def timeseries(self, kind=None, **kwargs): + # Change is negative convert to compare + try: + raw_amount = [(float(denom) * -1 / 100000) for denom in + kwargs['extras'].split('-')] + kwargs['query'] = Q(change__gte=raw_amount[1]) & Q( + change__lte=raw_amount[0]) & Q(subscriber__role='retailer') + return self.aggregate_timeseries(kind, **kwargs) + except ValueError: + # If no denominations available in this network + raise ValueError('no denominations available in current network') diff --git a/cloud/endagaweb/templates/dashboard/report/billing.html b/cloud/endagaweb/templates/dashboard/report/billing.html new file mode 100644 index 00000000..56024ac8 --- /dev/null +++ b/cloud/endagaweb/templates/dashboard/report/billing.html @@ -0,0 +1,368 @@ +{% extends "dashboard/layout.html" %} +{% comment %} +Copyright (c) 2016-present, Facebook, Inc. +All rights reserved. + +This source code is licensed under the BSD-style license found in the +LICENSE file in the root directory of this source tree. An additional grant +of patent rights can be found in the PATENTS file in the same directory. +{% endcomment %} +{% load apptags %} +{% load humanize %} +{% load crispy_forms_tags %} + + +{% block title %} + {% tmpl_const "SITENAME" %} | Report + {% if report_summary %} + | "{{ report_summary }}" + {% endif %} +{% endblock %} + +{% block pagestyle %} + + + + +{% endblock %} + +{% block content %} + {% include "dashboard/report/header.html" with header='Billing' %} + +
+ + {% include "dashboard/report/nav.html" with active_tab='billing_reports' %} +
+ {% include "dashboard/report/filter.html" with action_url='/dashboard/reports/billing' %} + {% if network_has_activity %} +
+
+
+
+
+ {% if 'Loader' in reports %} +
+
+ {% include "dashboard/report/waterfall.html" with title="Waterfall - Loader" %} +
+
+ {% endif %} + {% if 'Reload Rate' in reports %} +
 
+
+
+ {% include "dashboard/report/waterfall.html" with title="Waterfall - Reload Rate" %} +
+
+ {% endif %} + {% if 'Reload Amount' in reports %} +
 
+
+
+ {% include "dashboard/report/waterfall.html" with title="Waterfall - Reload Amount" %} +
+
+ {% endif %} + {% if 'Reload Transaction' in reports %} +
 
+
+
+ {% include "dashboard/report/waterfall.html" with title="Waterfall - Reload Transaction" %} +
+
+ {% endif %} + {% if 'Average Frequency' in reports %} +
 
+
+
+ {% include "dashboard/report/waterfall.html" with title="Waterfall - Average Frequency" %} +
+
+ {% endif %} +
+
+
+
+
+
+
+ + {% else %} +

There is no network activity to display.

+ {% endif %} +
+ {% include 'dashboard/timezone-notice.html' %} +
+ +
+
+{% endblock %} + +{% block js %} +{% if network_has_activity %} + + + + + + + + + + + + + +{% endif %} + +{% endblock %} \ No newline at end of file diff --git a/cloud/endagaweb/tests/test_reports_ui.py b/cloud/endagaweb/tests/test_reports_ui.py new file mode 100644 index 00000000..87421e55 --- /dev/null +++ b/cloud/endagaweb/tests/test_reports_ui.py @@ -0,0 +1,79 @@ +"""Tests for the dashboard UI. + +Verifying that we can GET and POST to various pages. + +Copyright (c) 2016-present, Facebook, Inc. +All rights reserved. + +This source code is licensed under the BSD-style license found in the +LICENSE file in the root directory of this source tree. An additional grant +of patent rights can be found in the PATENTS file in the same directory. +""" + +from django import test + +from endagaweb import models +from endagaweb.util import parse_usage_event + + +class ReportUITest(test.TestCase): + """Testing that we can add BTS (towers) in the UI.""" + + @classmethod + def setUpClass(cls): + cls.username = 'y' + cls.password = 'pw' + cls.user = models.User(username=cls.username, email='y@l.com') + cls.user.set_password(cls.password) + cls.user.save() + cls.user_profile = models.UserProfile.objects.get(user=cls.user) + cls.uuid = "59216199-d664-4b7a-a2db-6f26e9a5d208" + inbound_url = "http://localhost:8090" + cls.bts = models.BTS( + uuid=cls.uuid, nickname='test-name', inbound_url=inbound_url, + network=cls.user_profile.network) + cls.bts.save() + cls.primary_network = cls.user_profile.network + cls.secondary_network = models.Network.objects.create() + # Create a test client. + cls.client = test.Client() + + @classmethod + def tearDownClass(cls): + cls.user.delete() + cls.user_profile.delete() + cls.bts.delete() + cls.primary_network.delete() + cls.secondary_network.delete() + + def tearDown(self): + self.logout() + + def login(self): + """Log the client in.""" + data = { + 'email': self.username, + 'password': self.password, + } + self.client.post('/auth/', data) + + def logout(self): + """Log the client out.""" + self.client.get('/logout') + + def test_get_billing_report_sans_auth(self): + try: + self.logout() + response = self.client.get('/dashboard/reports/billing') + self.assertEqual(302, response.status_code) + except: + self.assertIsNone(None, response.status_code) + + def test_get_billing_report_with_auth(self): + try: + self.login() + response = self.client.get('/dashboard/reports/billing') + self.assertEqual(200, response.status_code) + except: + self.assertIsNone(None, response.status_code) + diff --git a/cloud/endagaweb/views/__init__.py b/cloud/endagaweb/views/__init__.py index 02f215ba..1fc905a6 100644 --- a/cloud/endagaweb/views/__init__.py +++ b/cloud/endagaweb/views/__init__.py @@ -19,3 +19,4 @@ import debug import internalapi import file_upload +import reports diff --git a/cloud/endagaweb/views/reports.py b/cloud/endagaweb/views/reports.py new file mode 100644 index 00000000..888d46f1 --- /dev/null +++ b/cloud/endagaweb/views/reports.py @@ -0,0 +1,105 @@ + + +class BillingReportView(ProtectedView): + def __init__(self, **kwargs): + super(BillingReportView, self).__init__(**kwargs) + self.template = "dashboard/report/billing.html" + self.url_namespace = 'billing-report' + self.reports = REPORTS_DICT + + def get(self, request): + return self.handle_request(request) + + def post(self, request): + return self.handle_request(request) + + def handle_request(self, request): + user_profile = UserProfile.objects.get(user=request.user) + network = user_profile.network + report_list = list({x for v in self.reports.itervalues() for x in v}) + if request.method == "POST": + request.session['topup_percent'] = request.POST.get( + 'top_percent') or 100 + request.session['level_id'] = request.POST.get('level_id') or 0 + if request.session['level_id']: + request.session['level'] = 'tower' + else: + request.session['level'] = "network" + request.session['level_id'] = network.id + request.session['reports'] = request.POST.getlist('reports', None) + return redirect( + urlresolvers.reverse(self.url_namespace) + '?filter=1') + elif request.method == "GET": + if 'filter' not in request.GET: + # Reset filtering params. + request.session['level'] = 'network' + if self.url_namespace == 'subscriber-report': + request.session['level'] = 'network' + request.session['level_id'] = network.id + request.session['reports'] = report_list + request.session['topup_percent'] = 100 + else: + return HttpResponseBadRequest() + + # For top top-up percentage + denom_list = [] + denom_list2 = [] + # Get denominatations available for that network + denomination = NetworkDenomination.objects.filter( + network_id=network.id) + + for denom in denomination: + start_amount = humanize_credits( + denom.start_amount, currency=CURRENCIES[network.currency]) + end_amount = humanize_credits( + denom.end_amount, currency=CURRENCIES[network.currency]) + denom_list.append( + (start_amount.amount_raw, end_amount.amount_raw)) + formatted_denomnation = [] + for denom in denom_list: + # Now format to set them as stat-types + formatted_denomnation.append( + str(humanize_credits( + denom[0], + CURRENCIES[network.subscriber_currency])).replace(',', '') + + ' - ' + + str(humanize_credits( + denom[1], + CURRENCIES[network.subscriber_currency])).replace(',', '')) + denom_list2.append( + str(denom[0]) + + '-' + + str(denom[1])) + currency = CURRENCIES[network.subscriber_currency].symbol + timezone_offset = pytz.timezone(user_profile.timezone).utcoffset( + datetime.datetime.now()).total_seconds() + level = request.session['level'] + level_id = int(request.session['level_id']) + reports = request.session['reports'] + topup_percent = float(request.session['topup_percent']) + + towers = models.BTS.objects.filter( + network=user_profile.network).values('nickname', 'uuid', 'id') + network_has_activity = UsageEvent.objects.filter( + network=network).exists() + context = { + 'networks': get_objects_for_user(request.user, 'view_network', + klass=Network), + 'towers': towers, + 'level': level, + 'level_id': level_id, + 'reports': reports, + 'report_list': self.reports, + 'user_profile': user_profile, + 'current_time_epoch': int(time.time()), + 'timezone_offset': timezone_offset, + 'network_has_activity': network_has_activity, + 'kinds': ','.join(formatted_denomnation), + 'extra_param': ','.join(denom_list2), + 'topup_percent': topup_percent, + 'value_type': currency, + } + template = get_template(self.template) + html = template.render(context, request) + return HttpResponse(html) +