Skip to content
This repository has been archived by the owner on Sep 26, 2018. It is now read-only.

Sprint2__Subscriber Features 1 of 2 (Client dependent) #43

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions cloud/endagaweb/celery.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,13 @@
'task': 'endagaweb.tasks.usageevents_to_sftp',
# Run this at 15:00 UTC (10:00 PDT, 02:00 Papua time)
'schedule': crontab(minute=0, hour=17),
},'validity-expiry-sms': {
'task': 'endagaweb.tasks.validity_expiry_sms',
# Run this at 14:00 UTC (09:00 PDT, 01:00 Papua time).
'schedule': crontab(minute=0, hour=15),
},'subscriber-validity-state': {
'task': 'endagaweb.tasks.subscriber_validity_state',
# Run this at 14:00 UTC (09:00 PDT, 01:00 Papua time).
'schedule': crontab(minute=0, hour=07),
}
})
19 changes: 17 additions & 2 deletions cloud/endagaweb/forms/dashboard_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,20 @@ class SubVacuumForm(forms.Form):
label='Automatically delete inactive subscribers',
help_text=inactive_help_text,
choices=enabled_choices, widget=forms.RadioSelect())
inactive_days = forms.CharField(
required=False, label='Outbound inactivity threshold (days)')
inactive_days = forms.IntegerField(
required=False, label='Outbound inactivity threshold (days)',
min_value=0, max_value=10000, widget=
forms.TextInput(attrs={'class': 'form-control', 'pattern':'[0-9]+',
'oninvalid':"setCustomValidity('Enter days only!')",
'onchange':"try{"
"setCustomValidity('')}catch(e){}"}))
grace_days = forms.IntegerField(
required=False, label='Grace Period (days)', min_value=0,
max_value=1000, widget=
forms.TextInput(attrs={'class': 'form-control', 'pattern':'[0-9]+',
'oninvalid':"setCustomValidity('Enter days only!')",
'onchange':"try{"
"setCustomValidity('')}catch(e){}"}))

def __init__(self, *args, **kwargs):
super(SubVacuumForm, self).__init__(*args, **kwargs)
Expand All @@ -221,11 +233,14 @@ def __init__(self, *args, **kwargs):
# not this feature is active.
if args[0]['sub_vacuum_enabled']:
days_field = Field('inactive_days')
grace_field = Field('grace_days')
else:
days_field = Field('inactive_days', disabled=True)
grace_field = Field('grace_days', disabled=True)
self.helper.layout = Layout(
'sub_vacuum_enabled',
days_field,
grace_field,
Submit('submit', 'Save', css_class='pull-right'),
)

Expand Down
5 changes: 3 additions & 2 deletions cloud/endagaweb/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ class Subscriber(models.Model):
imsi = models.CharField(max_length=50, unique=True)
name = models.TextField()
crdt_balance = models.TextField(default=crdt.PNCounter("default").serialize())
state = models.CharField(max_length=10)
state = models.CharField(max_length=15)
# Time of the last received UsageEvent that's not in NON_ACTIVITIES.
last_active = models.DateTimeField(null=True, blank=True)
# Time of the last received UsageEvent that is in OUTBOUND_ACTIVITIES. We
Expand Down Expand Up @@ -940,7 +940,8 @@ class Network(models.Model):
# Whether or not to automatically delete inactive subscribers, and
# associated parameters.
sub_vacuum_enabled = models.BooleanField(default=False)
sub_vacuum_inactive_days = models.IntegerField(default=180)
sub_vacuum_inactive_days = models.PositiveIntegerField(default=180)
sub_vacuum_grace_days = models.PositiveIntegerField(default=30)

# csv of endpoints to notify for downtime
notify_emails = models.TextField(blank=True, default='')
Expand Down
129 changes: 125 additions & 4 deletions cloud/endagaweb/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,11 +246,11 @@ def vacuum_inactive_subscribers(self):
# Do nothing if subscriber vacuuming is disabled for the network.
if not network.sub_vacuum_enabled:
continue
inactives = network.get_outbound_inactive_subscribers(
network.sub_vacuum_inactive_days)
inactives = Subscriber.objects.filter(
state='recycle', network_id=network.id,
prevent_automatic_deactivation=False
)
for subscriber in inactives:
if subscriber.prevent_automatic_deactivation:
continue
print 'vacuuming %s from network %s' % (subscriber.imsi, network)
subscriber.deactivate()
# Sleep a bit in between each deactivation so we don't flood the
Expand Down Expand Up @@ -439,3 +439,124 @@ def req_bts_log(self, obj, retry_delay=60*10, max_retries=432):
raise
finally:
obj.save()


@app.task(bind=True)
def subscriber_validity_state(self):
""" Updates the subscribers state to inactive/active/"""

