From f599dc6ca91206d643c24d81d004cb2bf9787b1e Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Mon, 17 Feb 2020 11:07:21 +0100 Subject: [PATCH 1/7] Create api.retrieve_mail_address_list --- musician/api.py | 31 +++++++++++++++++++++++++++++++ musician/views.py | 29 ++--------------------------- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/musician/api.py b/musician/api.py index bbf77a5..95443b8 100644 --- a/musician/api.py +++ b/musician/api.py @@ -1,6 +1,7 @@ import requests import urllib.parse +from itertools import groupby from django.conf import settings from django.http import Http404 from django.urls.exceptions import NoReverseMatch @@ -108,6 +109,36 @@ class Orchestra(object): raise Http404(_("No domain found matching the query")) return bill_pdf + def retrieve_mail_address_list(self, querystring=None): + def get_mailbox_id(value): + mailboxes = value.get('mailboxes') + + # forwarded address should not grouped + if len(mailboxes) == 0: + return value.get('name') + + return mailboxes[0]['id'] + + # retrieve mails applying filters (if any) + raw_data = self.retrieve_service_list( + MailService.api_name, + querystring=querystring, + ) + + # group addresses with the same mailbox + addresses = [] + for key, group in groupby(raw_data, get_mailbox_id): + aliases = [] + data = {} + for thing in group: + aliases.append(thing.pop('name')) + data = thing + + data['names'] = aliases + addresses.append(MailService.new_from_json(data)) + + return addresses + def retrieve_domain(self, pk): path = API_PATHS.get('domain-detail').format_map({'pk': pk}) diff --git a/musician/views.py b/musician/views.py index 102a66d..85a6d4e 100644 --- a/musician/views.py +++ b/musician/views.py @@ -1,5 +1,3 @@ -from itertools import groupby - from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponse, HttpResponseRedirect @@ -170,34 +168,11 @@ class MailView(ServiceListView): } def get_queryset(self): - def retrieve_mailbox(value): - mailboxes = value.get('mailboxes') - - # forwarded address should not grouped - if len(mailboxes) == 0: - return value.get('name') - - return mailboxes[0]['id'] - # retrieve mails applying filters (if any) queryfilter = self.get_queryfilter() - raw_data = self.orchestra.retrieve_service_list( - self.service_class.api_name, - querystring=queryfilter, + addresses = self.orchestra.retrieve_mail_address_list( + querystring=queryfilter ) - - # group addresses with the same mailbox - addresses = [] - for key, group in groupby(raw_data, retrieve_mailbox): - aliases = [] - data = {} - for thing in group: - aliases.append(thing.pop('name')) - data = thing - - data['names'] = aliases - addresses.append(self.service_class.new_from_json(data)) - return addresses def get_queryfilter(self): From 425d090522a5ca41ba2a84f611606c325a4a57a0 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Mon, 17 Feb 2020 11:29:58 +0100 Subject: [PATCH 2/7] Patch to list pangea mail addresses (#4) --- musician/api.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/musician/api.py b/musician/api.py index 95443b8..7a93081 100644 --- a/musician/api.py +++ b/musician/api.py @@ -137,6 +137,22 @@ class Orchestra(object): data['names'] = aliases addresses.append(MailService.new_from_json(data)) + # PATCH to include Pangea addresses not shown by orchestra + # described on issue #4 + raw_mailboxes = self.retrieve_service_list('mailbox') + for mailbox in raw_mailboxes: + if mailbox['addresses'] == []: + address_data = { + 'names': [mailbox['name']], + 'forward': '', + 'domain': { + 'name': 'pangea.org.', + }, + 'mailboxes': [mailbox], + } + pangea_address = MailService.new_from_json(address_data) + addresses.append(pangea_address) + return addresses def retrieve_domain(self, pk): From 0fa26d799b952c9677c007c05337f2674f5af4e7 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Mon, 17 Feb 2020 12:05:55 +0100 Subject: [PATCH 3/7] Show address mailbox disk usage. --- musician/models.py | 30 ++++++++++++++----- .../components/usage_progress_bar.html | 7 ++++- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/musician/models.py b/musician/models.py index 0718aaa..4863638 100644 --- a/musician/models.py +++ b/musician/models.py @@ -229,13 +229,29 @@ class MailService(OrchestraModel): def type_detail(self): if self.type == self.FORWARD: return self.data['forward'] - # TODO(@slamora) retrieve mailbox usage - return { - 'usage': 250, - 'total': 500, - 'unit': 'MB', - 'percent': 50, - } + + # retrieve mailbox usage + try: + resource = self.data['mailboxes'][0]['resources'] + resource_disk = {} + for r in resource: + if r['name'] == 'disk': + resource_disk = r + break + + mailbox_details = { + 'usage': resource_disk['used'], + 'total': resource_disk['allocated'], + 'unit': resource_disk['unit'], + } + + # get percent and round to be 0, 25, 50 or 100 + # to set progress bar width using CSS classes (e.g. w-25) + percent = float(resource_disk['used']) / resource_disk['allocated'] + mailbox_details['percent'] = round(percent * 4) * 100 // 4 + except (IndexError, KeyError): + mailbox_details = {} + return mailbox_details class MailinglistService(OrchestraModel): diff --git a/musician/templates/musician/components/usage_progress_bar.html b/musician/templates/musician/components/usage_progress_bar.html index e3772e4..bf3d2d6 100644 --- a/musician/templates/musician/components/usage_progress_bar.html +++ b/musician/templates/musician/components/usage_progress_bar.html @@ -1,5 +1,5 @@ {% comment %} -Resource usage rendered as bootstrap progress bar +Resource usage rendered as bootstrap progress bar Expected parameter: detail Expected structure: dictionary or object with attributes: @@ -8,8 +8,13 @@ Expected structure: dictionary or object with attributes: - unit (string): 'MB' - percent (int: [0, 25, 50, 75, 100]: 75 {% endcomment %} +
+ {% if detail %} {{ detail.usage }} of {{ detail.total }}{{ detail.unit }} + {% else %} + N/A + {% endif %}
Date: Mon, 17 Feb 2020 12:39:49 +0100 Subject: [PATCH 4/7] Include mailboxes resource usage details. --- musician/templates/musician/dashboard.html | 2 +- musician/tests.py | 30 +++++++++++ musician/utils.py | 15 ++++++ musician/views.py | 60 ++++++++++++---------- 4 files changed, 80 insertions(+), 27 deletions(-) create mode 100644 musician/utils.py diff --git a/musician/templates/musician/dashboard.html b/musician/templates/musician/dashboard.html index 0b2b09b..addca3d 100644 --- a/musician/templates/musician/dashboard.html +++ b/musician/templates/musician/dashboard.html @@ -15,7 +15,7 @@
{{ usage.verbose_name }}
- {% include "musician/components/usage_progress_bar.html" with detail=usage %} + {% include "musician/components/usage_progress_bar.html" with detail=usage.data %}
{% endfor %} diff --git a/musician/tests.py b/musician/tests.py index 805ff05..13c365e 100644 --- a/musician/tests.py +++ b/musician/tests.py @@ -1,6 +1,7 @@ from django.test import TestCase from .models import UserAccount +from .utils import get_bootstraped_percent class DomainsTestCase(TestCase): @@ -37,3 +38,32 @@ class UserAccountTest(TestCase): } account = UserAccount.new_from_json(data) self.assertIsNone(account.last_login) + + +class GetBootstrapedPercentTest(TestCase): + BS_WIDTH = [0, 25, 50, 100] + + def test_exact_value(self): + value = get_bootstraped_percent(25, 100) + self.assertIn(value, self.BS_WIDTH) + self.assertEqual(value, 25) + + def test_round_to_lower(self): + value = get_bootstraped_percent(26, 100) + self.assertIn(value, self.BS_WIDTH) + self.assertEqual(value, 25) + + def test_round_to_higher(self): + value = get_bootstraped_percent(48, 100) + self.assertIn(value, self.BS_WIDTH) + self.assertEqual(value, 50) + + def test_max_boundary(self): + value = get_bootstraped_percent(200, 100) + self.assertIn(value, self.BS_WIDTH) + self.assertEqual(value, 100) + + def test_min_boundary(self): + value = get_bootstraped_percent(-10, 100) + self.assertIn(value, self.BS_WIDTH) + self.assertEqual(value, 0) diff --git a/musician/utils.py b/musician/utils.py new file mode 100644 index 0000000..7b029c1 --- /dev/null +++ b/musician/utils.py @@ -0,0 +1,15 @@ +def get_bootstraped_percent(value, total): + """ + Get percent and round to be 0, 25, 50 or 100 + + Useful to set progress bar width using CSS classes (e.g. w-25) + """ + + percent = value / total + bootstraped = round(percent * 4) * 100 // 4 + + # handle min and max boundaries + bootstraped = max(0, bootstraped) + bootstraped = min(100, bootstraped) + + return bootstraped diff --git a/musician/views.py b/musician/views.py index 85a6d4e..b70fbf5 100644 --- a/musician/views.py +++ b/musician/views.py @@ -21,6 +21,7 @@ from .mixins import (CustomContextMixin, ExtendedPaginationMixin, from .models import (Bill, DatabaseService, MailinglistService, MailService, PaymentSource, SaasService, UserAccount) from .settings import ALLOWED_RESOURCES +from .utils import get_bootstraped_percent class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): @@ -34,38 +35,14 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): context = super().get_context_data(**kwargs) domains = self.orchestra.retrieve_domain_list() - # TODO(@slamora) update when backend provides resource usage data - resource_usage = { - 'disk': { - 'verbose_name': _('Disk usage'), - 'usage': 534, - 'total': 1024, - 'unit': 'MB', - 'percent': 50, - }, - 'traffic': { - 'verbose_name': _('Traffic'), - 'usage': 300, - 'total': 2048, - 'unit': 'MB/month', - 'percent': 25, - }, - 'mailbox': { - 'verbose_name': _('Mailbox usage'), - 'usage': 1, - 'total': 2, - 'unit': 'accounts', - 'percent': 50, - }, - } - # TODO(@slamora) update when backend supports notifications notifications = [] # show resource usage based on plan definition - # TODO(@slamora): validate concept of limits with Pangea profile_type = context['profile'].type + total_mailboxes = 0 for domain in domains: + total_mailboxes += len(domain.mails) addresses_left = ALLOWED_RESOURCES[profile_type]['mailbox'] - len(domain.mails) alert_level = None if addresses_left == 1: @@ -78,6 +55,37 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): 'alert_level': alert_level, } + # TODO(@slamora) update when backend provides resource usage data + resource_usage = { + 'disk': { + 'verbose_name': _('Disk usage'), + 'data': { + # 'usage': 534, + # 'total': 1024, + # 'unit': 'MB', + # 'percent': 50, + }, + }, + 'traffic': { + 'verbose_name': _('Traffic'), + 'data': { + # 'usage': 300, + # 'total': 2048, + # 'unit': 'MB/month', + # 'percent': 25, + }, + }, + 'mailbox': { + 'verbose_name': _('Mailbox usage'), + 'data': { + 'usage': total_mailboxes, + 'total': ALLOWED_RESOURCES[profile_type]['mailbox'], + 'unit': 'accounts', + 'percent': get_bootstraped_percent(total_mailboxes, ALLOWED_RESOURCES[profile_type]['mailbox']), + }, + }, + } + context.update({ 'domains': domains, 'resource_usage': resource_usage, From 33d5cd2719ab6279b94850782c52971ac5320f87 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Mon, 17 Feb 2020 12:40:27 +0100 Subject: [PATCH 5/7] Refactor to use utils.get_bootstraped_percent --- musician/models.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/musician/models.py b/musician/models.py index 4863638..f3cbfc1 100644 --- a/musician/models.py +++ b/musician/models.py @@ -6,6 +6,7 @@ from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from . import settings as musician_settings +from .utils import get_bootstraped_percent logger = logging.getLogger(__name__) @@ -240,15 +241,16 @@ class MailService(OrchestraModel): break mailbox_details = { - 'usage': resource_disk['used'], + 'usage': float(resource_disk['used']), 'total': resource_disk['allocated'], 'unit': resource_disk['unit'], } - # get percent and round to be 0, 25, 50 or 100 - # to set progress bar width using CSS classes (e.g. w-25) - percent = float(resource_disk['used']) / resource_disk['allocated'] - mailbox_details['percent'] = round(percent * 4) * 100 // 4 + percent = get_bootstraped_percent( + mailbox_details['used'], + mailbox_details['total'] + ) + mailbox_details['percent'] = percent except (IndexError, KeyError): mailbox_details = {} return mailbox_details From b9a2860dcb8daf446530c1a80f05741d9dff63e3 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Mon, 17 Feb 2020 12:40:56 +0100 Subject: [PATCH 6/7] Add space between value and unit --- musician/templates/musician/components/usage_progress_bar.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/musician/templates/musician/components/usage_progress_bar.html b/musician/templates/musician/components/usage_progress_bar.html index bf3d2d6..7dd9537 100644 --- a/musician/templates/musician/components/usage_progress_bar.html +++ b/musician/templates/musician/components/usage_progress_bar.html @@ -11,7 +11,7 @@ Expected structure: dictionary or object with attributes:
{% if detail %} - {{ detail.usage }} of {{ detail.total }}{{ detail.unit }} + {{ detail.usage }} of {{ detail.total }} {{ detail.unit }} {% else %} N/A {% endif %} From 8a5d9f36cd65dfb39b7cc556c9c20d6f24cc74d7 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Mon, 17 Feb 2020 12:41:30 +0100 Subject: [PATCH 7/7] Remove old TODOs and comment mock data. --- musician/api.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/musician/api.py b/musician/api.py index 7a93081..7bb6ee0 100644 --- a/musician/api.py +++ b/musician/api.py @@ -180,16 +180,12 @@ class Orchestra(object): # retrieve websites (as they cannot be filtered by domain on the API we should do it here) domain_json['websites'] = self.filter_websites_by_domain(websites, domain_json['id']) - # TODO(@slamora): databases and sass are not related to a domain, so cannot be filtered - # domain_json['databases'] = self.retrieve_service_list(DatabaseService.api_name, querystring) - # domain_json['saas'] = self.retrieve_service_list(SaasService.api_name, querystring) - # TODO(@slamora): update when backend provides resource disk usage data domain_json['usage'] = { - 'usage': 300, - 'total': 650, - 'unit': 'MB', - 'percent': 50, + # 'usage': 300, + # 'total': 650, + # 'unit': 'MB', + # 'percent': 50, } # append to list a Domain object