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 %}
+
+
+
+
+
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 users
+
+ {% for user in resource.users %}
+ - {{ user.username }}
+ {% empty %}
+ - No users for this database.
+ {% endfor %}
+
+
+
+
+
+
+
+ {% 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 }}
+
+
+
+ Mail address |
+ |
+ Type |
+ Type details |
+
+
+
+ {% for obj in object_list %}
+
+ {{ obj.mail_address }} |
+ {{ obj.aliases|join:" , " }} |
+ {{ obj.type }} |
+ {{ obj.type_detail }} |
+
+ {% endfor %}
+
+ {% include "musician/components/table_paginator.html" %}
+
{% 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...
+
+
+
+
+
+
+
{{ profile.username }}
+
{{ profile.type }}
+
Preferred language: {{ profile.language }}
+
+
+
+
+{% with profile.billing as contact %}
+
+
+
+
{{ 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 %}
+ {{ field_name }} |
+ {% endfor %}
+
+
+
+ {% for resource in object_list %}
+
+ {% for field_name in service.fields %}
+ {{ resource|get_item:field_name }} |
+ {% endfor %}
+
+ {% endfor %}
+
+ {% include "musician/components/table_paginator.html" %}
+
+{% 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):