Merge branch 'master' into i18n
This commit is contained in:
commit
7744ce6d85
|
@ -2,7 +2,12 @@ import requests
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.http import Http404
|
||||||
from django.urls.exceptions import NoReverseMatch
|
from django.urls.exceptions import NoReverseMatch
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from .models import Domain, DatabaseService, MailService, SaasService, UserAccount
|
||||||
|
|
||||||
|
|
||||||
DOMAINS_PATH = 'domains/'
|
DOMAINS_PATH = 'domains/'
|
||||||
TOKEN_PATH = '/api-token-auth/'
|
TOKEN_PATH = '/api-token-auth/'
|
||||||
|
@ -15,12 +20,15 @@ API_PATHS = {
|
||||||
# services
|
# services
|
||||||
'database-list': 'databases/',
|
'database-list': 'databases/',
|
||||||
'domain-list': 'domains/',
|
'domain-list': 'domains/',
|
||||||
|
'domain-detail': 'domains/{pk}/',
|
||||||
'address-list': 'addresses/',
|
'address-list': 'addresses/',
|
||||||
'mailbox-list': 'mailboxes/',
|
'mailbox-list': 'mailboxes/',
|
||||||
'mailinglist-list': 'lists/',
|
'mailinglist-list': 'lists/',
|
||||||
'saas-list': 'saas/',
|
'saas-list': 'saas/',
|
||||||
|
|
||||||
# other
|
# other
|
||||||
|
'bill-list': 'bills/',
|
||||||
|
'bill-document': 'bills/{pk}/document/',
|
||||||
'payment-source-list': 'payment-sources/',
|
'payment-source-list': 'payment-sources/',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,9 +60,15 @@ class Orchestra(object):
|
||||||
|
|
||||||
return response.json().get("token", None)
|
return response.json().get("token", None)
|
||||||
|
|
||||||
def request(self, verb, resource, raise_exception=True):
|
def request(self, verb, resource=None, url=None, render_as="json", querystring=None, raise_exception=True):
|
||||||
assert verb in ["HEAD", "GET", "POST", "PATCH", "PUT", "DELETE"]
|
assert verb in ["HEAD", "GET", "POST", "PATCH", "PUT", "DELETE"]
|
||||||
|
if resource is not None:
|
||||||
url = self.build_absolute_uri(resource)
|
url = self.build_absolute_uri(resource)
|
||||||
|
elif url is None:
|
||||||
|
raise AttributeError("Provide `resource` or `url` params")
|
||||||
|
|
||||||
|
if querystring is not None:
|
||||||
|
url = "{}?{}".format(url, querystring)
|
||||||
|
|
||||||
verb = getattr(self.session, verb.lower())
|
verb = getattr(self.session, verb.lower())
|
||||||
response = verb(url, headers={"Authorization": "Token {}".format(
|
response = verb(url, headers={"Authorization": "Token {}".format(
|
||||||
|
@ -64,20 +78,70 @@ class Orchestra(object):
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
status = response.status_code
|
status = response.status_code
|
||||||
|
if render_as == "json":
|
||||||
output = response.json()
|
output = response.json()
|
||||||
|
else:
|
||||||
|
output = response.content
|
||||||
|
|
||||||
return status, output
|
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)
|
pattern_name = '{}-list'.format(service_name)
|
||||||
if pattern_name not in API_PATHS:
|
if pattern_name not in API_PATHS:
|
||||||
raise ValueError("Unknown service {}".format(service_name))
|
raise ValueError("Unknown service {}".format(service_name))
|
||||||
_, output = self.request("GET", pattern_name)
|
_, output = self.request("GET", pattern_name, querystring=querystring)
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def retreve_profile(self):
|
def retrieve_profile(self):
|
||||||
_, output = self.request("GET", 'my-account')
|
status, output = self.request("GET", 'my-account')
|
||||||
return output
|
if status >= 400:
|
||||||
|
raise PermissionError("Cannot retrieve profile of an anonymous user.")
|
||||||
|
return UserAccount.new_from_json(output[0])
|
||||||
|
|
||||||
|
def retrieve_bill_document(self, pk):
|
||||||
|
path = API_PATHS.get('bill-document').format_map({'pk': pk})
|
||||||
|
|
||||||
|
url = urllib.parse.urljoin(self.base_url, path)
|
||||||
|
status, bill_pdf = self.request("GET", render_as="html", url=url, raise_exception=False)
|
||||||
|
if status == 404:
|
||||||
|
raise Http404(_("No domain found matching the query"))
|
||||||
|
return bill_pdf
|
||||||
|
|
||||||
|
def retrieve_domain(self, pk):
|
||||||
|
path = API_PATHS.get('domain-detail').format_map({'pk': pk})
|
||||||
|
|
||||||
|
url = urllib.parse.urljoin(self.base_url, path)
|
||||||
|
status, domain_json = self.request("GET", url=url, raise_exception=False)
|
||||||
|
if status == 404:
|
||||||
|
raise Http404(_("No domain found matching the query"))
|
||||||
|
return Domain.new_from_json(domain_json)
|
||||||
|
|
||||||
|
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):
|
def verify_credentials(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -46,6 +46,11 @@ class ExtendedPaginationMixin:
|
||||||
|
|
||||||
|
|
||||||
class UserTokenRequiredMixin(UserPassesTestMixin):
|
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):
|
def test_func(self):
|
||||||
"""Check that the user has an authorized token."""
|
"""Check that the user has an authorized token."""
|
||||||
token = self.request.session.get(SESSION_KEY_TOKEN, None)
|
token = self.request.session.get(SESSION_KEY_TOKEN, None)
|
||||||
|
@ -60,3 +65,10 @@ class UserTokenRequiredMixin(UserPassesTestMixin):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context.update({
|
||||||
|
'profile': self.orchestra.retrieve_profile(),
|
||||||
|
})
|
||||||
|
return context
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
|
import ast
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class OrchestraModel:
|
class OrchestraModel:
|
||||||
""" Base class from which all orchestra models will inherit. """
|
""" Base class from which all orchestra models will inherit. """
|
||||||
api_name = None
|
api_name = None
|
||||||
verbose_name = None
|
verbose_name = None
|
||||||
fields = ()
|
fields = ()
|
||||||
param_defaults = {}
|
id = None
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
if self.verbose_name is None:
|
if self.verbose_name is None:
|
||||||
|
@ -16,9 +22,6 @@ class OrchestraModel:
|
||||||
for (param, default) in self.param_defaults.items():
|
for (param, default) in self.param_defaults.items():
|
||||||
setattr(self, param, kwargs.get(param, default))
|
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
|
@classmethod
|
||||||
def new_from_json(cls, data, **kwargs):
|
def new_from_json(cls, data, **kwargs):
|
||||||
|
@ -35,10 +38,29 @@ class OrchestraModel:
|
||||||
|
|
||||||
c = cls(**json_data)
|
c = cls(**json_data)
|
||||||
c._json = data
|
c._json = data
|
||||||
# TODO(@slamora) remove/replace by private variable to ovoid name collisions
|
|
||||||
c.data = data
|
|
||||||
return c
|
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 Bill(OrchestraModel):
|
||||||
|
api_name = 'bill'
|
||||||
|
param_defaults = {
|
||||||
|
"id": None,
|
||||||
|
"number": "1",
|
||||||
|
"type": "INVOICE",
|
||||||
|
"total": 0.0,
|
||||||
|
"is_sent": False,
|
||||||
|
"created_on": "",
|
||||||
|
"due_on": "",
|
||||||
|
"comments": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class BillingContact(OrchestraModel):
|
class BillingContact(OrchestraModel):
|
||||||
param_defaults = {
|
param_defaults = {
|
||||||
|
@ -59,6 +81,15 @@ class PaymentSource(OrchestraModel):
|
||||||
"is_active": False,
|
"is_active": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
# payment details are passed as a plain string
|
||||||
|
# try to convert to a python structure
|
||||||
|
try:
|
||||||
|
self.data = ast.literal_eval(self.data)
|
||||||
|
except (ValueError, SyntaxError) as e:
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
|
|
||||||
class UserAccount(OrchestraModel):
|
class UserAccount(OrchestraModel):
|
||||||
api_name = 'accounts'
|
api_name = 'accounts'
|
||||||
|
@ -117,15 +148,51 @@ class DatabaseService(OrchestraModel):
|
||||||
return super().new_from_json(data=data, users=users, usage=usage)
|
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):
|
class MailService(OrchestraModel):
|
||||||
api_name = 'address'
|
api_name = 'address'
|
||||||
verbose_name = _('Mail addresses')
|
verbose_name = _('Mail addresses')
|
||||||
description = _('Litle description of what to be expected in this section to aid the user. Even a link to more help if there is one available.')
|
description = _('Litle description of what to be expected in this section to aid the user. Even a link to more help if there is one available.')
|
||||||
fields = ('mail_address', 'aliases', 'type', 'type_detail')
|
fields = ('mail_address', 'aliases', 'type', 'type_detail')
|
||||||
|
param_defaults = {}
|
||||||
|
|
||||||
FORWARD = 'forward'
|
FORWARD = 'forward'
|
||||||
MAILBOX = 'mailbox'
|
MAILBOX = 'mailbox'
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.data = kwargs
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def aliases(self):
|
def aliases(self):
|
||||||
return [
|
return [
|
||||||
|
@ -165,6 +232,10 @@ class MailinglistService(OrchestraModel):
|
||||||
'admin_email': None,
|
'admin_email': None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.data = kwargs
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status(self):
|
def status(self):
|
||||||
# TODO(@slamora): where retrieve if the list is active?
|
# TODO(@slamora): where retrieve if the list is active?
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,32 @@ a:hover {
|
||||||
color: rgba(0,0,0,.7);
|
color: rgba(0,0,0,.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-arrow-left{
|
||||||
|
color: #eee;
|
||||||
|
background: #D3D0DA;
|
||||||
|
position: relative;
|
||||||
|
padding: 8px 20px 8px 30px;
|
||||||
|
margin-left: 1em; /** equal value than arrow.left **/
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-arrow-left::after,
|
||||||
|
.btn-arrow-left::before{
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: -1em;
|
||||||
|
|
||||||
|
margin-top: -19px;
|
||||||
|
border-top: 19px solid transparent;
|
||||||
|
border-bottom: 19px solid transparent;
|
||||||
|
border-right: 1em solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-arrow-left::after{
|
||||||
|
border-right-color: #D3D0DA;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -172,3 +198,88 @@ h1.service-name {
|
||||||
.service-card .card-body .service-brand i.fab {
|
.service-card .card-body .service-brand i.fab {
|
||||||
color: #9C9AA7;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.card-profile .card-header {
|
||||||
|
background: white;
|
||||||
|
border-bottom: none;
|
||||||
|
font-size: large;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
|
@ -59,7 +59,7 @@
|
||||||
<div class="dropdown dropright">
|
<div class="dropdown dropright">
|
||||||
<button type="button" class="btn btn-primary nav-link text-light w-100" data-toggle="dropdown">
|
<button type="button" class="btn btn-primary nav-link text-light w-100" data-toggle="dropdown">
|
||||||
<img id="user-avatar" class="float-right" width="64" height="64" src="{% static "musician/images/default-profile-picture.png" %}" alt="user-profile-picture"/>
|
<img id="user-avatar" class="float-right" width="64" height="64" src="{% static "musician/images/default-profile-picture.png" %}" alt="user-profile-picture"/>
|
||||||
<strong>{{ user.username|default:"Username" }}</strong><br/>
|
<strong>{{ profile.username }}</strong><br/>
|
||||||
<i class="fas fa-cog"></i> Settings
|
<i class="fas fa-cog"></i> Settings
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
|
@ -93,6 +93,7 @@
|
||||||
<script src="{% static "musician/js/jquery-3.3.1.slim.min.js" %}"></script>
|
<script src="{% static "musician/js/jquery-3.3.1.slim.min.js" %}"></script>
|
||||||
<script src="{% static "musician/js/popper.min.js" %}"></script>
|
<script src="{% static "musician/js/popper.min.js" %}"></script>
|
||||||
<script src="{% static "musician/js/bootstrap.min.js" %}"></script>
|
<script src="{% static "musician/js/bootstrap.min.js" %}"></script>
|
||||||
|
{% block extrascript %}{% endblock %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends "musician/base.html" %}
|
{% extends "musician/base.html" %}
|
||||||
{% load i18n %}
|
{% load i18n l10n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
@ -13,7 +13,6 @@
|
||||||
<col span="1" style="width: 40%;">
|
<col span="1" style="width: 40%;">
|
||||||
<col span="1" style="width: 10%;">
|
<col span="1" style="width: 10%;">
|
||||||
<col span="1" style="width: 10%;">
|
<col span="1" style="width: 10%;">
|
||||||
<col span="1" style="width: 10%;">
|
|
||||||
</colgroup>
|
</colgroup>
|
||||||
<thead class="thead-dark">
|
<thead class="thead-dark">
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -21,7 +20,6 @@
|
||||||
<th scope="col">Bill date</th>
|
<th scope="col">Bill date</th>
|
||||||
<th scope="col">Type</th>
|
<th scope="col">Type</th>
|
||||||
<th scope="col">Total</th>
|
<th scope="col">Total</th>
|
||||||
<th scope="col">Status</th>
|
|
||||||
<th scope="col">Download PDF</th>
|
<th scope="col">Download PDF</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -29,11 +27,10 @@
|
||||||
{% for bill in object_list %}
|
{% for bill in object_list %}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{{ bill.number }}</th>
|
<th scope="row">{{ bill.number }}</th>
|
||||||
<td>{{ bill.date|date:"SHORT_DATE_FORMAT" }}</td>
|
<td>{{ bill.created_on }}</td>
|
||||||
<td>{{ bill.type }}</td>
|
<td>{{ bill.type }}</td>
|
||||||
<td>{{ bill.total_amount }}</td>
|
<td>{{ bill.total|floatformat:2|localize }}€</td>
|
||||||
<td class="font-weight-bold">{{ bill.status }}</td>
|
<td><a class="text-dark" href="{% url 'musician:bill-download' bill.id %}" target="_blank" rel="noopener noreferrer"><i class="fas fa-file-pdf"></i></a></td>
|
||||||
<td><a class="text-dark" href="{{ bill.pdf_url }}" target="_blank" rel="noopener noreferrer"><i class="fas fa-file-pdf"></i></a></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -3,38 +3,142 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<h2>Welcome back {{ user.username }}</h2>
|
<h2>{% trans "Welcome back" %} <strong>{{ profile.username }}</strong></h2>
|
||||||
<h3>Last time you logged in was {{ user.last_login }}</h3>
|
<p>{% blocktrans with last_login=profile.last_login|default:"N/A" %}Last time you logged in was: {{ last_login }}{% endblocktrans %}</p>
|
||||||
|
|
||||||
<div class="row">
|
<div class="card-deck">
|
||||||
{% for i in "1234"|make_list %}
|
{% for resource, usage in resource_usage.items %}
|
||||||
<div class="col-3 border">
|
<div class="card resource-usage resource-{{ resource }}">
|
||||||
Resource usage block
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{{ usage.verbose_name }}</h5>
|
||||||
|
{% include "musician/components/usage_progress_bar.html" with detail=usage %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<div class="card resource-usage resource-notifications">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{% trans "Notifications" %}</h5>
|
||||||
|
{% for message in notifications %}
|
||||||
|
<p class="card-text">{{ message }}</p>
|
||||||
|
{% empty %}
|
||||||
|
<p class="card-text">{% trans "There is no notifications at this time." %}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<h1>Domains and websites</h1>
|
<h1 class="service-name">{% trans "Your domains and websites" %}</h1>
|
||||||
<p>Little description of what to be expected...</p>
|
<p class="service-description">Little description of what to be expected...</p>
|
||||||
|
|
||||||
{% for domain in domains %}
|
{% for domain in domains %}
|
||||||
<div class="row border mt-4">
|
<div class="card service-card">
|
||||||
<div class="col-12 bg-light">
|
<div class="card-header">
|
||||||
<h3>{{ domain.name }}</h3>
|
<div class="row">
|
||||||
|
<div class="col-md">
|
||||||
|
<strong>{{ domain.name }}</strong>
|
||||||
</div>
|
</div>
|
||||||
{% for service in "123"|make_list %}
|
<div class="col-md-8">
|
||||||
<div class="card" style="width: 18rem;">
|
<button type="button" class="btn text-secondary" data-toggle="modal" data-target="#configDetailsModal"
|
||||||
<div class="card-body">
|
data-domain="{{ domain.name }}" data-username="john" data-password="s3cre3t" data-root="/domainname/"
|
||||||
<h5 class="card-title">{% cycle 'Mail' 'Mailing list' 'Databases' %}</h5>
|
data-url="{% url 'musician:domain-detail' domain.id %}">
|
||||||
|
{% trans "view configuration" %} <strong class="fas fa-tools"></strong>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<img class="card-img-bottom" src="..." alt="Card image cap">
|
<div class="col-md text-right">
|
||||||
<div class="card-body">
|
{% trans "Expiration date" %}: <strong>{{ domain.expiration_date|date:"SHORT_DATE_FORMAT" }}</strong>
|
||||||
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's
|
|
||||||
content.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor card %}
|
</div><!-- /card-header-->
|
||||||
|
<div class="card-body row text-center">
|
||||||
|
<div class="col-md-2 border-right">
|
||||||
|
<h4>{% trans "Mail" %}</h4>
|
||||||
|
<p class="card-text"><i class="fas fa-envelope fa-3x"></i></p>
|
||||||
|
<p class="card-text text-dark">
|
||||||
|
{{ domain.mails|length }} {% trans "mail addresses created" %}
|
||||||
|
{% if domain.address_left.alert %}
|
||||||
|
<br/>
|
||||||
|
<span class="text-{{ domain.address_left.alert }}">{{ domain.address_left.count }} mail address left</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<a class="stretched-link" href="{% url 'musician:mails' %}?domain={{ domain.id }}"></a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-2 border-right">
|
||||||
|
<h4>{% trans "Mail list" %}</h4>
|
||||||
|
<p class="card-text"><i class="fas fa-mail-bulk fa-3x"></i></p>
|
||||||
|
<a class="stretched-link" href="{% url 'musician:mailing-lists' %}?domain={{ domain.id }}"></a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 border-right">
|
||||||
|
<h4>{% trans "Databases" %}</h4>
|
||||||
|
<p class="card-text"><i class="fas fa-database fa-3x"></i></p>
|
||||||
|
<p class="card-text text-dark">
|
||||||
|
0 {% trans "databases created" %}
|
||||||
|
{% comment %}
|
||||||
|
<!-- TODO databases related to a domain and resource usage
|
||||||
|
{{ domain.databases|length }} {% trans "databases created" %}<br/>
|
||||||
|
20 MB of 45MB
|
||||||
|
-->
|
||||||
|
{% endcomment %}
|
||||||
|
</p>
|
||||||
|
<a class="stretched-link" href="{% url 'musician:databases' %}?domain={{ domain.id }}"></a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 border-right">
|
||||||
|
<h4>{% trans "Software as a Service" %}</h4>
|
||||||
|
<p class="card-text"><i class="fas fa-fire fa-3x"></i></p>
|
||||||
|
<p class="card-text text-dark">Nothing installed</p>
|
||||||
|
<a class="stretched-link" href="{% url 'musician:saas' %}?domain={{ domain.id }}"></a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h4>{% trans "Disk usage" %}</h4>
|
||||||
|
<p class="card-text"><i class="fas fa-hdd fa-3x"></i></p>
|
||||||
|
<div class="w-75 m-auto">
|
||||||
|
{% include "musician/components/usage_progress_bar.html" with detail=domain.usage %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- configuration details modal -->
|
||||||
|
<div class="modal fade" id="configDetailsModal" tabindex="-1" role="dialog" aria-labelledby="configDetailsModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title text-secondary" id="configDetailsModalLabel">Configuration details</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<h6 class="pl-4 mb-4">{% trans "FTP access:" %}</h6>
|
||||||
|
<div class="">
|
||||||
|
<p>
|
||||||
|
<label>{% trans "Username" %}:</label> <span id="config-username" class="font-weight-bold">username</span><br/>
|
||||||
|
<label>{% trans "Password:" %}</label> <span id="config-password" class="font-weight-bold">password</span>
|
||||||
|
</p>
|
||||||
|
<p class="border-top pt-3"><label>Root directory:</label> <span id="config-root" class="font-weight-bold">root directory</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<a href="#domain-detail" class="btn btn-primary">{% trans "View DNS records" %}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% block extrascript %}
|
||||||
|
<script>
|
||||||
|
$('#configDetailsModal').on('show.bs.modal', function (event) {
|
||||||
|
var button = $(event.relatedTarget); // Button that triggered the modal
|
||||||
|
var modal = $(this);
|
||||||
|
|
||||||
|
// Extract info from data-* attributes
|
||||||
|
modal.find('.modal-title').text(button.data('domain'));
|
||||||
|
modal.find('.modal-body #config-username').text(button.data('username'));
|
||||||
|
modal.find('.modal-body #config-password').text(button.data('password'));
|
||||||
|
modal.find('.modal-body #config-root').text(button.data('root'));
|
||||||
|
modal.find('.modal-footer .btn').attr('href', button.data('url'));
|
||||||
|
})
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
{% extends "musician/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<a class="btn-arrow-left" href="{% url 'musician:dashboard' %}">{% trans "Go back" %}</a>
|
||||||
|
|
||||||
|
<h1 class="service-name">{% trans "DNS settings for" %} <span class="font-weight-light">{{ object.name }}</span></h1>
|
||||||
|
<p class="service-description">Litle description of what to be expected in this section to aid the user. Even a link to more help if there is one available.</p>
|
||||||
|
|
||||||
|
<table class="table service-list">
|
||||||
|
<colgroup>
|
||||||
|
<col span="1" style="width: 12%;">
|
||||||
|
<col span="1" style="width: 88%;">
|
||||||
|
</colgroup>
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{% trans "Type" %}</th>
|
||||||
|
<th scope="col">{% trans "Value" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for record in object.records %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ record.type }}</td>
|
||||||
|
<td>{{ record.value }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
|
@ -2,8 +2,11 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{% if active_domain %}
|
||||||
|
<a class="btn-arrow-left" href="{% url 'musician:mails' %}">{% trans "Go to global" %}</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<h1 class="service-name">{{ service.verbose_name }}</h1>
|
<h1 class="service-name">{{ service.verbose_name }}{% if active_domain %} <span class="font-weight-light">{% trans "for" %} {{ active_domain.name }}</span>{% endif %}</h1>
|
||||||
<p class="service-description">{{ service.description }}</p>
|
<p class="service-description">{{ service.description }}</p>
|
||||||
<table class="table service-list">
|
<table class="table service-list">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
|
|
|
@ -2,8 +2,11 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{% if active_domain %}
|
||||||
|
<a class="btn-arrow-left" href="{% url 'musician:mailing-lists' %}">{% trans "Go to global" %}</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<h1 class="service-name">{{ service.verbose_name }}</h1>
|
<h1 class="service-name">{{ service.verbose_name }}{% if active_domain %} <span class="font-weight-light">{% trans "for" %} {{ active_domain.name }}</span>{% endif %}</h1>
|
||||||
<p class="service-description">{{ service.description }}</p>
|
<p class="service-description">{{ service.description }}</p>
|
||||||
|
|
||||||
<table class="table service-list">
|
<table class="table service-list">
|
||||||
|
|
|
@ -3,28 +3,35 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<h1>Profile</h1>
|
<h1 class="service-name">Profile</h1>
|
||||||
<p>Little description of what to be expected...</p>
|
<p class="service-description">Little description of what to be expected...</p>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card-deck">
|
||||||
<h5 class="card-header text-right">User information</h5>
|
<div class="card card-profile">
|
||||||
|
<h5 class="card-header">User information</h5>
|
||||||
<div class="card-body row">
|
<div class="card-body row">
|
||||||
<div class="col-md-4">
|
<div class="col-md">
|
||||||
<div class="m-auto border border-secondary rounded-circle rounded-lg" style="width:125px; height:125px;">
|
<div class="border-primary rounded-circle d-inline-block p-1" style="background-color: white; border: 5px solid grey">
|
||||||
{# <!-- <img class="" src="#" alt="User profile image" /> -->#}
|
<img id="user-avatar" width="160" height="160" src="/static/musician/images/default-profile-picture-primary-color.png" alt="user-profile-picture">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-8">
|
<div class="col-md-9">
|
||||||
<p class="card-text">{{ profile.username }}</p>
|
<p class="card-text">{{ profile.username }}</p>
|
||||||
<p class="card-text">{{ profile.type }}</p>
|
<p class="card-text">{{ profile.type }}</p>
|
||||||
<p class="card-text">Preferred language: {{ profile.language }}</p>
|
<p class="card-text">Preferred language: {{ profile.language }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{% comment %}
|
||||||
|
<!-- disabled until set_password is implemented -->
|
||||||
|
<div class="col-md-12 text-right">
|
||||||
|
<a class="btn btn-primary pl-5 pr-5" href="#">Set new password</a>
|
||||||
|
</div>
|
||||||
|
{% endcomment %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% with profile.billing as contact %}
|
{% with profile.billing as contact %}
|
||||||
<div class="card mt-4">
|
<div class="card card-profile">
|
||||||
<h5 class="card-header text-right">Billing information</h5>
|
<h5 class="card-header">Billing information</h5>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="form-group">{{ contact.name }}</div>
|
<div class="form-group">{{ contact.name }}</div>
|
||||||
<div class="form-group">{{ contact.address }}</div>
|
<div class="form-group">{{ contact.address }}</div>
|
||||||
|
@ -41,11 +48,16 @@
|
||||||
payment method: {{ payment.method }}
|
payment method: {{ payment.method }}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
{# TODO(@slamora) format payment method details #}
|
{% if payment.method == 'SEPADirectDebit' %}
|
||||||
{{ payment.data.data }}
|
IBAN {{ payment.data.iban }}
|
||||||
|
{% else %}
|
||||||
|
{# <!-- "TODO handle Credit Card" --> #}
|
||||||
|
Details: {{ payment.data }}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,3 +1,14 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
|
class DomainsTestCase(TestCase):
|
||||||
|
def test_domain_not_found(self):
|
||||||
|
response = self.client.post(
|
||||||
|
'/auth/login/',
|
||||||
|
{'username': 'admin', 'password': 'admin'},
|
||||||
|
follow=True
|
||||||
|
)
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
|
response = self.client.get('/domains/3/')
|
||||||
|
self.assertEqual(404, response.status_code)
|
||||||
|
|
|
@ -15,7 +15,9 @@ 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('billing/', views.BillingView.as_view(), name='billing'),
|
path('domains/<int:pk>/', views.DomainDetailView.as_view(), name='domain-detail'),
|
||||||
|
path('bills/', views.BillingView.as_view(), name='billing'),
|
||||||
|
path('bills/<int:pk>/download/', views.BillDownloadView.as_view(), name='bill-download'),
|
||||||
path('profile/', views.ProfileView.as_view(), name='profile'),
|
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'),
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
|
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.http import is_safe_url
|
from django.utils.http import is_safe_url
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.views import View
|
||||||
from django.views.generic.base import RedirectView, TemplateView
|
from django.views.generic.base import RedirectView, TemplateView
|
||||||
from django.views.generic.detail import DetailView
|
from django.views.generic.detail import DetailView
|
||||||
from django.views.generic.edit import FormView
|
from django.views.generic.edit import FormView
|
||||||
|
@ -17,8 +18,9 @@ from .auth import logout as auth_logout
|
||||||
from .forms import LoginForm
|
from .forms import LoginForm
|
||||||
from .mixins import (CustomContextMixin, ExtendedPaginationMixin,
|
from .mixins import (CustomContextMixin, ExtendedPaginationMixin,
|
||||||
UserTokenRequiredMixin)
|
UserTokenRequiredMixin)
|
||||||
from .models import (DatabaseService, MailinglistService, MailService,
|
from .models import (Bill, DatabaseService, MailinglistService, MailService,
|
||||||
PaymentSource, SaasService, UserAccount)
|
PaymentSource, SaasService, UserAccount)
|
||||||
|
from .settings import ALLOWED_RESOURCES
|
||||||
|
|
||||||
|
|
||||||
class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
||||||
|
@ -26,48 +28,72 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
domains = self.orchestra.retrieve_domain_list()
|
||||||
|
|
||||||
# TODO retrieve all data needed from orchestra
|
# TODO(@slamora) update when backend provides resource usage data
|
||||||
raw_domains = self.orchestra.retrieve_service_list('domain')
|
resource_usage = {
|
||||||
|
'disk': {
|
||||||
|
'verbose_name': _('Disk usage'),
|
||||||
|
'usage': 534,
|
||||||
|
'total': 1024,
|
||||||
|
'unit': 'MB',
|
||||||
|
'percent': 50,
|
||||||
|
},
|
||||||
|
'traffic': {
|
||||||
|
'verbose_name': _('Traffic'),
|
||||||
|
'usage': 300,
|
||||||
|
'total': 2048,
|
||||||
|
'unit': 'MB/month',
|
||||||
|
'percent': 25,
|
||||||
|
},
|
||||||
|
'mailbox': {
|
||||||
|
'verbose_name': _('Mailbox usage'),
|
||||||
|
'usage': 1,
|
||||||
|
'total': 2,
|
||||||
|
'unit': 'accounts',
|
||||||
|
'percent': 50,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# TODO(@slamora) update when backend supports notifications
|
||||||
|
notifications = []
|
||||||
|
|
||||||
|
# show resource usage based on plan definition
|
||||||
|
# TODO(@slamora): validate concept of limits with Pangea
|
||||||
|
profile_type = context['profile'].type
|
||||||
|
for domain in domains:
|
||||||
|
address_left = ALLOWED_RESOURCES[profile_type]['mailbox'] - len(domain.mails)
|
||||||
|
alert = None
|
||||||
|
if address_left == 1:
|
||||||
|
alert = 'warning'
|
||||||
|
elif address_left < 1:
|
||||||
|
alert = 'danger'
|
||||||
|
|
||||||
|
domain.address_left = {
|
||||||
|
'count': address_left,
|
||||||
|
'alert': alert,
|
||||||
|
}
|
||||||
|
|
||||||
context.update({
|
context.update({
|
||||||
'domains': raw_domains
|
'domains': domains,
|
||||||
|
'resource_usage': resource_usage,
|
||||||
|
'notifications': notifications,
|
||||||
})
|
})
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class BillingView(CustomContextMixin, ExtendedPaginationMixin, UserTokenRequiredMixin, ListView):
|
|
||||||
template_name = "musician/billing.html"
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
# TODO (@slamora) retrieve user bills
|
|
||||||
from django.utils import timezone
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
'number': 24,
|
|
||||||
'date': timezone.now(),
|
|
||||||
'type': 'subscription',
|
|
||||||
'total_amount': '25,00 €',
|
|
||||||
'status': 'paid',
|
|
||||||
'pdf_url': 'https://example.org/bill.pdf'
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class ProfileView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
class ProfileView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
||||||
template_name = "musician/profile.html"
|
template_name = "musician/profile.html"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
json_data = self.orchestra.retreve_profile()
|
|
||||||
try:
|
try:
|
||||||
pay_source = self.orchestra.retrieve_service_list(
|
pay_source = self.orchestra.retrieve_service_list(
|
||||||
PaymentSource.api_name)[0]
|
PaymentSource.api_name)[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pay_source = {}
|
pay_source = {}
|
||||||
context.update({
|
context.update({
|
||||||
'profile': UserAccount.new_from_json(json_data[0]),
|
|
||||||
'payment': PaymentSource.new_from_json(pay_source)
|
'payment': PaymentSource.new_from_json(pay_source)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -84,10 +110,17 @@ class ServiceListView(CustomContextMixin, ExtendedPaginationMixin, UserTokenRequ
|
||||||
raise ImproperlyConfigured(
|
raise ImproperlyConfigured(
|
||||||
"ServiceListView requires a definiton of 'service'")
|
"ServiceListView requires a definiton of 'service'")
|
||||||
|
|
||||||
|
queryfilter = self.get_queryfilter()
|
||||||
json_qs = self.orchestra.retrieve_service_list(
|
json_qs = self.orchestra.retrieve_service_list(
|
||||||
self.service_class.api_name)
|
self.service_class.api_name,
|
||||||
|
querystring=queryfilter,
|
||||||
|
)
|
||||||
return [self.service_class.new_from_json(data) for data in json_qs]
|
return [self.service_class.new_from_json(data) for data in json_qs]
|
||||||
|
|
||||||
|
def get_queryfilter(self):
|
||||||
|
"""Does nothing by default. Should be implemented on subclasses"""
|
||||||
|
return ''
|
||||||
|
|
||||||
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({
|
||||||
|
@ -96,6 +129,19 @@ class ServiceListView(CustomContextMixin, ExtendedPaginationMixin, UserTokenRequ
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class BillingView(ServiceListView):
|
||||||
|
service_class = Bill
|
||||||
|
template_name = "musician/billing.html"
|
||||||
|
|
||||||
|
|
||||||
|
class BillDownloadView(CustomContextMixin, UserTokenRequiredMixin, View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
pk = self.kwargs.get('pk')
|
||||||
|
bill = self.orchestra.retrieve_bill_document(pk)
|
||||||
|
|
||||||
|
return HttpResponse(bill)
|
||||||
|
|
||||||
|
|
||||||
class MailView(ServiceListView):
|
class MailView(ServiceListView):
|
||||||
service_class = MailService
|
service_class = MailService
|
||||||
template_name = "musician/mail.html"
|
template_name = "musician/mail.html"
|
||||||
|
@ -104,14 +150,20 @@ class MailView(ServiceListView):
|
||||||
def retrieve_mailbox(value):
|
def retrieve_mailbox(value):
|
||||||
mailboxes = value.get('mailboxes')
|
mailboxes = value.get('mailboxes')
|
||||||
|
|
||||||
|
# forwarded address should not grouped
|
||||||
if len(mailboxes) == 0:
|
if len(mailboxes) == 0:
|
||||||
return ''
|
return value.get('name')
|
||||||
|
|
||||||
return mailboxes[0]['id']
|
return mailboxes[0]['id']
|
||||||
|
|
||||||
# group addresses with the same mailbox
|
# retrieve mails applying filters (if any)
|
||||||
|
queryfilter = self.get_queryfilter()
|
||||||
raw_data = self.orchestra.retrieve_service_list(
|
raw_data = self.orchestra.retrieve_service_list(
|
||||||
self.service_class.api_name)
|
self.service_class.api_name,
|
||||||
|
querystring=queryfilter,
|
||||||
|
)
|
||||||
|
|
||||||
|
# group addresses with the same mailbox
|
||||||
addresses = []
|
addresses = []
|
||||||
for key, group in groupby(raw_data, retrieve_mailbox):
|
for key, group in groupby(raw_data, retrieve_mailbox):
|
||||||
aliases = []
|
aliases = []
|
||||||
|
@ -125,12 +177,49 @@ class MailView(ServiceListView):
|
||||||
|
|
||||||
return addresses
|
return addresses
|
||||||
|
|
||||||
|
def get_queryfilter(self):
|
||||||
|
"""Retrieve query params (if any) to filter queryset"""
|
||||||
|
domain_id = self.request.GET.get('domain')
|
||||||
|
if domain_id:
|
||||||
|
return "domain={}".format(domain_id)
|
||||||
|
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
domain_id = self.request.GET.get('domain')
|
||||||
|
if domain_id:
|
||||||
|
context.update({
|
||||||
|
'active_domain': self.orchestra.retrieve_domain(domain_id)
|
||||||
|
})
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class MailingListsView(ServiceListView):
|
class MailingListsView(ServiceListView):
|
||||||
service_class = MailinglistService
|
service_class = MailinglistService
|
||||||
template_name = "musician/mailinglists.html"
|
template_name = "musician/mailinglists.html"
|
||||||
|
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
domain_id = self.request.GET.get('domain')
|
||||||
|
if domain_id:
|
||||||
|
context.update({
|
||||||
|
'active_domain': self.orchestra.retrieve_domain(domain_id)
|
||||||
|
})
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_queryfilter(self):
|
||||||
|
"""Retrieve query params (if any) to filter queryset"""
|
||||||
|
# TODO(@slamora): this is not working because backend API
|
||||||
|
# doesn't support filtering by domain
|
||||||
|
domain_id = self.request.GET.get('domain')
|
||||||
|
if domain_id:
|
||||||
|
return "domain={}".format(domain_id)
|
||||||
|
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
class DatabasesView(ServiceListView):
|
class DatabasesView(ServiceListView):
|
||||||
template_name = "musician/databases.html"
|
template_name = "musician/databases.html"
|
||||||
service_class = DatabaseService
|
service_class = DatabaseService
|
||||||
|
@ -141,6 +230,25 @@ class SaasView(ServiceListView):
|
||||||
template_name = "musician/saas.html"
|
template_name = "musician/saas.html"
|
||||||
|
|
||||||
|
|
||||||
|
class DomainDetailView(CustomContextMixin, UserTokenRequiredMixin, DetailView):
|
||||||
|
template_name = "musician/domain_detail.html"
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
# Return an empty list to avoid a request to retrieve all the
|
||||||
|
# user domains. We will get a 404 if the domain doesn't exists
|
||||||
|
# while invoking `get_object`
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_object(self, queryset=None):
|
||||||
|
if queryset is None:
|
||||||
|
queryset = self.get_queryset()
|
||||||
|
|
||||||
|
pk = self.kwargs.get(self.pk_url_kwarg)
|
||||||
|
domain = self.orchestra.retrieve_domain(pk)
|
||||||
|
|
||||||
|
return domain
|
||||||
|
|
||||||
|
|
||||||
class LoginView(FormView):
|
class LoginView(FormView):
|
||||||
template_name = 'auth/login.html'
|
template_name = 'auth/login.html'
|
||||||
form_class = LoginForm
|
form_class = LoginForm
|
||||||
|
|
Loading…
Reference in New Issue