diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b87a1a4 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/musician/__init__.py b/musician/__init__.py index 4d761dc..d97a598 100644 --- a/musician/__init__.py +++ b/musician/__init__.py @@ -2,7 +2,7 @@ Package metadata definition. """ -VERSION = (0, 1, 0, 'beta', 1) +VERSION = (0, 1, 0, 'beta', 2) def get_version(): diff --git a/musician/api.py b/musician/api.py index 8b4139c..bbf77a5 100644 --- a/musician/api.py +++ b/musician/api.py @@ -6,7 +6,7 @@ from django.http import Http404 from django.urls.exceptions import NoReverseMatch 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/' @@ -25,6 +25,7 @@ API_PATHS = { 'mailbox-list': 'mailboxes/', 'mailinglist-list': 'lists/', 'saas-list': 'saas/', + 'website-list': 'websites/', # other 'bill-list': 'bills/', @@ -118,6 +119,8 @@ class Orchestra(object): def retrieve_domain_list(self): output = self.retrieve_service_list(Domain.api_name) + websites = self.retrieve_website_list() + domains = [] for domain_json in output: # filter querystring @@ -126,6 +129,10 @@ class Orchestra(object): # retrieve services associated to a domain domain_json['mails'] = self.retrieve_service_list( 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 # domain_json['databases'] = self.retrieve_service_list(DatabaseService.api_name, querystring) # domain_json['saas'] = self.retrieve_service_list(SaasService.api_name, querystring) @@ -143,6 +150,19 @@ class Orchestra(object): 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): """ Returns: diff --git a/musician/models.py b/musician/models.py index 039128c..de43a1a 100644 --- a/musician/models.py +++ b/musician/models.py @@ -1,6 +1,7 @@ import ast import logging +from django.utils.dateparse import parse_datetime from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ @@ -100,15 +101,20 @@ class UserAccount(OrchestraModel): 'short_name': None, 'full_name': None, 'billing': {}, + 'last_login': None, } @classmethod def new_from_json(cls, data, **kwargs): billing = None + last_login = None if 'billcontact' in data: 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): @@ -157,6 +163,7 @@ class Domain(OrchestraModel): "records": [], "mails": [], "usage": {}, + "websites": [], } @classmethod @@ -230,6 +237,7 @@ class MailinglistService(OrchestraModel): fields = ('name', 'status', 'address_name', 'admin_email', 'configure') param_defaults = { 'name': None, + 'is_active': True, 'admin_email': None, } @@ -237,11 +245,6 @@ class MailinglistService(OrchestraModel): self.data = kwargs super().__init__(**kwargs) - @property - def status(self): - # TODO(@slamora): where retrieve if the list is active? - return 'active' - @property def address_name(self): return "{}@{}".format(self.data['address_name'], self.data['address_domain']['name']) @@ -262,3 +265,23 @@ class SaasService(OrchestraModel): 'is_active': True, '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) diff --git a/musician/static/musician/css/default.css b/musician/static/musician/css/default.css index a9d7da4..a95a573 100644 --- a/musician/static/musician/css/default.css +++ b/musician/static/musician/css/default.css @@ -40,15 +40,34 @@ a:hover { } #sidebar { - min-width: 250px; - max-width: 250px; + min-width: 280px; + max-width: 280px; min-height: 100vh; + + display: flex; + flex-direction: column; +} +#sidebar #sidebar-services { + flex-grow: 1; } #sidebar.active { 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 { padding: 20px 0; } @@ -76,7 +95,7 @@ a:hover { /** login **/ #body-login .jumbotron { - background: #282532;/**#50466E;**/ + background: #282532 no-repeat url("../images/logo-pangea-lilla-bg.svg") right; } #login-content { @@ -104,8 +123,10 @@ a:hover { } #content { - background: #ECECEB; + background: #ECECEB no-repeat url("../images/logo-pangea-light-gray-bg.svg"); + background-position: right 5% top 10%; color: #343434; + padding-left: 2rem; } /** services **/ diff --git a/musician/static/musician/images/logo-pangea-light-gray-bg.svg b/musician/static/musician/images/logo-pangea-light-gray-bg.svg new file mode 100644 index 0000000..c4fe411 --- /dev/null +++ b/musician/static/musician/images/logo-pangea-light-gray-bg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/musician/static/musician/images/logo-pangea-lilla-bg.svg b/musician/static/musician/images/logo-pangea-lilla-bg.svg new file mode 100644 index 0000000..3db2325 --- /dev/null +++ b/musician/static/musician/images/logo-pangea-lilla-bg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/musician/templates/musician/base.html b/musician/templates/musician/base.html index 3782089..27a1eb8 100644 --- a/musician/templates/musician/base.html +++ b/musician/templates/musician/base.html @@ -11,7 +11,7 @@ {% endblock %} - {% block title %}{% if name %}{{ name }} – {% endif %}Django musician{% endblock %} + {% block title %}{% if title %}{{ title }} – {% endif %}Django musician{% endblock %} {% block style %} {% block bootstrap_theme %} @@ -23,9 +23,6 @@ - {% if code_style %}{% endif %} {% endblock %} {% endblock %} @@ -37,27 +34,27 @@
- - - + +
+ Panel Version {{ version }} +
{% endblock sidebar %}
diff --git a/musician/templates/musician/dashboard.html b/musician/templates/musician/dashboard.html index dd23697..2e9c4d0 100644 --- a/musician/templates/musician/dashboard.html +++ b/musician/templates/musician/dashboard.html @@ -4,7 +4,11 @@ {% block content %}

