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

Sprint2__Subscriber Features 2 of 2 (Client dependent) #44

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
4 changes: 4 additions & 0 deletions cloud/endagaweb/celery.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,9 @@
'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),
},'unblock-blocked-subscriber': {
'task': 'endagaweb.tasks.unblock_blocked_subscribers',
# Run this in every hour
'schedule': crontab(minute=59),
}
})
105 changes: 96 additions & 9 deletions cloud/endagaweb/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,23 @@
import time
import uuid

import django.utils.timezone
import itsdangerous
import pytz
import stripe
from django.conf import settings
from django.contrib.auth.models import Group, User
from django.contrib.gis.db import models as geomodels
from django.core.validators import MinValueValidator
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import connection
from django.db import models
from django.db import transaction
from django.db.models import F
from django.db.models.signals import post_save
from django.db.models.signals import post_save, pre_save
from guardian.shortcuts import (assign_perm, get_users_with_perms)
from rest_framework.authtoken.models import Token
import django.utils.timezone
import itsdangerous
import pytz
import stripe

from ccm.common import crdt, logger
from ccm.common.currency import humanize_credits, CURRENCIES
Expand All @@ -47,7 +48,6 @@

stripe.api_key = settings.STRIPE_API_KEY


# These UsageEvent kinds do not count towards subscriber activity.
NON_ACTIVITIES = (
'deactivate_number', 'deactivate_subscriber', 'add_money',
Expand All @@ -58,6 +58,9 @@
OUTBOUND_ACTIVITIES = (
'outside_call', 'outside_sms', 'local_call', 'local_sms',
)
# These UsageEvent events are not allowed block the Subscriber if repeated
# for 3 times
INVALID_EVENTS = ('error_call', 'error_sms')
Copy link
Contributor

Choose a reason for hiding this comment

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

We should keep this list empty/commented out until we define the use cases and implement/validate the dialplan/chatplan event generation

Copy link
Author

Choose a reason for hiding this comment

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

yes, we have kept this as dummy for demo, as soon that functionality gets added we will replace with new EVENT.



class UserProfile(models.Model):
Expand Down Expand Up @@ -525,6 +528,19 @@ 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)
# Block subscriber if repeated unauthorized events.
is_blocked = models.BooleanField(default=False)
block_reason = models.TextField(default='No reason to block yet!',
max_length=255)
block_time = models.DateTimeField(null=True, blank=True)

class Meta:
Copy link
Contributor

Choose a reason for hiding this comment

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

why are there new permissions introduced here?

default_permissions = ()
permissions = (
('view_subscriber', 'View subscriber list'),
('change_subscriber', 'Edit subscriber'),
('deactive_subscriber', 'Deactive subscriber'),
)

@classmethod
def update_balance(cls, imsi, other_bal):
Expand Down Expand Up @@ -765,7 +781,7 @@ class UsageEvent(models.Model):
downloaded_bytes: number of downloaded bytes for a GPRS event
timespan: the duration of time over which the GPRS data was sampled
"""
transaction_id = models.UUIDField(editable=False, default=uuid.uuid4)
transaction_id = models.TextField()
subscriber = models.ForeignKey(Subscriber, null=True,
on_delete=models.SET_NULL)
subscriber_imsi = models.TextField(null=True)
Expand Down Expand Up @@ -868,10 +884,73 @@ def set_subscriber_last_active(sender, instance=None, created=False,
event.subscriber.last_active = event.date
event.subscriber.save()

@staticmethod
def if_invalid_events(sender, instance=None, created=False, **kwargs):
# Check for any invalid event and make an entry
if not created:
return
event = instance
if event.kind in INVALID_EVENTS:
subscriber = Subscriber.objects.get(imsi=event.subscriber_imsi)
if SubscriberInvalidEvents.objects.filter(
subscriber=event.subscriber).exists():
# Subscriber is blocked after 3 counts i.e there won't be UEs
# unless unblocked
subscriber_event = SubscriberInvalidEvents.objects.get(
subscriber=event.subscriber)
# if it is a 3rd event in 24hr block the subscriber
negative_transactions_ids = subscriber_event.negative_transactions + [
event.transaction_id]
subscriber_event.count = subscriber_event.count + 1
subscriber_event.event_time = event.date
Copy link
Contributor

Choose a reason for hiding this comment

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

we should only update the event_time of the first event so you can do the 24 hour check

subscriber_event.negative_transactions = negative_transactions_ids
subscriber_event.save()

max_transactions = event.subscriber.network.max_failure_transaction
if subscriber_event.count == max_transactions:
Copy link
Contributor

Choose a reason for hiding this comment

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

24 hour check missing?

subscriber.is_blocked = True
subscriber.block_reason = 'Repeated %s within 24 hours ' % (
Copy link
Contributor

Choose a reason for hiding this comment

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

do we need block reason? Can we just list out the negative_transactions in the UI?

Copy link
Author

Choose a reason for hiding this comment

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

Block reason is kept for REPORTING purpose, and we are already listing negative transactions.

Copy link
Contributor

Choose a reason for hiding this comment

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

if we need the reports, we have the usageevents too

'/'.join(INVALID_EVENTS),)
subscriber.block_time = django.utils.timezone.now()
subscriber.save()
logger.info('Subscriber %s blocked for 30 minutes, '
'repeated invalid transactions within 24 '
'hours' % (
subscriber.imsi))
else:
subscriber_event = SubscriberInvalidEvents.objects.create(
subscriber=event.subscriber, count=1)
subscriber_event.event_time = event.date
subscriber_event.negative_transactions = [event.transaction_id]
subscriber_event.save()
elif SubscriberInvalidEvents.objects.filter(
subscriber=event.subscriber).count() > 0:
# Delete the event if events are non-consecutive
if not event.subscriber.is_blocked:
subscriber_event = SubscriberInvalidEvents.objects.get(
subscriber=event.subscriber)
logger.info('Subscriber %s invalid event removed' % (
event.subscriber_imsi))
subscriber_event.delete()

@staticmethod
def set_transaction_id(sender, instance=None, **kwargs):
"""
Create transaction id to some readable format
Set transaction as negative transaction if error event
"""
event = instance
if event.kind in INVALID_EVENTS:
negative = True
else:
negative = False
event.transaction_id = dbutils.format_transaction(instance.date,
negative)

post_save.connect(UsageEvent.set_imsi_and_uuid_and_network, sender=UsageEvent)
post_save.connect(UsageEvent.set_subscriber_last_active, sender=UsageEvent)

post_save.connect(UsageEvent.if_invalid_events, sender=UsageEvent)
pre_save.connect(UsageEvent.set_transaction_id, sender=UsageEvent)

class PendingCreditUpdate(models.Model):
"""A credit update that has yet to be acked by a BTS.
Expand Down Expand Up @@ -1769,3 +1848,11 @@ class FileUpload(models.Model):
created_time = models.DateTimeField(auto_now_add=True)
modified_time = models.DateTimeField(auto_now_add=True)
accessed_time = models.DateTimeField(auto_now=True)


