Merge branch 'include-pangea-mail-addresses'

Fixes #4
This commit is contained in:
Santiago Lamora 2020-02-17 13:29:10 +01:00
commit 85fc900b6a
9 changed files with 167 additions and 72 deletions

View file

@ -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). and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## master ## master
- [changed] Include @pangea.org mail addresses (#4).
## [0.1] - 2020-01-29 ## [0.1] - 2020-01-29
- Login & logout methods using backend as auth method - Login & logout methods using backend as auth method

View file

@ -2,7 +2,7 @@
Package metadata definition. Package metadata definition.
""" """
VERSION = (0, 1, 0, 'final', 0) VERSION = (0, 2, 0, 'alpha', 0)
def get_version(): def get_version():

View file

@ -1,6 +1,7 @@
import requests import requests
import urllib.parse import urllib.parse
from itertools import groupby
from django.conf import settings from django.conf import settings
from django.http import Http404 from django.http import Http404
from django.urls.exceptions import NoReverseMatch from django.urls.exceptions import NoReverseMatch
@ -108,6 +109,52 @@ class Orchestra(object):
raise Http404(_("No domain found matching the query")) raise Http404(_("No domain found matching the query"))
return bill_pdf 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): def retrieve_domain(self, pk):
path = API_PATHS.get('domain-detail').format_map({'pk': 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) # 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']) 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 # TODO(@slamora): update when backend provides resource disk usage data
domain_json['usage'] = { domain_json['usage'] = {
'usage': 300, # 'usage': 300,
'total': 650, # 'total': 650,
'unit': 'MB', # 'unit': 'MB',
'percent': 50, # 'percent': 50,
} }
# append to list a Domain object # append to list a Domain object

View file

@ -6,6 +6,7 @@ from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from . import settings as musician_settings from . import settings as musician_settings
from .utils import get_bootstraped_percent
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -229,14 +230,31 @@ class MailService(OrchestraModel):
def type_detail(self): def type_detail(self):
if self.type == self.FORWARD: if self.type == self.FORWARD:
return self.data['forward'] return self.data['forward']
# TODO(@slamora) retrieve mailbox usage
return { # retrieve mailbox usage
'usage': 250, try:
'total': 500, resource = self.data['mailboxes'][0]['resources']
'unit': 'MB', resource_disk = {}
'percent': 50, 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): class MailinglistService(OrchestraModel):
api_name = 'mailinglist' api_name = 'mailinglist'

View file

@ -8,8 +8,13 @@ Expected structure: dictionary or object with attributes:
- unit (string): 'MB' - unit (string): 'MB'
- percent (int: [0, 25, 50, 75, 100]: 75 - percent (int: [0, 25, 50, 75, 100]: 75
{% endcomment %} {% endcomment %}
<div class="text-center"> <div class="text-center">
{% if detail %}
{{ detail.usage }} of {{ detail.total }} {{ detail.unit }} {{ detail.usage }} of {{ detail.total }} {{ detail.unit }}
{% else %}
N/A
{% endif %}
</div> </div>
<div class="progress"> <div class="progress">
<div class="progress-bar bg-secondary w-{{ detail.percent }}" role="progressbar" aria-valuenow="{{ detail.usage }}" <div class="progress-bar bg-secondary w-{{ detail.percent }}" role="progressbar" aria-valuenow="{{ detail.usage }}"

View file

@ -15,7 +15,7 @@
<div class="card resource-usage resource-{{ resource }}"> <div class="card resource-usage resource-{{ resource }}">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">{{ usage.verbose_name }}</h5> <h5 class="card-title">{{ usage.verbose_name }}</h5>
{% include "musician/components/usage_progress_bar.html" with detail=usage %} {% include "musician/components/usage_progress_bar.html" with detail=usage.data %}
</div> </div>
</div> </div>
{% endfor %} {% endfor %}

View file

@ -1,6 +1,7 @@
from django.test import TestCase from django.test import TestCase
from .models import UserAccount from .models import UserAccount
from .utils import get_bootstraped_percent
class DomainsTestCase(TestCase): class DomainsTestCase(TestCase):
@ -37,3 +38,32 @@ class UserAccountTest(TestCase):
} }
account = UserAccount.new_from_json(data) account = UserAccount.new_from_json(data)
self.assertIsNone(account.last_login) 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)

15
musician/utils.py Normal file
View file

@ -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

View file

@ -1,5 +1,3 @@
from itertools import groupby
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponse, HttpResponseRedirect from django.http import HttpResponse, HttpResponseRedirect
@ -23,6 +21,7 @@ from .mixins import (CustomContextMixin, ExtendedPaginationMixin,
from .models import (Bill, DatabaseService, MailinglistService, MailService, from .models import (Bill, DatabaseService, MailinglistService, MailService,
PaymentSource, SaasService, UserAccount) PaymentSource, SaasService, UserAccount)
from .settings import ALLOWED_RESOURCES from .settings import ALLOWED_RESOURCES
from .utils import get_bootstraped_percent
class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
@ -36,38 +35,14 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
domains = self.orchestra.retrieve_domain_list() 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 # TODO(@slamora) update when backend supports notifications
notifications = [] notifications = []
# show resource usage based on plan definition # show resource usage based on plan definition
# TODO(@slamora): validate concept of limits with Pangea
profile_type = context['profile'].type profile_type = context['profile'].type
total_mailboxes = 0
for domain in domains: for domain in domains:
total_mailboxes += len(domain.mails)
addresses_left = ALLOWED_RESOURCES[profile_type]['mailbox'] - len(domain.mails) addresses_left = ALLOWED_RESOURCES[profile_type]['mailbox'] - len(domain.mails)
alert_level = None alert_level = None
if addresses_left == 1: if addresses_left == 1:
@ -80,6 +55,37 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
'alert_level': alert_level, '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({ context.update({
'domains': domains, 'domains': domains,
'resource_usage': resource_usage, 'resource_usage': resource_usage,
@ -170,34 +176,11 @@ class MailView(ServiceListView):
} }
def get_queryset(self): 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) # retrieve mails applying filters (if any)
queryfilter = self.get_queryfilter() queryfilter = self.get_queryfilter()
raw_data = self.orchestra.retrieve_service_list( addresses = self.orchestra.retrieve_mail_address_list(
self.service_class.api_name, querystring=queryfilter
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 return addresses
def get_queryfilter(self): def get_queryfilter(self):