Merge branch 'master' into i18n

This commit is contained in:
Santiago Lamora 2020-01-21 13:31:06 +01:00
commit 4727a931b4
16 changed files with 298 additions and 80 deletions

35
LICENSE Normal file
View File

@ -0,0 +1,35 @@
Copyright 2020 Santiago Lamora and individual contributors.
All Rights Reserved.
django-musician is licensed under The BSD License (3 Clause, also known as
the new BSD license). The license is an OSI approved Open Source
license and is GPL-compatible(1).
The license text can also be found here:
http://www.opensource.org/licenses/BSD-3-Clause
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Ask Solem, nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Ask Solem OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

View File

@ -2,7 +2,7 @@
Package metadata definition. Package metadata definition.
""" """
VERSION = (0, 1, 0, 'beta', 1) VERSION = (0, 1, 0, 'beta', 2)
def get_version(): def get_version():

View File

@ -6,7 +6,7 @@ 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 django.utils.translation import gettext_lazy as _
from .models import Domain, DatabaseService, MailService, SaasService, UserAccount from .models import Domain, DatabaseService, MailService, SaasService, UserAccount, WebSite
DOMAINS_PATH = 'domains/' DOMAINS_PATH = 'domains/'
@ -25,6 +25,7 @@ API_PATHS = {
'mailbox-list': 'mailboxes/', 'mailbox-list': 'mailboxes/',
'mailinglist-list': 'lists/', 'mailinglist-list': 'lists/',
'saas-list': 'saas/', 'saas-list': 'saas/',
'website-list': 'websites/',
# other # other
'bill-list': 'bills/', 'bill-list': 'bills/',
@ -118,6 +119,8 @@ class Orchestra(object):
def retrieve_domain_list(self): def retrieve_domain_list(self):
output = self.retrieve_service_list(Domain.api_name) output = self.retrieve_service_list(Domain.api_name)
websites = self.retrieve_website_list()
domains = [] domains = []
for domain_json in output: for domain_json in output:
# filter querystring # filter querystring
@ -126,6 +129,10 @@ class Orchestra(object):
# retrieve services associated to a domain # retrieve services associated to a domain
domain_json['mails'] = self.retrieve_service_list( domain_json['mails'] = self.retrieve_service_list(
MailService.api_name, querystring) MailService.api_name, querystring)
# retrieve websites (as they cannot be filtered by domain on the API we should do it here)
domain_json['websites'] = self.filter_websites_by_domain(websites, domain_json['id'])
# TODO(@slamora): databases and sass are not related to a domain, so cannot be filtered # 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['databases'] = self.retrieve_service_list(DatabaseService.api_name, querystring)
# domain_json['saas'] = self.retrieve_service_list(SaasService.api_name, querystring) # domain_json['saas'] = self.retrieve_service_list(SaasService.api_name, querystring)
@ -143,6 +150,19 @@ class Orchestra(object):
return domains return domains
def retrieve_website_list(self):
output = self.retrieve_service_list(WebSite.api_name)
return [WebSite.new_from_json(website_data) for website_data in output]
def filter_websites_by_domain(self, websites, domain_id):
matching = []
for website in websites:
web_domains = [web_domain.id for web_domain in website.domains]
if domain_id in web_domains:
matching.append(website)
return matching
def verify_credentials(self): def verify_credentials(self):
""" """
Returns: Returns:

View File

@ -1,6 +1,7 @@
import ast import ast
import logging import logging
from django.utils.dateparse import parse_datetime
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 _
@ -100,15 +101,20 @@ class UserAccount(OrchestraModel):
'short_name': None, 'short_name': None,
'full_name': None, 'full_name': None,
'billing': {}, 'billing': {},
'last_login': None,
} }
@classmethod @classmethod
def new_from_json(cls, data, **kwargs): def new_from_json(cls, data, **kwargs):
billing = None billing = None
last_login = None
if 'billcontact' in data: if 'billcontact' in data:
billing = BillingContact.new_from_json(data['billcontact']) billing = BillingContact.new_from_json(data['billcontact'])
return super().new_from_json(data=data, billing=billing)
if 'last_login' in data:
last_login = parse_datetime(data['last_login'])
return super().new_from_json(data=data, billing=billing, last_login=last_login)
class DatabaseUser(OrchestraModel): class DatabaseUser(OrchestraModel):
@ -157,6 +163,7 @@ class Domain(OrchestraModel):
"records": [], "records": [],
"mails": [], "mails": [],
"usage": {}, "usage": {},
"websites": [],
} }
@classmethod @classmethod
@ -230,6 +237,7 @@ class MailinglistService(OrchestraModel):
fields = ('name', 'status', 'address_name', 'admin_email', 'configure') fields = ('name', 'status', 'address_name', 'admin_email', 'configure')
param_defaults = { param_defaults = {
'name': None, 'name': None,
'is_active': True,
'admin_email': None, 'admin_email': None,
} }
@ -237,11 +245,6 @@ class MailinglistService(OrchestraModel):
self.data = kwargs self.data = kwargs
super().__init__(**kwargs) super().__init__(**kwargs)
@property
def status(self):
# TODO(@slamora): where retrieve if the list is active?
return 'active'
@property @property
def address_name(self): def address_name(self):
return "{}@{}".format(self.data['address_name'], self.data['address_domain']['name']) return "{}@{}".format(self.data['address_name'], self.data['address_domain']['name'])
@ -262,3 +265,23 @@ class SaasService(OrchestraModel):
'is_active': True, 'is_active': True,
'data': {}, 'data': {},
} }
class WebSite(OrchestraModel):
api_name = 'website'
param_defaults = {
"id": None,
"name": None,
"protocol": None,
"is_active": True,
"domains": [],
"contents": [],
}
@classmethod
def new_from_json(cls, data, **kwargs):
domains = cls.param_defaults.get("domains")
if 'domains' in data:
domains = [Domain.new_from_json(domain_data) for domain_data in data['domains']]
return super().new_from_json(data=data, domains=domains)

