From d328cb7980f6127a8792d78e7628fa8338bd476d Mon Sep 17 00:00:00 2001 From: Shiv K Sah Date: Fri, 2 Jun 2017 21:34:47 +0530 Subject: [PATCH 1/2] Denomination bracket --- cloud/endagaweb/forms/dashboard_forms.py | 2 +- cloud/endagaweb/models.py | 20 ++ .../network_detail/denomination.html | 305 ++++++++++++++++++ .../dashboard/network_detail/nav.html | 7 +- cloud/endagaweb/tests/test_denomination.py | 107 ++++++ cloud/endagaweb/urls.py | 3 + cloud/endagaweb/views/django_tables.py | 51 +++ cloud/endagaweb/views/network.py | 194 ++++++++++- 8 files changed, 686 insertions(+), 3 deletions(-) create mode 100644 cloud/endagaweb/templates/dashboard/network_detail/denomination.html create mode 100644 cloud/endagaweb/tests/test_denomination.py diff --git a/cloud/endagaweb/forms/dashboard_forms.py b/cloud/endagaweb/forms/dashboard_forms.py index 2f9a6ddc..963616cc 100644 --- a/cloud/endagaweb/forms/dashboard_forms.py +++ b/cloud/endagaweb/forms/dashboard_forms.py @@ -367,4 +367,4 @@ def __init__(self, *args, **kwargs): self.helper.form_method = 'post' self.helper.form_action = '/dashboard/staff/tower-monitoring' self.helper.add_input(Submit('submit', 'Select')) - self.helper.layout = Layout('tower') + self.helper.layout = Layout('tower') \ No newline at end of file diff --git a/cloud/endagaweb/models.py b/cloud/endagaweb/models.py index d2429f9d..141ad542 100644 --- a/cloud/endagaweb/models.py +++ b/cloud/endagaweb/models.py @@ -1460,6 +1460,26 @@ def create_ledger(sender, instance, created, **kwargs): post_save.connect(Network.create_billing_tiers, sender=Network) +class NetworkDenomination(models.Model): + """Network has its own denomination bracket for rechange and validity + + Subscriber status depends on recharge under denomination bracket + """ + start_amount = models.BigIntegerField() + end_amount = models.BigIntegerField() + validity_days = models.PositiveIntegerField(blank=True, default=0) + + # The denomination group associated with the network + network = models.ForeignKey('Network', null=True, on_delete=models.CASCADE) + + def __unicode__(self): + return "Amount %s - %d for %s(days)" % ( + self.start_amount, self.end_amount, self.validity_days) + + class Meta: + ordering = ('start_amount',) + + class ConfigurationKey(models.Model): """A key->value mapping for storing settings. diff --git a/cloud/endagaweb/templates/dashboard/network_detail/denomination.html b/cloud/endagaweb/templates/dashboard/network_detail/denomination.html new file mode 100644 index 00000000..2101bf1a --- /dev/null +++ b/cloud/endagaweb/templates/dashboard/network_detail/denomination.html @@ -0,0 +1,305 @@ +{% 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 %} +{% load render_table from django_tables2 %} + + +{% block title %} + {% if network.name %} + {% tmpl_const "SITENAME" %} | "{{ network.name }}" + {% else %} + {% tmpl_const "SITENAME" %} | Network + {% endif %} +{% endblock %} + +{% block pagestyle %} + +{% endblock %} + +{% block content %} + {% include "dashboard/network_detail/header.html" with network=network %} + +
+ {% include "dashboard/network_detail/nav.html" with active_tab='network-denominations' %} + +
+ {% for message in messages %} +
+ × + {{ message }} +
+ {% endfor %} +
+ {% if denomination %} + {% render_table denominations_table %} + {% else %} +

There are currently no denominations associated with this network.

