Merge branch 'wf-services'

This commit is contained in:
Santiago Lamora 2019-12-02 17:45:24 +01:00
commit fdcf5b2aa4
11 changed files with 442 additions and 24 deletions

View file

@ -13,9 +13,15 @@ API_PATHS = {
'my-account': 'accounts/', 'my-account': 'accounts/',
# services # services
'database-list': 'databases/',
'domain-list': 'domains/', 'domain-list': 'domains/',
'address-list': 'addresses/',
'mailbox-list': 'mailboxes/',
'mailinglist-list': 'lists/', 'mailinglist-list': 'lists/',
# ... TODO (@slamora) complete list of backend URLs # ... TODO (@slamora) complete list of backend URLs
# other
'payment-source-list': 'payment-sources/',
} }

View file

@ -24,6 +24,27 @@ class CustomContextMixin(ContextMixin):
return context 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): class UserTokenRequiredMixin(UserPassesTestMixin):
def test_func(self): def test_func(self):
"""Check that the user has an authorized token.""" """Check that the user has an authorized token."""

161
musician/models.py Normal file
View file

@ -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('<a href="#TODO">Mailtrain</a>')

View file

@ -0,0 +1,29 @@
{# <!-- paginator component --> #}
<div class="row object-list-paginator">
<div class="col-md-4">{{ page_obj.paginator.count }} items in total</div>
<div class="col-md-4 text-center">
{% if page_obj.has_previous %}
<a href="?page=1&per_page={{ page_obj.paginator.per_page }}">&laquo;</a>
<a href="?page={{ page_obj.previous_page_number }}&per_page={{ page_obj.paginator.per_page }}">&lsaquo;</a>
{% endif %}
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}&per_page={{ page_obj.paginator.per_page }}">&rsaquo;</a>
<a href="?page={{ page_obj.paginator.num_pages }}&per_page={{ page_obj.paginator.per_page }}">&raquo;</a>
{% endif %}
</div>
<div class="col-md-4 text-right">
<form method="get">
Showing
<select name="{{ per_page_param }}">
{% for value in per_page_values %}
{% with page_obj.paginator.per_page as per_page %}
<option value="{{ value }}" {% if value == per_page %}selected{% endif %}>{{ value }}</option>
{% endwith %}
{% endfor %}
</select>
per page
<input type="submit" value="apply" />
</form>
</div>
</div>

View file

@ -3,7 +3,50 @@
{% block content %} {% block content %}
<h1>Section title</h1> <h1>{{ service.verbose_name }}</h1>
<p>Little description of what to be expected...</p> <p>{{ service.description }}</p>
{% for database in object_list %}
<div class="card">
<div class="card-header row">
<div class="col-md-8">
<strong>{{ database.name }}</strong>
</div>
<div class="col-md">
Type: <strong>{{ database.type }}</strong>
</div>
<div class="col-md text-right">
associated to: <strong>{{ database.domain|default:"-" }}</strong>
<div class="card-collapse-button d-inline-block nav-item dropdown">
<button class="btn dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-expanded="false">
</button>
</div>
</div>
</div><!-- /card-header-->
<div class="card-body row">
<div class="col-md-4">
<h4>Database users</h4>
<ul>
{% for user in resource.users %}
<li>{{ user.username }}</li>
{% empty %}
<li>No users for this database.</li>
{% endfor %}
</ul>
</div>
<div class="col-md-2 border-left border-right">
<h4>Database usage</h4>
<div class="progress">
<div class="progress-bar w-75" role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100">75%</div>
</div>
</div>
<div class="col-md-6 text-right">
<a class="btn btn-secondary" href="#open-phpmyadmin">Open database manager</a>
</div>
</div>
</div>
{% endfor %}
{% include "musician/components/paginator.html" %}
{% endblock %} {% endblock %}

View file

