Merge branch 'master' into i18n
This commit is contained in:
commit
4727a931b4
|
@ -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.
|
|
@ -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():
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 |
|
@ -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">
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue