diff --git a/cloud/endagaweb/models.py b/cloud/endagaweb/models.py index 65b0780f..eb38cb36 100644 --- a/cloud/endagaweb/models.py +++ b/cloud/endagaweb/models.py @@ -525,6 +525,8 @@ 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) + # role of subscriber + role = models.TextField(null=True, blank=True, default="Subscriber") @classmethod def update_balance(cls, imsi, other_bal): @@ -576,8 +578,8 @@ def change_balance(self, amt): self.crdt_balance = bal.serialize() def __unicode__(self): - return "Sub %s, %s, network: %s, balance: %d" % ( - self.name, self.imsi, self.network, self.balance) + return "Sub %s, %s, network: %s, balance: %d, role: %s" % ( + self.name, self.imsi, self.network, self.balance, self.role) def numbers(self): n = self.number_set.all() diff --git a/cloud/endagaweb/templates/dashboard/subscribers.html b/cloud/endagaweb/templates/dashboard/subscribers.html index a0e018e3..c09c5b31 100644 --- a/cloud/endagaweb/templates/dashboard/subscribers.html +++ b/cloud/endagaweb/templates/dashboard/subscribers.html @@ -42,20 +42,246 @@

Subscribers

{% if number_of_filtered_subscribers %} {% render_table subscriber_table %} + + + +
+ {% elif total_number_of_subscribers > 0 %} +

No subscribers matched your search.

+ + {% else %} +

+ There are currently no subscribers registered on this network. + Go find some users! +