@ -3,7 +3,27 @@
{% block content %} {% block content %}
<h1>Section title</h1> <h1>{{ service.verbose_name }}</h1>
<p>Little description of what to be expected...</p> <p>{{ service.description }}</p>
<table class="table table-hover">
<thead class="thead-dark">
<tr>
<th scope="col">Mail address</th>
<th scope="col"></th>
<th scope="col">Type</th>
<th scope="col">Type details</th>
</tr>
</thead>
<tbody>
{% for obj in object_list %}
<tr>
<td>{{ obj.mail_address }}</td>
<td>{{ obj.aliases|join:" , " }}</td>
<td>{{ obj.type }}</td>
<td>{{ obj.type_detail }}</td>
</tr>
{% endfor %}
</tbody>
{% include "musician/components/table_paginator.html" %}
</table>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,51 @@
{% extends "musician/base.html" %}
{% load i18n %}
{% block content %}
<h1>Profile</h1>
<p>Little description of what to be expected...</p>
<div class="card">
<h5 class="card-header text-right">User information</h5>
<div class="card-body row">
<div class="col-md-4">
<div class="m-auto border border-secondary rounded-circle rounded-lg" style="width:125px; height:125px;">
{# <!-- <img class="" src="#" alt="User profile image" /> -->#}
</div>
</div>
<div class="col-md-8">
<p class="card-text">{{ profile.username }}</p>
<p class="card-text">{{ profile.type }}</p>
<p class="card-text">Preferred language: {{ profile.language }}</p>
</div>
</div>
</div>
{% with profile.billing as contact %}
<div class="card mt-4">
<h5 class="card-header text-right">Billing information</h5>
<div class="card-body">
<div class="form-group">{{ contact.name }}</div>
<div class="form-group">{{ contact.address }}</div>
<div class="form-group">
{{ contact.zipcode }}
{{ contact.city }}
{{ contact.country }}
</div>
<div class="form-group">
{{ contact.vat }}
</div>
<!-- payment method -->
<div class="form-group">
payment method: {{ payment.method }}
</div>
<div class="form-group">
{# TODO(@slamora) format payment method details #}
{{ payment.data.data }}
</div>
</div>
</div>
{% endwith %}
{% endblock %}

View file

@ -0,0 +1,28 @@
{% extends "musician/base.html" %}
{% load i18n musician %}
{% block content %}
<h1>{{ service.verbose_name }}</h1>
<p>{{ service.description }}</p>
<table class="table table-hover">
<thead class="thead-dark">
<tr>
{% for field_name in service.fields %}
<th scope="col">{{ field_name }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for resource in object_list %}
<tr>
{% for field_name in service.fields %}
<td>{{ resource|get_item:field_name }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
{% include "musician/components/table_paginator.html" %}
</table>
{% endblock %}

View file

@ -0,0 +1,6 @@
from django.template.defaulttags import register
@register.filter
def get_item(dictionary, key):
return dictionary.get(key)

View file

@ -15,6 +15,7 @@ urlpatterns = [
path('auth/login/', views.LoginView.as_view(), name='login'), path('auth/login/', views.LoginView.as_view(), name='login'),
path('auth/logout/', views.LogoutView.as_view(), name='logout'), path('auth/logout/', views.LogoutView.as_view(), name='logout'),
path('dashboard/', views.DashboardView.as_view(), name='dashboard'), 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('mails/', views.MailView.as_view(), name='mails'),
path('mailing-lists/', views.MailingListsView.as_view(), name='mailing-lists'), path('mailing-lists/', views.MailingListsView.as_view(), name='mailing-lists'),
path('databases/', views.DatabasesView.as_view(), name='databases'), path('databases/', views.DatabasesView.as_view(), name='databases'),

View file

@ -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.http import HttpResponseRedirect
from django.shortcuts import render from django.shortcuts import render
from django.urls import reverse_lazy 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 login as auth_login
from .auth import logout as auth_logout from .auth import logout as auth_logout
from .forms import LoginForm 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): class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
@ -30,38 +36,84 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
return context return context
class MailView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): class ProfileView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
template_name = "musician/mail.html" 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): class ServiceListView(CustomContextMixin, ExtendedPaginationMixin, UserTokenRequiredMixin, ListView):
template_name = "musician/mailinglists.html" """Base list view to all services"""
paginate_by = 20 service_class = None
paginate_by_kwarg = 'per_page' template_name = "musician/service_list.html" # TODO move to ServiceListView
def get_queryset(self): 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): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context.update({ context.update({
'page_param': self.page_kwarg, 'service': self.service_class,
'per_page_values': [5, 10, 20, 50],
'per_page_param': self.paginate_by_kwarg,
}) })
return context return context
def get_paginate_by(self, queryset):
per_page = self.request.GET.get(self.paginate_by_kwarg) or self.paginate_by class MailView(ServiceListView):
try: service_class = MailService
paginate_by = int(per_page) template_name = "musician/mail.html"
except ValueError:
paginate_by = self.paginate_by def get_queryset(self):
return paginate_by 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" template_name = "musician/databases.html"
service_class = DatabaseService
class SaasView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): class SaasView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):