{% trans "Welcome back" %} {{ profile.username }}

-

{% blocktrans with last_login=profile.last_login|default:"N/A" %}Last time you logged in was: {{ last_login }}{% endblocktrans %}

+{% if profile.last_login %} +

{% blocktrans with last_login=profile.last_login|date:"SHORT_DATE_FORMAT" %}Last time you logged in was: {{ last_login }}{% endblocktrans %}

+{% else %} +

{% trans "It's the first time you log into the system, welcome on board!" %}

+{% endif %}
{% for resource, usage in resource_usage.items %} @@ -39,14 +43,20 @@ {{ domain.name }}
+ {% with domain.websites.0 as website %} + {% with website.contents.0 as content %} + {% endwith %} + {% endwith %}
+ {% comment "@slamora: orchestra doesn't have this information [won't fix] See issue #2" %} {% trans "Expiration date" %}: {{ domain.expiration_date|date:"SHORT_DATE_FORMAT" }} + {% endcomment %}
@@ -56,9 +66,9 @@

{{ domain.mails|length }} {% trans "mail addresses created" %} - {% if domain.address_left.alert %} + {% if domain.address_left.alert_level %}
- {{ domain.address_left.count }} {% trans "mail address left" %} + {{ domain.address_left.count }} {% trans "mail address left" %} {% endif %}

@@ -68,26 +78,13 @@

-
-

{% trans "Databases" %}

-

-

- 0 {% trans "databases created" %} - {% comment %} - - {% endcomment %} -

- -

{% trans "Software as a Service" %}

{% trans "Nothing installed" %}

+

{% trans "Disk usage" %}

@@ -95,6 +92,7 @@ {% include "musician/components/usage_progress_bar.html" with detail=domain.usage %}
+
@@ -111,13 +109,22 @@ @@ -46,7 +48,21 @@ - {% endfor %} +{% empty %} +
+
+
+
+

