Merge pull request #8 from ribaguifi/dev/api-writable
Write operations for addresses and mailboxes
This commit is contained in:
commit
44f9390bee
135
musician/api.py
135
musician/api.py
|
@ -1,14 +1,12 @@
|
||||||
import requests
|
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from itertools import groupby
|
import requests
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import Http404
|
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, WebSite
|
from .models import Address, DatabaseService, Domain, Mailbox, SaasService, UserAccount, WebSite
|
||||||
|
|
||||||
|
|
||||||
DOMAINS_PATH = 'domains/'
|
DOMAINS_PATH = 'domains/'
|
||||||
TOKEN_PATH = '/api-token-auth/'
|
TOKEN_PATH = '/api-token-auth/'
|
||||||
|
@ -23,7 +21,10 @@ API_PATHS = {
|
||||||
'domain-list': 'domains/',
|
'domain-list': 'domains/',
|
||||||
'domain-detail': 'domains/{pk}/',
|
'domain-detail': 'domains/{pk}/',
|
||||||
'address-list': 'addresses/',
|
'address-list': 'addresses/',
|
||||||
|
'address-detail': 'addresses/{pk}/',
|
||||||
'mailbox-list': 'mailboxes/',
|
'mailbox-list': 'mailboxes/',
|
||||||
|
'mailbox-detail': 'mailboxes/{pk}/',
|
||||||
|
'mailbox-password': 'mailboxes/{pk}/set_password/',
|
||||||
'mailinglist-list': 'lists/',
|
'mailinglist-list': 'lists/',
|
||||||
'saas-list': 'saas/',
|
'saas-list': 'saas/',
|
||||||
'website-list': 'websites/',
|
'website-list': 'websites/',
|
||||||
|
@ -62,7 +63,7 @@ class Orchestra(object):
|
||||||
|
|
||||||
return response.json().get("token", None)
|
return response.json().get("token", None)
|
||||||
|
|
||||||
def request(self, verb, resource=None, url=None, render_as="json", querystring=None, raise_exception=True):
|
def request(self, verb, resource=None, url=None, data=None, render_as="json", querystring=None, raise_exception=True):
|
||||||
assert verb in ["HEAD", "GET", "POST", "PATCH", "PUT", "DELETE"]
|
assert verb in ["HEAD", "GET", "POST", "PATCH", "PUT", "DELETE"]
|
||||||
if resource is not None:
|
if resource is not None:
|
||||||
url = self.build_absolute_uri(resource)
|
url = self.build_absolute_uri(resource)
|
||||||
|
@ -73,14 +74,17 @@ class Orchestra(object):
|
||||||
url = "{}?{}".format(url, querystring)
|
url = "{}?{}".format(url, querystring)
|
||||||
|
|
||||||
verb = getattr(self.session, verb.lower())
|
verb = getattr(self.session, verb.lower())
|
||||||
response = verb(url, headers={"Authorization": "Token {}".format(
|
headers = {
|
||||||
self.auth_token)}, allow_redirects=False)
|
"Authorization": "Token {}".format(self.auth_token),
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
response = verb(url, json=data, headers=headers, allow_redirects=False)
|
||||||
|
|
||||||
if raise_exception:
|
if raise_exception:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
status = response.status_code
|
status = response.status_code
|
||||||
if render_as == "json":
|
if status < 500 and render_as == "json":
|
||||||
output = response.json()
|
output = response.json()
|
||||||
else:
|
else:
|
||||||
output = response.content
|
output = response.content
|
||||||
|
@ -109,52 +113,95 @@ class Orchestra(object):
|
||||||
raise Http404(_("No domain found matching the query"))
|
raise Http404(_("No domain found matching the query"))
|
||||||
return bill_pdf
|
return bill_pdf
|
||||||
|
|
||||||
|
def create_mail_address(self, data):
|
||||||
|
resource = '{}-list'.format(Address.api_name)
|
||||||
|
return self.request("POST", resource=resource, data=data)
|
||||||
|
|
||||||
|
def retrieve_mail_address(self, pk):
|
||||||
|
path = API_PATHS.get('address-detail').format_map({'pk': pk})
|
||||||
|
url = urllib.parse.urljoin(self.base_url, path)
|
||||||
|
status, data = self.request("GET", url=url, raise_exception=False)
|
||||||
|
if status == 404:
|
||||||
|
raise Http404(_("No object found matching the query"))
|
||||||
|
|
||||||
|
return Address.new_from_json(data)
|
||||||
|
|
||||||
|
def update_mail_address(self, pk, data):
|
||||||
|
path = API_PATHS.get('address-detail').format_map({'pk': pk})
|
||||||
|
url = urllib.parse.urljoin(self.base_url, path)
|
||||||
|
return self.request("PUT", url=url, data=data)
|
||||||
|
|
||||||
def retrieve_mail_address_list(self, querystring=None):
|
def retrieve_mail_address_list(self, querystring=None):
|
||||||
def get_mailbox_id(value):
|
|
||||||
mailboxes = value.get('mailboxes')
|
|
||||||
|
|
||||||
# forwarded address should not grouped
|
|
||||||
if len(mailboxes) == 0:
|
|
||||||
return value.get('name')
|
|
||||||
|
|
||||||
return mailboxes[0]['id']
|
|
||||||
|
|
||||||
# retrieve mails applying filters (if any)
|
# retrieve mails applying filters (if any)
|
||||||
raw_data = self.retrieve_service_list(
|
raw_data = self.retrieve_service_list(
|
||||||
MailService.api_name,
|
Address.api_name,
|
||||||
querystring=querystring,
|
querystring=querystring,
|
||||||
)
|
)
|
||||||
|
|
||||||
# group addresses with the same mailbox
|
addresses = [Address.new_from_json(data) for data in raw_data]
|
||||||
addresses = []
|
|
||||||
for key, group in groupby(raw_data, get_mailbox_id):
|
|
||||||
aliases = []
|
|
||||||
data = {}
|
|
||||||
for thing in group:
|
|
||||||
aliases.append(thing.pop('name'))
|
|
||||||
data = thing
|
|
||||||
|
|
||||||
data['names'] = aliases
|
|
||||||
addresses.append(MailService.new_from_json(data))
|
|
||||||
|
|
||||||
# PATCH to include Pangea addresses not shown by orchestra
|
# PATCH to include Pangea addresses not shown by orchestra
|
||||||
# described on issue #4
|
# described on issue #4
|
||||||
raw_mailboxes = self.retrieve_service_list('mailbox')
|
# TODO(@slamora) disabled hacky patch because breaks another funtionalities
|
||||||
for mailbox in raw_mailboxes:
|
# XXX Fix it on orchestra instead of here???
|
||||||
if mailbox['addresses'] == []:
|
# raw_mailboxes = self.retrieve_mailbox_list()
|
||||||
address_data = {
|
# for mailbox in raw_mailboxes:
|
||||||
'names': [mailbox['name']],
|
# if mailbox['addresses'] == []:
|
||||||
'forward': '',
|
# address_data = {
|
||||||
'domain': {
|
# 'names': [mailbox['name']],
|
||||||
'name': 'pangea.org.',
|
# 'forward': '',
|
||||||
},
|
# 'domain': {
|
||||||
'mailboxes': [mailbox],
|
# 'name': 'pangea.org.',
|
||||||
}
|
# },
|
||||||
pangea_address = MailService.new_from_json(address_data)
|
# 'mailboxes': [mailbox],
|
||||||
addresses.append(pangea_address)
|
# }
|
||||||
|
# pangea_address = Address.new_from_json(address_data)
|
||||||
|
# addresses.append(pangea_address)
|
||||||
|
|
||||||
return addresses
|
return addresses
|
||||||
|
|
||||||
|
def delete_mail_address(self, pk):
|
||||||
|
path = API_PATHS.get('address-detail').format_map({'pk': pk})
|
||||||
|
url = urllib.parse.urljoin(self.base_url, path)
|
||||||
|
return self.request("DELETE", url=url, render_as=None)
|
||||||
|
|
||||||
|
def create_mailbox(self, data):
|
||||||
|
resource = '{}-list'.format(Mailbox.api_name)
|
||||||
|
return self.request("POST", resource=resource, data=data, raise_exception=False)
|
||||||
|
|
||||||
|
def retrieve_mailbox(self, pk):
|
||||||
|
path = API_PATHS.get('mailbox-detail').format_map({'pk': pk})
|
||||||
|
|
||||||
|
url = urllib.parse.urljoin(self.base_url, path)
|
||||||
|
status, data_json = self.request("GET", url=url, raise_exception=False)
|
||||||
|
if status == 404:
|
||||||
|
raise Http404(_("No mailbox found matching the query"))
|
||||||
|
return Mailbox.new_from_json(data_json)
|
||||||
|
|
||||||
|
def update_mailbox(self, pk, data):
|
||||||
|
path = API_PATHS.get('mailbox-detail').format_map({'pk': pk})
|
||||||
|
url = urllib.parse.urljoin(self.base_url, path)
|
||||||
|
status, response = self.request("PATCH", url=url, data=data, raise_exception=False)
|
||||||
|
return status, response
|
||||||
|
|
||||||
|
def retrieve_mailbox_list(self):
|
||||||
|
mailboxes = self.retrieve_service_list(Mailbox.api_name)
|
||||||
|
return [Mailbox.new_from_json(mailbox_data) for mailbox_data in mailboxes]
|
||||||
|
|
||||||
|
def delete_mailbox(self, pk):
|
||||||
|
path = API_PATHS.get('mailbox-detail').format_map({'pk': pk})
|
||||||
|
url = urllib.parse.urljoin(self.base_url, path)
|
||||||
|
# Mark as inactive instead of deleting
|
||||||
|
# return self.request("DELETE", url=url, render_as=None)
|
||||||
|
return self.request("PATCH", url=url, data={"is_active": False})
|
||||||
|
|
||||||
|
def set_password_mailbox(self, pk, data):
|
||||||
|
path = API_PATHS.get('mailbox-password').format_map({'pk': pk})
|
||||||
|
url = urllib.parse.urljoin(self.base_url, path)
|
||||||
|
status, response = self.request("POST", url=url, data=data, raise_exception=False)
|
||||||
|
return status, response
|
||||||
|
|
||||||
|
|
||||||
def retrieve_domain(self, pk):
|
def retrieve_domain(self, pk):
|
||||||
path = API_PATHS.get('domain-detail').format_map({'pk': pk})
|
path = API_PATHS.get('domain-detail').format_map({'pk': pk})
|
||||||
|
|
||||||
|
@ -174,8 +221,8 @@ class Orchestra(object):
|
||||||
querystring = "domain={}".format(domain_json['id'])
|
querystring = "domain={}".format(domain_json['id'])
|
||||||
|
|
||||||
# retrieve services associated to a domain
|
# retrieve services associated to a domain
|
||||||
domain_json['mails'] = self.retrieve_service_list(
|
domain_json['addresses'] = self.retrieve_service_list(
|
||||||
MailService.api_name, querystring)
|
Address.api_name, querystring)
|
||||||
|
|
||||||
# retrieve websites (as they cannot be filtered by domain on the API we should do it here)
|
# 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'])
|
domain_json['websites'] = self.filter_websites_by_domain(websites, domain_json['id'])
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
|
|
||||||
|
from django import forms
|
||||||
from django.contrib.auth.forms import AuthenticationForm
|
from django.contrib.auth.forms import AuthenticationForm
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from . import api
|
from . import api
|
||||||
|
|
||||||
|
@ -20,3 +23,135 @@ class LoginForm(AuthenticationForm):
|
||||||
self.user = orchestra.retrieve_profile()
|
self.user = orchestra.retrieve_profile()
|
||||||
|
|
||||||
return self.cleaned_data
|
return self.cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
class MailForm(forms.Form):
|
||||||
|
name = forms.CharField()
|
||||||
|
domain = forms.ChoiceField()
|
||||||
|
mailboxes = forms.MultipleChoiceField(required=False)
|
||||||
|
forward = forms.EmailField(required=False)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.instance = kwargs.pop('instance', None)
|
||||||
|
if self.instance is not None:
|
||||||
|
kwargs['initial'] = self.instance.deserialize()
|
||||||
|
|
||||||
|
domains = kwargs.pop('domains')
|
||||||
|
mailboxes = kwargs.pop('mailboxes')
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['domain'].choices = [(d.url, d.name) for d in domains]
|
||||||
|
self.fields['mailboxes'].choices = [(m.url, m.name) for m in mailboxes]
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
if not cleaned_data.get('mailboxes') and not cleaned_data.get('forward'):
|
||||||
|
raise ValidationError("A mailbox or forward address should be provided.")
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
assert hasattr(self, 'cleaned_data')
|
||||||
|
serialized_data = {
|
||||||
|
"name": self.cleaned_data["name"],
|
||||||
|
"domain": {"url": self.cleaned_data["domain"]},
|
||||||
|
"mailboxes": [{"url": mbox} for mbox in self.cleaned_data["mailboxes"]],
|
||||||
|
"forward": self.cleaned_data["forward"],
|
||||||
|
}
|
||||||
|
return serialized_data
|
||||||
|
|
||||||
|
|
||||||
|
class MailboxChangePasswordForm(forms.Form):
|
||||||
|
error_messages = {
|
||||||
|
'password_mismatch': _('The two password fields didn’t match.'),
|
||||||
|
}
|
||||||
|
password = forms.CharField(
|
||||||
|
label=_("Password"),
|
||||||
|
strip=False,
|
||||||
|
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
|
||||||
|
)
|
||||||
|
password2 = forms.CharField(
|
||||||
|
label=_("Password confirmation"),
|
||||||
|
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
|
||||||
|
strip=False,
|
||||||
|
help_text=_("Enter the same password as before, for verification."),
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean_password2(self):
|
||||||
|
password = self.cleaned_data.get("password")
|
||||||
|
password2 = self.cleaned_data.get("password2")
|
||||||
|
if password and password2 and password != password2:
|
||||||
|
raise ValidationError(
|
||||||
|
self.error_messages['password_mismatch'],
|
||||||
|
code='password_mismatch',
|
||||||
|
)
|
||||||
|
return password2
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
assert self.is_valid()
|
||||||
|
serialized_data = {
|
||||||
|
"password": self.cleaned_data["password2"],
|
||||||
|
}
|
||||||
|
return serialized_data
|
||||||
|
|
||||||
|
|
||||||
|
class MailboxCreateForm(forms.Form):
|
||||||
|
error_messages = {
|
||||||
|
'password_mismatch': _('The two password fields didn’t match.'),
|
||||||
|
}
|
||||||
|
name = forms.CharField()
|
||||||
|
password = forms.CharField(
|
||||||
|
label=_("Password"),
|
||||||
|
strip=False,
|
||||||
|
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
|
||||||
|
)
|
||||||
|
password2 = forms.CharField(
|
||||||
|
label=_("Password confirmation"),
|
||||||
|
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
|
||||||
|
strip=False,
|
||||||
|
help_text=_("Enter the same password as before, for verification."),
|
||||||
|
)
|
||||||
|
addresses = forms.MultipleChoiceField(required=False)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
addresses = kwargs.pop('addresses')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['addresses'].choices = [(addr.url, addr.full_address_name) for addr in addresses]
|
||||||
|
|
||||||
|
def clean_password2(self):
|
||||||
|
password = self.cleaned_data.get("password")
|
||||||
|
password2 = self.cleaned_data.get("password2")
|
||||||
|
if password and password2 and password != password2:
|
||||||
|
raise ValidationError(
|
||||||
|
self.error_messages['password_mismatch'],
|
||||||
|
code='password_mismatch',
|
||||||
|
)
|
||||||
|
return password2
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
assert self.is_valid()
|
||||||
|
serialized_data = {
|
||||||
|
"name": self.cleaned_data["name"],
|
||||||
|
"password": self.cleaned_data["password2"],
|
||||||
|
"addresses": self.cleaned_data["addresses"],
|
||||||
|
}
|
||||||
|
return serialized_data
|
||||||
|
|
||||||
|
|
||||||
|
class MailboxUpdateForm(forms.Form):
|
||||||
|
addresses = forms.MultipleChoiceField(required=False)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.instance = kwargs.pop('instance', None)
|
||||||
|
if self.instance is not None:
|
||||||
|
kwargs['initial'] = self.instance.deserialize()
|
||||||
|
|
||||||
|
addresses = kwargs.pop('addresses')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['addresses'].choices = [(addr.url, addr.full_address_name) for addr in addresses]
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
assert self.is_valid()
|
||||||
|
serialized_data = {
|
||||||
|
"addresses": self.cleaned_data["addresses"],
|
||||||
|
}
|
||||||
|
return serialized_data
|
||||||
|
|
|
@ -7,7 +7,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2020-01-23 17:49+0100\n"
|
"POT-Creation-Date: 2021-10-08 11:49+0200\n"
|
||||||
"PO-Revision-Date: 2020-01-28 17:27+0100\n"
|
"PO-Revision-Date: 2020-01-28 17:27+0100\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
|
@ -18,10 +18,34 @@ msgstr ""
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
"X-Generator: Poedit 2.2.4\n"
|
"X-Generator: Poedit 2.2.4\n"
|
||||||
|
|
||||||
#: api.py:108 api.py:117
|
#: api.py:112 api.py:203
|
||||||
msgid "No domain found matching the query"
|
msgid "No domain found matching the query"
|
||||||
msgstr "No trobem cap domini que coincideixi amb la teva consulta"
|
msgstr "No trobem cap domini que coincideixi amb la teva consulta"
|
||||||
|
|
||||||
|
#: api.py:124
|
||||||
|
msgid "No object found matching the query"
|
||||||
|
msgstr "No trobem cap objecte que coincideixi amb la teva consulta"
|
||||||
|
|
||||||
|
#: api.py:177
|
||||||
|
msgid "No mailbox found matching the query"
|
||||||
|
msgstr "No trobem cap bústia que coincideixi amb la teva consulta"
|
||||||
|
|
||||||
|
#: forms.py:65
|
||||||
|
msgid "The two password fields didn’t match."
|
||||||
|
msgstr "Les contrasenyes introduides no coincideixen."
|
||||||
|
|
||||||
|
#: forms.py:69
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "Contrasenya"
|
||||||
|
|
||||||
|
#: forms.py:74
|
||||||
|
msgid "Password confirmation"
|
||||||
|
msgstr "Verificació de la contrasenya"
|
||||||
|
|
||||||
|
#: forms.py:77
|
||||||
|
msgid "Enter the same password as before, for verification."
|
||||||
|
msgstr "Introdueix la mateixa contrasenya per verificar."
|
||||||
|
|
||||||
#: mixins.py:14
|
#: mixins.py:14
|
||||||
msgid "Domains & websites"
|
msgid "Domains & websites"
|
||||||
msgstr "Dominis i llocs web"
|
msgstr "Dominis i llocs web"
|
||||||
|
@ -31,12 +55,12 @@ msgid "Mails"
|
||||||
msgstr "Correus"
|
msgstr "Correus"
|
||||||
|
|
||||||
#. Translators: This message appears on the page title
|
#. Translators: This message appears on the page title
|
||||||
#: mixins.py:16 views.py:226
|
#: mixins.py:16 views.py:291
|
||||||
msgid "Mailing lists"
|
msgid "Mailing lists"
|
||||||
msgstr "Llistes de correu"
|
msgstr "Llistes de correu"
|
||||||
|
|
||||||
#. Translators: This message appears on the page title
|
#. Translators: This message appears on the page title
|
||||||
#: mixins.py:17 models.py:138 views.py:255
|
#: mixins.py:17 models.py:147 views.py:427
|
||||||
msgid "Databases"
|
msgid "Databases"
|
||||||
msgstr "Bases de dades"
|
msgstr "Bases de dades"
|
||||||
|
|
||||||
|
@ -44,36 +68,41 @@ msgstr "Bases de dades"
|
||||||
msgid "SaaS"
|
msgid "SaaS"
|
||||||
msgstr "SaaS"
|
msgstr "SaaS"
|
||||||
|
|
||||||
#: models.py:139
|
#: models.py:148
|
||||||
#, fuzzy
|
|
||||||
msgid "Description details for databases page."
|
msgid "Description details for databases page."
|
||||||
msgstr "Consulta la configuració de les teves bases de dades."
|
msgstr "Consulta la configuració de les teves bases de dades."
|
||||||
|
|
||||||
#. Translators: This message appears on the page title
|
#. Translators: This message appears on the page title
|
||||||
#: models.py:200 views.py:169
|
#: models.py:235 views.py:181
|
||||||
msgid "Mail addresses"
|
msgid "Mail addresses"
|
||||||
msgstr "Adreces de correu"
|
msgstr "Adreces de correu"
|
||||||
|
|
||||||
#: models.py:201
|
#: models.py:236
|
||||||
#, fuzzy
|
|
||||||
msgid "Description details for mail addresses page."
|
msgid "Description details for mail addresses page."
|
||||||
msgstr "Consulta aquí totes les adreces de correu que tens actives."
|
msgstr "Consulta aquí totes les adreces de correu que tens actives."
|
||||||
|
|
||||||
#: models.py:243
|
#: models.py:311
|
||||||
|
msgid "Mailbox"
|
||||||
|
msgstr "Bústia de correu"
|
||||||
|
|
||||||
|
#: models.py:312
|
||||||
|
msgid "Description details for mailbox page."
|
||||||
|
msgstr ""
|
||||||
|
"Aquí trobaràs les teves bústies de correu i els seus detalls de configuració."
|
||||||
|
|
||||||
|
#: models.py:337
|
||||||
msgid "Mailing list"
|
msgid "Mailing list"
|
||||||
msgstr "Llista de correu"
|
msgstr "Llista de correu"
|
||||||
|
|
||||||
#: models.py:244
|
#: models.py:338
|
||||||
#, fuzzy
|
|
||||||
msgid "Description details for mailinglist page."
|
msgid "Description details for mailinglist page."
|
||||||
msgstr "Consulta aquí els detalls de les teves llistes de correu."
|
msgstr "Consulta aquí els detalls de les teves llistes de correu."
|
||||||
|
|
||||||
#: models.py:267
|
#: models.py:364
|
||||||
msgid "Software as a Service (SaaS)"
|
msgid "Software as a Service (SaaS)"
|
||||||
msgstr "Software as a Service (SaaS)"
|
msgstr "Software as a Service (SaaS)"
|
||||||
|
|
||||||
#: models.py:268
|
#: models.py:365
|
||||||
#, fuzzy
|
|
||||||
msgid "Description details for SaaS page."
|
msgid "Description details for SaaS page."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Si tens algun servei SaaS (Software as a Service) contractat, aquí trobaràs "
|
"Si tens algun servei SaaS (Software as a Service) contractat, aquí trobaràs "
|
||||||
|
@ -100,6 +129,58 @@ msgstr ""
|
||||||
"Envia un correu a <a href=\"mailto:%(support_email)s\">%(support_email)s</a> "
|
"Envia un correu a <a href=\"mailto:%(support_email)s\">%(support_email)s</a> "
|
||||||
"indicant el teu nom d’usuari/a i t’explicarem què fer."
|
"indicant el teu nom d’usuari/a i t’explicarem què fer."
|
||||||
|
|
||||||
|
#: templates/musician/address_check_delete.html:7
|
||||||
|
#, python-format
|
||||||
|
msgid "Are you sure that you want remove the address: \"%(address_name)s\"?"
|
||||||
|
msgstr ""
|
||||||
|
"Estàs segur de que vols esborrar la adreça de correu: \"%(address_name)s\"?"
|
||||||
|
|
||||||
|
#: templates/musician/address_check_delete.html:8
|
||||||
|
#: templates/musician/mailbox_check_delete.html:11
|
||||||
|
msgid "WARNING: This action cannot be undone."
|
||||||
|
msgstr "AVÍS: Aquesta acció es irreversible."
|
||||||
|
|
||||||
|
#: templates/musician/address_check_delete.html:9
|
||||||
|
#: templates/musician/address_form.html:15
|
||||||
|
#: templates/musician/mailbox_check_delete.html:12
|
||||||
|
#: templates/musician/mailbox_form.html:24
|
||||||
|
msgid "Delete"
|
||||||
|
msgstr "Esborrar"
|
||||||
|
|
||||||
|
#: templates/musician/address_check_delete.html:10
|
||||||
|
#: templates/musician/address_form.html:11
|
||||||
|
#: templates/musician/mailbox_check_delete.html:13
|
||||||
|
#: templates/musician/mailbox_form.html:20
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr "Cancel·lar"
|
||||||
|
|
||||||
|
#: templates/musician/address_form.html:12
|
||||||
|
#: templates/musician/mailbox_form.html:21
|
||||||
|
msgid "Save"
|
||||||
|
msgstr "Desar"
|
||||||
|
|
||||||
|
#: templates/musician/addresses.html:15
|
||||||
|
msgid "Email"
|
||||||
|
msgstr "E-mail"
|
||||||
|
|
||||||
|
#: templates/musician/addresses.html:16
|
||||||
|
msgid "Domain"
|
||||||
|
msgstr "Domini"
|
||||||
|
|
||||||
|
#. Translators: This message appears on the page title
|
||||||
|
#: templates/musician/addresses.html:17 templates/musician/mail_base.html:22
|
||||||
|
#: views.py:320
|
||||||
|
msgid "Mailboxes"
|
||||||
|
msgstr "Bústia de correu"
|
||||||
|
|
||||||
|
#: templates/musician/addresses.html:18
|
||||||
|
msgid "Forward"
|
||||||
|
msgstr "Redirecció"
|
||||||
|
|
||||||
|
#: templates/musician/addresses.html:38
|
||||||
|
msgid "New mail address"
|
||||||
|
msgstr "Nova adreça de correu"
|
||||||
|
|
||||||
#: templates/musician/base.html:60
|
#: templates/musician/base.html:60
|
||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
msgstr "Configuració"
|
msgstr "Configuració"
|
||||||
|
@ -110,7 +191,7 @@ msgstr "Perfil"
|
||||||
|
|
||||||
#. Translators: This message appears on the page title
|
#. Translators: This message appears on the page title
|
||||||
#: templates/musician/base.html:64 templates/musician/billing.html:6
|
#: templates/musician/base.html:64 templates/musician/billing.html:6
|
||||||
#: views.py:147
|
#: views.py:159
|
||||||
msgid "Billing"
|
msgid "Billing"
|
||||||
msgstr "Factures"
|
msgstr "Factures"
|
||||||
|
|
||||||
|
@ -119,7 +200,6 @@ msgid "Log out"
|
||||||
msgstr "Surt"
|
msgstr "Surt"
|
||||||
|
|
||||||
#: templates/musician/billing.html:7
|
#: templates/musician/billing.html:7
|
||||||
#, fuzzy
|
|
||||||
msgid "Billing page description."
|
msgid "Billing page description."
|
||||||
msgstr "Consulta i descarrega les teves factures."
|
msgstr "Consulta i descarrega les teves factures."
|
||||||
|
|
||||||
|
@ -132,7 +212,7 @@ msgid "Bill date"
|
||||||
msgstr "Data de la factura"
|
msgstr "Data de la factura"
|
||||||
|
|
||||||
#: templates/musician/billing.html:21 templates/musician/databases.html:17
|
#: templates/musician/billing.html:21 templates/musician/databases.html:17
|
||||||
#: templates/musician/domain_detail.html:17 templates/musician/mail.html:22
|
#: templates/musician/domain_detail.html:17
|
||||||
msgid "Type"
|
msgid "Type"
|
||||||
msgstr "Tipus"
|
msgstr "Tipus"
|
||||||
|
|
||||||
|
@ -178,7 +258,6 @@ msgid "Your domains and websites"
|
||||||
msgstr "Els teus dominis i llocs web"
|
msgstr "Els teus dominis i llocs web"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:36
|
#: templates/musician/dashboard.html:36
|
||||||
#, fuzzy
|
|
||||||
msgid "Dashboard page description."
|
msgid "Dashboard page description."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Aquest és el teu panell de gestió, des d’on podràs consultar la configuració "
|
"Aquest és el teu panell de gestió, des d’on podràs consultar la configuració "
|
||||||
|
@ -209,7 +288,7 @@ msgid "Mail list"
|
||||||
msgstr "Llista de correu"
|
msgstr "Llista de correu"
|
||||||
|
|
||||||
#. Translators: This message appears on the page title
|
#. Translators: This message appears on the page title
|
||||||
#: templates/musician/dashboard.html:82 views.py:264
|
#: templates/musician/dashboard.html:82 views.py:436
|
||||||
msgid "Software as a Service"
|
msgid "Software as a Service"
|
||||||
msgstr "Software as a Service"
|
msgstr "Software as a Service"
|
||||||
|
|
||||||
|
@ -217,7 +296,7 @@ msgstr "Software as a Service"
|
||||||
msgid "Nothing installed"
|
msgid "Nothing installed"
|
||||||
msgstr "No tens res instal·lat"
|
msgstr "No tens res instal·lat"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:89 views.py:42
|
#: templates/musician/dashboard.html:89 views.py:67
|
||||||
msgid "Disk usage"
|
msgid "Disk usage"
|
||||||
msgstr "Ús del disc"
|
msgstr "Ús del disc"
|
||||||
|
|
||||||
|
@ -279,7 +358,6 @@ msgid "DNS settings for"
|
||||||
msgstr "Configuració DNS per a"
|
msgstr "Configuració DNS per a"
|
||||||
|
|
||||||
#: templates/musician/domain_detail.html:8
|
#: templates/musician/domain_detail.html:8
|
||||||
#, fuzzy
|
|
||||||
msgid "DNS settings page description."
|
msgid "DNS settings page description."
|
||||||
msgstr "Consulta aquí la teva configuració DNS."
|
msgstr "Consulta aquí la teva configuració DNS."
|
||||||
|
|
||||||
|
@ -287,25 +365,57 @@ msgstr "Consulta aquí la teva configuració DNS."
|
||||||
msgid "Value"
|
msgid "Value"
|
||||||
msgstr "Valor"
|
msgstr "Valor"
|
||||||
|
|
||||||
#: templates/musician/mail.html:6 templates/musician/mailinglists.html:6
|
#: templates/musician/mail_base.html:6 templates/musician/mailinglists.html:6
|
||||||
msgid "Go to global"
|
msgid "Go to global"
|
||||||
msgstr "Totes les adreces"
|
msgstr "Totes les adreces"
|
||||||
|
|
||||||
#: templates/musician/mail.html:9 templates/musician/mailinglists.html:9
|
#: templates/musician/mail_base.html:10 templates/musician/mailinglists.html:9
|
||||||
msgid "for"
|
msgid "for"
|
||||||
msgstr "per a"
|
msgstr "per a"
|
||||||
|
|
||||||
#: templates/musician/mail.html:20
|
#: templates/musician/mail_base.html:18 templates/musician/mailboxes.html:16
|
||||||
msgid "Mail address"
|
msgid "Addresses"
|
||||||
msgstr "Adreça de correu"
|
msgstr "Adreces de correu"
|
||||||
|
|
||||||
#: templates/musician/mail.html:21
|
#: templates/musician/mailbox_check_delete.html:7
|
||||||
msgid "Aliases"
|
#, python-format
|
||||||
msgstr "Àlies"
|
msgid "Are you sure that you want remove the mailbox: \"%(name)s\"?"
|
||||||
|
msgstr "Estàs segur de que vols esborrar la bústia de correu: \"%(name)s\"?"
|
||||||
|
|
||||||
#: templates/musician/mail.html:23
|
#: templates/musician/mailbox_check_delete.html:9
|
||||||
msgid "Type details"
|
msgid ""
|
||||||
msgstr "Detalls de cada tipus"
|
"All mailbox's messages will be <strong>deleted and cannot be recovered"
|
||||||
|
"</strong>."
|
||||||
|
msgstr ""
|
||||||
|
"Tots els missatges <strong>s'esborraran i no es podran recuperar</strong>"
|
||||||
|
|
||||||
|
#: templates/musician/mailbox_form.html:9
|
||||||
|
msgid "Warning!"
|
||||||
|
msgstr "Atenció!"
|
||||||
|
|
||||||
|
#: templates/musician/mailbox_form.html:9
|
||||||
|
msgid ""
|
||||||
|
"You have reached the limit of mailboxes of your subscription so "
|
||||||
|
"<strong>extra fees</strong> may apply."
|
||||||
|
msgstr ""
|
||||||
|
"Has assolit el llímit de bústies de correu de la teva suscripció, les noves "
|
||||||
|
"bústies poden implicar <strong>costos adicionals</strong>."
|
||||||
|
|
||||||
|
#: templates/musician/mailbox_form.html:10
|
||||||
|
msgid "Close"
|
||||||
|
msgstr "Tancar"
|
||||||
|
|
||||||
|
#: templates/musician/mailboxes.html:14
|
||||||
|
msgid "Name"
|
||||||
|
msgstr "Nombre"
|
||||||
|
|
||||||
|
#: templates/musician/mailboxes.html:15
|
||||||
|
msgid "Filtering"
|
||||||
|
msgstr "Filtrat"
|
||||||
|
|
||||||
|
#: templates/musician/mailboxes.html:39
|
||||||
|
msgid "New mailbox"
|
||||||
|
msgstr "Nova bústia de correu"
|
||||||
|
|
||||||
#: templates/musician/mailinglists.html:34
|
#: templates/musician/mailinglists.html:34
|
||||||
msgid "Active"
|
msgid "Active"
|
||||||
|
@ -316,7 +426,6 @@ msgid "Inactive"
|
||||||
msgstr "Inactiu"
|
msgstr "Inactiu"
|
||||||
|
|
||||||
#: templates/musician/profile.html:7
|
#: templates/musician/profile.html:7
|
||||||
#, fuzzy
|
|
||||||
msgid "Little description on profile page."
|
msgid "Little description on profile page."
|
||||||
msgstr "Canvia les teves dades d’accés i opcions de perfil des d’aquí."
|
msgstr "Canvia les teves dades d’accés i opcions de perfil des d’aquí."
|
||||||
|
|
||||||
|
@ -357,43 +466,46 @@ msgid "Open service admin panel"
|
||||||
msgstr "Obre el panell d’administració del servei"
|
msgstr "Obre el panell d’administració del servei"
|
||||||
|
|
||||||
#. Translators: This message appears on the page title
|
#. Translators: This message appears on the page title
|
||||||
#: views.py:32
|
#: views.py:37
|
||||||
msgid "Dashboard"
|
msgid "Dashboard"
|
||||||
msgstr "Panell de gestió"
|
msgstr "Panell de gestió"
|
||||||
|
|
||||||
#: views.py:49
|
#: views.py:76
|
||||||
msgid "Traffic"
|
msgid "Traffic"
|
||||||
msgstr "Tràfic"
|
msgstr "Tràfic"
|
||||||
|
|
||||||
#: views.py:56
|
#: views.py:85
|
||||||
msgid "Mailbox usage"
|
msgid "Mailbox usage"
|
||||||
msgstr "Ús d’espai a la bústia de correu"
|
msgstr "Ús d’espai a la bústia de correu"
|
||||||
|
|
||||||
#. Translators: This message appears on the page title
|
#. Translators: This message appears on the page title
|
||||||
#: views.py:96
|
#: views.py:108
|
||||||
msgid "User profile"
|
msgid "User profile"
|
||||||
msgstr "El teu perfil"
|
msgstr "El teu perfil"
|
||||||
|
|
||||||
#. Translators: This message appears on the page title
|
#. Translators: This message appears on the page title
|
||||||
#: views.py:154
|
#: views.py:166
|
||||||
msgid "Download bill"
|
msgid "Download bill"
|
||||||
msgstr "Descarrega la factura"
|
msgstr "Descarrega la factura"
|
||||||
|
|
||||||
#. Translators: This message appears on the page title
|
#. Translators: This message appears on the page title
|
||||||
#: views.py:272
|
#: views.py:444
|
||||||
msgid "Domain details"
|
msgid "Domain details"
|
||||||
msgstr "Detalls del domini"
|
msgstr "Detalls del domini"
|
||||||
|
|
||||||
#. Translators: This message appears on the page title
|
#. Translators: This message appears on the page title
|
||||||
#: views.py:298
|
#: views.py:470
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr "Accés"
|
msgstr "Accés"
|
||||||
|
|
||||||
|
#~ msgid "Aliases"
|
||||||
|
#~ msgstr "Àlies"
|
||||||
|
|
||||||
|
#~ msgid "Type details"
|
||||||
|
#~ msgstr "Detalls de cada tipus"
|
||||||
|
|
||||||
#~ msgid "databases created"
|
#~ msgid "databases created"
|
||||||
#~ msgstr "bases de dades creades"
|
#~ msgstr "bases de dades creades"
|
||||||
|
|
||||||
#~ msgid "Username"
|
#~ msgid "Username"
|
||||||
#~ msgstr "Nom d’usuari/a"
|
#~ msgstr "Nom d’usuari/a"
|
||||||
|
|
||||||
#~ msgid "Password:"
|
|
||||||
#~ msgstr "Contrasenya:"
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2020-01-23 17:49+0100\n"
|
"POT-Creation-Date: 2021-10-08 11:49+0200\n"
|
||||||
"PO-Revision-Date: 2020-01-28 17:27+0100\n"
|
"PO-Revision-Date: 2020-01-28 17:27+0100\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
|
@ -18,10 +18,34 @@ msgstr ""
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
"X-Generator: Poedit 2.2.4\n"
|
"X-Generator: Poedit 2.2.4\n"
|
||||||
|
|
||||||
#: api.py:108 api.py:117
|
#: api.py:112 api.py:203
|
||||||
msgid "No domain found matching the query"
|
msgid "No domain found matching the query"
|
||||||
msgstr "No hay dominios que coincidan con tu búsqueda"
|
msgstr "No hay dominios que coincidan con tu búsqueda"
|
||||||
|
|
||||||
|
#: api.py:124
|
||||||
|
msgid "No object found matching the query"
|
||||||
|
msgstr "No hay objetos que coincidan con tu búsqueda"
|
||||||
|
|
||||||
|
#: api.py:177
|
||||||
|
msgid "No mailbox found matching the query"
|
||||||
|
msgstr "No hay buzones de correo que coincidan con tu búsqueda"
|
||||||
|
|
||||||
|
#: forms.py:65
|
||||||
|
msgid "The two password fields didn’t match."
|
||||||
|
msgstr "Las contraseñas introducidas no coinciden."
|
||||||
|
|
||||||
|
#: forms.py:69
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "Contraseña"
|
||||||
|
|
||||||
|
#: forms.py:74
|
||||||
|
msgid "Password confirmation"
|
||||||
|
msgstr "Confirma la contraseña"
|
||||||
|
|
||||||
|
#: forms.py:77
|
||||||
|
msgid "Enter the same password as before, for verification."
|
||||||
|
msgstr "Introduce la misma contraseña para verificarla"
|
||||||
|
|
||||||
#: mixins.py:14
|
#: mixins.py:14
|
||||||
msgid "Domains & websites"
|
msgid "Domains & websites"
|
||||||
msgstr "Dominios y sitios web"
|
msgstr "Dominios y sitios web"
|
||||||
|
@ -31,12 +55,12 @@ msgid "Mails"
|
||||||
msgstr "Correos"
|
msgstr "Correos"
|
||||||
|
|
||||||
#. Translators: This message appears on the page title
|
#. Translators: This message appears on the page title
|
||||||
#: mixins.py:16 views.py:226
|
#: mixins.py:16 views.py:291
|
||||||
msgid "Mailing lists"
|
msgid "Mailing lists"
|
||||||
msgstr "Listas de correo"
|
msgstr "Listas de correo"
|
||||||
|
|
||||||
#. Translators: This message appears on the page title
|
#. Translators: This message appears on the page title
|
||||||
#: mixins.py:17 models.py:138 views.py:255
|
#: mixins.py:17 models.py:147 views.py:427
|
||||||
msgid "Databases"
|
msgid "Databases"
|
||||||
msgstr "Bases de datos"
|
msgstr "Bases de datos"
|
||||||
|
|
||||||
|
@ -44,36 +68,41 @@ msgstr "Bases de datos"
|
||||||
msgid "SaaS"
|
msgid "SaaS"
|
||||||
msgstr "SaaS"
|
msgstr "SaaS"
|
||||||
|
|
||||||
#: models.py:139
|
#: models.py:148
|
||||||
#, fuzzy
|
|
||||||
msgid "Description details for databases page."
|
msgid "Description details for databases page."
|
||||||
msgstr "Consulta la configuración de tus bases de datos."
|
msgstr "Consulta la configuración de tus bases de datos."
|
||||||
|
|
||||||
#. Translators: This message appears on the page title
|
#. Translators: This message appears on the page title
|
||||||
#: models.py:200 views.py:169
|
#: models.py:235 views.py:181
|
||||||
msgid "Mail addresses"
|
msgid "Mail addresses"
|
||||||
msgstr "Direcciones de correo"
|
msgstr "Direcciones de correo"
|
||||||
|
|
||||||
#: models.py:201
|
#: models.py:236
|
||||||
#, fuzzy
|
|
||||||
msgid "Description details for mail addresses page."
|
msgid "Description details for mail addresses page."
|
||||||
msgstr "Consulta aquí todas las direcciones de correo que tienes activas."
|
msgstr "Consulta aquí todas las direcciones de correo que tienes activas."
|
||||||
|
|
||||||
#: models.py:243
|
#: models.py:311
|
||||||
|
msgid "Mailbox"
|
||||||
|
msgstr "Buzón de correo"
|
||||||
|
|
||||||
|
#: models.py:312
|
||||||
|
msgid "Description details for mailbox page."
|
||||||
|
msgstr ""
|
||||||
|
"Aquí encontrarás tus buzones de correo y sus detalles de configuración."
|
||||||
|
|
||||||
|
#: models.py:337
|
||||||
msgid "Mailing list"
|
msgid "Mailing list"
|
||||||
msgstr "Lista de correo"
|
msgstr "Lista de correo"
|
||||||
|
|
||||||
#: models.py:244
|
#: models.py:338
|
||||||
#, fuzzy
|
|
||||||
msgid "Description details for mailinglist page."
|
msgid "Description details for mailinglist page."
|
||||||
msgstr "Consulta aquí los detalles de tus listas de correo."
|
msgstr "Consulta aquí los detalles de tus listas de correo."
|
||||||
|
|
||||||
#: models.py:267
|
#: models.py:364
|
||||||
msgid "Software as a Service (SaaS)"
|
msgid "Software as a Service (SaaS)"
|
||||||
msgstr "Software as a Service (SaaS)"
|
msgstr "Software as a Service (SaaS)"
|
||||||
|
|
||||||
#: models.py:268
|
#: models.py:365
|
||||||
#, fuzzy
|
|
||||||
msgid "Description details for SaaS page."
|
msgid "Description details for SaaS page."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Si tienes algún servicio SaaS (Software as a Service) contratado, aquí "
|
"Si tienes algún servicio SaaS (Software as a Service) contratado, aquí "
|
||||||
|
@ -100,6 +129,59 @@ msgstr ""
|
||||||
"Envía un correo a <a href=“mailto:%(support_email)s”>%(support_email)s</a> "
|
"Envía un correo a <a href=“mailto:%(support_email)s”>%(support_email)s</a> "
|
||||||
"indicando tu nombre de usuaria/o y te explicaremos qué hacer."
|
"indicando tu nombre de usuaria/o y te explicaremos qué hacer."
|
||||||
|
|
||||||
|
#: templates/musician/address_check_delete.html:7
|
||||||
|
#, python-format
|
||||||
|
msgid "Are you sure that you want remove the address: \"%(address_name)s\"?"
|
||||||
|
msgstr ""
|
||||||
|
"¿Estás seguro de que quieres borrar la dirección de correo \"%(address_name)s"
|
||||||
|
"\"?"
|
||||||
|
|
||||||
|
#: templates/musician/address_check_delete.html:8
|
||||||
|
#: templates/musician/mailbox_check_delete.html:11
|
||||||
|
msgid "WARNING: This action cannot be undone."
|
||||||
|
msgstr "AVISO: Esta acción es irreversible."
|
||||||
|
|
||||||
|
#: templates/musician/address_check_delete.html:9
|
||||||
|
#: templates/musician/address_form.html:15
|
||||||
|
#: templates/musician/mailbox_check_delete.html:12
|
||||||
|
#: templates/musician/mailbox_form.html:24
|
||||||
|
msgid "Delete"
|
||||||
|
msgstr "Borrar"
|
||||||
|
|
||||||
|
#: templates/musician/address_check_delete.html:10
|
||||||
|
#: templates/musician/address_form.html:11
|
||||||
|
#: templates/musician/mailbox_check_delete.html:13
|
||||||
|
#: templates/musician/mailbox_form.html:20
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr "Cancelar"
|
||||||
|
|
||||||
|
#: templates/musician/address_form.html:12
|
||||||
|
#: templates/musician/mailbox_form.html:21
|
||||||
|
msgid "Save"
|
||||||
|
msgstr "Guardar"
|
||||||
|
|
||||||
|
#: templates/musician/addresses.html:15
|
||||||
|
msgid "Email"
|
||||||
|
msgstr "E-mail"
|
||||||
|
|
||||||
|
#: templates/musician/addresses.html:16
|
||||||
|
msgid "Domain"
|
||||||
|
msgstr "Dominio"
|
||||||
|
|
||||||
|
#. Translators: This message appears on the page title
|
||||||
|
#: templates/musician/addresses.html:17 templates/musician/mail_base.html:22
|
||||||
|
#: views.py:320
|
||||||
|
msgid "Mailboxes"
|
||||||
|
msgstr "Buzones de correo"
|
||||||
|
|
||||||
|
#: templates/musician/addresses.html:18
|
||||||
|
msgid "Forward"
|
||||||
|
msgstr "Redirección"
|
||||||
|
|
||||||
|
#: templates/musician/addresses.html:38
|
||||||
|
msgid "New mail address"
|
||||||
|
msgstr "Nueva dirección de correo"
|
||||||
|
|
||||||
#: templates/musician/base.html:60
|
#: templates/musician/base.html:60
|
||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
msgstr "Configuración"
|
msgstr "Configuración"
|
||||||
|
@ -110,7 +192,7 @@ msgstr "Perfil"
|
||||||
|
|
||||||
#. Translators: This message appears on the page title
|
#. Translators: This message appears on the page title
|
||||||
#: templates/musician/base.html:64 templates/musician/billing.html:6
|
#: templates/musician/base.html:64 templates/musician/billing.html:6
|
||||||
#: views.py:147
|
#: views.py:159
|
||||||
msgid "Billing"
|
msgid "Billing"
|
||||||
msgstr "Facturas"
|
msgstr "Facturas"
|
||||||
|
|
||||||
|
@ -119,7 +201,6 @@ msgid "Log out"
|
||||||
msgstr "Desconéctate"
|
msgstr "Desconéctate"
|
||||||
|
|
||||||
#: templates/musician/billing.html:7
|
#: templates/musician/billing.html:7
|
||||||
#, fuzzy
|
|
||||||
msgid "Billing page description."
|
msgid "Billing page description."
|
||||||
msgstr "Consulta y descarga tus facturas."
|
msgstr "Consulta y descarga tus facturas."
|
||||||
|
|
||||||
|
@ -132,7 +213,7 @@ msgid "Bill date"
|
||||||
msgstr "Fecha de la factura"
|
msgstr "Fecha de la factura"
|
||||||
|
|
||||||
#: templates/musician/billing.html:21 templates/musician/databases.html:17
|
#: templates/musician/billing.html:21 templates/musician/databases.html:17
|
||||||
#: templates/musician/domain_detail.html:17 templates/musician/mail.html:22
|
#: templates/musician/domain_detail.html:17
|
||||||
msgid "Type"
|
msgid "Type"
|
||||||
msgstr "Tipo"
|
msgstr "Tipo"
|
||||||
|
|
||||||
|
@ -178,7 +259,6 @@ msgid "Your domains and websites"
|
||||||
msgstr "Tus dominios y sitios web"
|
msgstr "Tus dominios y sitios web"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:36
|
#: templates/musician/dashboard.html:36
|
||||||
#, fuzzy
|
|
||||||
msgid "Dashboard page description."
|
msgid "Dashboard page description."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Este es tu panel de gestión, desde donde podrás consultar la configuración "
|
"Este es tu panel de gestión, desde donde podrás consultar la configuración "
|
||||||
|
@ -209,7 +289,7 @@ msgid "Mail list"
|
||||||
msgstr "Lista de correo"
|
msgstr "Lista de correo"
|
||||||
|
|
||||||
#. Translators: This message appears on the page title
|
#. Translators: This message appears on the page title
|
||||||
#: templates/musician/dashboard.html:82 views.py:264
|
#: templates/musician/dashboard.html:82 views.py:436
|
||||||
msgid "Software as a Service"
|
msgid "Software as a Service"
|
||||||
msgstr "Software as a Service"
|
msgstr "Software as a Service"
|
||||||
|
|
||||||
|
@ -217,7 +297,7 @@ msgstr "Software as a Service"
|
||||||
msgid "Nothing installed"
|
msgid "Nothing installed"
|
||||||
msgstr "No tienes nada instalado"
|
msgstr "No tienes nada instalado"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:89 views.py:42
|
#: templates/musician/dashboard.html:89 views.py:67
|
||||||
msgid "Disk usage"
|
msgid "Disk usage"
|
||||||
msgstr "Uso del disco"
|
msgstr "Uso del disco"
|
||||||
|
|
||||||
|
@ -233,8 +313,8 @@ msgstr "Acceso FTP:"
|
||||||
#: templates/musician/dashboard.html:115
|
#: templates/musician/dashboard.html:115
|
||||||
msgid "Contact with the support team to get details concerning FTP access."
|
msgid "Contact with the support team to get details concerning FTP access."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Contactadnos a <a href=“mailto:%(support_email)s”>%(support_email)s</a> "
|
"Contactadnos a <a href=“mailto:%(support_email)s”>%(support_email)s</a> para "
|
||||||
"para saber cómo acceder al FTP."
|
"saber cómo acceder al FTP."
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:124
|
#: templates/musician/dashboard.html:124
|
||||||
msgid "No website configured."
|
msgid "No website configured."
|
||||||
|
@ -279,7 +359,6 @@ msgid "DNS settings for"
|
||||||
msgstr "Configuración DNS para"
|
msgstr "Configuración DNS para"
|
||||||
|
|
||||||
#: templates/musician/domain_detail.html:8
|
#: templates/musician/domain_detail.html:8
|
||||||
#, fuzzy
|
|
||||||
msgid "DNS settings page description."
|
msgid "DNS settings page description."
|
||||||
msgstr "Consulta aquí tu configuración DNS."
|
msgstr "Consulta aquí tu configuración DNS."
|
||||||
|
|
||||||
|
@ -287,25 +366,55 @@ msgstr "Consulta aquí tu configuración DNS."
|
||||||
msgid "Value"
|
msgid "Value"
|
||||||
msgstr "Valor"
|
msgstr "Valor"
|
||||||
|
|
||||||
#: templates/musician/mail.html:6 templates/musician/mailinglists.html:6
|
#: templates/musician/mail_base.html:6 templates/musician/mailinglists.html:6
|
||||||
msgid "Go to global"
|
msgid "Go to global"
|
||||||
msgstr "Todas las direcciones"
|
msgstr "Todas las direcciones"
|
||||||
|
|
||||||
#: templates/musician/mail.html:9 templates/musician/mailinglists.html:9
|
#: templates/musician/mail_base.html:10 templates/musician/mailinglists.html:9
|
||||||
msgid "for"
|
msgid "for"
|
||||||
msgstr "para"
|
msgstr "para"
|
||||||
|
|
||||||
#: templates/musician/mail.html:20
|
#: templates/musician/mail_base.html:18 templates/musician/mailboxes.html:16
|
||||||
msgid "Mail address"
|
msgid "Addresses"
|
||||||
msgstr "Dirección de correo"
|
msgstr "Direcciones de correo"
|
||||||
|
|
||||||
#: templates/musician/mail.html:21
|
#: templates/musician/mailbox_check_delete.html:7
|
||||||
msgid "Aliases"
|
#, python-format
|
||||||
msgstr "Alias"
|
msgid "Are you sure that you want remove the mailbox: \"%(name)s\"?"
|
||||||
|
msgstr "¿Estás seguro de que quieres borrar el buzón de correo \"%(name)s\"?"
|
||||||
|
|
||||||
#: templates/musician/mail.html:23
|
#: templates/musician/mailbox_check_delete.html:9
|
||||||
msgid "Type details"
|
msgid ""
|
||||||
msgstr "Detalles de cada tipo"
|
"All mailbox's messages will be <strong>deleted and cannot be recovered</"
|
||||||
|
"strong>."
|
||||||
|
msgstr ""
|
||||||
|
"Todos los mensajes <strong>se borrarán y no se podrán recuperar</strong>"
|
||||||
|
|
||||||
|
#: templates/musician/mailbox_form.html:9
|
||||||
|
msgid "Warning!"
|
||||||
|
msgstr "¡Aviso!"
|
||||||
|
|
||||||
|
#: templates/musician/mailbox_form.html:9
|
||||||
|
msgid ""
|
||||||
|
"You have reached the limit of mailboxes of your subscription so "
|
||||||
|
"<strong>extra fees</strong> may apply."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/musician/mailbox_form.html:10
|
||||||
|
msgid "Close"
|
||||||
|
msgstr "Cerrar"
|
||||||
|
|
||||||
|
#: templates/musician/mailboxes.html:14
|
||||||
|
msgid "Name"
|
||||||
|
msgstr "Nombre"
|
||||||
|
|
||||||
|
#: templates/musician/mailboxes.html:15
|
||||||
|
msgid "Filtering"
|
||||||
|
msgstr "Filtrado"
|
||||||
|
|
||||||
|
#: templates/musician/mailboxes.html:39
|
||||||
|
msgid "New mailbox"
|
||||||
|
msgstr "Nuevo buzón de correo"
|
||||||
|
|
||||||
#: templates/musician/mailinglists.html:34
|
#: templates/musician/mailinglists.html:34
|
||||||
msgid "Active"
|
msgid "Active"
|
||||||
|
@ -316,7 +425,6 @@ msgid "Inactive"
|
||||||
msgstr "Inactivo"
|
msgstr "Inactivo"
|
||||||
|
|
||||||
#: templates/musician/profile.html:7
|
#: templates/musician/profile.html:7
|
||||||
#, fuzzy
|
|
||||||
msgid "Little description on profile page."
|
msgid "Little description on profile page."
|
||||||
msgstr "Cambia tus datos de acceso y opciones de perfil desde aquí."
|
msgstr "Cambia tus datos de acceso y opciones de perfil desde aquí."
|
||||||
|
|
||||||
|
@ -357,43 +465,46 @@ msgid "Open service admin panel"
|
||||||
msgstr "Abre el panel de administración del servicio"
|
msgstr "Abre el panel de administración del servicio"
|
||||||
|
|
||||||
#. Translators: This message appears on the page title
|
#. Translators: This message appears on the page title
|
||||||
#: views.py:32
|
#: views.py:37
|
||||||
msgid "Dashboard"
|
msgid "Dashboard"
|
||||||
msgstr "Panel de gestión"
|
msgstr "Panel de gestión"
|
||||||
|
|
||||||
#: views.py:49
|
#: views.py:76
|
||||||
msgid "Traffic"
|
msgid "Traffic"
|
||||||
msgstr "Tráfico"
|
msgstr "Tráfico"
|
||||||
|
|
||||||
#: views.py:56
|
#: views.py:85
|
||||||
msgid "Mailbox usage"
|
msgid "Mailbox usage"
|
||||||
msgstr "Uso de espacio en tu buzón de correo"
|
msgstr "Uso de espacio en tu buzón de correo"
|
||||||
|
|
||||||
#. Translators: This message appears on the page title
|
#. Translators: This message appears on the page title
|
||||||
#: views.py:96
|
#: views.py:108
|
||||||
msgid "User profile"
|
msgid "User profile"
|
||||||
msgstr "Tu perfil"
|
msgstr "Tu perfil"
|
||||||
|
|
||||||
#. Translators: This message appears on the page title
|
#. Translators: This message appears on the page title
|
||||||
#: views.py:154
|
#: views.py:166
|
||||||
msgid "Download bill"
|
msgid "Download bill"
|
||||||
msgstr "Descarga la factura"
|
msgstr "Descarga la factura"
|
||||||
|
|
||||||
#. Translators: This message appears on the page title
|
#. Translators: This message appears on the page title
|
||||||
#: views.py:272
|
#: views.py:444
|
||||||
msgid "Domain details"
|
msgid "Domain details"
|
||||||
msgstr "Detalles del dominio"
|
msgstr "Detalles del dominio"
|
||||||
|
|
||||||
#. Translators: This message appears on the page title
|
#. Translators: This message appears on the page title
|
||||||
#: views.py:298
|
#: views.py:470
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr "Accede"
|
msgstr "Accede"
|
||||||
|
|
||||||
|
#~ msgid "Aliases"
|
||||||
|
#~ msgstr "Alias"
|
||||||
|
|
||||||
|
#~ msgid "Type details"
|
||||||
|
#~ msgstr "Detalles de cada tipo"
|
||||||
|
|
||||||
#~ msgid "databases created"
|
#~ msgid "databases created"
|
||||||
#~ msgstr "bases de datos creadas"
|
#~ msgstr "bases de datos creadas"
|
||||||
|
|
||||||
#~ msgid "Username"
|
#~ msgid "Username"
|
||||||
#~ msgstr "Nombre de usuario/a"
|
#~ msgstr "Nombre de usuario/a"
|
||||||
|
|
||||||
#~ msgid "Password:"
|
|
||||||
#~ msgstr "Contraseña:"
|
|
||||||
|
|
|
@ -12,10 +12,10 @@ class CustomContextMixin(ContextMixin):
|
||||||
# generate services menu items
|
# generate services menu items
|
||||||
services_menu = [
|
services_menu = [
|
||||||
{'icon': 'globe-europe', 'pattern_name': 'musician:dashboard', 'title': _('Domains & websites')},
|
{'icon': 'globe-europe', 'pattern_name': 'musician:dashboard', 'title': _('Domains & websites')},
|
||||||
{'icon': 'envelope', 'pattern_name': 'musician:mails', 'title': _('Mails')},
|
{'icon': 'envelope', 'pattern_name': 'musician:address-list', 'title': _('Mails')},
|
||||||
{'icon': 'mail-bulk', 'pattern_name': 'musician:mailing-lists', 'title': _('Mailing lists')},
|
{'icon': 'mail-bulk', 'pattern_name': 'musician:mailing-lists', 'title': _('Mailing lists')},
|
||||||
{'icon': 'database', 'pattern_name': 'musician:databases', 'title': _('Databases')},
|
{'icon': 'database', 'pattern_name': 'musician:database-list', 'title': _('Databases')},
|
||||||
{'icon': 'fire', 'pattern_name': 'musician:saas', 'title': _('SaaS')},
|
{'icon': 'fire', 'pattern_name': 'musician:saas-list', 'title': _('SaaS')},
|
||||||
]
|
]
|
||||||
context.update({
|
context.update({
|
||||||
'services_menu': services_menu,
|
'services_menu': services_menu,
|
||||||
|
|
|
@ -17,6 +17,7 @@ class OrchestraModel:
|
||||||
api_name = None
|
api_name = None
|
||||||
verbose_name = None
|
verbose_name = None
|
||||||
fields = ()
|
fields = ()
|
||||||
|
param_defaults = {}
|
||||||
id = None
|
id = None
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
@ -128,6 +129,10 @@ class UserAccount(OrchestraModel):
|
||||||
|
|
||||||
return super().new_from_json(data=data, billing=billing, language=language, last_login=last_login)
|
return super().new_from_json(data=data, billing=billing, language=language, last_login=last_login)
|
||||||
|
|
||||||
|
def allowed_resources(self, resource):
|
||||||
|
allowed_by_type = musician_settings.ALLOWED_RESOURCES[self.type]
|
||||||
|
return allowed_by_type[resource]
|
||||||
|
|
||||||
|
|
||||||
class DatabaseUser(OrchestraModel):
|
class DatabaseUser(OrchestraModel):
|
||||||
api_name = 'databaseusers'
|
api_name = 'databaseusers'
|
||||||
|
@ -161,7 +166,7 @@ class DatabaseService(OrchestraModel):
|
||||||
return super().new_from_json(data=data, users=users, usage=usage)
|
return super().new_from_json(data=data, users=users, usage=usage)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_usage(self, data):
|
def get_usage(cls, data):
|
||||||
try:
|
try:
|
||||||
resources = data['resources']
|
resources = data['resources']
|
||||||
resource_disk = {}
|
resource_disk = {}
|
||||||
|
@ -198,9 +203,10 @@ class Domain(OrchestraModel):
|
||||||
"id": None,
|
"id": None,
|
||||||
"name": None,
|
"name": None,
|
||||||
"records": [],
|
"records": [],
|
||||||
"mails": [],
|
"addresses": [],
|
||||||
"usage": {},
|
"usage": {},
|
||||||
"websites": [],
|
"websites": [],
|
||||||
|
"url": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -224,12 +230,19 @@ class DomainRecord(OrchestraModel):
|
||||||
return '<%s: %s>' % (self.type, self.value)
|
return '<%s: %s>' % (self.type, self.value)
|
||||||
|
|
||||||
|
|
||||||
class MailService(OrchestraModel):
|
class Address(OrchestraModel):
|
||||||
api_name = 'address'
|
api_name = 'address'
|
||||||
verbose_name = _('Mail addresses')
|
verbose_name = _('Mail addresses')
|
||||||
description = _('Description details for mail addresses page.')
|
description = _('Description details for mail addresses page.')
|
||||||
fields = ('mail_address', 'aliases', 'type', 'type_detail')
|
fields = ('mail_address', 'aliases', 'type', 'type_detail')
|
||||||
param_defaults = {}
|
param_defaults = {
|
||||||
|
"id": None,
|
||||||
|
"name": None,
|
||||||
|
"domain": None,
|
||||||
|
"mailboxes": [],
|
||||||
|
"forward": None,
|
||||||
|
'url': None,
|
||||||
|
}
|
||||||
|
|
||||||
FORWARD = 'forward'
|
FORWARD = 'forward'
|
||||||
MAILBOX = 'mailbox'
|
MAILBOX = 'mailbox'
|
||||||
|
@ -238,6 +251,15 @@ class MailService(OrchestraModel):
|
||||||
self.data = kwargs
|
self.data = kwargs
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
def deserialize(self):
|
||||||
|
data = {
|
||||||
|
'name': self.data['name'],
|
||||||
|
'domain': self.data['domain']['url'],
|
||||||
|
'mailboxes': [mbox['url'] for mbox in self.data['mailboxes']],
|
||||||
|
'forward': self.data['forward'],
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def aliases(self):
|
def aliases(self):
|
||||||
return [
|
return [
|
||||||
|
@ -245,8 +267,8 @@ class MailService(OrchestraModel):
|
||||||
]
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mail_address(self):
|
def full_address_name(self):
|
||||||
return self.data['names'][0] + '@' + self.data['domain']['name']
|
return "{}@{}".format(self.name, self.domain['name'])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self):
|
def type(self):
|
||||||
|
@ -284,6 +306,32 @@ class MailService(OrchestraModel):
|
||||||
return mailbox_details
|
return mailbox_details
|
||||||
|
|
||||||
|
|
||||||
|
class Mailbox(OrchestraModel):
|
||||||
|
api_name = 'mailbox'
|
||||||
|
verbose_name = _('Mailbox')
|
||||||
|
description = _('Description details for mailbox page.')
|
||||||
|
fields = ('name', 'filtering', 'addresses', 'active')
|
||||||
|
param_defaults = {
|
||||||
|
'id': None,
|
||||||
|
'name': None,
|
||||||
|
'filtering': None,
|
||||||
|
'is_active': True,
|
||||||
|
'addresses': [],
|
||||||
|
'url': None,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def new_from_json(cls, data, **kwargs):
|
||||||
|
addresses = [Address.new_from_json(addr) for addr in data.get('addresses', [])]
|
||||||
|
return super().new_from_json(data=data, addresses=addresses)
|
||||||
|
|
||||||
|
def deserialize(self):
|
||||||
|
data = {
|
||||||
|
'addresses': [addr.url for addr in self.addresses],
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class MailinglistService(OrchestraModel):
|
class MailinglistService(OrchestraModel):
|
||||||
api_name = 'mailinglist'
|
api_name = 'mailinglist'
|
||||||
verbose_name = _('Mailing list')
|
verbose_name = _('Mailing list')
|
||||||
|
@ -301,7 +349,10 @@ class MailinglistService(OrchestraModel):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def address_name(self):
|
def address_name(self):
|
||||||
return "{}@{}".format(self.data['address_name'], self.data['address_domain']['name'])
|
address_domain = self.data['address_domain']
|
||||||
|
if address_domain is None:
|
||||||
|
return self.data['address_name']
|
||||||
|
return "{}@{}".format(self.data['address_name'], address_domain['name'])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def manager_url(self):
|
def manager_url(self):
|
||||||
|
|
|
@ -4,31 +4,30 @@ a, a:hover, a:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: rgba(0,0,0,.7);
|
color: rgba(0, 0, 0, .7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-arrow-left{
|
.btn-arrow-left {
|
||||||
color: #eee;
|
color: #eee;
|
||||||
background: #D3D0DA;
|
background: #D3D0DA;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 8px 20px 8px 30px;
|
padding: 8px 20px 8px 30px;
|
||||||
margin-left: 1em; /** equal value than arrow.left **/
|
margin-left: 1em;
|
||||||
|
/** equal value than arrow.left **/
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-arrow-left::after,
|
.btn-arrow-left::after, .btn-arrow-left::before {
|
||||||
.btn-arrow-left::before{
|
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: -1em;
|
left: -1em;
|
||||||
|
|
||||||
margin-top: -19px;
|
margin-top: -19px;
|
||||||
border-top: 19px solid transparent;
|
border-top: 19px solid transparent;
|
||||||
border-bottom: 19px solid transparent;
|
border-bottom: 19px solid transparent;
|
||||||
border-right: 1em solid;
|
border-right: 1em solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-arrow-left::after{
|
.btn-arrow-left::after {
|
||||||
border-right-color: #D3D0DA;
|
border-right-color: #D3D0DA;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
@ -43,13 +42,12 @@ a:hover {
|
||||||
min-width: 280px;
|
min-width: 280px;
|
||||||
max-width: 280px;
|
max-width: 280px;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar #sidebar-services {
|
#sidebar #sidebar-services {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
@ -62,20 +60,20 @@ a:hover {
|
||||||
padding-left: 2rem;
|
padding-left: 2rem;
|
||||||
padding-right: 2rem;
|
padding-right: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar #sidebar-services {
|
#sidebar #sidebar-services {
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar #user-profile-menu {
|
#sidebar #user-profile-menu {
|
||||||
background:rgba(254, 251, 242, 0.25);
|
background: rgba(254, 251, 242, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar ul.components {
|
#sidebar ul.components {
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#sidebar ul li a {
|
#sidebar ul li a {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
|
@ -89,25 +87,26 @@ a:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
.vertical-center {
|
.vertical-center {
|
||||||
min-height: 100%; /* Fallback for browsers do NOT support vh unit */
|
min-height: 100%;
|
||||||
min-height: 100vh; /* These two lines are counted as one :-) */
|
/* Fallback for browsers do NOT support vh unit */
|
||||||
|
min-height: 100vh;
|
||||||
|
/* These two lines are counted as one :-) */
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** login **/
|
/** login **/
|
||||||
|
|
||||||
#body-login .jumbotron {
|
#body-login .jumbotron {
|
||||||
background: #282532 no-repeat url("../images/logo-pangea-lilla-bg.svg") right;
|
background: #282532 no-repeat url("../images/logo-pangea-lilla-bg.svg") right;
|
||||||
}
|
}
|
||||||
|
|
||||||
#login-content {
|
#login-content {
|
||||||
background:white;
|
background: white;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#login-content input[type="text"].form-control,
|
#login-content input[type="text"].form-control, #login-content input[type="password"].form-control {
|
||||||
#login-content input[type="password"].form-control {
|
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-bottom: 2px solid #8E8E8E;
|
border-bottom: 2px solid #8E8E8E;
|
||||||
|
@ -121,6 +120,7 @@ a:hover {
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#login-footer a {
|
#login-footer a {
|
||||||
color: #FEFBF2;
|
color: #FEFBF2;
|
||||||
}
|
}
|
||||||
|
@ -130,34 +130,37 @@ a:hover {
|
||||||
background-position: right 5% top 10%;
|
background-position: right 5% top 10%;
|
||||||
color: #343434;
|
color: #343434;
|
||||||
padding-left: 2rem;
|
padding-left: 2rem;
|
||||||
margin-left: 280px; /** sidebar width **/
|
margin-left: 280px;
|
||||||
|
/** sidebar width **/
|
||||||
}
|
}
|
||||||
|
|
||||||
/** services **/
|
/** services **/
|
||||||
h1.service-name {
|
|
||||||
|
|
||||||
|
h1.service-name {
|
||||||
font: Bold 26px/34px Roboto;
|
font: Bold 26px/34px Roboto;
|
||||||
margin-top: 3rem;
|
margin-top: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-description {
|
.service-description {
|
||||||
|
|
||||||
font: 16px/21px Roboto;
|
font: 16px/21px Roboto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table.service-list {
|
.table.service-list {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** TODO update theme instead of overriding **/
|
/** TODO update theme instead of overriding **/
|
||||||
.service-list thead.thead-dark th,
|
|
||||||
.service-card .card-header {
|
.service-list thead.thead-dark th, .service-card .card-header {
|
||||||
background: rgba(80, 70, 110, 0.25);
|
background: rgba(80, 70, 110, 0.25);
|
||||||
color: #50466E;
|
color: #50466E;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** /TODO **/
|
/** /TODO **/
|
||||||
.table.service-list td,
|
|
||||||
.table.service-list th {
|
.table.service-list td, .table.service-list th {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,11 +205,10 @@ h1.service-name {
|
||||||
|
|
||||||
.service-card .card-body {
|
.service-card .card-body {
|
||||||
color: #787878;
|
color: #787878;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-card .card-body i.fas {
|
.service-card .card-body i.fas {
|
||||||
color:#9C9AA7;
|
color: #9C9AA7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-manager-link {
|
.service-manager-link {
|
||||||
|
@ -215,8 +217,7 @@ h1.service-name {
|
||||||
right: 15px;
|
right: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-card .service-manager-link a,
|
.service-card .service-manager-link a, .service-card .service-manager-link a i.fas {
|
||||||
.service-card .service-manager-link a i.fas {
|
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,11 +244,9 @@ h1.service-name {
|
||||||
font-variant: normal;
|
font-variant: normal;
|
||||||
text-rendering: auto;
|
text-rendering: auto;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
|
|
||||||
color: #E8E7EB;
|
color: #E8E7EB;
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
}
|
}
|
||||||
|
@ -308,3 +307,13 @@ h1.service-name {
|
||||||
border-top: 0;
|
border-top: 0;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.roll-hover {
|
||||||
|
visibility: hidden;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:hover .roll-hover {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
12
musician/templates/musician/address_check_delete.html
Normal file
12
musician/templates/musician/address_check_delete.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{% extends "musician/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>{% blocktrans with address_name=object.full_address_name %}Are you sure that you want remove the address: "{{ address_name }}"?{% endblocktrans %}</p>
|
||||||
|
<p class="alert alert-warning"><strong>{% trans 'WARNING: This action cannot be undone.' %}</strong></p>
|
||||||
|
<input class="btn btn-danger" type="submit" value="{% trans 'Delete' %}">
|
||||||
|
<a class="btn btn-secondary" href="{% url 'musician:address-update' view.kwargs.pk %}">{% trans 'Cancel' %}</a>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
20
musician/templates/musician/address_form.html
Normal file
20
musician/templates/musician/address_form.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{% extends "musician/base.html" %}
|
||||||
|
{% load bootstrap4 i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="service-name">{{ service.verbose_name }}</h1>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form form %}
|
||||||
|
{% buttons %}
|
||||||
|
<a class="btn btn-light mr-2" href="{% url 'musician:address-list' %}">{% trans "Cancel" %}</a>
|
||||||
|
<button type="submit" class="btn btn-secondary">{% trans "Save" %}</button>
|
||||||
|
{% if form.instance %}
|
||||||
|
<div class="float-right">
|
||||||
|
<a class="btn btn-danger" href="{% url 'musician:address-delete' view.kwargs.pk %}">{% trans "Delete" %}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endbuttons %}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
41
musician/templates/musician/addresses.html
Normal file
41
musician/templates/musician/addresses.html
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{% extends "musician/mail_base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block tabcontent %}
|
||||||
|
<div class="tab-pane fade show active" id="addresses" role="tabpanel" aria-labelledby="addresses-tab">
|
||||||
|
<table class="table service-list">
|
||||||
|
<colgroup>
|
||||||
|
<col span="1" style="width: 25%;">
|
||||||
|
<col span="1" style="width: 25%;">
|
||||||
|
<col span="1" style="width: 25%;">
|
||||||
|
<col span="1" style="width: 25%;">
|
||||||
|
</colgroup>
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{% trans "Email" %}</th>
|
||||||
|
<th scope="col">{% trans "Domain" %}</th>
|
||||||
|
<th scope="col">{% trans "Mailboxes" %}</th>
|
||||||
|
<th scope="col">{% trans "Forward" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for obj in object_list %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{% url 'musician:address-update' obj.id %}">{{ obj.full_address_name }}</a></td>
|
||||||
|
<td>{{ obj.domain.name }}</td>
|
||||||
|
<td>
|
||||||
|
{% for mailbox in obj.mailboxes %}
|
||||||
|
<a href="{% url 'musician:mailbox-update' mailbox.id %}">{{ mailbox.name }}</a>
|
||||||
|
{% if not forloop.last %}<br/> {% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td>{{ obj.forward }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
{% include "musician/components/table_paginator.html" %}
|
||||||
|
</table>
|
||||||
|
<a class="btn btn-primary mt-4 mb-4" href="{% url 'musician:address-create' %}">{% trans "New mail address" %}</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -82,6 +82,16 @@
|
||||||
{% endblock sidebar %}
|
{% endblock sidebar %}
|
||||||
</nav><!-- ./sidebar -->
|
</nav><!-- ./sidebar -->
|
||||||
<div id="content" class="container-fluid pt-4">
|
<div id="content" class="container-fluid pt-4">
|
||||||
|
{% block messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.tags|default:'info' }} alert-dismissible fade show" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock messages %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
</div><!-- ./content -->
|
</div><!-- ./content -->
|
||||||
|
|
|
@ -16,6 +16,11 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">{{ usage.verbose_name }}</h5>
|
<h5 class="card-title">{{ usage.verbose_name }}</h5>
|
||||||
{% include "musician/components/usage_progress_bar.html" with detail=usage.data %}
|
{% include "musician/components/usage_progress_bar.html" with detail=usage.data %}
|
||||||
|
{% if usage.data.alert %}
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
{{ usage.data.alert }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -65,13 +70,9 @@
|
||||||
<h4>{% trans "Mail" %}</h4>
|
<h4>{% trans "Mail" %}</h4>
|
||||||
<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.addresses|length }} {% trans "mail addresses created" %}
|
||||||
{% if domain.addresses_left.alert_level %}
|
|
||||||
<br/>
|
|
||||||
<span class="text-{{ domain.addresses_left.alert_level }}">{{ domain.addresses_left.count }} {% trans "mail address left" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
</p>
|
||||||
<a class="stretched-link" href="{% url 'musician:mails' %}?domain={{ domain.id }}"></a>
|
<a class="stretched-link" href="{% url 'musician:address-list' %}?domain={{ domain.id }}"></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2 border-right">
|
<div class="col-md-2 border-right">
|
||||||
<h4>{% trans "Mail list" %}</h4>
|
<h4>{% trans "Mail list" %}</h4>
|
||||||
|
@ -82,7 +83,7 @@
|
||||||
<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-list' %}?domain={{ domain.id }}"></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-1"></div>
|
<div class="col-md-1"></div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
{% extends "musician/base.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% if active_domain %}
|
|
||||||
<a class="btn-arrow-left" href="{% url 'musician:mails' %}">{% trans "Go to global" %}</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<h1 class="service-name">{{ service.verbose_name }}{% if active_domain %} <span class="font-weight-light">{% trans "for" %} {{ active_domain.name }}</span>{% endif %}</h1>
|
|
||||||
<p class="service-description">{{ service.description }}</p>
|
|
||||||
<table class="table service-list">
|
|
||||||
<colgroup>
|
|
||||||
<col span="1" style="width: 25%;">
|
|
||||||
<col span="1" style="width: 50%;">
|
|
||||||
<col span="1" style="width: 5%;">
|
|
||||||
<col span="1" style="width: 20%;">
|
|
||||||
</colgroup>
|
|
||||||
<thead class="thead-dark">
|
|
||||||
<tr>
|
|
||||||
<th scope="col">{% trans "Mail address" %}</th>
|
|
||||||
<th scope="col">{% trans "Aliases" %}</th>
|
|
||||||
<th scope="col">{% trans "Type" %}</th>
|
|
||||||
<th scope="col">{% trans "Type details" %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for obj in object_list %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ obj.mail_address }}</td>
|
|
||||||
<td>{{ obj.aliases|join:" , " }}</td>
|
|
||||||
<td>{{ obj.type|capfirst }}</td>
|
|
||||||
<td>
|
|
||||||
{% if obj.type == 'mailbox' %}
|
|
||||||
{% include "musician/components/usage_progress_bar.html" with detail=obj.type_detail %}
|
|
||||||
{% else %}
|
|
||||||
{{ obj.type_detail }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
{% include "musician/components/table_paginator.html" %}
|
|
||||||
</table>
|
|
||||||
{% endblock %}
|
|
32
musician/templates/musician/mail_base.html
Normal file
32
musician/templates/musician/mail_base.html
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{% extends "musician/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if active_domain %}
|
||||||
|
<a class="btn-arrow-left" href="{% url 'musician:address-list' %}">{% trans "Go to global" %}</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h1 class="service-name">{{ service.verbose_name }}
|
||||||
|
{% if active_domain %}<span class="font-weight-light">{% trans "for" %} {{ active_domain.name }}</span>{% endif %}
|
||||||
|
</h1>
|
||||||
|
<p class="service-description">{{ service.description }}</p>
|
||||||
|
|
||||||
|
{% with request.resolver_match.url_name as url_name %}
|
||||||
|
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if url_name == 'address-list' %}active{% endif %}" href="{% url 'musician:address-list' %}" role="tab"
|
||||||
|
aria-selected="{% if url_name == 'address-list' %}true{% else %}false{% endif %}">{% trans "Addresses" %}</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if url_name == 'mailbox-list' %}active{% endif %}" href="{% url 'musician:mailbox-list' %}" role="tab"
|
||||||
|
aria-selected="{% if url_name == 'mailbox-list' %}true{% else %}false{% endif %}">{% trans "Mailboxes" %}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div class="tab-content" id="myTabContent">
|
||||||
|
{% block tabcontent %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% endblock %}
|
15
musician/templates/musician/mailbox_change_password.html
Normal file
15
musician/templates/musician/mailbox_change_password.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends "musician/base.html" %}
|
||||||
|
{% load bootstrap4 i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="service-name">{% trans "Change password" %}: <span class="font-weight-light">{{ object.name }}</span></h1>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form form %}
|
||||||
|
{% buttons %}
|
||||||
|
<a class="btn btn-light mr-2" href="{% url 'musician:mailbox-list' %}">{% trans "Cancel" %}</a>
|
||||||
|
<button type="submit" class="btn btn-secondary">{% trans "Save" %}</button>
|
||||||
|
{% endbuttons %}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
15
musician/templates/musician/mailbox_check_delete.html
Normal file
15
musician/templates/musician/mailbox_check_delete.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends "musician/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>{% blocktrans with name=object.name %}Are you sure that you want remove the mailbox: "{{ name }}"?{% endblocktrans %}</p>
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
{% trans "All mailbox's messages will be <strong>deleted and cannot be recovered</strong>." %}
|
||||||
|
</div>
|
||||||
|
<p class="alert alert-warning"><strong>{% trans 'WARNING: This action cannot be undone.' %}</strong></p>
|
||||||
|
<input class="btn btn-danger" type="submit" value="{% trans 'Delete' %}">
|
||||||
|
<a class="btn btn-secondary" href="{% url 'musician:mailbox-list' %}">{% trans 'Cancel' %}</a>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
30
musician/templates/musician/mailbox_form.html
Normal file
30
musician/templates/musician/mailbox_form.html
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{% extends "musician/base.html" %}
|
||||||
|
{% load bootstrap4 i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="service-name">{{ service.verbose_name }}</h1>
|
||||||
|
|
||||||
|
{% if extra_mailbox %}
|
||||||
|
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
||||||
|
<strong>{% trans "Warning!" %}</strong> {% trans "You have reached the limit of mailboxes of your subscription so <strong>extra fees</strong> may apply." %}
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="{% trans 'Close' %}">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form form %}
|
||||||
|
{% buttons %}
|
||||||
|
<a class="btn btn-light mr-2" href="{% url 'musician:mailbox-list' %}">{% trans "Cancel" %}</a>
|
||||||
|
<button type="submit" class="btn btn-secondary">{% trans "Save" %}</button>
|
||||||
|
{% if form.instance %}
|
||||||
|
<div class="float-right">
|
||||||
|
<a class="btn btn-outline-warning" href="{% url 'musician:mailbox-password' view.kwargs.pk %}"><i class="fas fa-key"></i> {% trans "Change password" %}</a>
|
||||||
|
<a class="btn btn-danger" href="{% url 'musician:mailbox-delete' view.kwargs.pk %}">{% trans "Delete" %}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endbuttons %}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
46
musician/templates/musician/mailboxes.html
Normal file
46
musician/templates/musician/mailboxes.html
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
{% extends "musician/mail_base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block tabcontent %}
|
||||||
|
<div class="tab-pane fade show active" id="mailboxes" role="tabpanel" aria-labelledby="mailboxes-tab">
|
||||||
|
<table class="table service-list">
|
||||||
|
<colgroup>
|
||||||
|
<col span="1" style="width: 25%;">
|
||||||
|
<col span="1" style="width: 10%;">
|
||||||
|
<col span="1" style="width: 65%;">
|
||||||
|
</colgroup>
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{% trans "Name" %}</th>
|
||||||
|
<th scope="col">{% trans "Filtering" %}</th>
|
||||||
|
<th scope="col">{% trans "Addresses" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for mailbox in object_list %}
|
||||||
|
{# <!-- Exclude (don't render) inactive mailboxes -->#}
|
||||||
|
{% if mailbox.is_active %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'musician:mailbox-update' mailbox.id %}">{{ mailbox.name }}</a>
|
||||||
|
<a class="roll-hover btn btn-outline-warning" href="{% url 'musician:mailbox-password' mailbox.id %}">
|
||||||
|
<i class="fas fa-key"></i> {% trans "Update password" %}</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ mailbox.filtering }}</td>
|
||||||
|
<td>
|
||||||
|
{% for addr in mailbox.addresses %}
|
||||||
|
<a href="{% url 'musician:address-update' addr.data.id %}">
|
||||||
|
{{ addr.full_address_name }}
|
||||||
|
</a><br/>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}{# <!-- /is_active --> #}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
{% include "musician/components/table_paginator.html" %}
|
||||||
|
</table>
|
||||||
|
<a class="btn btn-primary mt-4 mb-4" href="{% url 'musician:mailbox-create' %}">{% trans "New mailbox" %}</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -1,9 +1,37 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from .models import UserAccount
|
from .models import DatabaseService, UserAccount
|
||||||
from .utils import get_bootstraped_percent
|
from .utils import get_bootstraped_percent
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseTest(TestCase):
|
||||||
|
def test_database_from_json(self):
|
||||||
|
data = {
|
||||||
|
"url": "https://example.org/api/databases/1/",
|
||||||
|
"id": 1,
|
||||||
|
"name": "bluebird",
|
||||||
|
"type": "mysql",
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"url": "https://example.org/api/databaseusers/2/",
|
||||||
|
"id": 2,
|
||||||
|
"username": "bluebird"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"name": "disk",
|
||||||
|
"used": "1.798",
|
||||||
|
"allocated": None,
|
||||||
|
"unit": "MiB"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
database = DatabaseService.new_from_json(data)
|
||||||
|
self.assertEqual(0, database.usage['percent'])
|
||||||
|
|
||||||
|
|
||||||
class DomainsTestCase(TestCase):
|
class DomainsTestCase(TestCase):
|
||||||
def test_domain_not_found(self):
|
def test_domain_not_found(self):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
|
@ -118,3 +146,8 @@ class GetBootstrapedPercentTest(TestCase):
|
||||||
|
|
||||||
def test_invalid_total_is_zero(self):
|
def test_invalid_total_is_zero(self):
|
||||||
value = get_bootstraped_percent(25, 0)
|
value = get_bootstraped_percent(25, 0)
|
||||||
|
self.assertEqual(value, 0)
|
||||||
|
|
||||||
|
def test_invalid_total_is_none(self):
|
||||||
|
value = get_bootstraped_percent(25, None)
|
||||||
|
self.assertEqual(value, 0)
|
||||||
|
|
|
@ -16,11 +16,19 @@ urlpatterns = [
|
||||||
path('auth/logout/', views.LogoutView.as_view(), name='logout'),
|
path('auth/logout/', views.LogoutView.as_view(), name='logout'),
|
||||||
path('dashboard/', views.DashboardView.as_view(), name='dashboard'),
|
path('dashboard/', views.DashboardView.as_view(), name='dashboard'),
|
||||||
path('domains/<int:pk>/', views.DomainDetailView.as_view(), name='domain-detail'),
|
path('domains/<int:pk>/', views.DomainDetailView.as_view(), name='domain-detail'),
|
||||||
path('bills/', views.BillingView.as_view(), name='billing'),
|
path('billing/', views.BillingView.as_view(), name='billing'),
|
||||||
path('bills/<int:pk>/download/', views.BillDownloadView.as_view(), name='bill-download'),
|
path('bills/<int:pk>/download/', views.BillDownloadView.as_view(), name='bill-download'),
|
||||||
path('profile/', views.ProfileView.as_view(), name='profile'),
|
path('profile/', views.ProfileView.as_view(), name='profile'),
|
||||||
path('mails/', views.MailView.as_view(), name='mails'),
|
path('address/', views.MailView.as_view(), name='address-list'),
|
||||||
|
path('address/new/', views.MailCreateView.as_view(), name='address-create'),
|
||||||
|
path('address/<int:pk>/', views.MailUpdateView.as_view(), name='address-update'),
|
||||||
|
path('address/<int:pk>/delete/', views.AddressDeleteView.as_view(), name='address-delete'),
|
||||||
|
path('mailboxes/', views.MailboxesView.as_view(), name='mailbox-list'),
|
||||||
|
path('mailboxes/new/', views.MailboxCreateView.as_view(), name='mailbox-create'),
|
||||||
|
path('mailboxes/<int:pk>/', views.MailboxUpdateView.as_view(), name='mailbox-update'),
|
||||||
|
path('mailboxes/<int:pk>/delete/', views.MailboxDeleteView.as_view(), name='mailbox-delete'),
|
||||||
|
path('mailboxes/<int:pk>/change-password/', views.MailboxChangePasswordView.as_view(), name='mailbox-password'),
|
||||||
path('mailing-lists/', views.MailingListsView.as_view(), name='mailing-lists'),
|
path('mailing-lists/', views.MailingListsView.as_view(), name='mailing-lists'),
|
||||||
path('databases/', views.DatabasesView.as_view(), name='databases'),
|
path('databases/', views.DatabasesView.as_view(), name='database-list'),
|
||||||
path('software-as-a-service/', views.SaasView.as_view(), name='saas'),
|
path('saas/', views.SaasView.as_view(), name='saas-list'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,7 +6,7 @@ def get_bootstraped_percent(value, total):
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
percent = value / total
|
percent = value / total
|
||||||
except ZeroDivisionError:
|
except (TypeError, ZeroDivisionError):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
bootstraped = round(percent * 4) * 100 // 4
|
bootstraped = round(percent * 4) * 100 // 4
|
||||||
|
|
|
@ -1,28 +1,38 @@
|
||||||
|
import logging
|
||||||
|
from os import stat
|
||||||
|
import smtplib
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib import messages
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.core.mail import mail_managers
|
||||||
from django.http import HttpResponse, HttpResponseRedirect
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
|
from django.utils.html import format_html
|
||||||
from django.utils.http import is_safe_url
|
from django.utils.http import is_safe_url
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.generic.base import RedirectView, TemplateView
|
from django.views.generic.base import RedirectView, TemplateView
|
||||||
from django.views.generic.detail import DetailView
|
from django.views.generic.detail import DetailView
|
||||||
from django.views.generic.edit import FormView
|
from django.views.generic.edit import DeleteView, FormView
|
||||||
from django.views.generic.list import ListView
|
from django.views.generic.list import ListView
|
||||||
|
from requests.exceptions import HTTPError
|
||||||
|
|
||||||
from . import api, get_version
|
from . import api, get_version
|
||||||
from .auth import login as auth_login
|
from .auth import login as auth_login
|
||||||
from .auth import logout as auth_logout
|
from .auth import logout as auth_logout
|
||||||
from .forms import LoginForm
|
from .forms import LoginForm, MailboxChangePasswordForm, MailboxCreateForm, MailboxUpdateForm, MailForm
|
||||||
from .mixins import (CustomContextMixin, ExtendedPaginationMixin,
|
from .mixins import (CustomContextMixin, ExtendedPaginationMixin,
|
||||||
UserTokenRequiredMixin)
|
UserTokenRequiredMixin)
|
||||||
from .models import (Bill, DatabaseService, MailinglistService, MailService,
|
from .models import (Address, Bill, DatabaseService, Mailbox,
|
||||||
PaymentSource, SaasService, UserAccount)
|
MailinglistService, PaymentSource, SaasService)
|
||||||
from .settings import ALLOWED_RESOURCES
|
from .settings import ALLOWED_RESOURCES
|
||||||
from .utils import get_bootstraped_percent
|
from .utils import get_bootstraped_percent
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
||||||
template_name = "musician/dashboard.html"
|
template_name = "musician/dashboard.html"
|
||||||
|
@ -40,20 +50,6 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
||||||
|
|
||||||
# show resource usage based on plan definition
|
# show resource usage based on plan definition
|
||||||
profile_type = context['profile'].type
|
profile_type = context['profile'].type
|
||||||
total_mailboxes = 0
|
|
||||||
for domain in domains:
|
|
||||||
total_mailboxes += len(domain.mails)
|
|
||||||
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.addresses_left = {
|
|
||||||
'count': addresses_left,
|
|
||||||
'alert_level': alert_level,
|
|
||||||
}
|
|
||||||
|
|
||||||
# TODO(@slamora) update when backend provides resource usage data
|
# TODO(@slamora) update when backend provides resource usage data
|
||||||
resource_usage = {
|
resource_usage = {
|
||||||
|
@ -75,15 +71,7 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
||||||
# 'percent': 25,
|
# 'percent': 25,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'mailbox': {
|
'mailbox': self.get_mailbox_usage(profile_type),
|
||||||
'verbose_name': _('Mailbox usage'),
|
|
||||||
'data': {
|
|
||||||
'usage': total_mailboxes,
|
|
||||||
'total': ALLOWED_RESOURCES[profile_type]['mailbox'],
|
|
||||||
'unit': 'accounts',
|
|
||||||
'percent': get_bootstraped_percent(total_mailboxes, ALLOWED_RESOURCES[profile_type]['mailbox']),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
context.update({
|
context.update({
|
||||||
|
@ -94,6 +82,28 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def get_mailbox_usage(self, profile_type):
|
||||||
|
allowed_mailboxes = ALLOWED_RESOURCES[profile_type]['mailbox']
|
||||||
|
total_mailboxes = len(self.orchestra.retrieve_mailbox_list())
|
||||||
|
mailboxes_left = allowed_mailboxes - total_mailboxes
|
||||||
|
|
||||||
|
alert = ''
|
||||||
|
if mailboxes_left < 0:
|
||||||
|
alert = format_html("<span class='text-danger'>{} extra mailboxes</span>", mailboxes_left * -1)
|
||||||
|
elif mailboxes_left <= 1:
|
||||||
|
alert = format_html("<span class='text-warning'>{} mailbox left</span>", mailboxes_left)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'verbose_name': _('Mailbox usage'),
|
||||||
|
'data': {
|
||||||
|
'usage': total_mailboxes,
|
||||||
|
'total': allowed_mailboxes,
|
||||||
|
'alert': alert,
|
||||||
|
'unit': 'mailboxes',
|
||||||
|
'percent': get_bootstraped_percent(total_mailboxes, allowed_mailboxes),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ProfileView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
class ProfileView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
||||||
template_name = "musician/profile.html"
|
template_name = "musician/profile.html"
|
||||||
|
@ -168,8 +178,8 @@ class BillDownloadView(CustomContextMixin, UserTokenRequiredMixin, View):
|
||||||
|
|
||||||
|
|
||||||
class MailView(ServiceListView):
|
class MailView(ServiceListView):
|
||||||
service_class = MailService
|
service_class = Address
|
||||||
template_name = "musician/mail.html"
|
template_name = "musician/addresses.html"
|
||||||
extra_context = {
|
extra_context = {
|
||||||
# Translators: This message appears on the page title
|
# Translators: This message appears on the page title
|
||||||
'title': _('Mail addresses'),
|
'title': _('Mail addresses'),
|
||||||
|
@ -198,9 +208,86 @@ class MailView(ServiceListView):
|
||||||
context.update({
|
context.update({
|
||||||
'active_domain': self.orchestra.retrieve_domain(domain_id)
|
'active_domain': self.orchestra.retrieve_domain(domain_id)
|
||||||
})
|
})
|
||||||
|
context['mailboxes'] = self.orchestra.retrieve_mailbox_list()
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class MailCreateView(CustomContextMixin, UserTokenRequiredMixin, FormView):
|
||||||
|
service_class = Address
|
||||||
|
template_name = "musician/address_form.html"
|
||||||
|
form_class = MailForm
|
||||||
|
success_url = reverse_lazy("musician:address-list")
|
||||||
|
extra_context = {'service': service_class}
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs['domains'] = self.orchestra.retrieve_domain_list()
|
||||||
|
kwargs['mailboxes'] = self.orchestra.retrieve_mailbox_list()
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
# handle request errors e.g. 400 validation
|
||||||
|
try:
|
||||||
|
serialized_data = form.serialize()
|
||||||
|
self.orchestra.create_mail_address(serialized_data)
|
||||||
|
except HTTPError as e:
|
||||||
|
form.add_error(field='__all__', error=e)
|
||||||
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
class MailUpdateView(CustomContextMixin, UserTokenRequiredMixin, FormView):
|
||||||
|
service_class = Address
|
||||||
|
template_name = "musician/address_form.html"
|
||||||
|
form_class = MailForm
|
||||||
|
success_url = reverse_lazy("musician:address-list")
|
||||||
|
extra_context = {'service': service_class}
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
instance = self.orchestra.retrieve_mail_address(self.kwargs['pk'])
|
||||||
|
|
||||||
|
kwargs.update({
|
||||||
|
'instance': instance,
|
||||||
|
'domains': self.orchestra.retrieve_domain_list(),
|
||||||
|
'mailboxes': self.orchestra.retrieve_mailbox_list(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
# handle request errors e.g. 400 validation
|
||||||
|
try:
|
||||||
|
serialized_data = form.serialize()
|
||||||
|
self.orchestra.update_mail_address(self.kwargs['pk'], serialized_data)
|
||||||
|
except HTTPError as e:
|
||||||
|
form.add_error(field='__all__', error=e)
|
||||||
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
class AddressDeleteView(CustomContextMixin, UserTokenRequiredMixin, DeleteView):
|
||||||
|
template_name = "musician/address_check_delete.html"
|
||||||
|
success_url = reverse_lazy("musician:address-list")
|
||||||
|
|
||||||
|
def get_object(self, queryset=None):
|
||||||
|
obj = self.orchestra.retrieve_mail_address(self.kwargs['pk'])
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def delete(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object()
|
||||||
|
try:
|
||||||
|
self.orchestra.delete_mail_address(self.object.id)
|
||||||
|
messages.success(self.request, _('Address deleted!'))
|
||||||
|
except HTTPError as e:
|
||||||
|
messages.error(self.request, _('Cannot process your request, please try again later.'))
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
|
return HttpResponseRedirect(self.success_url)
|
||||||
|
|
||||||
|
|
||||||
class MailingListsView(ServiceListView):
|
class MailingListsView(ServiceListView):
|
||||||
service_class = MailinglistService
|
service_class = MailinglistService
|
||||||
template_name = "musician/mailinglists.html"
|
template_name = "musician/mailinglists.html"
|
||||||
|
@ -230,6 +317,161 @@ class MailingListsView(ServiceListView):
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
class MailboxesView(ServiceListView):
|
||||||
|
service_class = Mailbox
|
||||||
|
template_name = "musician/mailboxes.html"
|
||||||
|
extra_context = {
|
||||||
|
# Translators: This message appears on the page title
|
||||||
|
'title': _('Mailboxes'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MailboxCreateView(CustomContextMixin, UserTokenRequiredMixin, FormView):
|
||||||
|
service_class = Mailbox
|
||||||
|
template_name = "musician/mailbox_form.html"
|
||||||
|
form_class = MailboxCreateForm
|
||||||
|
success_url = reverse_lazy("musician:mailbox-list")
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context.update({
|
||||||
|
'extra_mailbox': self.is_extra_mailbox(context['profile']),
|
||||||
|
'service': self.service_class,
|
||||||
|
})
|
||||||
|
return context
|
||||||
|
|
||||||
|
def is_extra_mailbox(self, profile):
|
||||||
|
number_of_mailboxes = len(self.orchestra.retrieve_mailbox_list())
|
||||||
|
return number_of_mailboxes >= profile.allowed_resources('mailbox')
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs.update({
|
||||||
|
'addresses': self.orchestra.retrieve_mail_address_list(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
serialized_data = form.serialize()
|
||||||
|
status, response = self.orchestra.create_mailbox(serialized_data)
|
||||||
|
|
||||||
|
if status >= 400:
|
||||||
|
if status == 400:
|
||||||
|
# handle errors & add to form (they will be rendered)
|
||||||
|
form.add_error(field=None, error=response)
|
||||||
|
else:
|
||||||
|
logger.error("{}: {}".format(status, response[:120]))
|
||||||
|
msg = "Sorry, an error occurred while processing your request ({})".format(status)
|
||||||
|
form.add_error(field='__all__', error=msg)
|
||||||
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
class MailboxUpdateView(CustomContextMixin, UserTokenRequiredMixin, FormView):
|
||||||
|
service_class = Mailbox
|
||||||
|
template_name = "musician/mailbox_form.html"
|
||||||
|
form_class = MailboxUpdateForm
|
||||||
|
success_url = reverse_lazy("musician:mailbox-list")
|
||||||
|
extra_context = {'service': service_class}
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
instance = self.orchestra.retrieve_mailbox(self.kwargs['pk'])
|
||||||
|
|
||||||
|
kwargs.update({
|
||||||
|
'instance': instance,
|
||||||
|
'addresses': self.orchestra.retrieve_mail_address_list(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
serialized_data = form.serialize()
|
||||||
|
status, response = self.orchestra.update_mailbox(self.kwargs['pk'], serialized_data)
|
||||||
|
|
||||||
|
if status >= 400:
|
||||||
|
if status == 400:
|
||||||
|
# handle errors & add to form (they will be rendered)
|
||||||
|
form.add_error(field=None, error=response)
|
||||||
|
else:
|
||||||
|
logger.error("{}: {}".format(status, response[:120]))
|
||||||
|
msg = "Sorry, an error occurred while processing your request ({})".format(status)
|
||||||
|
form.add_error(field='__all__', error=msg)
|
||||||
|
|
||||||
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
class MailboxDeleteView(CustomContextMixin, UserTokenRequiredMixin, DeleteView):
|
||||||
|
template_name = "musician/mailbox_check_delete.html"
|
||||||
|
success_url = reverse_lazy("musician:mailbox-list")
|
||||||
|
|
||||||
|
def get_object(self, queryset=None):
|
||||||
|
obj = self.orchestra.retrieve_mailbox(self.kwargs['pk'])
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def delete(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object()
|
||||||
|
try:
|
||||||
|
self.orchestra.delete_mailbox(self.object.id)
|
||||||
|
messages.success(self.request, _('Mailbox deleted!'))
|
||||||
|
except HTTPError as e:
|
||||||
|
messages.error(self.request, _('Cannot process your request, please try again later.'))
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
|
self.notify_managers(self.object)
|
||||||
|
|
||||||
|
return HttpResponseRedirect(self.success_url)
|
||||||
|
|
||||||
|
def notify_managers(self, mailbox):
|
||||||
|
user = self.get_context_data()['profile']
|
||||||
|
subject = 'Mailbox {} ({}) deleted | Musician'.format(mailbox.id, mailbox.name)
|
||||||
|
content = (
|
||||||
|
"User {} ({}) has deleted its mailbox {} ({}) via musician.\n"
|
||||||
|
"The mailbox has been marked as inactive but has not been removed."
|
||||||
|
).format(user.username, user.full_name, mailbox.id, mailbox.name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
mail_managers(subject, content, fail_silently=False)
|
||||||
|
except (smtplib.SMTPException, ConnectionRefusedError):
|
||||||
|
logger.error("Error sending email to managers", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
class MailboxChangePasswordView(CustomContextMixin, UserTokenRequiredMixin, FormView):
|
||||||
|
template_name = "musician/mailbox_change_password.html"
|
||||||
|
form_class = MailboxChangePasswordForm
|
||||||
|
success_url = reverse_lazy("musician:mailbox-list")
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
self.object = self.get_object()
|
||||||
|
context.update({
|
||||||
|
'object': self.object,
|
||||||
|
})
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_object(self, queryset=None):
|
||||||
|
obj = self.orchestra.retrieve_mailbox(self.kwargs['pk'])
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
data = {
|
||||||
|
'password': form.cleaned_data['password2']
|
||||||
|
}
|
||||||
|
status, response = self.orchestra.set_password_mailbox(self.kwargs['pk'], data)
|
||||||
|
|
||||||
|
if status < 400:
|
||||||
|
messages.success(self.request, _('Password updated!'))
|
||||||
|
else:
|
||||||
|
messages.error(self.request, _('Cannot process your request, please try again later.'))
|
||||||
|
logger.error("{}: {}".format(status, str(response)[:100]))
|
||||||
|
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
class DatabasesView(ServiceListView):
|
class DatabasesView(ServiceListView):
|
||||||
template_name = "musician/databases.html"
|
template_name = "musician/databases.html"
|
||||||
service_class = DatabaseService
|
service_class = DatabaseService
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
django==2.2.24
|
django==2.2.24
|
||||||
python-decouple==3.1
|
python-decouple==3.1
|
||||||
|
django-bootstrap4
|
||||||
django-extensions
|
django-extensions
|
||||||
dj_database_url==0.5.0
|
dj_database_url==0.5.0
|
||||||
requests==2.22.0
|
requests==2.22.0
|
||||||
|
|
|
@ -13,6 +13,7 @@ https://docs.djangoproject.com/en/2.2/ref/settings/
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from decouple import config, Csv
|
from decouple import config, Csv
|
||||||
|
from django.contrib.messages import constants as messages
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from dj_database_url import parse as db_url
|
from dj_database_url import parse as db_url
|
||||||
|
|
||||||
|
@ -41,6 +42,8 @@ EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='')
|
||||||
|
|
||||||
EMAIL_PORT = config('EMAIL_PORT', default=25, cast=int)
|
EMAIL_PORT = config('EMAIL_PORT', default=25, cast=int)
|
||||||
|
|
||||||
|
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||||
|
|
||||||
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=[], cast=Csv())
|
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=[], cast=Csv())
|
||||||
|
|
||||||
|
|
||||||
|
@ -53,6 +56,7 @@ INSTALLED_APPS = [
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'django_extensions',
|
'django_extensions',
|
||||||
|
'bootstrap4',
|
||||||
'musician',
|
'musician',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -148,12 +152,6 @@ USE_L10N = True
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
LANGUAGES = (
|
|
||||||
('ca', _('Catalan')),
|
|
||||||
('es', _('Spanish')),
|
|
||||||
('en', _('English')),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
||||||
|
|
||||||
|
@ -176,3 +174,18 @@ URL_SAAS_GITLAB = config('URL_SAAS_GITLAB', None)
|
||||||
URL_SAAS_OWNCLOUD = config('URL_SAAS_OWNCLOUD', None)
|
URL_SAAS_OWNCLOUD = config('URL_SAAS_OWNCLOUD', None)
|
||||||
|
|
||||||
URL_SAAS_WORDPRESS = config('URL_SAAS_WORDPRESS', None)
|
URL_SAAS_WORDPRESS = config('URL_SAAS_WORDPRESS', None)
|
||||||
|
|
||||||
|
|
||||||
|
# Managers: who should get notifications about services changes that
|
||||||
|
# may require human actions (e.g. deleted mailboxes)
|
||||||
|
MANAGERS = []
|
||||||
|
|
||||||
|
|
||||||
|
# redefine MESSAGE_TAGS for a better integration with bootstrap
|
||||||
|
MESSAGE_TAGS = {
|
||||||
|
messages.DEBUG: 'debug',
|
||||||
|
messages.INFO: 'info',
|
||||||
|
messages.SUCCESS: 'success',
|
||||||
|
messages.WARNING: 'warning',
|
||||||
|
messages.ERROR: 'danger',
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue