diff --git a/CHANGELOG.md b/CHANGELOG.md index c884e0f..ce4e3c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## master +- [changed] Include @pangea.org mail addresses (#4). ## [0.1] - 2020-01-29 - Login & logout methods using backend as auth method diff --git a/musician/__init__.py b/musician/__init__.py index fcd8f08..5b64a68 100644 --- a/musician/__init__.py +++ b/musician/__init__.py @@ -2,7 +2,7 @@ Package metadata definition. """ -VERSION = (0, 1, 0, 'final', 0) +VERSION = (0, 2, 0, 'alpha', 0) def get_version(): diff --git a/musician/api.py b/musician/api.py index bbf77a5..7bb6ee0 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,52 @@ 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)) + + # 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): path = API_PATHS.get('domain-detail').format_map({'pk': pk}) @@ -133,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 diff --git a/musician/models.py b/musician/models.py index 0718aaa..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__) @@ -229,13 +230,30 @@ 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': float(resource_disk['used']), + 'total': resource_disk['allocated'], + 'unit': resource_disk['unit'], + } + + percent = get_bootstraped_percent( + mailbox_details['used'], + mailbox_details['total'] + ) + mailbox_details['percent'] = percent + 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..7dd9537 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 %} +
- {{ detail.usage }} of {{ detail.total }}{{ detail.unit }} + {% if detail %} + {{ detail.usage }} of {{ detail.total }} {{ detail.unit }} + {% else %} + N/A + {% endif %}
{{ 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 102a66d..b70fbf5 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 @@ -23,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): @@ -36,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: @@ -80,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, @@ -170,34 +176,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):