diff --git a/musician/api.py b/musician/api.py index 5e36372..048f34e 100644 --- a/musician/api.py +++ b/musician/api.py @@ -13,9 +13,15 @@ API_PATHS = { 'my-account': 'accounts/', # services + 'database-list': 'databases/', 'domain-list': 'domains/', + 'address-list': 'addresses/', + 'mailbox-list': 'mailboxes/', 'mailinglist-list': 'lists/', # ... TODO (@slamora) complete list of backend URLs + + # other + 'payment-source-list': 'payment-sources/', } diff --git a/musician/mixins.py b/musician/mixins.py index cf58bff..681141a 100644 --- a/musician/mixins.py +++ b/musician/mixins.py @@ -24,6 +24,27 @@ class CustomContextMixin(ContextMixin): return context +class ExtendedPaginationMixin: + paginate_by = 20 + paginate_by_kwarg = 'per_page' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context.update({ + 'per_page_values': [5, 10, 20, 50], + 'per_page_param': self.paginate_by_kwarg, + }) + return context + + def get_paginate_by(self, queryset): + per_page = self.request.GET.get(self.paginate_by_kwarg) or self.paginate_by + try: + paginate_by = int(per_page) + except ValueError: + paginate_by = self.paginate_by + return paginate_by + + class UserTokenRequiredMixin(UserPassesTestMixin): def test_func(self): """Check that the user has an authorized token.""" diff --git a/musician/models.py b/musician/models.py new file mode 100644 index 0000000..28eb035 --- /dev/null +++ b/musician/models.py @@ -0,0 +1,161 @@ +from django.utils.html import format_html + + +class OrchestraModel: + """ Base class from which all orchestra models will inherit. """ + api_name = None + verbose_name = None + fields = () + param_defaults = {} + + def __init__(self, **kwargs): + if self.verbose_name is None: + self.verbose_name = self.api_name + + for (param, default) in self.param_defaults.items(): + setattr(self, param, kwargs.get(param, default)) + + # def get(self, key): + # # retrieve attr of the object and if undefined get raw data + # return getattr(self, key, self.data.get(key)) + + @classmethod + def new_from_json(cls, data, **kwargs): + """ Create a new instance based on a JSON dict. Any kwargs should be + supplied by the inherited, calling class. + Args: + data: A JSON dict, as converted from the JSON in the orchestra API. + """ + + json_data = data.copy() + if kwargs: + for key, val in kwargs.items(): + json_data[key] = val + + c = cls(**json_data) + c._json = data + # TODO(@slamora) remove/replace by private variable to ovoid name collisions + c.data = data + return c + + +class BillingContact(OrchestraModel): + param_defaults = { + 'name': None, + 'address': None, + 'city': None, + 'zipcode': None, + 'country': None, + 'vat': None, + } + + +class PaymentSource(OrchestraModel): + api_name = 'payment-source' + param_defaults = { + "method": None, + "data": [], + "is_active": False, + } + + +class UserAccount(OrchestraModel): + api_name = 'accounts' + param_defaults = { + 'username': None, + 'type': None, + 'language': None, + 'short_name': None, + 'full_name': None, + 'billing': {}, + } + + @classmethod + def new_from_json(cls, data, **kwargs): + billing = None + + if 'billcontact' in data: + billing = BillingContact.new_from_json(data['billcontact']) + return super().new_from_json(data=data, billing=billing) + + +class DatabaseUser(OrchestraModel): + api_name = 'databaseusers' + fields = ('username',) + param_defaults = { + 'username': None, + } + + +class DatabaseService(OrchestraModel): + api_name = 'database' + fields = ('name', 'type', 'users') + param_defaults = { + "id": None, + "name": None, + "type": None, + "users": None, + } + + @classmethod + def new_from_json(cls, data, **kwargs): + users = None + if 'users' in data: + users = [DatabaseUser.new_from_json(user_data) for user_data in data['users']] + return super().new_from_json(data=data, users=users) + + +class MailService(OrchestraModel): + api_name = 'address' + verbose_name = 'Mail' + fields = ('mail_address', 'aliases', 'type', 'type_detail') + + FORWARD = 'forward' + MAILBOX = 'mailbox' + + @property + def aliases(self): + return [ + name + '@' + self.data['domain']['name'] for name in self.data['names'][1:] + ] + + @property + def mail_address(self): + return self.data['names'][0] + '@' + self.data['domain']['name'] + + @property + def type(self): + if self.data['forward']: + return self.FORWARD + return self.MAILBOX + + @property + def type_detail(self): + if self.type == self.FORWARD: + return self.data['forward'] + # TODO(@slamora) retrieve mailbox usage + return {'usage': 0, 'total': 213} + + +class MailinglistService(OrchestraModel): + api_name = 'mailinglist' + verbose_name = 'Mailing list' + fields = ('name', 'status', 'address_name', 'admin_email', 'configure') + param_defaults = { + 'name': None, + 'admin_email': None, + } + + @property + def status(self): + # TODO(@slamora): where retrieve if the list is active? + return 'active' + + @property + def address_name(self): + return "{}@{}".format(self.data['address_name'], self.data['address_domain']['name']) + + @property + def configure(self): + # TODO(@slamora): build mailtran absolute URL + return format_html('Mailtrain') diff --git a/musician/templates/musician/components/paginator.html b/musician/templates/musician/components/paginator.html new file mode 100644 index 0000000..9a7a406 --- /dev/null +++ b/musician/templates/musician/components/paginator.html @@ -0,0 +1,29 @@ +{# #} +
+
{{ page_obj.paginator.count }} items in total
+
+ {% if page_obj.has_previous %} + « + + {% endif %} + Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }} + {% if page_obj.has_next %} + + » + {% endif %} +
+
+
+ Showing + + per page + +
+
+
diff --git a/musician/templates/musician/databases.html b/musician/templates/musician/databases.html index 927ff1c..448432c 100644 --- a/musician/templates/musician/databases.html +++ b/musician/templates/musician/databases.html @@ -3,7 +3,50 @@ {% block content %} -