+ {# Translators: database page when there isn't any database. #} +
{% trans "Ooops! Looks like there is nothing here!" %}
+
+
+
+
+{% endfor %} + {% if object_list|length > 0 %} {% include "musician/components/paginator.html" %} + {% endif %} {% endblock %} diff --git a/musician/templates/musician/mailinglists.html b/musician/templates/musician/mailinglists.html index a5f0824..22a1b87 100644 --- a/musician/templates/musician/mailinglists.html +++ b/musician/templates/musician/mailinglists.html @@ -30,7 +30,11 @@ {% for resource in object_list %} {{ resource.name }} - {{ resource.status|capfirst }} + {% if resource.is_active %} + {% trans "Active" %} + {% else %} + {% trans "Inactive" %} + {% endif %} {{ resource.address_name}} {{ resource.admin_email }} Mailtrain diff --git a/musician/templates/musician/profile.html b/musician/templates/musician/profile.html index 3ecabe5..45aad9d 100644 --- a/musician/templates/musician/profile.html +++ b/musician/templates/musician/profile.html @@ -55,7 +55,9 @@ Details: {{ payment.data }} {% endif %} - +
+ {% trans "Check your last bills" %} +
diff --git a/musician/templates/musician/saas.html b/musician/templates/musician/saas.html index eaad20a..cff1e83 100644 --- a/musician/templates/musician/saas.html +++ b/musician/templates/musician/saas.html @@ -13,9 +13,11 @@
{{ saas.name }}
+ {% comment "Hidden until API provides this information" %}
{% trans "Installed on" %}: {{ saas.domain|default:"-" }}
+ {% endcomment %}
@@ -24,12 +26,11 @@

-

{% trans "Service info" %}

-

{% trans "Active" %}: {{ saas.is_active|yesno }}

- {# TODO (@slamora): implement saas details #} -
-                {{ saas.data }}
-            
+

{% trans "Service info" %}

+ {{ saas.is_active|yesno }}
+ {% for key, value in saas.data.items %} + {{ value }}
+ {% endfor %}
+ {% empty %} +
+
+
+
+

+ {# Translators: saas page when there isn't any saas. #} +
{% trans "Ooops! Looks like there is nothing here!" %}
+
+
+
+
{% endfor %} {% endblock %} diff --git a/musician/tests.py b/musician/tests.py index b25e23a..805ff05 100644 --- a/musician/tests.py +++ b/musician/tests.py @@ -1,5 +1,7 @@ from django.test import TestCase +from .models import UserAccount + class DomainsTestCase(TestCase): def test_domain_not_found(self): @@ -12,3 +14,26 @@ class DomainsTestCase(TestCase): response = self.client.get('/domains/3/') 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) diff --git a/musician/views.py b/musician/views.py index 7a8a2d1..e96fa24 100644 --- a/musician/views.py +++ b/musician/views.py @@ -25,6 +25,10 @@ from .settings import ALLOWED_RESOURCES class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): template_name = "musician/dashboard.html" + extra_context = { + # Translators: This message appears on the page title + 'title': _('Dashboard'), + } def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -62,16 +66,16 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): # TODO(@slamora): validate concept of limits with Pangea profile_type = context['profile'].type for domain in domains: - address_left = ALLOWED_RESOURCES[profile_type]['mailbox'] - len(domain.mails) - alert = None - if address_left == 1: - alert = 'warning' - elif address_left < 1: - alert = 'danger' + addresses_left = ALLOWED_RESOURCES[profile_type]['mailbox'] - len(domain.mails) + alert_level = None + if addresses_left == 1: + alert_level = 'warning' + elif addresses_left < 1: + alert_level = 'danger' - domain.address_left = { - 'count': address_left, - 'alert': alert, + domain.addresses_left = { + 'count': addresses_left, + 'alert_level': alert_level, } context.update({ @@ -85,6 +89,10 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): class ProfileView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): template_name = "musician/profile.html" + extra_context = { + # Translators: This message appears on the page title + 'title': _('User profile'), + } def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -103,7 +111,7 @@ class ProfileView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): class ServiceListView(CustomContextMixin, ExtendedPaginationMixin, UserTokenRequiredMixin, ListView): """Base list view to all services""" service_class = None - template_name = "musician/service_list.html" # TODO move to ServiceListView + template_name = "musician/service_list.html" def get_queryset(self): 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): service_class = Bill template_name = "musician/billing.html" + extra_context = { + # Translators: This message appears on the page title + 'title': _('Billing'), + } class BillDownloadView(CustomContextMixin, UserTokenRequiredMixin, View): + extra_context = { + # Translators: This message appears on the page title + 'title': _('Download bill'), + } + def get(self, request, *args, **kwargs): pk = self.kwargs.get('pk') bill = self.orchestra.retrieve_bill_document(pk) @@ -145,6 +162,10 @@ class BillDownloadView(CustomContextMixin, UserTokenRequiredMixin, View): class MailView(ServiceListView): service_class = MailService template_name = "musician/mail.html" + extra_context = { + # Translators: This message appears on the page title + 'title': _('Mail addresses'), + } def get_queryset(self): def retrieve_mailbox(value): @@ -198,6 +219,10 @@ class MailView(ServiceListView): class MailingListsView(ServiceListView): service_class = MailinglistService template_name = "musician/mailinglists.html" + extra_context = { + # Translators: This message appears on the page title + 'title': _('Mailing lists'), + } def get_context_data(self, **kwargs): @@ -223,15 +248,27 @@ class MailingListsView(ServiceListView): class DatabasesView(ServiceListView): template_name = "musician/databases.html" service_class = DatabaseService + extra_context = { + # Translators: This message appears on the page title + 'title': _('Databases'), + } class SaasView(ServiceListView): service_class = SaasService 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): template_name = "musician/domain_detail.html" + extra_context = { + # Translators: This message appears on the page title + 'title': _('Domain details'), + } def get_queryset(self): # Return an empty list to avoid a request to retrieve all the @@ -254,7 +291,11 @@ class LoginView(FormView): form_class = LoginForm success_url = reverse_lazy('musician:dashboard') 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): kwargs = super().get_form_kwargs() diff --git a/userpanel/urls.py b/userpanel/urls.py index 1f0caec..84d4ec9 100644 --- a/userpanel/urls.py +++ b/userpanel/urls.py @@ -18,11 +18,13 @@ from django.conf import settings from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path +from django.views.generic.base import RedirectView import musician urlpatterns = [ path('', include('musician.urls')), + path('', RedirectView.as_view(pattern_name='musician:dashboard', permanent=False), name='root_index') ] DEVELOPMENT = config('DEVELOPMENT', default=False, cast=bool)