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
|
||||
|
||||
from itertools import groupby
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.http import Http404
|
||||
from django.urls.exceptions import NoReverseMatch
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Domain, DatabaseService, MailService, SaasService, UserAccount, WebSite
|
||||
|
||||
from .models import Address, DatabaseService, Domain, Mailbox, SaasService, UserAccount, WebSite
|
||||
|
||||
DOMAINS_PATH = 'domains/'
|
||||
TOKEN_PATH = '/api-token-auth/'
|
||||
|
@ -23,7 +21,10 @@ API_PATHS = {
|
|||
'domain-list': 'domains/',
|
||||
'domain-detail': 'domains/{pk}/',
|
||||
'address-list': 'addresses/',
|
||||
'address-detail': 'addresses/{pk}/',
|
||||
'mailbox-list': 'mailboxes/',
|
||||
'mailbox-detail': 'mailboxes/{pk}/',
|
||||
'mailbox-password': 'mailboxes/{pk}/set_password/',
|
||||
'mailinglist-list': 'lists/',
|
||||
'saas-list': 'saas/',
|
||||
'website-list': 'websites/',
|
||||
|
@ -62,7 +63,7 @@ class Orchestra(object):
|
|||
|
||||
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"]
|
||||
if resource is not None:
|
||||
url = self.build_absolute_uri(resource)
|
||||
|
@ -73,14 +74,17 @@ class Orchestra(object):
|
|||
url = "{}?{}".format(url, querystring)
|
||||
|
||||
verb = getattr(self.session, verb.lower())
|
||||
response = verb(url, headers={"Authorization": "Token {}".format(
|
||||
self.auth_token)}, allow_redirects=False)
|
||||
headers = {
|
||||
"Authorization": "Token {}".format(self.auth_token),
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
response = verb(url, json=data, headers=headers, allow_redirects=False)
|
||||
|
||||
if raise_exception:
|
||||
response.raise_for_status()
|
||||
|
||||
status = response.status_code
|
||||
if render_as == "json":
|
||||
if status < 500 and render_as == "json":
|
||||
output = response.json()
|
||||
else:
|
||||
output = response.content
|
||||
|
@ -109,52 +113,95 @@ class Orchestra(object):
|
|||
raise Http404(_("No domain found matching the query"))
|
||||
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 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)
|
||||
raw_data = self.retrieve_service_list(
|
||||
MailService.api_name,
|
||||
Address.api_name,
|
||||
querystring=querystring,
|
||||
)
|
||||
|
||||
# group addresses with the same mailbox
|
||||
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))
|
||||
addresses = [Address.new_from_json(data) for data in raw_data]
|
||||
|
||||
# PATCH to include Pangea addresses not shown by orchestra
|
||||
# described on issue #4
|
||||
raw_mailboxes = self.retrieve_service_list('mailbox')
|
||||
for mailbox in raw_mailboxes:
|
||||
if mailbox['addresses'] == []:
|
||||
address_data = {
|
||||
'names': [mailbox['name']],
|
||||
'forward': '',
|
||||
'domain': {
|
||||
'name': 'pangea.org.',
|
||||
},
|
||||
'mailboxes': [mailbox],
|
||||
}
|
||||
pangea_address = MailService.new_from_json(address_data)
|
||||
addresses.append(pangea_address)
|
||||
# TODO(@slamora) disabled hacky patch because breaks another funtionalities
|
||||
# XXX Fix it on orchestra instead of here???
|
||||
# raw_mailboxes = self.retrieve_mailbox_list()
|
||||
# for mailbox in raw_mailboxes:
|
||||
# if mailbox['addresses'] == []:
|
||||
# address_data = {
|
||||
# 'names': [mailbox['name']],
|
||||
# 'forward': '',
|
||||
# 'domain': {
|
||||
# 'name': 'pangea.org.',
|
||||
# },
|
||||
# 'mailboxes': [mailbox],
|
||||
# }
|
||||
# pangea_address = Address.new_from_json(address_data)
|
||||
# addresses.append(pangea_address)
|
||||
|
||||
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):
|
||||
path = API_PATHS.get('domain-detail').format_map({'pk': pk})
|
||||
|
||||
|
@ -174,8 +221,8 @@ class Orchestra(object):
|
|||
querystring = "domain={}".format(domain_json['id'])
|
||||
|
||||
# retrieve services associated to a domain
|
||||
domain_json['mails'] = self.retrieve_service_list(
|
||||
MailService.api_name, querystring)
|
||||
domain_json['addresses'] = self.retrieve_service_list(
|
||||
Address.api_name, querystring)
|
||||
|
||||
# retrieve websites (as they cannot be filtered by domain on the API we should do it here)
|
||||
domain_json['websites'] = self.filter_websites_by_domain(websites, domain_json['id'])
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
|
||||
from django import forms
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from . import api
|
||||
|
||||
|
@ -20,3 +23,135 @@ class LoginForm(AuthenticationForm):
|
|||
self.user = orchestra.retrieve_profile()
|
||||
|
||||
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 ""
|
||||
"Project-Id-Version: \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"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
|
@ -18,10 +18,34 @@ msgstr ""
|
|||
"Plural-Forms: nplurals=2; plural=(n != 1);\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"
|
||||
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
|
||||
msgid "Domains & websites"
|
||||
msgstr "Dominis i llocs web"
|
||||
|
@ -31,12 +55,12 @@ msgid "Mails"
|
|||
msgstr "Correus"
|
||||
|
||||
#. Translators: This message appears on the page title
|
||||
#: mixins.py:16 views.py:226
|
||||
#: mixins.py:16 views.py:291
|
||||
msgid "Mailing lists"
|
||||
msgstr "Llistes de correu"
|
||||
|
||||
#. 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"
|
||||
msgstr "Bases de dades"
|
||||
|
||||
|
@ -44,36 +68,41 @@ msgstr "Bases de dades"
|
|||
msgid "SaaS"
|
||||
msgstr "SaaS"
|
||||
|
||||
#: models.py:139
|
||||
#, fuzzy
|
||||
#: models.py:148
|
||||
msgid "Description details for databases page."
|
||||
msgstr "Consulta la configuració de les teves bases de dades."
|
||||
|
||||
#. Translators: This message appears on the page title
|
||||
#: models.py:200 views.py:169
|
||||
#: models.py:235 views.py:181
|
||||
msgid "Mail addresses"
|
||||
msgstr "Adreces de correu"
|
||||
|
||||
#: models.py:201
|
||||
#, fuzzy
|
||||
#: models.py:236
|
||||
msgid "Description details for mail addresses page."
|
||||
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"
|
||||
msgstr "Llista de correu"
|
||||
|
||||
#: models.py:244
|
||||
#, fuzzy
|
||||
#: models.py:338
|
||||
msgid "Description details for mailinglist page."
|
||||
msgstr "Consulta aquí els detalls de les teves llistes de correu."
|
||||
|
||||
#: models.py:267
|
||||
#: models.py:364
|
||||
msgid "Software as a Service (SaaS)"
|
||||
msgstr "Software as a Service (SaaS)"
|
||||
|
||||
#: models.py:268
|
||||
#, fuzzy
|
||||
#: models.py:365
|
||||
msgid "Description details for SaaS page."
|
||||
msgstr ""
|
||||
"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> "
|
||||
"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
|
||||
msgid "Settings"
|
||||
msgstr "Configuració"
|
||||
|
@ -110,7 +191,7 @@ msgstr "Perfil"
|
|||
|
||||
#. Translators: This message appears on the page title
|
||||
#: templates/musician/base.html:64 templates/musician/billing.html:6
|
||||
#: views.py:147
|
||||
#: views.py:159
|
||||
msgid "Billing"
|
||||
msgstr "Factures"
|
||||
|
||||
|
@ -119,7 +200,6 @@ msgid "Log out"
|
|||
msgstr "Surt"
|
||||
|
||||
#: templates/musician/billing.html:7
|
||||
#, fuzzy
|
||||
msgid "Billing page description."
|
||||
msgstr "Consulta i descarrega les teves factures."
|
||||
|
||||
|
@ -132,7 +212,7 @@ msgid "Bill date"
|
|||
msgstr "Data de la factura"
|
||||
|
||||
#: 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"
|
||||
msgstr "Tipus"
|
||||
|
||||
|
@ -178,7 +258,6 @@ msgid "Your domains and websites"
|
|||
msgstr "Els teus dominis i llocs web"
|
||||
|
||||
#: templates/musician/dashboard.html:36
|
||||
#, fuzzy
|
||||
msgid "Dashboard page description."
|
||||
msgstr ""
|
||||
"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"
|
||||
|
||||
#. 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"
|
||||
msgstr "Software as a Service"
|
||||
|
||||
|
@ -217,7 +296,7 @@ msgstr "Software as a Service"
|
|||
msgid "Nothing installed"
|
||||
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"
|
||||
msgstr "Ús del disc"
|
||||
|
||||
|
@ -279,7 +358,6 @@ msgid "DNS settings for"
|
|||
msgstr "Configuració DNS per a"
|
||||
|
||||
#: templates/musician/domain_detail.html:8
|
||||
#, fuzzy
|
||||
msgid "DNS settings page description."
|
||||
msgstr "Consulta aquí la teva configuració DNS."
|
||||
|
||||
|
@ -287,25 +365,57 @@ msgstr "Consulta aquí la teva configuració DNS."
|
|||
msgid "Value"
|
||||
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"
|
||||
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"
|
||||
msgstr "per a"
|
||||
|
||||
#: templates/musician/mail.html:20
|
||||
msgid "Mail address"
|
||||
msgstr "Adreça de correu"
|
||||
#: templates/musician/mail_base.html:18 templates/musician/mailboxes.html:16
|
||||
msgid "Addresses"
|
||||
msgstr "Adreces de correu"
|
||||
|
||||
#: templates/musician/mail.html:21
|
||||
msgid "Aliases"
|
||||
msgstr "Àlies"
|
||||
#: templates/musician/mailbox_check_delete.html:7
|
||||
#, python-format
|
||||
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
|
||||
msgid "Type details"
|
||||
msgstr "Detalls de cada tipus"
|
||||
#: templates/musician/mailbox_check_delete.html:9
|
||||
msgid ""
|
||||
"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
|
||||
msgid "Active"
|
||||
|
@ -316,7 +426,6 @@ msgid "Inactive"
|
|||
msgstr "Inactiu"
|
||||
|
||||
#: templates/musician/profile.html:7
|
||||
#, fuzzy
|
||||
msgid "Little description on profile page."
|
||||
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"
|
||||
|
||||
#. Translators: This message appears on the page title
|
||||
#: views.py:32
|
||||
#: views.py:37
|
||||
msgid "Dashboard"
|
||||
msgstr "Panell de gestió"
|
||||
|
||||
#: views.py:49
|
||||
#: views.py:76
|
||||
msgid "Traffic"
|
||||
msgstr "Tràfic"
|
||||
|
||||
#: views.py:56
|
||||
#: views.py:85
|
||||
msgid "Mailbox usage"
|
||||
msgstr "Ús d’espai a la bústia de correu"
|
||||
|
||||
#. Translators: This message appears on the page title
|
||||
#: views.py:96
|
||||
#: views.py:108
|
||||
msgid "User profile"
|
||||
msgstr "El teu perfil"
|
||||
|
||||
#. Translators: This message appears on the page title
|
||||
#: views.py:154
|
||||
#: views.py:166
|
||||
msgid "Download bill"
|
||||
msgstr "Descarrega la factura"
|
||||
|
||||
#. Translators: This message appears on the page title
|
||||
#: views.py:272
|
||||
#: views.py:444
|
||||
msgid "Domain details"
|
||||
msgstr "Detalls del domini"
|
||||
|
||||
#. Translators: This message appears on the page title
|
||||
#: views.py:298
|
||||
#: views.py:470
|
||||
msgid "Login"
|
||||
msgstr "Accés"
|
||||
|
||||
#~ msgid "Aliases"
|
||||
#~ msgstr "Àlies"
|
||||
|
||||
#~ msgid "Type details"
|
||||
#~ msgstr "Detalls de cada tipus"
|
||||
|
||||
#~ msgid "databases created"
|
||||
#~ msgstr "bases de dades creades"
|
||||
|
||||
#~ msgid "Username"
|
||||
#~ msgstr "Nom d’usuari/a"
|
||||
|
||||
#~ msgid "Password:"
|
||||
#~ msgstr "Contrasenya:"
|
||||
|
|
|
@ -7,7 +7,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: \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"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
|
@ -18,10 +18,34 @@ msgstr ""
|
|||
"Plural-Forms: nplurals=2; plural=(n != 1);\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"
|
||||
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
|
||||
msgid "Domains & websites"
|
||||
msgstr "Dominios y sitios web"
|
||||
|
@ -31,12 +55,12 @@ msgid "Mails"
|
|||
msgstr "Correos"
|
||||
|
||||
#. Translators: This message appears on the page title
|
||||
#: mixins.py:16 views.py:226
|
||||
#: mixins.py:16 views.py:291
|
||||
msgid "Mailing lists"
|
||||
msgstr "Listas de correo"
|
||||
|
||||
#. 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"
|
||||
msgstr "Bases de datos"
|
||||
|
||||
|
@ -44,36 +68,41 @@ msgstr "Bases de datos"
|
|||
msgid "SaaS"
|
||||
msgstr "SaaS"
|
||||
|
||||
#: models.py:139
|
||||
#, fuzzy
|
||||
#: models.py:148
|
||||
msgid "Description details for databases page."
|
||||
msgstr "Consulta la configuración de tus bases de datos."
|
||||
|
||||
#. Translators: This message appears on the page title
|
||||
#: models.py:200 views.py:169
|
||||
#: models.py:235 views.py:181
|
||||
msgid "Mail addresses"
|
||||
msgstr "Direcciones de correo"
|
||||
|
||||
#: models.py:201
|
||||
#, fuzzy
|
||||
#: models.py:236
|
||||
msgid "Description details for mail addresses page."
|
||||
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"
|
||||
msgstr "Lista de correo"
|
||||
|
||||
#: models.py:244
|
||||
#, fuzzy
|
||||
#: models.py:338
|
||||
msgid "Description details for mailinglist page."
|
||||
msgstr "Consulta aquí los detalles de tus listas de correo."
|
||||
|
||||
#: models.py:267
|
||||
#: models.py:364
|
||||
msgid "Software as a Service (SaaS)"
|
||||
msgstr "Software as a Service (SaaS)"
|
||||
|
||||
#: models.py:268
|
||||
#, fuzzy
|
||||
#: models.py:365
|
||||
msgid "Description details for SaaS page."
|
||||
msgstr ""
|
||||
"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> "
|
||||
"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
|
||||
msgid "Settings"
|
||||
msgstr "Configuración"
|
||||
|
@ -110,7 +192,7 @@ msgstr "Perfil"
|
|||
|
||||
#. Translators: This message appears on the page title
|
||||
#: templates/musician/base.html:64 templates/musician/billing.html:6
|
||||
#: views.py:147
|
||||
#: views.py:159
|
||||
msgid "Billing"
|
||||
msgstr "Facturas"
|
||||
|
||||
|
@ -119,7 +201,6 @@ msgid "Log out"
|
|||
msgstr "Desconéctate"
|
||||
|
||||
#: templates/musician/billing.html:7
|
||||
#, fuzzy
|
||||
msgid "Billing page description."
|
||||
msgstr "Consulta y descarga tus facturas."
|
||||
|
||||
|
@ -132,7 +213,7 @@ msgid "Bill date"
|
|||
msgstr "Fecha de la factura"
|
||||
|
||||
#: 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"
|
||||
msgstr "Tipo"
|
||||
|
||||
|
@ -178,7 +259,6 @@ msgid "Your domains and websites"
|
|||
msgstr "Tus dominios y sitios web"
|
||||
|
||||
#: templates/musician/dashboard.html:36
|
||||
#, fuzzy
|
||||
msgid "Dashboard page description."
|
||||
msgstr ""
|
||||
"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"
|
||||
|
||||
#. 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"
|
||||
msgstr "Software as a Service"
|
||||
|
||||
|
@ -217,7 +297,7 @@ msgstr "Software as a Service"
|
|||
msgid "Nothing installed"
|
||||
msgstr "No tienes nada instalado"
|
||||
|
||||
#: templates/musician/dashboard.html:89 views.py:42
|
||||
#: templates/musician/dashboard.html:89 views.py:67
|
||||
msgid "Disk usage"
|
||||
msgstr "Uso del disco"
|
||||
|
||||
|
@ -233,8 +313,8 @@ msgstr "Acceso FTP:"
|
|||
#: templates/musician/dashboard.html:115
|
||||
msgid "Contact with the support team to get details concerning FTP access."
|
||||
msgstr ""
|
||||
"Contactadnos a <a href=“mailto:%(support_email)s”>%(support_email)s</a> "
|
||||
"para saber cómo acceder al FTP."
|
||||
"Contactadnos a <a href=“mailto:%(support_email)s”>%(support_email)s</a> para "
|
||||
"saber cómo acceder al FTP."
|
||||
|
||||
#: templates/musician/dashboard.html:124
|
||||
msgid "No website configured."
|
||||
|
@ -279,7 +359,6 @@ msgid "DNS settings for"
|
|||
msgstr "Configuración DNS para"
|
||||
|
||||
#: templates/musician/domain_detail.html:8
|
||||
#, fuzzy
|
||||
msgid "DNS settings page description."
|
||||
msgstr "Consulta aquí tu configuración DNS."
|
||||
|
||||
|
@ -287,25 +366,55 @@ msgstr "Consulta aquí tu configuración DNS."
|
|||
msgid "Value"
|
||||
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"
|
||||
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"
|
||||
msgstr "para"
|
||||
|
||||
#: templates/musician/mail.html:20
|
||||
msgid "Mail address"
|
||||
msgstr "Dirección de correo"
|
||||
#: templates/musician/mail_base.html:18 templates/musician/mailboxes.html:16
|
||||
msgid "Addresses"
|
||||
msgstr "Direcciones de correo"
|
||||
|
||||
#: templates/musician/mail.html:21
|
||||
msgid "Aliases"
|
||||
msgstr "Alias"
|
||||
#: templates/musician/mailbox_check_delete.html:7
|
||||
#, python-format
|
||||
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
|
||||
msgid "Type details"
|
||||
msgstr "Detalles de cada tipo"
|
||||
#: templates/musician/mailbox_check_delete.html:9
|
||||
msgid ""
|
||||
"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
|
||||
msgid "Active"
|
||||
|
@ -316,7 +425,6 @@ msgid "Inactive"
|
|||
msgstr "Inactivo"
|
||||
|
||||
#: templates/musician/profile.html:7
|
||||
#, fuzzy
|
||||
msgid "Little description on profile page."
|
||||
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"
|
||||
|
||||
#. Translators: This message appears on the page title
|
||||
#: views.py:32
|
||||
#: views.py:37
|
||||
msgid "Dashboard"
|
||||
msgstr "Panel de gestión"
|
||||
|
||||
#: views.py:49
|
||||
#: views.py:76
|
||||
msgid "Traffic"
|
||||
msgstr "Tráfico"
|
||||
|
||||
#: views.py:56
|
||||
#: views.py:85
|
||||
msgid "Mailbox usage"
|
||||
msgstr "Uso de espacio en tu buzón de correo"
|
||||
|
||||
#. Translators: This message appears on the page title
|
||||
#: views.py:96
|
||||
#: views.py:108
|
||||
msgid "User profile"
|
||||
msgstr "Tu perfil"
|
||||
|
||||
#. Translators: This message appears on the page title
|
||||
#: views.py:154
|
||||
#: views.py:166
|
||||
msgid "Download bill"
|
||||
msgstr "Descarga la factura"
|
||||
|
||||
#. Translators: This message appears on the page title
|
||||
#: views.py:272
|
||||
#: views.py:444
|
||||
msgid "Domain details"
|
||||
msgstr "Detalles del dominio"
|
||||
|
||||
#. Translators: This message appears on the page title
|
||||
#: views.py:298
|
||||
#: views.py:470
|
||||
msgid "Login"
|
||||
msgstr "Accede"
|
||||
|
||||
#~ msgid "Aliases"
|
||||
#~ msgstr "Alias"
|
||||
|
||||
#~ msgid "Type details"
|
||||
#~ msgstr "Detalles de cada tipo"
|
||||
|
||||
#~ msgid "databases created"
|
||||
#~ msgstr "bases de datos creadas"
|
||||
|
||||
#~ msgid "Username"
|
||||
#~ msgstr "Nombre de usuario/a"
|
||||
|
||||
#~ msgid "Password:"
|
||||
#~ msgstr "Contraseña:"
|
||||
|
|
|
@ -12,10 +12,10 @@ class CustomContextMixin(ContextMixin):
|
|||
# generate services menu items
|
||||
services_menu = [
|
||||
{'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': 'database', 'pattern_name': 'musician:databases', 'title': _('Databases')},
|
||||
{'icon': 'fire', 'pattern_name': 'musician:saas', 'title': _('SaaS')},
|
||||
{'icon': 'database', 'pattern_name': 'musician:database-list', 'title': _('Databases')},
|
||||
{'icon': 'fire', 'pattern_name': 'musician:saas-list', 'title': _('SaaS')},
|
||||
]
|
||||
context.update({
|
||||
'services_menu': services_menu,
|
||||
|
|
|
@ -17,6 +17,7 @@ class OrchestraModel:
|
|||
api_name = None
|
||||
verbose_name = None
|
||||
fields = ()
|
||||
param_defaults = {}
|
||||
id = None
|
||||
|
||||
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)
|
||||
|
||||
def allowed_resources(self, resource):
|
||||
allowed_by_type = musician_settings.ALLOWED_RESOURCES[self.type]
|
||||
return allowed_by_type[resource]
|
||||
|
||||
|
||||
class DatabaseUser(OrchestraModel):
|
||||
api_name = 'databaseusers'
|
||||
|
@ -161,7 +166,7 @@ class DatabaseService(OrchestraModel):
|
|||
return super().new_from_json(data=data, users=users, usage=usage)
|
||||
|
||||
@classmethod
|
||||
def get_usage(self, data):
|
||||
def get_usage(cls, data):
|
||||
try:
|
||||
resources = data['resources']
|
||||
resource_disk = {}
|
||||
|
@ -198,9 +203,10 @@ class Domain(OrchestraModel):
|
|||
"id": None,
|
||||
"name": None,
|
||||
"records": [],
|
||||
"mails": [],
|
||||
"addresses": [],
|
||||
"usage": {},
|
||||
"websites": [],
|
||||
"url": None,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
@ -224,12 +230,19 @@ class DomainRecord(OrchestraModel):
|
|||
return '<%s: %s>' % (self.type, self.value)
|
||||
|
||||
|
||||
class MailService(OrchestraModel):
|
||||
class Address(OrchestraModel):
|
||||
api_name = 'address'
|
||||
verbose_name = _('Mail addresses')
|
||||
description = _('Description details for mail addresses page.')
|
||||
fields = ('mail_address', 'aliases', 'type', 'type_detail')
|
||||
param_defaults = {}
|
||||
param_defaults = {
|
||||
"id": None,
|
||||
"name": None,
|
||||
"domain": None,
|
||||
"mailboxes": [],
|
||||
"forward": None,
|
||||
'url': None,
|
||||
}
|
||||
|
||||
FORWARD = 'forward'
|
||||
MAILBOX = 'mailbox'
|
||||
|
@ -238,6 +251,15 @@ class MailService(OrchestraModel):
|
|||
self.data = 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
|
||||
def aliases(self):
|
||||
return [
|
||||
|
@ -245,8 +267,8 @@ class MailService(OrchestraModel):
|
|||
]
|
||||
|
||||
@property
|
||||
def mail_address(self):
|
||||
return self.data['names'][0] + '@' + self.data['domain']['name']
|
||||
def full_address_name(self):
|
||||
return "{}@{}".format(self.name, self.domain['name'])
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
|
@ -284,6 +306,32 @@ class MailService(OrchestraModel):
|
|||
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):
|
||||
api_name = 'mailinglist'
|
||||
verbose_name = _('Mailing list')
|
||||
|
@ -301,7 +349,10 @@ class MailinglistService(OrchestraModel):
|
|||
|
||||
@property
|
||||
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
|
||||
def manager_url(self):
|
||||
|
|
|
@ -12,16 +12,15 @@ a:hover {
|
|||
background: #D3D0DA;
|
||||
position: relative;
|
||||
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::before{
|
||||
.btn-arrow-left::after, .btn-arrow-left::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: -1em;
|
||||
|
||||
margin-top: -19px;
|
||||
border-top: 19px solid transparent;
|
||||
border-bottom: 19px solid transparent;
|
||||
|
@ -43,13 +42,12 @@ a:hover {
|
|||
min-width: 280px;
|
||||
max-width: 280px;
|
||||
min-height: 100vh;
|
||||
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#sidebar #sidebar-services {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
@ -62,6 +60,7 @@ a:hover {
|
|||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
#sidebar #sidebar-services {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
|
@ -75,7 +74,6 @@ a:hover {
|
|||
padding: 20px 0;
|
||||
}
|
||||
|
||||
|
||||
#sidebar ul li a {
|
||||
padding: 10px;
|
||||
font-size: 1.1em;
|
||||
|
@ -89,14 +87,16 @@ a:hover {
|
|||
}
|
||||
|
||||
.vertical-center {
|
||||
min-height: 100%; /* Fallback for browsers do NOT support vh unit */
|
||||
min-height: 100vh; /* These two lines are counted as one :-) */
|
||||
|
||||
min-height: 100%;
|
||||
/* Fallback for browsers do NOT support vh unit */
|
||||
min-height: 100vh;
|
||||
/* These two lines are counted as one :-) */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/** login **/
|
||||
|
||||
#body-login .jumbotron {
|
||||
background: #282532 no-repeat url("../images/logo-pangea-lilla-bg.svg") right;
|
||||
}
|
||||
|
@ -106,8 +106,7 @@ a:hover {
|
|||
padding: 2rem;
|
||||
}
|
||||
|
||||
#login-content input[type="text"].form-control,
|
||||
#login-content input[type="password"].form-control {
|
||||
#login-content input[type="text"].form-control, #login-content input[type="password"].form-control {
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
border-bottom: 2px solid #8E8E8E;
|
||||
|
@ -121,6 +120,7 @@ a:hover {
|
|||
margin-top: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#login-footer a {
|
||||
color: #FEFBF2;
|
||||
}
|
||||
|
@ -130,34 +130,37 @@ a:hover {
|
|||
background-position: right 5% top 10%;
|
||||
color: #343434;
|
||||
padding-left: 2rem;
|
||||
margin-left: 280px; /** sidebar width **/
|
||||
margin-left: 280px;
|
||||
/** sidebar width **/
|
||||
}
|
||||
|
||||
/** services **/
|
||||
h1.service-name {
|
||||
|
||||
h1.service-name {
|
||||
font: Bold 26px/34px Roboto;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.service-description {
|
||||
|
||||
font: 16px/21px Roboto;
|
||||
}
|
||||
|
||||
.table.service-list {
|
||||
margin-top: 2rem;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
/** 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);
|
||||
color: #50466E;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
/** /TODO **/
|
||||
.table.service-list td,
|
||||
.table.service-list th {
|
||||
|
||||
.table.service-list td, .table.service-list th {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
@ -202,7 +205,6 @@ h1.service-name {
|
|||
|
||||
.service-card .card-body {
|
||||
color: #787878;
|
||||
|
||||
}
|
||||
|
||||
.service-card .card-body i.fas {
|
||||
|
@ -215,8 +217,7 @@ h1.service-name {
|
|||
right: 15px;
|
||||
}
|
||||
|
||||
.service-card .service-manager-link a,
|
||||
.service-card .service-manager-link a i.fas {
|
||||
.service-card .service-manager-link a, .service-card .service-manager-link a i.fas {
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
@ -243,11 +244,9 @@ h1.service-name {
|
|||
font-variant: normal;
|
||||
text-rendering: auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 10px;
|
||||
|
||||
color: #E8E7EB;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
@ -308,3 +307,13 @@ h1.service-name {
|
|||
border-top: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.roll-hover {
|
||||
visibility: hidden;
|
||||
display: inline-block;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
td:hover .roll-hover {
|
||||
visibility: visible;
|
||||
}
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
||||
</nav><!-- ./sidebar -->
|
||||
<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 %}
|
||||
{% endblock content %}
|
||||
</div><!-- ./content -->
|
||||
|
|
|
@ -16,6 +16,11 @@
|
|||
<div class="card-body">
|
||||
<h5 class="card-title">{{ usage.verbose_name }}</h5>
|
||||
{% 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>
|
||||
{% endfor %}
|
||||
|
@ -65,13 +70,9 @@
|
|||
<h4>{% trans "Mail" %}</h4>
|
||||
<p class="card-text"><i class="fas fa-envelope fa-3x"></i></p>
|
||||
<p class="card-text text-dark">
|
||||
{{ domain.mails|length }} {% trans "mail addresses created" %}
|
||||
{% if domain.addresses_left.alert_level %}
|
||||
<br/>
|
||||
<span class="text-{{ domain.addresses_left.alert_level }}">{{ domain.addresses_left.count }} {% trans "mail address left" %}</span>
|
||||
{% endif %}
|
||||
{{ domain.addresses|length }} {% trans "mail addresses created" %}
|
||||
</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 class="col-md-2 border-right">
|
||||
<h4>{% trans "Mail list" %}</h4>
|
||||
|
@ -82,7 +83,7 @@
|
|||
<h4>{% trans "Software as a Service" %}</h4>
|
||||
<p class="card-text"><i class="fas fa-fire fa-3x"></i></p>
|
||||
<p class="card-text text-dark">{% trans "Nothing installed" %}</p>
|
||||
<a class="stretched-link" href="{% url 'musician:saas' %}?domain={{ domain.id }}"></a>
|
||||
<a class="stretched-link" href="{% url 'musician:saas-list' %}?domain={{ domain.id }}"></a>
|
||||
</div>
|
||||
<div class="col-md-1"></div>
|
||||
<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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 .models import UserAccount
|
||||
from .models import DatabaseService, UserAccount
|
||||
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):
|
||||
def test_domain_not_found(self):
|
||||
response = self.client.post(
|
||||
|
@ -118,3 +146,8 @@ class GetBootstrapedPercentTest(TestCase):
|
|||
|
||||
def test_invalid_total_is_zero(self):
|
||||
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('dashboard/', views.DashboardView.as_view(), name='dashboard'),
|
||||
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('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('databases/', views.DatabasesView.as_view(), name='databases'),
|
||||
path('software-as-a-service/', views.SaasView.as_view(), name='saas'),
|
||||
path('databases/', views.DatabasesView.as_view(), name='database-list'),
|
||||
path('saas/', views.SaasView.as_view(), name='saas-list'),
|
||||
]
|
||||
|
|
|
@ -6,7 +6,7 @@ def get_bootstraped_percent(value, total):
|
|||
"""
|
||||
try:
|
||||
percent = value / total
|
||||
except ZeroDivisionError:
|
||||
except (TypeError, ZeroDivisionError):
|
||||
return 0
|
||||
|
||||
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.contrib import messages
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.mail import mail_managers
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils import translation
|
||||
from django.utils.html import format_html
|
||||
from django.utils.http import is_safe_url
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from django.views.generic.base import RedirectView, TemplateView
|
||||
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 requests.exceptions import HTTPError
|
||||
|
||||
from . import api, get_version
|
||||
from .auth import login as auth_login
|
||||
from .auth import logout as auth_logout
|
||||
from .forms import LoginForm
|
||||
from .forms import LoginForm, MailboxChangePasswordForm, MailboxCreateForm, MailboxUpdateForm, MailForm
|
||||
from .mixins import (CustomContextMixin, ExtendedPaginationMixin,
|
||||
UserTokenRequiredMixin)
|
||||
from .models import (Bill, DatabaseService, MailinglistService, MailService,
|
||||
PaymentSource, SaasService, UserAccount)
|
||||
from .models import (Address, Bill, DatabaseService, Mailbox,
|
||||
MailinglistService, PaymentSource, SaasService)
|
||||
from .settings import ALLOWED_RESOURCES
|
||||
from .utils import get_bootstraped_percent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
||||
template_name = "musician/dashboard.html"
|
||||
|
@ -40,20 +50,6 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
|||
|
||||
# show resource usage based on plan definition
|
||||
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
|
||||
resource_usage = {
|
||||
|
@ -75,15 +71,7 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
|||
# 'percent': 25,
|
||||
},
|
||||
},
|
||||
'mailbox': {
|
||||
'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']),
|
||||
},
|
||||
},
|
||||
'mailbox': self.get_mailbox_usage(profile_type),
|
||||
}
|
||||
|
||||
context.update({
|
||||
|
@ -94,6 +82,28 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
|||
|
||||
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):
|
||||
template_name = "musician/profile.html"
|
||||
|
@ -168,8 +178,8 @@ class BillDownloadView(CustomContextMixin, UserTokenRequiredMixin, View):
|
|||
|
||||
|
||||
class MailView(ServiceListView):
|
||||
service_class = MailService
|
||||
template_name = "musician/mail.html"
|
||||
service_class = Address
|
||||
template_name = "musician/addresses.html"
|
||||
extra_context = {
|
||||
# Translators: This message appears on the page title
|
||||
'title': _('Mail addresses'),
|
||||
|
@ -198,9 +208,86 @@ class MailView(ServiceListView):
|
|||
context.update({
|
||||
'active_domain': self.orchestra.retrieve_domain(domain_id)
|
||||
})
|
||||
context['mailboxes'] = self.orchestra.retrieve_mailbox_list()
|
||||
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):
|
||||
service_class = MailinglistService
|
||||
template_name = "musician/mailinglists.html"
|
||||
|
@ -230,6 +317,161 @@ class MailingListsView(ServiceListView):
|
|||
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):
|
||||
template_name = "musician/databases.html"
|
||||
service_class = DatabaseService
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
django==2.2.24
|
||||
python-decouple==3.1
|
||||
django-bootstrap4
|
||||
django-extensions
|
||||
dj_database_url==0.5.0
|
||||
requests==2.22.0
|
||||
|
|
|
@ -13,6 +13,7 @@ https://docs.djangoproject.com/en/2.2/ref/settings/
|
|||
import os
|
||||
|
||||
from decouple import config, Csv
|
||||
from django.contrib.messages import constants as messages
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
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_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
|
||||
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=[], cast=Csv())
|
||||
|
||||
|
||||
|
@ -53,6 +56,7 @@ INSTALLED_APPS = [
|
|||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django_extensions',
|
||||
'bootstrap4',
|
||||
'musician',
|
||||
]
|
||||
|
||||
|
@ -148,12 +152,6 @@ USE_L10N = True
|
|||
USE_TZ = True
|
||||
|
||||
|
||||
LANGUAGES = (
|
||||
('ca', _('Catalan')),
|
||||
('es', _('Spanish')),
|
||||
('en', _('English')),
|
||||
)
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# 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_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 New Issue