+ + {% endif %} + - {% endblock %} +{% block js %} + + {% endblock %} diff --git a/cloud/endagaweb/tests/test_api_v2.py b/cloud/endagaweb/tests/test_api_v2.py index c77179d2..ca6830c9 100644 --- a/cloud/endagaweb/tests/test_api_v2.py +++ b/cloud/endagaweb/tests/test_api_v2.py @@ -496,6 +496,21 @@ def setUpClass(cls): cls.pcu = models.PendingCreditUpdate(subscriber=cls.sub, amount=100, uuid='abc123') cls.pcu.save() + # create more subscriber for same network with different number + cls.imsi3 = "IMSI999990000000455" + cls.sub2_network1 = models.Subscriber( + balance=100000, name='sub-two', imsi=cls.imsi3, + network=cls.bts.network, bts=cls.bts) + cls.sub2_network1.save() + cls.number4 = models.Number(number='6285574719326', + state="inuse", + network=cls.user_profile.network, + subscriber=cls.sub2_network1, + kind="number.nexmo.monthly") + cls.number4.save() + cls.pcu2 = models.PendingCreditUpdate(subscriber=cls.sub2_network1, amount=300, + uuid='abc345') + cls.pcu2.save() @classmethod def tearDownClass(cls): @@ -511,6 +526,9 @@ def tearDownClass(cls): cls.number2.delete() cls.number3.delete() cls.pcu.delete() + cls.sub2_network1.delete() + cls.number4.delete() + cls.pcu2.delete() def setUp(self): self.client = Client() @@ -627,3 +645,59 @@ def test_deactivate_subscriber_sans_bts(self): event_count = models.UsageEvent.objects.filter( subscriber_imsi=self.sub2.imsi, kind='delete_imsi').count() self.assertEqual(1, event_count) + + def test_bulk_deactivate_subscriber(self): + """We can deactivate the bulk Subscriber via DELETE """ + + + url = '/api/v2/subscribers/%s,%s' % (self.sub.imsi,self.sub2_network1.imsi) + print(url) + header = { + 'HTTP_AUTHORIZATION': 'Token %s' % self.user_profile.network.api_token + } + with mock.patch('endagaweb.celery.app.send_task') as mocked_task: + response = self.client.delete(url, **header) + self.assertEqual(200, response.status_code) + # The both subscriber should no longer be in the DB. + self.assertEqual(0, models.Subscriber.objects.filter( + imsi=self.sub.imsi).count()) + self.assertEqual(0, models.Subscriber.objects.filter( + imsi=self.sub2_network1.imsi).count()) + # associated numbers of both subscriber of same network should + # have been deactivated -- reload + # them from the DB to check their state. + number = models.Number.objects.get(id=self.number.id) + self.assertEqual('available', number.state) + self.assertEqual(None, number.network) + self.assertEqual(None, number.subscriber) + number2 = models.Number.objects.get(id=self.number2.id) + self.assertEqual('available', number2.state) + number3 = models.Number.objects.get(id=self.number4.id) + self.assertEqual('available', number3.state) + # The associated PendingCreditUpdate should be gone. + self.assertEqual(0, models.PendingCreditUpdate.objects.filter( + pk=self.pcu.pk).count()) + self.assertEqual(0, models.PendingCreditUpdate.objects.filter( + pk=self.pcu2.pk).count()) + # The mocked task should have been called with specific arguments + self.assertTrue(mocked_task.called) + args, _ = mocked_task.call_args + task_name, task_args = args + task_endpoint, task_data = task_args + self.assertEqual('endagaweb.tasks.async_post', task_name) + expected_url = '%s/config/deactivate_subscriber' % self.bts.inbound_url + self.assertEqual(expected_url, task_endpoint) + # The task_data should be signed with the BTS UUID and should have a + # jwt key which is a dict with a imsi key. + + serializer = itsdangerous.JSONWebSignatureSerializer(self.bts.secret) + task_data = serializer.loads(task_data['jwt']) + self.assertEqual(self.sub2_network1.imsi, task_data['imsi']) + # A 'delete_imsi' UsageEvent should have been created for both subscriber + event_count = models.UsageEvent.objects.filter( + subscriber_imsi=self.sub.imsi, kind='delete_imsi').count() + event_count1 = models.UsageEvent.objects.filter( + subscriber_imsi=self.sub2_network1.imsi, kind='delete_imsi').count() + total_event = event_count + event_count1 + self.assertEqual(2, total_event) + diff --git a/cloud/endagaweb/tests/test_subscriber_views.py b/cloud/endagaweb/tests/test_subscriber_views.py index 61b26621..b47e49c3 100644 --- a/cloud/endagaweb/tests/test_subscriber_views.py +++ b/cloud/endagaweb/tests/test_subscriber_views.py @@ -11,6 +11,7 @@ from django import test import mock + from endagaweb import models @@ -33,16 +34,29 @@ def setUpClass(cls): cls.subscriber_imsi = 'IMSI000123' cls.subscriber_num = '5551234' + cls.subscriber_role = 'Subscriber' cls.subscriber = models.Subscriber.objects.create( balance=100, name='test-name', imsi=cls.subscriber_imsi, - network=cls.bts.network, bts=cls.bts) + role=cls.subscriber_role, network=cls.bts.network, bts=cls.bts) cls.subscriber.save() + cls.subscriber_imsi2 = 'IMSI000124' + cls.subscriber_num2 = '5551235' + cls.subscriber_role2 ='Retailer' + cls.subscriber2 = models.Subscriber.objects.create( + balance=1000, name='test-name', imsi=cls.subscriber_imsi2, + role=cls.subscriber_role2, network=cls.bts.network, bts=cls.bts) + cls.subscriber2.save() cls.number = models.Number(number=cls.subscriber_num, state="inuse", network=cls.user_profile.network, kind="number.nexmo.monthly", subscriber=cls.subscriber) cls.number.save() + cls.number2 = models.Number(number=cls.subscriber_num2, state="inuse", + network=cls.user_profile.network, + kind="number.nexmo.monthly", + subscriber=cls.subscriber2) + cls.number2.save() cls.adjust_credit_endpoint = ( '/dashboard/subscribers/%s/adjust-credit' % cls.subscriber_imsi) @@ -58,6 +72,8 @@ def tearDownClass(cls): cls.bts.delete() cls.subscriber.delete() cls.number.delete() + cls.subscriber2.delete() + cls.number2.delete() def setUp(self): self.client = test.Client() @@ -72,6 +88,46 @@ def test_get(self): self.subscriber_imsi) self.assertEqual(200, response.status_code) + def test_post_update_role_single_subscriber(self): + data = { + 'category': 'Retailer', + 'imsi_val[] ': 'IMSI000123' + } + url = '/dashboard/subscribers' + response = self.client.post( + url, data) + self.assertEqual(200, response.status_code) + update_subscriber =models.Subscriber.objects.get(imsi='IMSI000123') + self.assertEqual(update_subscriber.role,'Retailer') + + def test_post_update_test_sim_role_single_subscriber(self): + data = { + 'category': 'Test Sim', + 'imsi_val[] ': 'IMSI000123' + } + url = '/dashboard/subscribers' + response = self.client.post( + url, data) + self.assertEqual(200, response.status_code) + update_subscriber =models.Subscriber.objects.get(imsi='IMSI000123') + self.assertEqual(update_subscriber.role,'Test Sim') + + def test_post_update_role_bulk_subscriber(self): + imsi_list =[self.subscriber_imsi,self.subscriber_imsi2] + data = { + 'category': 'Subscriber', + 'imsi_val[] ':imsi_list + } + + url = '/dashboard/subscribers' + response = self.client.post( + url, data) + self.assertEqual(200, response.status_code) + update_subscriber = models.Subscriber.objects.get(imsi=self.subscriber_imsi) + self.assertEqual(update_subscriber.role,'Subscriber') + update_subscriber2 = models.Subscriber.objects.get(imsi=self.subscriber_imsi2) + self.assertEqual(update_subscriber2.role, 'Subscriber') + class SubscriberActivityTest(SubscriberBaseTest): """Testing endagaweb.views.dashboard.SubscriberActivity.""" diff --git a/cloud/endagaweb/views/api_v2.py b/cloud/endagaweb/views/api_v2.py index c3cdab2c..29dbab2f 100644 --- a/cloud/endagaweb/views/api_v2.py +++ b/cloud/endagaweb/views/api_v2.py @@ -137,12 +137,14 @@ class Subscriber(APIView): def delete(self, request, imsi): network = get_network_from_user(request.user) - subscriber = models.Subscriber.objects.get(imsi=imsi) - if subscriber.network != network: - return Response("Network is not associated with that Subscriber.", + imsi_list = imsi.split(",") + for imsi in imsi_list: + subscriber = models.Subscriber.objects.get(imsi=imsi) + if subscriber.network != network: + return Response("Network is not associated with that Subscriber.", status=status.HTTP_403_FORBIDDEN) # This is a valid request, begin processing. - subscriber.deactivate() + subscriber.deactivate() return Response("") @@ -178,4 +180,4 @@ def delete(self, request, tower_uuid): # TODO(matt): generate revocation certs # And finally delete the BTS. tower.delete() - return Response("") + return Response("") \ No newline at end of file diff --git a/cloud/endagaweb/views/dashboard.py b/cloud/endagaweb/views/dashboard.py index 5dc78be4..41c6aa0d 100644 --- a/cloud/endagaweb/views/dashboard.py +++ b/cloud/endagaweb/views/dashboard.py @@ -218,47 +218,61 @@ def subscriber_list_view(request): Returns: an HttpResponse - """ - user_profile = UserProfile.objects.get(user=request.user) - network = user_profile.network - all_subscribers = Subscriber.objects.filter(network=network) - - query = request.GET.get('query', None) - if query: - # Get actual subs with partial IMSI matches or partial name matches. - query_subscribers = ( - network.subscriber_set.filter(imsi__icontains=query) | - network.subscriber_set.filter(name__icontains=query)) - # Get ids of subs with partial number matches. - sub_ids = network.number_set.filter( - number__icontains=query - ).values_list('subscriber_id', flat=True) - # Or them together to get list of actual matching subscribers. - query_subscribers |= network.subscriber_set.filter( - id__in=sub_ids) - else: - # Display all subscribers. - query_subscribers = all_subscribers + check with POST method to update the subscriber role""" - # Setup the subscriber table. - subscriber_table = django_tables.SubscriberTable(list(query_subscribers)) - tables.RequestConfig(request, paginate={'per_page': 15}).configure( - subscriber_table) + if request.method == 'GET': + user_profile = UserProfile.objects.get(user=request.user) + network = user_profile.network + all_subscribers = Subscriber.objects.filter(network=network) + query = request.GET.get('query', None) + if query: + # Get actual subs with partial IMSI matches or partial name matches. + query_subscribers = ( + network.subscriber_set.filter(imsi__icontains=query) | + network.subscriber_set.filter(name__icontains=query)) + # Get ids of subs with partial number matches. + sub_ids = network.number_set.filter( + number__icontains=query + ).values_list('subscriber_id', flat=True) + # Or them together to get list of actual matching subscribers. + query_subscribers |= network.subscriber_set.filter( + id__in=sub_ids) + else: + # Display all subscribers. + query_subscribers = all_subscribers - # Render the response with context. - context = { - 'networks': get_objects_for_user(request.user, 'view_network', klass=Network), - 'currency': CURRENCIES[network.subscriber_currency], - 'user_profile': user_profile, - 'total_number_of_subscribers': len(all_subscribers), - 'number_of_filtered_subscribers': len(query_subscribers), - 'subscriber_table': subscriber_table, - 'search': dform.SubscriberSearchForm({'query': query}), - } - template = get_template("dashboard/subscribers.html") - html = template.render(context, request) - return HttpResponse(html) + # Setup the subscriber table. + subscriber_table = django_tables.SubscriberTable( + list(query_subscribers)) + tables.RequestConfig(request, paginate={'per_page': 15}).configure( + subscriber_table) + + # Render the response with context. + context = { + 'networks': get_objects_for_user(request.user, 'view_network', + klass=Network), + 'currency': CURRENCIES[network.subscriber_currency], + 'user_profile': user_profile, + 'total_number_of_subscribers': len(all_subscribers), + 'number_of_filtered_subscribers': len(query_subscribers), + 'subscriber_table': subscriber_table, + 'search': dform.SubscriberSearchForm({'query': query}), + } + template = get_template("dashboard/subscribers.html") + html = template.render(context, request) + return HttpResponse(html) + + if request.method == 'POST': + subscriber_imsi_list = request.POST.getlist('imsi_val[]') + subscriber_role = request.POST.get('category') + try: + update_imsi = Subscriber.objects.filter(imsi__in=subscriber_imsi_list) + update_imsi.update(role=subscriber_role) + response_message = "Subscriber role updated successfully." + except Exception as e: + response_message = "Subscriber role update fail." + return HttpResponse(response_message) class SubscriberInfo(ProtectedView): diff --git a/cloud/endagaweb/views/django_tables.py b/cloud/endagaweb/views/django_tables.py index fe8daf18..265908ff 100644 --- a/cloud/endagaweb/views/django_tables.py +++ b/cloud/endagaweb/views/django_tables.py @@ -86,6 +86,13 @@ def render_balance(record): return humanize_credits(record.balance, CURRENCIES[record.network.subscriber_currency]) +def render_imsi(record): + element = " ".format(record.imsi) + + return safestring.mark_safe(element) + class MinimalSubscriberTable(tables.Table): """Showing just a few sub attributes.""" @@ -112,16 +119,26 @@ class SubscriberTable(tables.Table): class Meta: model = models.Subscriber - fields = ('name_and_imsi_link', 'numbers', 'balance', 'status', - 'last_active') + fields = ('imsi','name_and_imsi_link', 'numbers', 'balance', 'status', + 'last_active','role') attrs = {'class': 'table'} + imsi = tables.CheckBoxColumn(accessor="imsi", attrs={"th__input" + :{"id" + :"subscriber-select-all", + "onclick": "toggle(this)", + }}, + orderable=False) name_and_imsi_link = tables.Column( empty_values=(), verbose_name='Name / IMSI', order_by=('name', 'imsi')) status = tables.Column(empty_values=(), order_by=('last_camped')) numbers = tables.Column(orderable=False, verbose_name='Number(s)') balance = tables.Column(verbose_name='Balance') last_active = tables.Column(verbose_name='Last Active') + role = tables.Column(empty_values=(), order_by='role') + + def render_imsi(self, record): + return render_imsi(record) def render_name_and_imsi_link(self, record): return render_name_and_imsi_link(record)