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.
"""
VERSION = (0, 1, 0, 'beta', 1)
VERSION = (0, 1, 0, 'beta', 2)
def get_version():

View File

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

View File

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

View File

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

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

View File

@ -4,7 +4,11 @@
{% block content %}
<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">
{% for resource, usage in resource_usage.items %}
@ -39,14 +43,20 @@
<strong>{{ domain.name }}</strong>
</div>
<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"
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 %}">
{% trans "view configuration" %} <strong class="fas fa-tools"></strong>
</button>
{% endwith %}
{% endwith %}
</div>
<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>
{% endcomment %}
</div>
</div>
</div><!-- /card-header-->
@ -56,9 +66,9 @@
<p class="card-text"><i class="fas fa-envelope fa-3x"></i></p>
<p class="card-text text-dark">
{{ domain.mails|length }} {% trans "mail addresses created" %}
{% if domain.address_left.alert %}
{% if domain.address_left.alert_level %}
<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 %}
</p>
<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>
<a class="stretched-link" href="{% url 'musician:mailing-lists' %}?domain={{ domain.id }}"></a>
</div>
<div class="col-md-2 border-right">
<h4>{% trans "Databases" %}</h4>
<p class="card-text"><i class="fas fa-database fa-3x"></i></p>
<p class="card-text text-dark">
0 {% trans "databases created" %}
{% comment %}
<!-- TODO databases related to a domain and resource usage
{{ domain.databases|length }} {% trans "databases created" %}<br/>
20 MB of 45MB
-->
{% endcomment %}
</p>
<a class="stretched-link" href="{% url 'musician:databases' %}?domain={{ domain.id }}"></a>
</div>
<div class="col-md-2 border-right">
<h4>{% trans "Software as a Service" %}</h4>
<p class="card-text"><i class="fas fa-fire fa-3x"></i></p>
<p class="card-text text-dark">{% trans "Nothing installed" %}</p>
<a class="stretched-link" href="{% url 'musician:saas' %}?domain={{ domain.id }}"></a>
</div>
<div class="col-md-1"></div>
<div class="col-md-4">
<h4>{% trans "Disk usage" %}</h4>
<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 %}
</div>
</div>
<div class="col-md-1"></div>
</div>
</div>
@ -111,13 +109,22 @@
</button>
</div>
<div class="modal-body">
<div class="domain-ftp pb-3 border-bottom">
<h6 class="pl-4 mb-4">{% trans "FTP access:" %}</h6>
<div class="">
<p>
{# Translators: domain configuration detail modal #}
<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 "Password:" %}</label> <span id="config-password" class="font-weight-bold">password</span>
</p>
<p class="border-top pt-3"><label>{% trans "Root directory:" %}</label> <span id="config-root" class="font-weight-bold">root directory</span></p>
{% 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>{% 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 class="modal-footer">
@ -135,10 +142,19 @@ $('#configDetailsModal').on('show.bs.modal', function (event) {
// Extract info from data-* attributes
modal.find('.modal-title').text(button.data('domain'));
modal.find('.modal-body #config-username').text(button.data('username'));
modal.find('.modal-body #config-password').text(button.data('password'));
modal.find('.modal-body #config-root').text(button.data('root'));
modal.find('.modal-body #config-webapp-type').text(button.data('webapp-type'));
modal.find('.modal-body #config-root-path').text(button.data('root-path'));
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>
{% endblock %}

View File

@ -17,7 +17,9 @@
{% trans "Type" %}: <strong>{{ database.type }}</strong>
</div>
<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>
{% endcomment %}
</div>
</div>
</div><!-- /card-header-->
@ -46,7 +48,21 @@
</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-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" %}
{% endif %}
{% endblock %}

View File

@ -30,7 +30,11 @@
{% for resource in object_list %}
<tr>
<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.admin_email }}</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 }}
{% endif %}
</div>
<div class="text-right">
<a href="{% url 'musician:billing' %}">{% trans "Check your last bills" %}</a>
</div>
</div>
</div>
</div>

View File

@ -13,9 +13,11 @@
<div class="col-md-8">
<strong>{{ saas.name }}</strong>
</div>
{% comment "Hidden until API provides this information" %}
<div class="col-md text-right">
{% trans "Installed on" %}: <strong>{{ saas.domain|default:"-" }}</strong>
</div>
{% endcomment %}
</div>
</div><!-- /card-header-->
<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>
</div>
<div class="col-md-3 border-left border-right">
<h4>{% trans "Service info" %}</h4>
<p>{% trans "Active" %}: {{ saas.is_active|yesno }}</p>
{# TODO (@slamora): implement saas details #}
<pre>
{{ saas.data }}
</pre>
<h4 class="mb-3">{% trans "Service info" %}</h4>
<label class="w-25">{% trans "active" %}:</label> <strong>{{ saas.is_active|yesno }}</strong><br/>
{% for key, value in saas.data.items %}
<label class="w-25">{{ key }}:</label> <strong>{{ value }}</strong><br/>
{% endfor %}
</div>
<div class="col-md-5 text-right">
<div class="service-manager-link">
@ -38,6 +39,18 @@
</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 %}
{% endblock %}

View File

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

View File

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

View File

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