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/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..65b0780f 100644
--- a/cloud/endagaweb/models.py
+++ b/cloud/endagaweb/models.py
@@ -1460,6 +1460,30 @@ 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 - %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',)
+
+
class ConfigurationKey(models.Model):
"""A key->value mapping for storing settings.
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
new file mode 100644
index 00000000..3e7582f0
--- /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 %}
+
+ {% 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
+
+
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+ Are you sure you want to delete ""?
+
+
+
+
+
+
+
+{% 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 %}
+
+ Denominations
+
Edit
-
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..41bea1da 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
@@ -20,8 +21,10 @@
from django.shortcuts import redirect
import django_tables2 as tables
from guardian.shortcuts import get_objects_for_user
+from django.conf import settings
-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 +456,200 @@ 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
+
+ dnm_id = int(request.POST.get('dnm_id')) or 0
+ if validity_days > settings.ENDAGA['MAX_VALIDITY_DAYS']:
+ message = 'Validity days value exceeds maximum permissible ' \
+ 'limit (%s Days).' % \
+ (settings.ENDAGA['MAX_VALIDITY_DAYS'])
+ messages.error(
+ request, message,
+ extra_tags='alert alert-danger')
+ return redirect(urlresolvers.reverse('network-denominations'))
+ elif 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