diff --git a/musician/api.py b/musician/api.py index d712a24..0536b5c 100644 --- a/musician/api.py +++ b/musician/api.py @@ -4,6 +4,9 @@ import urllib.parse from django.conf import settings from django.urls.exceptions import NoReverseMatch +from .models import Domain, DatabaseService, MailService, SaasService, UserAccount + + DOMAINS_PATH = 'domains/' TOKEN_PATH = '/api-token-auth/' @@ -52,9 +55,11 @@ class Orchestra(object): return response.json().get("token", None) - def request(self, verb, resource, raise_exception=True): + def request(self, verb, resource, querystring=None, raise_exception=True): assert verb in ["HEAD", "GET", "POST", "PATCH", "PUT", "DELETE"] url = self.build_absolute_uri(resource) + if querystring is not None: + url = "{}?{}".format(url, querystring) verb = getattr(self.session, verb.lower()) response = verb(url, headers={"Authorization": "Token {}".format( @@ -68,16 +73,45 @@ class Orchestra(object): return status, output - def retrieve_service_list(self, service_name): + def retrieve_service_list(self, service_name, querystring=None): pattern_name = '{}-list'.format(service_name) if pattern_name not in API_PATHS: raise ValueError("Unknown service {}".format(service_name)) - _, output = self.request("GET", pattern_name) + _, output = self.request("GET", pattern_name, querystring=querystring) return output - def retreve_profile(self): - _, output = self.request("GET", 'my-account') - return output + def retrieve_profile(self): + status, output = self.request("GET", 'my-account') + if status >= 400: + raise PermissionError("Cannot retrieve profile of an anonymous user.") + return UserAccount.new_from_json(output[0]) + + def retrieve_domain_list(self): + output = self.retrieve_service_list(Domain.api_name) + domains = [] + for domain_json in output: + # filter querystring + querystring = "domain={}".format(domain_json['id']) + + # retrieve services associated to a domain + domain_json['mails'] = self.retrieve_service_list( + MailService.api_name, querystring) + # 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, + } + + # append to list a Domain object + domains.append(Domain.new_from_json(domain_json)) + + return domains def verify_credentials(self): """ diff --git a/musician/mixins.py b/musician/mixins.py index 1785d98..5c65f47 100644 --- a/musician/mixins.py +++ b/musician/mixins.py @@ -46,6 +46,11 @@ class ExtendedPaginationMixin: class UserTokenRequiredMixin(UserPassesTestMixin): + """ + Checks that the request has a token that authenticates him/her. + If the user is logged adds context variable 'profile' with its information. + """ + def test_func(self): """Check that the user has an authorized token.""" token = self.request.session.get(SESSION_KEY_TOKEN, None) @@ -60,3 +65,10 @@ class UserTokenRequiredMixin(UserPassesTestMixin): return False return True + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context.update({ + 'profile': self.orchestra.retrieve_profile(), + }) + return context diff --git a/musician/models.py b/musician/models.py index ba7192f..68ef088 100644 --- a/musician/models.py +++ b/musician/models.py @@ -39,6 +39,12 @@ class OrchestraModel: c.data = data return c + def __repr__(self): + return '<%s: %s>' % (self.__class__.__name__, self) + + def __str__(self): + return '%s object (%s)' % (self.__class__.__name__, self.id) + class BillingContact(OrchestraModel): param_defaults = { @@ -117,6 +123,37 @@ class DatabaseService(OrchestraModel): return super().new_from_json(data=data, users=users, usage=usage) +class Domain(OrchestraModel): + api_name = 'domain' + param_defaults = { + "id": None, + "name": None, + "records": [], + "mails": [], + "usage": {}, + } + + @classmethod + def new_from_json(cls, data, **kwargs): + records = cls.param_defaults.get("records") + if 'records' in data: + records = [DomainRecord.new_from_json(record_data) for record_data in data['records']] + + return super().new_from_json(data=data, records=records) + + def __str__(self): + return self.name + + +class DomainRecord(OrchestraModel): + param_defaults = { + "type": None, + "value": None, + } + def __str__(self): + return '<%s: %s>' % (self.type, self.value) + + class MailService(OrchestraModel): api_name = 'address' verbose_name = _('Mail addresses') diff --git a/musician/settings.py b/musician/settings.py new file mode 100644 index 0000000..5581061 --- /dev/null +++ b/musician/settings.py @@ -0,0 +1,14 @@ +# allowed resources limit hardcoded because cannot be retrieved from the API. +ALLOWED_RESOURCES = { + 'INDIVIDUAL': + { + # 'disk': 1024, + # 'traffic': 2048, + 'mailbox': 2, + }, + 'ASSOCIATION': { + # 'disk': 5 * 1024, + # 'traffic': 20 * 1024, + 'mailbox': 10, + } +} diff --git a/musician/static/musician/css/default.css b/musician/static/musician/css/default.css index 23bd5b1..b2fb536 100644 --- a/musician/static/musician/css/default.css +++ b/musician/static/musician/css/default.css @@ -172,3 +172,81 @@ h1.service-name { .service-card .card-body .service-brand i.fab { color: #9C9AA7; } + +.card.resource-usage { + border-left: 5px solid #4C426A; +} + +.card.resource-usage .progress { + height: 0.5rem; + margin-top: 0.75rem; +} + +.card.resource-usage h5.card-title { + position: relative; +} + +.card.resource-usage h5.card-title:after { + font-family: "Font Awesome 5 Free"; + font-style: normal; + font-variant: normal; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + + position: absolute; + top: 0; + right: 10px; + + color: #E8E7EB; + font-size: 2em; +} + +.card.resource-usage.resource-disk h5.card-title:after { + content: "\f0a0"; + font-weight: 900; +} + +.card.resource-usage.resource-traffic h5.card-title:after { + content: "\f362"; + font-weight: 900; +} + +.card.resource-usage.resource-mailbox h5.card-title:after { + content: "\f0e0"; + font-weight: 900; +} + +.card.resource-usage.resource-notifications h5.card-title:after { + content: "\f0f3"; + font-weight: 900; +} + +#configDetailsModal .modal-header { + border-bottom: 0; + text-align: center; +} + +#configDetailsModal .modal-header .modal-title { + width: 100%; +} + +#configDetailsModal .modal-body { + padding-left: 4rem; + padding-right: 4rem; +} + +#configDetailsModal .modal-body label { + width: 50%; + text-align: right; + padding-right: 4%; +} + +#configDetailsModal .modal-body span { + display: inline-block; + width: 45%; +} + +#configDetailsModal .modal-footer { + border-top: 0; + justify-content: center; +} diff --git a/musician/templates/musician/base.html b/musician/templates/musician/base.html index 2839aee..9dc1ffc 100644 --- a/musician/templates/musician/base.html +++ b/musician/templates/musician/base.html @@ -59,7 +59,7 @@