today = django.utils.timezone.now()
subscribers = Subscriber.objects.filter(
number__valid_through__lte=today)
today = today.date()
for subscriber in subscribers:
try:
Copy link
Contributor

Choose a reason for hiding this comment

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

you should have a loop on each of the subscriber's number. there can be more than the one at index 0

number = subscriber.number_set.all()[0]
if number.valid_through is None:
continue
except IndexError:
continue
subscriber_validity = number.valid_through.date()
first_expire = subscriber_validity + datetime.timedelta(
days=subscriber.network.sub_vacuum_inactive_days)
recycle = first_expire + datetime.timedelta(
days=subscriber.network.sub_vacuum_grace_days)
current_state = str(subscriber.state)

if subscriber_validity < today:
if today <= first_expire:
# Do nothing if it's already first expired
if current_state != 'first_expired':
subscriber.state = 'first_expired'
subscriber.save()
print "Updating subscriber(%s) state to 'First Expired'" % (
subscriber.imsi,)
elif today > recycle:
# Let deactivation of subscriber be handled by
# vacuum_inactive_subscribers
# Do nothing if it's already recycle
if current_state != 'recycle':
subscriber.state = 'recycle'
subscriber.save()
print "Updating subscriber(%s) state to 'Recycle'" % (
subscriber.imsi,)
else:
if current_state != 'expired':
subscriber.state = 'expired'
subscriber.save()
print "Updating subscriber(%s) state to 'Expired'" % (
subscriber.imsi,)


@app.task(bind=True)
def validity_expiry_sms(self, days=7):
"""Sends SMS to the number whose validity is:
about to get expire,
if expired (i.e 1st expired), or
if the number is in grace period and is about to recycle.

Args:
days: Days prior (state change) which the SMS is sent to Subscriber.
Runs as everyday task managed by celerybeat.
"""
today = django.utils.timezone.datetime.now().date()
for subscriber in Subscriber.objects.iterator():
# Do nothing if subscriber vacuuming is disabled for the network.
Copy link
Contributor

Choose a reason for hiding this comment

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

all numbers :)

if not subscriber.network.sub_vacuum_enabled:
continue
try:
number = subscriber.number_set.all()[0]
subscriber_validity = number.valid_through
# In case where number has no validity
if subscriber_validity is None:
print '%s has no validity' % (subscriber.imsi,)
continue
except IndexError:
print 'No number attached to subscriber %s' % (subscriber.imsi,)
continue

subscriber_validity = subscriber_validity.date()
inactive_period = subscriber.network.sub_vacuum_inactive_days
grace_period = subscriber.network.sub_vacuum_grace_days

prior_first_expire = subscriber_validity + datetime.timedelta(
days=inactive_period) - datetime.timedelta(days=days)

prior_recycle = prior_first_expire + datetime.timedelta(
days=grace_period)

# Prior to expiry state (one on last day and before defined days)
if subscriber_validity > today and (
(subscriber_validity - datetime.timedelta(
days=days)
) == today or today == (
subscriber_validity - datetime.timedelta(
days=1)) or today == subscriber_validity):
body = 'Your validity is about to get expired on %s , Please ' \
Copy link
Contributor

Choose a reason for hiding this comment

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

this needs be translated so this will need to live on the client. you can construct a key/short phrase that you send that the client translates with local i18n

Copy link
Author

Choose a reason for hiding this comment

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

will update.

'recharge to continue the service. Please ignore if ' \
'already done! ' % (subscriber_validity,)
sms_notification(body=body, to=number)
Copy link
Contributor

Choose a reason for hiding this comment

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

This SMS will go through SMPP SMSC/Nexmo rather than into the internal network. We should add an endpoint on federer that will get triggered via RPC. Can you change this to an RPC POST to the client? We can implement the actual client handler later.

Copy link
Author

Choose a reason for hiding this comment

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

sure will try to do that way.

# Prior 1st_expired state
elif subscriber_validity < today:
if prior_first_expire == today or today == (
prior_first_expire + datetime.timedelta(
days=days - 1)):
body = 'Your validity has expired on %s, Please recharge ' \
'immediately to activate your services again! ' % (
subscriber_validity,)
sms_notification(body=body, to=number)
# Prior to recycle state
elif prior_recycle == today or today == (
prior_recycle + datetime.timedelta(days=days - 1)):
body = 'Warning: Your validity has expired on %s , Please ' \
'recharge immediately to avoid deactivation of your ' \
'connection! ' % (subscriber_validity,)
sms_notification(body=body, to=number)
# SMS on same day of expiry
elif subscriber_validity == today:
body = 'Your validity expiring today %s, Please recharge ' \
'immediately to continue your services again!, ' \
'Ignore if already done! ' % (subscriber_validity,)
sms_notification(body=body, to=number)
else:
return # Do nothing
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,10 @@
var sub_vacuum_enabled = 'True' == $('form input[name=sub_vacuum_enabled]:checked').val();
if (sub_vacuum_enabled) {
$('#id_inactive_days').prop('disabled', false);
$('#id_grace_days').prop('disabled', false);
} else {
$('#id_inactive_days').prop('disabled', true);
$('#id_grace_days').prop('disabled', true);
}
});
});
Expand Down
62 changes: 58 additions & 4 deletions cloud/endagaweb/tests/test_subscribers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,19 @@
from __future__ import print_function
from __future__ import unicode_literals