Section title

-

Little description of what to be expected...

+

{{ service.verbose_name }}

+

{{ service.description }}

+{% for database in object_list %} +
+
+
+ {{ database.name }} +
+
+ Type: {{ database.type }} +
+
+ associated to: {{ database.domain|default:"-" }} + +
+
+
+
+

Database users

+
    + {% for user in resource.users %} +
  • {{ user.username }}
  • + {% empty %} +
  • No users for this database.
  • + {% endfor %} +
+
+
+

Database usage

+
+
75%
+
+
+ +
+
+ + {% endfor %} + + {% include "musician/components/paginator.html" %} {% endblock %} diff --git a/musician/templates/musician/mail.html b/musician/templates/musician/mail.html index 927ff1c..116fae4 100644 --- a/musician/templates/musician/mail.html +++ b/musician/templates/musician/mail.html @@ -3,7 +3,27 @@ {% block content %} -

Section title

-

Little description of what to be expected...

- +

{{ service.verbose_name }}

+

{{ service.description }}

+ + + + + + + + + + + {% for obj in object_list %} + + + + + + + {% endfor %} + + {% include "musician/components/table_paginator.html" %} +
Mail addressTypeType details
{{ obj.mail_address }}{{ obj.aliases|join:" , " }}{{ obj.type }}{{ obj.type_detail }}
{% endblock %} diff --git a/musician/templates/musician/profile.html b/musician/templates/musician/profile.html new file mode 100644 index 0000000..e9da7be --- /dev/null +++ b/musician/templates/musician/profile.html @@ -0,0 +1,51 @@ +{% extends "musician/base.html" %} +{% load i18n %} + +{% block content %} + +

Profile

+

Little description of what to be expected...