+ {% endif %} +
+
+ {% if user_profile.user.is_staff %} +
+
+
Create Denomination
+
+
+
{% csrf_token %} +
+
+ +
+ +
+
+
+ +
+
+ +
+ +
+
+
+ +
+
+ +
+ +
+
+
+ +
+
+
+ + + +
+
+
+
+
+
+
+
+ {% endif %} + +
+ + + +{% endblock %} +{% block js %} + + + + +{% endblock %} diff --git a/cloud/endagaweb/templates/dashboard/network_detail/nav.html b/cloud/endagaweb/templates/dashboard/network_detail/nav.html index 80a0705a..d2c2d396 100644 --- a/cloud/endagaweb/templates/dashboard/network_detail/nav.html +++ b/cloud/endagaweb/templates/dashboard/network_detail/nav.html @@ -23,6 +23,12 @@ {% endif %} ">Prices {% endcomment %} + - diff --git a/cloud/endagaweb/tests/test_denomination.py b/cloud/endagaweb/tests/test_denomination.py new file mode 100644 index 00000000..d797824f --- /dev/null +++ b/cloud/endagaweb/tests/test_denomination.py @@ -0,0 +1,107 @@ +"""Tests for models.Users. + +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 __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +from datetime import datetime +from random import randrange +import uuid +from django import test +import json + +import pytz + +from django.test import TestCase + +from ccm.common import crdt +from endagaweb import models + + +class TestBase(TestCase): + + @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" + + # Create a test client. + cls.client = test.Client() + + @classmethod + def tearDownClass(cls): + cls.user.delete() + cls.user_profile.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') + + +class DenominationUITest(TestBase): + """Testing that we can add User in the UI.""" + + def test_add_denominaton(self): + self.logout() + response = self.client.get('/dashboard/network/denominations') + # Anonymous User can not see this page so returning permission denied. + self.assertEqual(302, response.status_code) + + def test_add_denominaton_auth(self): + self.login() + response = self.client.get('/dashboard/network/denominations') + self.assertEqual(200, response.status_code) + + def test_delete_denominaton(self): + self.logout() + response = self.client.delete('/dashboard/network/denominations') + # Anonymous User can not see this page so returning permission denied. + self.assertEqual(302, response.status_code) + + def test_delete_denominaton_auth(self): + self.login() + response = self.client.delete('/dashboard/network/denominations') + self.assertEqual(200, response.status_code) + + def test_post_add_denominaton(self): + self.logout() + data = {} + response = self.client.post('/dashboard/network/denominations', data) + # Anonymous User can not see this page so returning permission denied. + self.assertEqual(302, response.status_code) + + def test_post_add_denominaton_auth(self): + self.login() + data = { + 'start_amount': 1, + 'end_amount': 2, + 'validity_days': 3 + } + response = self.client.post('/dashboard/network/denominations', data) + self.assertEqual(302, response.status_code) diff --git a/cloud/endagaweb/urls.py b/cloud/endagaweb/urls.py index 4c7d2469..a44fe086 100644 --- a/cloud/endagaweb/urls.py +++ b/cloud/endagaweb/urls.py @@ -142,6 +142,9 @@ url(r'^dashboard/network/prices$', endagaweb.views.network.NetworkPrices.as_view(), name='network-prices'), + url(r'^dashboard/network/denominations$', + endagaweb.views.network.NetworkDenomination.as_view(), + name='network-denominations'), url(r'^dashboard/network/inactive-subscribers$', endagaweb.views.network.NetworkInactiveSubscribers.as_view(), name='network-inactive-subscribers'), diff --git a/cloud/endagaweb/views/django_tables.py b/cloud/endagaweb/views/django_tables.py index 7c02bca2..83484d97 100644 --- a/cloud/endagaweb/views/django_tables.py +++ b/cloud/endagaweb/views/django_tables.py @@ -314,3 +314,54 @@ def render_actions(self, record): " id='%s'>release") element = template % record.number return safestring.mark_safe(element) + + +class DenominationListTable(tables.Table): + """A django-tables2 Table definition for the table list.""" + + class Meta: + model = models.NetworkDenomination + fields = ('start_amount', 'end_amount', 'validity_days') + attrs = {'class': 'table table-hover'} + + start_amount = tables.Column(empty_values=(), verbose_name='Start Amount') + end_amount = tables.Column(empty_values=(), verbose_name='End Amount') + validity_days = tables.Column(empty_values=(), verbose_name='Validity(Days)') + + def render_start_amount(self, record): + return humanize_credits(record.start_amount, + CURRENCIES[record.network.subscriber_currency]) + + def render_end_amount(self, record): + return humanize_credits(record.end_amount, + CURRENCIES[record.network.subscriber_currency]) + + +class DenominationTable(tables.Table): + """A django-tables2 Table definition for the table list.""" + + class Meta: + model = models.NetworkDenomination + fields = ('start_amount', 'end_amount', 'validity_days') + attrs = {'class': 'table table-hover'} + + start_amount = tables.Column(empty_values=(), verbose_name='Start Amount') + end_amount = tables.Column(empty_values=(), verbose_name='End Amount') + validity_days = tables.Column(empty_values=(), verbose_name='Validity(Days)') + action = tables.Column(empty_values=(), verbose_name='Action', orderable=False) + + def render_start_amount(self, record): + return humanize_credits(record.start_amount, + CURRENCIES[record.network.subscriber_currency]) + + def render_end_amount(self, record): + return humanize_credits(record.end_amount, + CURRENCIES[record.network.subscriber_currency]) + + def render_action(self, record): + """Shows the edit and delete button.""" + element = "Edit   " % (record.id, record.id) + element += "Delete" % (record.id) + return safestring.mark_safe(element) \ No newline at end of file diff --git a/cloud/endagaweb/views/network.py b/cloud/endagaweb/views/network.py index bf0db230..4b42123c 100644 --- a/cloud/endagaweb/views/network.py +++ b/cloud/endagaweb/views/network.py @@ -10,6 +10,7 @@ import datetime import time +import json from django import http from django import template @@ -21,7 +22,8 @@ import django_tables2 as tables from guardian.shortcuts import get_objects_for_user -from ccm.common.currency import parse_credits, CURRENCIES, DEFAULT_CURRENCY +from ccm.common.currency import parse_credits, humanize_credits, \ + CURRENCIES, DEFAULT_CURRENCY from endagaweb import models from endagaweb.forms import dashboard_forms from endagaweb.views.dashboard import ProtectedView @@ -453,3 +455,193 @@ def get(self, request, network_id): user_profile.network = network user_profile.save() return http.HttpResponseRedirect(request.META.get('HTTP_REFERER', '/dashboard')) + + +class NetworkDenomination(ProtectedView): + """Assign denominations bracket for recharge/adjust-credit in network.""" + + def get(self, request): + """Handles GET requests.""" + user_profile = models.UserProfile.objects.get(user=request.user) + network = user_profile.network + currency = network.subscriber_currency + + # Count the associated denomination with selected network. + denom = models.NetworkDenomination.objects.filter(network=network) + denom_count = denom.count() + + dnm_id = request.GET.get('id', None) + if dnm_id: + response = { + 'status': 'ok', + 'messages': [], + 'data': {} + } + denom = models.NetworkDenomination.objects.get(id=dnm_id) + denom_data = { + 'id': denom.id, + 'start_amount': humanize_credits(denom.start_amount, + CURRENCIES[currency]).amount, + 'end_amount': humanize_credits(denom.end_amount, + CURRENCIES[currency]).amount, + 'validity_days': denom.validity_days + } + response["data"] = denom_data + return http.HttpResponse(json.dumps(response), + content_type="application/json") + + # Configure the table of denominations. Do not show any pagination + # controls if the total number of donominations is small. + if not user_profile.user.is_staff: + denom_table = django_tables.DenominationListTable(list(denom)) + else: + denom_table = django_tables.DenominationTable(list(denom)) + towers_per_page = 8 + paginate = False + if denom > towers_per_page: + paginate = {'per_page': towers_per_page} + tables.RequestConfig(request, paginate=paginate).configure(denom_table) + + # Set the context with various stats. + context = { + 'networks': get_objects_for_user(request.user, 'view_network', + klass=models.Network), + 'currency': CURRENCIES[user_profile.network.subscriber_currency], + 'user_profile': user_profile, + 'network': network, + 'number_country': NUMBER_COUNTRIES[network.number_country], + 'denomination': denom_count, + 'denominations_table': denom_table, + } + # Render template. + info_template = template.loader.get_template( + 'dashboard/network_detail/denomination.html') + html = info_template.render(context, request) + return http.HttpResponse(html) + + def post(self, request): + """Operators can use this API to add denomination to a network. + + These denomination bracket will be used to recharge subscriber, + set balance validity and status + """ + user_profile = models.UserProfile.objects.get(user=request.user) + network = user_profile.network + try: + currency = network.subscriber_currency + start_amount_raw = request.POST.get('start_amount') + start_amount = parse_credits(start_amount_raw, + CURRENCIES[currency]).amount_raw + end_amount_raw = request.POST.get('end_amount') + end_amount = parse_credits(end_amount_raw, + CURRENCIES[currency]).amount_raw + validity_days = int(request.POST.get('validity_days')) or 0 + if validity_days > 10000: + validity_days = 10000 + dnm_id = int(request.POST.get('dnm_id')) or 0 + if start_amount <= 0 or end_amount <= 0: + messages.error(request, 'Enter positive and non-zero value ' \ + 'for start/end amount.', + extra_tags='alert alert-danger') + return redirect(urlresolvers.reverse('network-denominations')) + elif validity_days <= 0: + messages.error( + request, 'Validity can not be 0 day.', + extra_tags='alert alert-danger') + return redirect(urlresolvers.reverse('network-denominations')) + elif end_amount <= start_amount: + messages.error( + request, 'End amount should be greater than start amount.', + extra_tags='alert alert-danger') + return redirect(urlresolvers.reverse('network-denominations')) + + user_profile = models.UserProfile.objects.get(user=request.user) + with transaction.atomic(): + if dnm_id > 0: + try: + denom = models.NetworkDenomination.objects.get( + id=dnm_id) + # Check for existing denomination range exist. + denom_exists = models.NetworkDenomination.objects.\ + filter( + end_amount__gte=start_amount, + start_amount__lte=end_amount, + network=user_profile.network).\ + exclude(id=dnm_id).count() + if denom_exists: + messages.error( + request, 'Denomination range already exists.', + extra_tags='alert alert-danger') + return redirect( + urlresolvers.reverse('network-denominations')) + denom.network = user_profile.network + denom.start_amount = start_amount + denom.end_amount = end_amount + denom.validity_days = validity_days + denom.save() + messages.success( + request, 'Denomination is updated successfully.', + extra_tags='alert alert-success') + except models.NetworkDenomination.DoesNotExist: + messages.error( + request, 'Invalid denomination ID.', + extra_tags='alert alert-danger') + return redirect( + urlresolvers.reverse('network-denominations')) + else: + # Check for existing denomination range exist. + denom_exists = models.NetworkDenomination.objects.filter( + end_amount__gte=start_amount, + start_amount__lte=end_amount, + network=user_profile.network).count() + if denom_exists: + messages.error( + request, 'Denomination range already exists.', + extra_tags='alert alert-danger') + return redirect( + urlresolvers.reverse('network-denominations')) + # Create new denomination for selected network + denom = models.NetworkDenomination( + network=user_profile.network) + denom.network = user_profile.network + denom.start_amount = start_amount + denom.end_amount = end_amount + denom.validity_days = validity_days + denom.save() + messages.success( + request, 'Denomination is created successfully.', + extra_tags='alert alert-success') + except Exception: + messages.error( + request, 'Invalid validity value. Enter greater than ' \ + '0 digit value', + extra_tags='alert alert-danger') + return redirect(urlresolvers.reverse('network-denominations')) + + def delete(self, request): + """Handles delete requests.""" + response = { + 'status': 'ok', + 'messages': [], + } + dnm_id = request.GET.get('id') or False + if dnm_id: + try: + denom = models.NetworkDenomination.objects.get(id=dnm_id) + denom.delete() + response['status'] = 'success' + messages.success(request, + 'Denomination deleted successfully.', + extra_tags='alert alert-success') + except models.NetworkDenomination.DoesNotExist: + response['status'] = 'failed' + messages.error( + request, 'Invalid denomination ID.', + extra_tags='alert alert-danger') + else: + response['status'] = 'failed' + messages.error( + request, 'Invalid request data.', + extra_tags='alert alert-danger') + return http.HttpResponse(json.dumps(response), + content_type="application/json") \ No newline at end of file From 1367cfdba98194e524b8443953ee30b0c7ab1058 Mon Sep 17 00:00:00 2001 From: Shiv K Sah Date: Tue, 6 Jun 2017 16:51:57 +0530 Subject: [PATCH 2/2] Code review incorporated --- cloud/endagaweb/admin.py | 1 + cloud/endagaweb/models.py | 8 ++++++-- cloud/endagaweb/settings/prod.py | 3 +++ .../dashboard/network_detail/denomination.html | 2 +- cloud/endagaweb/views/network.py | 14 +++++++++++--- 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/cloud/endagaweb/admin.py b/cloud/endagaweb/admin.py index 6c1bbbc6..d6b10113 100644 --- a/cloud/endagaweb/admin.py +++ b/cloud/endagaweb/admin.py @@ -62,6 +62,7 @@ class BTSLogfileAdmin(admin.ModelAdmin): admin.site.register(models.DestinationGroup) admin.site.register(models.Ledger) admin.site.register(models.Network, GuardedModelAdmin) +admin.site.register(models.NetworkDenomination) admin.site.register(models.Number) admin.site.register(models.PendingCreditUpdate) admin.site.register(models.Subscriber) diff --git a/cloud/endagaweb/models.py b/cloud/endagaweb/models.py index 141ad542..65b0780f 100644 --- a/cloud/endagaweb/models.py +++ b/cloud/endagaweb/models.py @@ -1473,8 +1473,12 @@ class NetworkDenomination(models.Model): network = models.ForeignKey('Network', null=True, on_delete=models.CASCADE) def __unicode__(self): - return "Amount %s - %d for %s(days)" % ( - self.start_amount, self.end_amount, self.validity_days) + return "Amount %s - %s for %s day(s)" % ( + humanize_credits(self.start_amount, + CURRENCIES[self.network.subscriber_currency]), + humanize_credits(self.end_amount, + CURRENCIES[self.network.subscriber_currency]), + self.validity_days) class Meta: ordering = ('start_amount',) diff --git a/cloud/endagaweb/settings/prod.py b/cloud/endagaweb/settings/prod.py index 653f2f0e..c94de28d 100644 --- a/cloud/endagaweb/settings/prod.py +++ b/cloud/endagaweb/settings/prod.py @@ -277,6 +277,9 @@ # Enable/disable billing for networks. If false, we ignore what's in the # network's account balance. 'NW_BILLING': os.environ.get("NW_BILLING", "True").lower() == "true", + + # Maximum permissible validity(in days) limit for denomination + 'MAX_VALIDITY_DAYS': 10000, } STRIPE_API_KEY = os.environ.get("STRIPE_API_KEY", diff --git a/cloud/endagaweb/templates/dashboard/network_detail/denomination.html b/cloud/endagaweb/templates/dashboard/network_detail/denomination.html index 2101bf1a..3e7582f0 100644 --- a/cloud/endagaweb/templates/dashboard/network_detail/denomination.html +++ b/cloud/endagaweb/templates/dashboard/network_detail/denomination.html @@ -148,7 +148,7 @@

{% block js %} + src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.10.0/jquery.validate.min.js">