Merge branch '3-revisio-v-0-1b1'

This commit is contained in:
Santiago Lamora 2020-01-21 13:28:10 +01:00
commit 59f80ecf69
10 changed files with 196 additions and 63 deletions

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

@ -107,6 +107,7 @@ class UserAccount(OrchestraModel):
@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'])
@ -161,6 +162,7 @@ class Domain(OrchestraModel):
"records": [], "records": [],
"mails": [], "mails": [],
"usage": {}, "usage": {},
"websites": [],
} }
@classmethod @classmethod
@ -262,3 +264,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;
} }
@ -107,6 +126,7 @@ a:hover {
background: #ECECEB no-repeat url("../images/logo-pangea-light-gray-bg.svg"); background: #ECECEB no-repeat url("../images/logo-pangea-light-gray-bg.svg");
background-position: right 5% top 10%; background-position: right 5% top 10%;
color: #343434; color: #343434;
padding-left: 2rem;
} }
/** services **/ /** services **/

View File

@ -34,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 --> #}
{% for item in services_menu %} <ul id="sidebar-services" class="nav flex-column">
<ul class="nav flex-column"> {% for item in services_menu %}
<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>
{% endfor %}
</ul> </ul>
{% endfor %}
{# <!-- user profile menu --> #}
<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> Settings <i class="fas fa-cog"></i> Settings
@ -65,9 +65,8 @@
</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' %}">
{% trans 'Log out' %} {% trans 'Log out' %}
@ -75,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>
{% 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> <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,11 +43,15 @@
<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" %} {% comment "@slamora: orchestra doesn't have this information [won't fix] See issue #2" %}
@ -58,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 }} mail address left</span> <span class="text-{{ domain.address_left.alert_level }}">{{ domain.address_left.count }} 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>
@ -70,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">Nothing installed</p> <p class="card-text text-dark">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>
@ -97,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>
@ -113,13 +109,22 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<h6 class="pl-4 mb-4">{% trans "FTP access:" %}</h6> <div class="domain-ftp pb-3 border-bottom">
<div class=""> <h6 class="pl-4 mb-4">{% trans "FTP access:" %}</h6>
<p> {# Translators: domain configuration detail modal #}
<label>{% trans "Username" %}:</label> <span id="config-username" class="font-weight-bold">username</span><br/> <p>{% trans "Contact with the support team to get details concerning FTP access." %}</p>
<label>{% trans "Password:" %}</label> <span id="config-password" class="font-weight-bold">password</span> {% comment %}
</p> <!-- hidden until API provides FTP information -->
<p class="border-top pt-3"><label>Root directory:</label> <span id="config-root" class="font-weight-bold">root directory</span></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>
{% endcomment %}
</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>Root directory:</label> <span id="config-root-path" class="font-weight-bold">root directory</span>
<label>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">
@ -137,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

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

@ -66,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({