+ +
+
User information
+
+
+
+ {# #} +
+
+
+

{{ profile.username }}

+

{{ profile.type }}

+

Preferred language: {{ profile.language }}

+
+
+
+ +{% with profile.billing as contact %} +
+
Billing information
+
+
{{ contact.name }}
+
{{ contact.address }}
+
+ {{ contact.zipcode }} + {{ contact.city }} + {{ contact.country }} +
+
+ {{ contact.vat }} +
+ +
+ payment method: {{ payment.method }} +
+
+ {# TODO(@slamora) format payment method details #} + {{ payment.data.data }} +
+ +
+
+{% endwith %} +{% endblock %} diff --git a/musician/templates/musician/service_list.html b/musician/templates/musician/service_list.html new file mode 100644 index 0000000..d413fc8 --- /dev/null +++ b/musician/templates/musician/service_list.html @@ -0,0 +1,28 @@ +{% extends "musician/base.html" %} +{% load i18n musician %} + +{% block content %} + +

{{ service.verbose_name }}

+

{{ service.description }}

+ + + + + {% for field_name in service.fields %} + + {% endfor %} + + + + {% for resource in object_list %} + + {% for field_name in service.fields %} + + {% endfor %} + + {% endfor %} + + {% include "musician/components/table_paginator.html" %} +
{{ field_name }}
{{ resource|get_item:field_name }}
+{% endblock %} diff --git a/musician/templatetags/musician.py b/musician/templatetags/musician.py new file mode 100644 index 0000000..181fd01 --- /dev/null +++ b/musician/templatetags/musician.py @@ -0,0 +1,6 @@ +from django.template.defaulttags import register + + +@register.filter +def get_item(dictionary, key): + return dictionary.get(key) diff --git a/musician/urls.py b/musician/urls.py index e18f828..e08bd0a 100644 --- a/musician/urls.py +++ b/musician/urls.py @@ -15,6 +15,7 @@ urlpatterns = [ path('auth/login/', views.LoginView.as_view(), name='login'), path('auth/logout/', views.LogoutView.as_view(), name='logout'), path('dashboard/', views.DashboardView.as_view(), name='dashboard'), + path('profile/', views.ProfileView.as_view(), name='profile'), path('mails/', views.MailView.as_view(), name='mails'), path('mailing-lists/', views.MailingListsView.as_view(), name='mailing-lists'), path('databases/', views.DatabasesView.as_view(), name='databases'), diff --git a/musician/views.py b/musician/views.py index ef45234..632f8ea 100644 --- a/musician/views.py +++ b/musician/views.py @@ -1,4 +1,8 @@ +from django.views.generic.detail import DetailView +from itertools import groupby + +from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponseRedirect from django.shortcuts import render from django.urls import reverse_lazy @@ -11,7 +15,9 @@ from . import api, get_version from .auth import login as auth_login from .auth import logout as auth_logout from .forms import LoginForm -from .mixins import CustomContextMixin, UserTokenRequiredMixin +from .mixins import (CustomContextMixin, + ExtendedPaginationMixin, UserTokenRequiredMixin) +from .models import DatabaseService, MailinglistService, MailService, UserAccount, PaymentSource class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): @@ -30,38 +36,84 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): return context -class MailView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): - template_name = "musician/mail.html" +class ProfileView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): + template_name = "musician/profile.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + json_data = self.orchestra.retreve_profile() + try: + pay_source = self.orchestra.retrieve_service_list( + PaymentSource.api_name)[0] + except IndexError: + pay_source = {} + context.update({ + 'profile': UserAccount.new_from_json(json_data[0]), + 'payment': PaymentSource.new_from_json(pay_source) + }) + + return context -class MailingListsView(CustomContextMixin, UserTokenRequiredMixin, ListView): - template_name = "musician/mailinglists.html" - paginate_by = 20 - paginate_by_kwarg = 'per_page' +class ServiceListView(CustomContextMixin, ExtendedPaginationMixin, UserTokenRequiredMixin, ListView): + """Base list view to all services""" + service_class = None + template_name = "musician/service_list.html" # TODO move to ServiceListView def get_queryset(self): - return self.orchestra.retrieve_service_list('mailinglist') + if self.service_class is None or self.service_class.api_name is None: + raise ImproperlyConfigured( + "ServiceListView requires a definiton of 'service'") + + json_qs = self.orchestra.retrieve_service_list( + self.service_class.api_name) + return [self.service_class.new_from_json(data) for data in json_qs] def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ - 'page_param': self.page_kwarg, - 'per_page_values': [5, 10, 20, 50], - 'per_page_param': self.paginate_by_kwarg, + 'service': self.service_class, }) return context - def get_paginate_by(self, queryset): - per_page = self.request.GET.get(self.paginate_by_kwarg) or self.paginate_by - try: - paginate_by = int(per_page) - except ValueError: - paginate_by = self.paginate_by - return paginate_by + +class MailView(ServiceListView): + service_class = MailService + template_name = "musician/mail.html" + + def get_queryset(self): + def retrieve_mailbox(value): + mailboxes = value.get('mailboxes') + + if len(mailboxes) == 0: + return '' + + return mailboxes[0]['id'] + + # group addresses with the same mailbox + raw_data = self.orchestra.retrieve_service_list( + self.service_class.api_name) + 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 -class DatabasesView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): +class MailingListsView(ServiceListView): + service_class = MailinglistService + + +class DatabasesView(ServiceListView): template_name = "musician/databases.html" + service_class = DatabaseService class SaasView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):