commit
85fc900b6a
|
@ -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
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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 }}"
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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
15
musician/utils.py
Normal 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
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in a new issue