View File

@ -40,15 +40,34 @@ a:hover {
} }
#sidebar { #sidebar {
min-width: 250px; min-width: 280px;
max-width: 250px; max-width: 280px;
min-height: 100vh; min-height: 100vh;
display: flex;
flex-direction: column;
}
#sidebar #sidebar-services {
flex-grow: 1;
} }
#sidebar.active { #sidebar.active {
margin-left: -250px; margin-left: -250px;
} }
#sidebar .sidebar-branding {
padding-left: 2rem;
padding-right: 2rem;
}
#sidebar #sidebar-services {
padding-left: 1rem;
padding-right: 1rem;
}
#sidebar #user-profile-menu {
background:rgba(254, 251, 242, 0.25);
}
#sidebar ul.components { #sidebar ul.components {
padding: 20px 0; padding: 20px 0;
} }
@ -76,7 +95,7 @@ a:hover {
/** login **/ /** login **/
#body-login .jumbotron { #body-login .jumbotron {
background: #282532;/**#50466E;**/ background: #282532 no-repeat url("../images/logo-pangea-lilla-bg.svg") right;
} }
#login-content { #login-content {
@ -104,8 +123,10 @@ a:hover {
} }
#content { #content {
background: #ECECEB; background: #ECECEB no-repeat url("../images/logo-pangea-light-gray-bg.svg");
background-position: right 5% top 10%;
color: #343434; color: #343434;
padding-left: 2rem;
} }
/** services **/ /** services **/

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -11,7 +11,7 @@
<meta name="robots" content="NONE,NOARCHIVE" /> <meta name="robots" content="NONE,NOARCHIVE" />
{% endblock %} {% endblock %}
<title>{% block title %}{% if name %}{{ name }} {% endif %}Django musician{% endblock %}</title> <title>{% block title %}{% if title %}{{ title }} {% endif %}Django musician{% endblock %}</title>
{% block style %} {% block style %}
{% block bootstrap_theme %} {% block bootstrap_theme %}
@ -23,9 +23,6 @@
<link rel="stylesheet" href="{% static "musician/fontawesome/css/all.min.css" %}" /> <link rel="stylesheet" href="{% static "musician/fontawesome/css/all.min.css" %}" />
<link rel="stylesheet" type="text/css" href="{% static "musician/css/default.css" %}" /> <link rel="stylesheet" type="text/css" href="{% static "musician/css/default.css" %}" />
{% if code_style %}<style>
{{ code_style }}
</style>{% endif %}
{% endblock %} {% endblock %}
{% endblock %} {% endblock %}
@ -37,27 +34,27 @@
<div class="wrapper"> <div class="wrapper">
<nav id="sidebar" class="bg-primary border-right pt-4"> <nav id="sidebar" class="bg-primary border-right pt-4">
{% block sidebar %} {% block sidebar %}
{# <!-- branding --> #} <div class="sidebar-branding">
<img class="img-fluid" src="{% static 'musician/images/logo-pangea-monocrome-white.png' %}" <img class="img-fluid" src="{% static 'musician/images/logo-pangea-monocrome-white.png' %}"
alt="Pangea.org - Internet etic i solidari" /> alt="Pangea.org - Internet etic i solidari" />
<span class="text-light d-block text-right">{{ version }}</span> </div>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
{# <!-- services menu --> #} {# <!-- services menu --> #}
<ul id="sidebar-services" class="nav flex-column">
{% for item in services_menu %} {% for item in services_menu %}
<ul class="nav flex-column">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-light active" href="{% url item.pattern_name %}"> <a class="nav-link text-light active" href="{% url item.pattern_name %}">
<i class="fas fa-{{ item.icon }}"></i> <i class="fas fa-{{ item.icon }}"></i>
{{ item.title }} {{ item.title }}
</a> </a>
</li> </li>
</ul>
{% endfor %} {% endfor %}
{# <!-- user profile menu --> #} </ul>
<div class="dropdown-divider mt-5"></div>
<div class="dropdown dropright"> {# <!-- user profile menu --> #}
<button type="button" class="btn btn-primary nav-link text-light w-100" data-toggle="dropdown"> <div id="user-profile-menu" class="mt-5 pt-1 dropdown dropright">
<button type="button" class="btn 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>{{ profile.username }}</strong><br/> <strong>{{ profile.username }}</strong><br/>
<i class="fas fa-cog"></i> {% trans "Settings" %} <i class="fas fa-cog"></i> {% trans "Settings" %}
@ -68,8 +65,7 @@
</div> </div>
</div> </div>
<div class="dropdown-divider"></div> <div class="sidebar-logout">
<ul class="nav flex-column"> <ul class="nav flex-column">
<li class="nav-item text-right"> <li class="nav-item text-right">
<a class="nav-link text-light" href="{% url 'musician:logout' %}"> <a class="nav-link text-light" href="{% url 'musician:logout' %}">
@ -78,9 +74,11 @@
</a> </a>
</li> </li>
</ul> </ul>
</div>
</ul> <div class="mt-4 pr-3 pb-2 text-light d-block text-right">
<small>Panel Version {{ version }}</small>
</div>
{% endblock sidebar %} {% endblock sidebar %}
</nav><!-- ./sidebar --> </nav><!-- ./sidebar -->
<div id="content" class="container-fluid pt-4"> <div id="content" class="container-fluid pt-4">

View File

@ -4,7 +4,11 @@
{% block content %} {% block content %}
<h2>{% trans "Welcome back" %} <strong>{{ profile.username }}</strong></h2> <h2>{% trans "Welcome back" %} <strong>{{ profile.username }}</strong></h2>
<p>{% blocktrans with last_login=profile.last_login|default:"N/A" %}Last time you logged in was: {{ last_login }}{% endblocktrans %}</p> {% if profile.last_login %}
<p>{% blocktrans with last_login=profile.last_login|date:"SHORT_DATE_FORMAT" %}Last time you logged in was: {{ last_login }}{% endblocktrans %}</p>
{% else %}
<p>{% trans "It's the first time you log into the system, welcome on board!" %}</p>
{% endif %}
<div class="card-deck"> <div class="card-deck">
{% for resource, usage in resource_usage.items %} {% for resource, usage in resource_usage.items %}
@ -39,14 +43,20 @@
<strong>{{ domain.name }}</strong> <strong>{{ domain.name }}</strong>
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{% with domain.websites.0 as website %}
{% with website.contents.0 as content %}
<button type="button" class="btn text-secondary" data-toggle="modal" data-target="#configDetailsModal" <button type="button" class="btn text-secondary" data-toggle="modal" data-target="#configDetailsModal"
data-domain="{{ domain.name }}" data-username="john" data-password="s3cre3t" data-root="/domainname/" data-domain="{{ domain.name }}" data-website="{{ website|yesno:'true,false' }}" data-webapp-type="{{ content.webapp.type }}" data-root-path="{{ content.path }}"
data-url="{% url 'musician:domain-detail' domain.id %}"> data-url="{% url 'musician:domain-detail' domain.id %}">
{% trans "view configuration" %} <strong class="fas fa-tools"></strong> {% trans "view configuration" %} <strong class="fas fa-tools"></strong>
</button> </button>
{% endwith %}
{% endwith %}
</div> </div>
<div class="col-md text-right"> <div class="col-md text-right">
{% comment "@slamora: orchestra doesn't have this information [won't fix] See issue #2" %}
{% trans "Expiration date" %}: <strong>{{ domain.expiration_date|date:"SHORT_DATE_FORMAT" }}</strong> {% trans "Expiration date" %}: <strong>{{ domain.expiration_date|date:"SHORT_DATE_FORMAT" }}</strong>
{% endcomment %}
</div> </div>
</div> </div>
</div><!-- /card-header--> </div><!-- /card-header-->
@ -56,9 +66,9 @@
<p class="card-text"><i class="fas fa-envelope fa-3x"></i></p> <p class="card-text"><i class="fas fa-envelope fa-3x"></i></p>
<p class="card-text text-dark"> <p class="card-text text-dark">
{{ domain.mails|length }} {% trans "mail addresses created" %} {{ domain.mails|length }} {% trans "mail addresses created" %}
{% if domain.address_left.alert %} {% if domain.address_left.alert_level %}
<br/> <br/>
<span class="text-{{ domain.address_left.alert }}">{{ domain.address_left.count }} {% trans "mail address left" %}</span> <span class="text-{{ domain.address_left.alert_level }}">{{ domain.address_left.count }} {% trans "mail address left" %}</span>
{% endif %} {% endif %}
</p> </p>
<a class="stretched-link" href="{% url 'musician:mails' %}?domain={{ domain.id }}"></a> <a class="stretched-link" href="{% url 'musician:mails' %}?domain={{ domain.id }}"></a>
@ -68,26 +78,13 @@
<p class="card-text"><i class="fas fa-mail-bulk fa-3x"></i></p> <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> <a class="stretched-link" href="{% url 'musician:mailing-lists' %}?domain={{ domain.id }}"></a>
</div> </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"> <div class="col-md-2 border-right">
<h4>{% trans "Software as a Service" %}</h4> <h4>{% trans "Software as a Service" %}</h4>
<p class="card-text"><i class="fas fa-fire fa-3x"></i></p> <p class="card-text"><i class="fas fa-fire fa-3x"></i></p>
<p class="card-text text-dark">{% trans "Nothing installed" %}</p> <p class="card-text text-dark">{% trans "Nothing installed" %}</p>
<a class="stretched-link" href="{% url 'musician:saas' %}?domain={{ domain.id }}"></a> <a class="stretched-link" href="{% url 'musician:saas' %}?domain={{ domain.id }}"></a>
</div> </div>
<div class="col-md-1"></div>
<div class="col-md-4"> <div class="col-md-4">
<h4>{% trans "Disk usage" %}</h4> <h4>{% trans "Disk usage" %}</h4>
<p class="card-text"><i class="fas fa-hdd fa-3x"></i></p> <p class="card-text"><i class="fas fa-hdd fa-3x"></i></p>
@ -95,6 +92,7 @@
{% include "musician/components/usage_progress_bar.html" with detail=domain.usage %} {% include "musician/components/usage_progress_bar.html" with detail=domain.usage %}
</div> </div>
</div> </div>
<div class="col-md-1"></div>
</div> </div>
</div> </div>
@ -111,13 +109,22 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="domain-ftp pb-3 border-bottom">
<h6 class="pl-4 mb-4">{% trans "FTP access:" %}</h6> <h6 class="pl-4 mb-4">{% trans "FTP access:" %}</h6>
<div class=""> {# Translators: domain configuration detail modal #}
<p> <p>{% trans "Contact with the support team to get details concerning FTP access." %}</p>
{% comment %}
<!-- hidden until API provides FTP information -->
<label>{% trans "Username" %}:</label> <span id="config-username" class="font-weight-bold">username</span><br/> <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> <label>{% trans "Password:" %}</label> <span id="config-password" class="font-weight-bold">password</span>
</p> {% endcomment %}
<p class="border-top pt-3"><label>{% trans "Root directory:" %}</label> <span id="config-root" class="font-weight-bold">root directory</span></p> </div>
<div class="domain-website pt-4">
<div id="no-website"><h6 class="pl-4">{% trans "No website configured." %}</h6></div>
<div id="config-website">
<label>{% trans "Root directory:" %}</label> <span id="config-root-path" class="font-weight-bold">root directory</span>
<label>{% trans "Type:" %}</label><span id="config-webapp-type" class="font-weight-bold">type</span>
</div>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@ -135,10 +142,19 @@ $('#configDetailsModal').on('show.bs.modal', function (event) {
// Extract info from data-* attributes // Extract info from data-* attributes
modal.find('.modal-title').text(button.data('domain')); modal.find('.modal-title').text(button.data('domain'));
modal.find('.modal-body #config-username').text(button.data('username')); modal.find('.modal-body #config-webapp-type').text(button.data('webapp-type'));
modal.find('.modal-body #config-password').text(button.data('password')); modal.find('.modal-body #config-root-path').text(button.data('root-path'));
modal.find('.modal-body #config-root').text(button.data('root'));
modal.find('.modal-footer .btn').attr('href', button.data('url')); modal.find('.modal-footer .btn').attr('href', button.data('url'));
var nowebsite = modal.find('.modal-body #no-website');
var websitecfg = modal.find('.modal-body #config-website');
if(button.data('website')) {
nowebsite.hide();
websitecfg.show();
} else {
nowebsite.show();
websitecfg.hide();
}
}) })
</script> </script>
{% endblock %} {% endblock %}

View File

@ -17,7 +17,9 @@
{% trans "Type" %}: <strong>{{ database.type }}</strong> {% trans "Type" %}: <strong>{{ database.type }}</strong>
</div> </div>
<div class="col-md text-right"> <div class="col-md text-right">
{% comment "@slamora: orchestra doesn't provide this information [won't fix] See issue #3" %}
{% trans "associated to" %}: <strong>{{ database.domain|default:"-" }}</strong> {% trans "associated to" %}: <strong>{{ database.domain|default:"-" }}</strong>
{% endcomment %}
</div> </div>
</div> </div>
</div><!-- /card-header--> </div><!-- /card-header-->
@ -46,7 +48,21 @@
</div> </div>
</div> </div>
{% endfor %} {% empty %}
<div class="row">
<div class="col-md-4">
<div class="card service-card shadow p-3 mb-5 bg-white rounded">
<div class="card-body text-center">
<p class="mb-4"><i class="fas fa-database fa-5x"></i></p>
{# Translators: database page when there isn't any database. #}
<h5 class="card-title text-dark">{% trans "Ooops! Looks like there is nothing here!" %}</h5>
</div>
</div>
</div>
</div>
{% endfor %}
{% if object_list|length > 0 %}
{% include "musician/components/paginator.html" %} {% include "musician/components/paginator.html" %}
{% endif %}
{% endblock %} {% endblock %}

View File

@ -30,7 +30,11 @@
{% for resource in object_list %} {% for resource in object_list %}
<tr> <tr>
<th scope="row">{{ resource.name }}</th> <th scope="row">{{ resource.name }}</th>
<td class="text-primary font-weight-bold">{{ resource.status|capfirst }}</td> {% if resource.is_active %}
<td class="text-primary font-weight-bold">{% trans "Active" %}</td>
{% else %}
<td class="text-danger font-weight-bold">{% trans "Inactive" %}</td>
{% endif %}
<td>{{ resource.address_name}}</td> <td>{{ resource.address_name}}</td>
<td>{{ resource.admin_email }}</td> <td>{{ resource.admin_email }}</td>
<td><a href="#TODO-{{ resource.manager_url }}" target="_blank" rel="noopener noreferrer">Mailtrain</a></td> <td><a href="#TODO-{{ resource.manager_url }}" target="_blank" rel="noopener noreferrer">Mailtrain</a></td>

View File

@ -55,7 +55,9 @@
Details: {{ payment.data }} Details: {{ payment.data }}
{% endif %} {% endif %}
</div> </div>
<div class="text-right">
<a href="{% url 'musician:billing' %}">{% trans "Check your last bills" %}</a>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -13,9 +13,11 @@
<div class="col-md-8"> <div class="col-md-8">
<strong>{{ saas.name }}</strong> <strong>{{ saas.name }}</strong>
</div> </div>
{% comment "Hidden until API provides this information" %}
<div class="col-md text-right"> <div class="col-md text-right">
{% trans "Installed on" %}: <strong>{{ saas.domain|default:"-" }}</strong> {% trans "Installed on" %}: <strong>{{ saas.domain|default:"-" }}</strong>
</div> </div>
{% endcomment %}
</div> </div>
</div><!-- /card-header--> </div><!-- /card-header-->
<div class="card-body row"> <div class="card-body row">
@ -24,12 +26,11 @@
<p class="text-center service-brand"><i class="fab fa-{{ saas.service }} fa-10x"></i></p> <p class="text-center service-brand"><i class="fab fa-{{ saas.service }} fa-10x"></i></p>
</div> </div>
<div class="col-md-3 border-left border-right"> <div class="col-md-3 border-left border-right">
<h4>{% trans "Service info" %}</h4> <h4 class="mb-3">{% trans "Service info" %}</h4>
<p>{% trans "Active" %}: {{ saas.is_active|yesno }}</p> <label class="w-25">{% trans "active" %}:</label> <strong>{{ saas.is_active|yesno }}</strong><br/>
{# TODO (@slamora): implement saas details #} {% for key, value in saas.data.items %}
<pre> <label class="w-25">{{ key }}:</label> <strong>{{ value }}</strong><br/>
{{ saas.data }} {% endfor %}
</pre>
</div> </div>
<div class="col-md-5 text-right"> <div class="col-md-5 text-right">
<div class="service-manager-link"> <div class="service-manager-link">
@ -38,6 +39,18 @@
</div> </div>
</div> </div>
</div> </div>
{% empty %}
<div class="row">
<div class="col-md-4">
<div class="card service-card shadow p-3 mb-5 bg-white rounded">
<div class="card-body text-center">
<p class="mb-4"><i class="fas fa-fire fa-5x"></i></p>
{# Translators: saas page when there isn't any saas. #}
<h5 class="card-title text-dark">{% trans "Ooops! Looks like there is nothing here!" %}</h5>
</div>
</div>
</div>
</div>
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}

View File

@ -1,5 +1,7 @@
from django.test import TestCase from django.test import TestCase
from .models import UserAccount
class DomainsTestCase(TestCase): class DomainsTestCase(TestCase):
def test_domain_not_found(self): def test_domain_not_found(self):
@ -12,3 +14,26 @@ class DomainsTestCase(TestCase):
response = self.client.get('/domains/3/') response = self.client.get('/domains/3/')
self.assertEqual(404, response.status_code) self.assertEqual(404, response.status_code)
class UserAccountTest(TestCase):
def test_user_never_logged(self):
data = {
'billcontact': {'address': 'foo',
'city': 'Barcelona',
'country': 'ES',
'name': '',
'vat': '12345678Z',
'zipcode': '08080'},
'date_joined': '2020-01-14T12:38:31.684495Z',
'full_name': 'Pep',
'id': 2,
'is_active': True,
'language': 'EN',
'short_name': '',
'type': 'INDIVIDUAL',
'url': 'http://example.org/api/accounts/2/',
'username': 'pepe'
}
account = UserAccount.new_from_json(data)
self.assertIsNone(account.last_login)

View File

@ -25,6 +25,10 @@ from .settings import ALLOWED_RESOURCES
class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
template_name = "musician/dashboard.html" template_name = "musician/dashboard.html"
extra_context = {
# Translators: This message appears on the page title
'title': _('Dashboard'),
}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -62,16 +66,16 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
# TODO(@slamora): validate concept of limits with Pangea # TODO(@slamora): validate concept of limits with Pangea
profile_type = context['profile'].type profile_type = context['profile'].type
for domain in domains: for domain in domains:
address_left = ALLOWED_RESOURCES[profile_type]['mailbox'] - len(domain.mails) addresses_left = ALLOWED_RESOURCES[profile_type]['mailbox'] - len(domain.mails)
alert = None alert_level = None
if address_left == 1: if addresses_left == 1:
alert = 'warning' alert_level = 'warning'
elif address_left < 1: elif addresses_left < 1:
alert = 'danger' alert_level = 'danger'
domain.address_left = { domain.addresses_left = {
'count': address_left, 'count': addresses_left,
'alert': alert, 'alert_level': alert_level,
} }
context.update({ context.update({
@ -85,6 +89,10 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
class ProfileView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): class ProfileView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
template_name = "musician/profile.html" template_name = "musician/profile.html"
extra_context = {
# Translators: This message appears on the page title
'title': _('User profile'),
}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -103,7 +111,7 @@ class ProfileView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
class ServiceListView(CustomContextMixin, ExtendedPaginationMixin, UserTokenRequiredMixin, ListView): class ServiceListView(CustomContextMixin, ExtendedPaginationMixin, UserTokenRequiredMixin, ListView):
"""Base list view to all services""" """Base list view to all services"""
service_class = None service_class = None
template_name = "musician/service_list.html" # TODO move to ServiceListView template_name = "musician/service_list.html"
def get_queryset(self): def get_queryset(self):
if self.service_class is None or self.service_class.api_name is None: if self.service_class is None or self.service_class.api_name is None:
@ -132,9 +140,18 @@ class ServiceListView(CustomContextMixin, ExtendedPaginationMixin, UserTokenRequ
class BillingView(ServiceListView): class BillingView(ServiceListView):
service_class = Bill service_class = Bill
template_name = "musician/billing.html" template_name = "musician/billing.html"
extra_context = {
# Translators: This message appears on the page title
'title': _('Billing'),
}
class BillDownloadView(CustomContextMixin, UserTokenRequiredMixin, View): class BillDownloadView(CustomContextMixin, UserTokenRequiredMixin, View):
extra_context = {
# Translators: This message appears on the page title
'title': _('Download bill'),
}
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
pk = self.kwargs.get('pk') pk = self.kwargs.get('pk')
bill = self.orchestra.retrieve_bill_document(pk) bill = self.orchestra.retrieve_bill_document(pk)
@ -145,6 +162,10 @@ class BillDownloadView(CustomContextMixin, UserTokenRequiredMixin, View):
class MailView(ServiceListView): class MailView(ServiceListView):
service_class = MailService service_class = MailService
template_name = "musician/mail.html" template_name = "musician/mail.html"
extra_context = {
# Translators: This message appears on the page title
'title': _('Mail addresses'),
}
def get_queryset(self): def get_queryset(self):
def retrieve_mailbox(value): def retrieve_mailbox(value):
@ -198,6 +219,10 @@ class MailView(ServiceListView):
class MailingListsView(ServiceListView): class MailingListsView(ServiceListView):
service_class = MailinglistService service_class = MailinglistService
template_name = "musician/mailinglists.html" template_name = "musician/mailinglists.html"
extra_context = {
# Translators: This message appears on the page title
'title': _('Mailing lists'),
}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -223,15 +248,27 @@ class MailingListsView(ServiceListView):
class DatabasesView(ServiceListView): class DatabasesView(ServiceListView):
template_name = "musician/databases.html" template_name = "musician/databases.html"
service_class = DatabaseService service_class = DatabaseService
extra_context = {
# Translators: This message appears on the page title
'title': _('Databases'),
}
class SaasView(ServiceListView): class SaasView(ServiceListView):
service_class = SaasService service_class = SaasService
template_name = "musician/saas.html" template_name = "musician/saas.html"
extra_context = {
# Translators: This message appears on the page title
'title': _('Software as a Service'),
}
class DomainDetailView(CustomContextMixin, UserTokenRequiredMixin, DetailView): class DomainDetailView(CustomContextMixin, UserTokenRequiredMixin, DetailView):
template_name = "musician/domain_detail.html" template_name = "musician/domain_detail.html"
extra_context = {
# Translators: This message appears on the page title
'title': _('Domain details'),
}
def get_queryset(self): def get_queryset(self):
# Return an empty list to avoid a request to retrieve all the # Return an empty list to avoid a request to retrieve all the
@ -254,7 +291,11 @@ class LoginView(FormView):
form_class = LoginForm form_class = LoginForm
success_url = reverse_lazy('musician:dashboard') success_url = reverse_lazy('musician:dashboard')
redirect_field_name = 'next' redirect_field_name = 'next'
extra_context = {'version': get_version()} extra_context = {
# Translators: This message appears on the page title
'title': _('Login'),
'version': get_version(),
}
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()

View File

@ -18,11 +18,13 @@ from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.views.generic.base import RedirectView
import musician import musician
urlpatterns = [ urlpatterns = [
path('', include('musician.urls')), path('', include('musician.urls')),
path('', RedirectView.as_view(pattern_name='musician:dashboard', permanent=False), name='root_index')
] ]
DEVELOPMENT = config('DEVELOPMENT', default=False, cast=bool) DEVELOPMENT = config('DEVELOPMENT', default=False, cast=bool)