class SubscriberInvalidEvents(models.Model):
""" Invalid Events logs by Subscriber"""
subscriber = models.ForeignKey(Subscriber, on_delete=models.CASCADE)
count = models.PositiveIntegerField()
event_time = models.DateTimeField(auto_now_add=True)
negative_transactions = ArrayField(models.TextField(), null=True)
17 changes: 17 additions & 0 deletions cloud/endagaweb/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,3 +439,20 @@ def req_bts_log(self, obj, retry_delay=60*10, max_retries=432):
raise
finally:
obj.save()


@app.task(bind=True)
def unblock_blocked_subscribers(self):
"""Unblock subscribers who are blocked for past 24 hrs.

This runs this as a periodic task managed by celerybeat.
"""
unblock_time = django.utils.timezone.now() - datetime.timedelta(days=1)
subscribers = Subscriber.objects.filter(is_blocked=True,
block_time__lte=unblock_time)
if not subscribers:
return # Do nothing
print 'Unblocking subscribers %s blocked for past 24 hours' % (
[subscriber.imsi for subscriber in subscribers], )
subscribers.update(is_blocked=False, block_time=None,
block_reason='No reason to block yet!')
Copy link
Contributor

Choose a reason for hiding this comment

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

we should also have another task to delete SubscriberInvalidEvents that are older than 24 hours? it seems like it would also need to update the event_time to the time of the 2nd invalid event in the negative actions list.

Copy link
Author

Choose a reason for hiding this comment

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

We are intentionally not deleting SubscriberInvalidEvents for reporting purpose.
Though it gets replaced after 24 hours by subscriber again if any negative transaction occur.

We are not updating 2nd invalid event time, as if we do we won't be able to calculate the 24 hour time span of the transactions for that particular subscriber.
Also if we need details of that transaction it can be captured from UsageEvent table as the transaction id remains same.

Copy link
Contributor

Choose a reason for hiding this comment

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

Its still not clear how we are blocking the subscriber only if it is a 3rd event in 24hr block. Can you perhaps explain the flow?

18 changes: 18 additions & 0 deletions cloud/endagaweb/util/dbutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,27 @@
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.
"""
import uuid

import django


def get_db_time(connection):
cursor = connection.cursor()
cursor.execute("SELECT statement_timestamp();")
return cursor.fetchone()[0]


def format_transaction(tansaction_date=None, transaction_type=False):
# Generating new transaction id using old transaction and date
if tansaction_date is None:
dt = django.utils.timezone.now()
else:
dt = tansaction_date
uuid_transaction = str(uuid.uuid4().hex[:6])
transaction = '{0}id{1}'.format(str(dt.date()).replace('-', ''),
uuid_transaction)
if transaction_type:
return '-%s' % (transaction,)
else:
return transaction