import datetime as datetime2
import json
import uuid
from datetime import datetime
from random import randrange
import uuid

import pytz

from django import test
from django.test import TestCase

from ccm.common import crdt
from endagaweb import models
from endagaweb import tasks


class TestBase(TestCase):
Expand All @@ -36,9 +39,9 @@ def setUpTestData(cls):
@classmethod
def add_sub(cls, imsi,
ev_kind=None, ev_reason=None, ev_date=None,
balance=0):
balance=0, state='active'):
sub = models.Subscriber.objects.create(
imsi=imsi, network=cls.network, balance=balance)
imsi=imsi, network=cls.network, balance=balance, state=state)
if ev_kind:
if ev_date is None:
ev_date = datetime.now(pytz.utc)
Expand Down Expand Up @@ -147,3 +150,54 @@ def test_sub_with_activity(self):
outbound_inactives = self.network.get_outbound_inactive_subscribers(
days)
self.assertFalse(sub in outbound_inactives)


class SubscriberValidityTests(TestBase):
"""
We can change subscriber state depending on its validity and can deactivate
after completion of threshold
"""

def setup_the_env(self, days=7):
imsi = self.gen_imsi()
self.subscriber = self.add_sub(imsi, balance=100, state='active')
# Set expired validity for the number
validity = datetime.now(pytz.utc) - datetime2.timedelta(days=days)
self.bts = models.BTS(uuid="133222", nickname="test-bts-name!",
inbound_url="http://localhost/133222/test",
network=self.network)
self.bts.save()
self.number = models.Number(
number='5559234', state="inuse", network=self.bts.network,
kind="number.nexmo.monthly", subscriber=self.subscriber,
valid_through=validity)
net = models.Network.objects.get(id=self.bts.network.id)
net.sub_vacuum_enabled = True
net.sub_vacuum_inactive_days = 180
net.sub_vacuum_grace_days = 30
self.number.save()
net.save()

def test_subscriber_inactive(self):
# Set subscriber's validity 7 days earlier then current date
self.setup_the_env(days=7)
tasks.subscriber_validity_state()
subscriber = models.Subscriber.objects.get(id=self.subscriber.id)
self.assertEqual(subscriber.state, 'inactive')

def test_subscriber_expired(self):
# Set subscriber's validity more than threshold days
days = self.network.sub_vacuum_inactive_days
self.setup_the_env(days=days + 1)
tasks.subscriber_validity_state()
subscriber = models.Subscriber.objects.get(id=self.subscriber.id)
self.assertEqual(subscriber.state, 'first_expire')

def test_subscriber_recycle(self):
# Set subscriber's validity days more than grace period and
# threshold days
days = self.network.sub_vacuum_inactive_days + self.network.sub_vacuum_grace_days
self.setup_the_env(days=days + 1)
tasks.subscriber_validity_state()
subscriber = models.Subscriber.objects.get(id=self.subscriber.id)
self.assertEqual(subscriber.state, 'recycle')
11 changes: 8 additions & 3 deletions cloud/endagaweb/views/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ def get(self, request):
'sub_vacuum_form': dashboard_forms.SubVacuumForm({
'sub_vacuum_enabled': network.sub_vacuum_enabled,
'inactive_days': network.sub_vacuum_inactive_days,
'grace_days': network.sub_vacuum_grace_days,
}),
'protected_subs': protected_subs,
'unprotected_subs': unprotected_subs,
Expand All @@ -167,17 +168,21 @@ def post(self, request):
if 'inactive_days' in request.POST:
try:
inactive_days = int(request.POST['inactive_days'])
grace_days = int(request.POST['grace_days'])
if inactive_days > 10000:
inactive_days = 10000
if grace_days > 1000:
grace_days = 1000
network.sub_vacuum_inactive_days = inactive_days
network.sub_vacuum_grace_days = grace_days
network.save()
messages.success(
request, 'Subscriber auto-deletion settings saved.',
extra_tags='alert alert-success')
except ValueError:
text = 'The "inactive days" parameter must be an integer.'
messages.error(request, text,
extra_tags="alert alert-danger")
messages.success(
request, 'Subscriber auto-deletion settings saved.',
extra_tags='alert alert-success')
return redirect(urlresolvers.reverse('network-inactive-subscribers'))


Expand Down