From 725a417e548ba326141a7561ad597b1fe98f7ee1 Mon Sep 17 00:00:00 2001 From: Sagar Date: Wed, 7 Jun 2017 17:57:26 +0530 Subject: [PATCH 1/2] Added Subscirber: Negative/Formated Transaction IDs for Usage Events Block/Unblock Subscriber on Invalid Events --- cloud/endagaweb/celery.py | 4 ++ cloud/endagaweb/models.py | 103 +++++++++++++++++++++++++++++--- cloud/endagaweb/tasks.py | 17 ++++++ cloud/endagaweb/util/dbutils.py | 18 ++++++ 4 files changed, 134 insertions(+), 8 deletions(-) diff --git a/cloud/endagaweb/celery.py b/cloud/endagaweb/celery.py index 8125f76f..c62bba1f 100644 --- a/cloud/endagaweb/celery.py +++ b/cloud/endagaweb/celery.py @@ -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), } }) diff --git a/cloud/endagaweb/models.py b/cloud/endagaweb/models.py index d2429f9d..da966b88 100644 --- a/cloud/endagaweb/models.py +++ b/cloud/endagaweb/models.py @@ -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 @@ -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', @@ -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') class UserProfile(models.Model): @@ -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: + default_permissions = () + permissions = ( + ('view_subscriber', 'View subscriber list'), + ('change_subscriber', 'Edit subscriber'), + ('deactive_subscriber', 'Deactive subscriber'), + ) @classmethod def update_balance(cls, imsi, other_bal): @@ -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 + subscriber_event.negative_transactions = negative_transactions_ids + subscriber_event.save() + + max_transactions = event.subscriber.network.max_failure_transaction + if subscriber_event.count == max_transactions: + subscriber.is_blocked = True + subscriber.block_reason = 'Repeated %s within 24 hours ' % ( + '/'.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. @@ -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) \ No newline at end of file diff --git a/cloud/endagaweb/tasks.py b/cloud/endagaweb/tasks.py index 405ca698..873ded46 100644 --- a/cloud/endagaweb/tasks.py +++ b/cloud/endagaweb/tasks.py @@ -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!') \ No newline at end of file diff --git a/cloud/endagaweb/util/dbutils.py b/cloud/endagaweb/util/dbutils.py index 42e0e7ee..56f4318b 100644 --- a/cloud/endagaweb/util/dbutils.py +++ b/cloud/endagaweb/util/dbutils.py @@ -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 From 2b1d9a1b07e56d153a9c4315c4f2c1bb0fbb0880 Mon Sep 17 00:00:00 2001 From: Sagar Date: Mon, 12 Jun 2017 18:55:55 +0530 Subject: [PATCH 2/2] Added missing merge (transaction_id as textfield) --- cloud/endagaweb/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/endagaweb/models.py b/cloud/endagaweb/models.py index da966b88..1ef525dd 100644 --- a/cloud/endagaweb/models.py +++ b/cloud/endagaweb/models.py @@ -781,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)