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/',
# 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/',
}

View file

@ -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."""

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 %}
<h1>Section title</h1>
<p>Little description of what to be expected...</p>
<h1>{{ service.verbose_name }}</h1>
<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 %}

View file

@ -3,7 +3,27 @@
{% block content %}
<h1>Section title</h1>
<p>Little description of what to be expected...</p>
<h1>{{ service.verbose_name }}</h1>
<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 %}

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/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'),

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.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):