Compare commits
76 Commits
whitesourc
...
master
Author | SHA1 | Date |
---|---|---|
Santiago L | 2062c0c519 | |
Santiago L | fc55c33c90 | |
Santiago L | 1e3a919390 | |
Santiago L | 9b4f2ba3da | |
Santiago L | 41f5493368 | |
RubenPX | c1f25a73da | |
RubenPX | ca3a8c4639 | |
RubenPX | ed3ad7cda0 | |
Santiago L | 49d49a3044 | |
Santiago L | 4911cf4226 | |
Santiago L | 9e193107cd | |
Santiago L | 872243a8c6 | |
Santiago L | 19f5229536 | |
Santiago L | 66530351ad | |
Santiago L | 249a1182d4 | |
RubenPX | 1816301952 | |
RubenPX | feb591ea79 | |
Santiago L | 179918bd62 | |
Santiago L | 57db1eed80 | |
Santiago L | afce4a5527 | |
Santiago L | 62a1d57f7d | |
RubenPX | 6b7cad86f2 | |
RubenPX | 80a93ea8c0 | |
RubenPX | 90b0956f71 | |
dependabot[bot] | b6b980ebaa | |
Santiago L | bb70914066 | |
Santiago L | bc84603b5e | |
Santiago L | 70b256d1ed | |
KryptoPX | 7d799092cd | |
KryptoPX | a7d025fc01 | |
KryptoPX | e203b43a69 | |
KryptoPX | 828bb5f0de | |
KryptoPX | 95d2998d05 | |
KryptoPX | a0cc4d0a41 | |
KryptoPX | 560c48ddaa | |
Santiago L | 74bcbb43a1 | |
Santiago L | 3a7d920611 | |
Santiago L | dd8e7f1f52 | |
Santiago L | 3f46809620 | |
Santiago L | 44f9390bee | |
Santiago L | bd42b83ea3 | |
Santiago L | aee0267f17 | |
Santiago L | d7bd21d865 | |
Santiago L | d77b876a54 | |
Santiago L | b171cbf641 | |
Santiago L | 33e68b5d07 | |
Santiago L | 6c773893f7 | |
Santiago L | 2aab4a666f | |
Santiago L | a13bdeac56 | |
Santiago L | 056f472ee0 | |
Santiago L | ddd8ecf634 | |
Santiago L | a0808896b4 | |
Santiago L | 9b52bc4b92 | |
Santiago L | 9e51457069 | |
Santiago L | ed5460c4b1 | |
Santiago L | b0366ff1d0 | |
Santiago L | 98dfa7a9f4 | |
Santiago L | 6d7ee0b76a | |
Santiago L | a9c59edbf2 | |
Santiago L | 0246d0a22e | |
Santiago L | 9ba1d0a23c | |
Santiago L | bb07bcd126 | |
Santiago L | 2a1a82f271 | |
Santiago L | 0d327127f5 | |
Santiago L | 77577a67da | |
Santiago L | 29c752e572 | |
Santiago L | 4d5497f2fa | |
Santiago L | 7ff01d60ef | |
Santiago L | f635721831 | |
dependabot[bot] | 3061ab34d3 | |
Santiago L | 30bb572589 | |
Santiago L | 5281e9595e | |
Santiago L | db6715808b | |
dependabot[bot] | 13ac215973 | |
Santiago Lamora | 1d5d3a5ed3 | |
Santiago Lamora | c68aec5dc3 |
|
@ -5,8 +5,17 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## master
|
## master
|
||||||
|
|
||||||
|
## [0.2.0] 2022-03-11
|
||||||
|
- [added] Language selector.
|
||||||
|
- [added] Help link.
|
||||||
|
- [changed] Order bills by creation date DESC.
|
||||||
|
|
||||||
|
## [0.2.0-beta1] 2022-02-05
|
||||||
|
- [added] Write operations on mails section (addresses, mailboxes and forward).
|
||||||
- [changed] Include @pangea.org mail addresses (#4).
|
- [changed] Include @pangea.org mail addresses (#4).
|
||||||
- [fixed] Error on login when user never has logged into the system (#6).
|
- [fixed] Error on login when user never has logged into the system (#6).
|
||||||
|
- [fixed] Dashboard layout issues on tablet and mobile.
|
||||||
|
|
||||||
## [0.1] - 2020-01-29
|
## [0.1] - 2020-01-29
|
||||||
- Login & logout methods using backend as auth method
|
- Login & logout methods using backend as auth method
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
FROM python
|
||||||
|
|
||||||
|
WORKDIR /home
|
||||||
|
|
||||||
|
RUN python3 -m pip install --upgrade pip
|
||||||
|
RUN pip install wheel
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
RUN python manage.py migrate
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENTRYPOINT [ "python", "manage.py", "runserver", "0.0.0.0:8080" ]
|
|
@ -1,6 +1,12 @@
|
||||||
# django musician
|
# django musician
|
||||||
Python code is written following [PEP 8](https://www.python.org/dev/peps/pep-0008/) sytle guide and it is based on [Django framework](https://djangoproject.com).
|
Python code is written following [PEP 8](https://www.python.org/dev/peps/pep-0008/) sytle guide and it is based on [Django framework](https://djangoproject.com).
|
||||||
|
|
||||||
|
## Quickstart development
|
||||||
|
Start development environment based on docker compose by running:
|
||||||
|
```bash
|
||||||
|
docker-compose up
|
||||||
|
```
|
||||||
|
|
||||||
## How do I get set up?
|
## How do I get set up?
|
||||||
|
|
||||||
1. Install Python and its packet manager (pip)
|
1. Install Python and its packet manager (pip)
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
version: "3.9"
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- .:/home
|
|
@ -2,7 +2,7 @@
|
||||||
Package metadata definition.
|
Package metadata definition.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VERSION = (0, 2, 0, 'alpha', 1)
|
VERSION = (0, 2, 0, 'final', 0)
|
||||||
|
|
||||||
|
|
||||||
def get_version():
|
def get_version():
|
||||||
|
|
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,8 +7,8 @@ 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-11-24 11:04+0100\n"
|
||||||
"PO-Revision-Date: 2020-01-28 17:27+0100\n"
|
"PO-Revision-Date: 2021-11-25 12:53+0100\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
"Language: ca\n"
|
"Language: ca\n"
|
||||||
|
@ -16,12 +16,36 @@ msgstr ""
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"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 3.0\n"
|
||||||
|
|
||||||
#: api.py:108 api.py:117
|
#: api.py:113 api.py:211
|
||||||
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:125
|
||||||
|
msgid "No object found matching the query"
|
||||||
|
msgstr "No trobem cap objecte que coincideixi amb la teva consulta"
|
||||||
|
|
||||||
|
#: api.py:178
|
||||||
|
msgid "No mailbox found matching the query"
|
||||||
|
msgstr "No trobem cap bústia que coincideixi amb la teva consulta"
|
||||||
|
|
||||||
|
#: forms.py:65 forms.py:99
|
||||||
|
msgid "The two password fields didn’t match."
|
||||||
|
msgstr "Les contrasenyes introduïdes no coincideixen."
|
||||||
|
|
||||||
|
#: forms.py:68 forms.py:103
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "Contrasenya"
|
||||||
|
|
||||||
|
#: forms.py:73 forms.py:108
|
||||||
|
msgid "Password confirmation"
|
||||||
|
msgstr "Confirma la contrasenya"
|
||||||
|
|
||||||
|
#: forms.py:76 forms.py:111
|
||||||
|
msgid "Enter the same password as before, for verification."
|
||||||
|
msgstr "Introdueix la mateixa contrasenya per verificar-la."
|
||||||
|
|
||||||
#: 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:296
|
||||||
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:480
|
||||||
msgid "Databases"
|
msgid "Databases"
|
||||||
msgstr "Bases de dades"
|
msgstr "Bases de dades"
|
||||||
|
|
||||||
|
@ -44,36 +68,40 @@ 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:185
|
||||||
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 el detall de les bústies de correu que tens actives."
|
||||||
|
|
||||||
|
#: 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 +128,60 @@ 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/a que vols esborrar l’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:25
|
||||||
|
msgid "Delete"
|
||||||
|
msgstr "Esborrar"
|
||||||
|
|
||||||
|
#: templates/musician/address_check_delete.html:10
|
||||||
|
#: templates/musician/address_form.html:11
|
||||||
|
#: templates/musician/mailbox_change_password.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_change_password.html:12
|
||||||
|
#: templates/musician/mailbox_form.html:21
|
||||||
|
msgid "Save"
|
||||||
|
msgstr "Desar"
|
||||||
|
|
||||||
|
#: templates/musician/addresses.html:15
|
||||||
|
msgid "Email"
|
||||||
|
msgstr "Correu electrònic"
|
||||||
|
|
||||||
|
#: 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:325
|
||||||
|
msgid "Mailboxes"
|
||||||
|
msgstr "Bústies 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 +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:163
|
||||||
msgid "Billing"
|
msgid "Billing"
|
||||||
msgstr "Factures"
|
msgstr "Factures"
|
||||||
|
|
||||||
|
@ -119,7 +201,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 +213,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"
|
||||||
|
|
||||||
|
@ -142,7 +223,7 @@ msgstr "Total"
|
||||||
|
|
||||||
#: templates/musician/billing.html:23
|
#: templates/musician/billing.html:23
|
||||||
msgid "Download PDF"
|
msgid "Download PDF"
|
||||||
msgstr "Descarrega un PDF"
|
msgstr "Descarrega el PDF"
|
||||||
|
|
||||||
#: templates/musician/components/table_paginator.html:15
|
#: templates/musician/components/table_paginator.html:15
|
||||||
msgid "Previous"
|
msgid "Previous"
|
||||||
|
@ -165,90 +246,85 @@ msgstr "El darrer cop que vas accedir va ser el dia: %(last_login)s"
|
||||||
msgid "It's the first time you log into the system, welcome on board!"
|
msgid "It's the first time you log into the system, welcome on board!"
|
||||||
msgstr "És el primer cop que accedeixes, et donem la benvinguda!"
|
msgstr "És el primer cop que accedeixes, et donem la benvinguda!"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:24
|
#: templates/musician/dashboard.html:29
|
||||||
msgid "Notifications"
|
msgid "Notifications"
|
||||||
msgstr "Notificacions"
|
msgstr "Notificacions"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:28
|
#: templates/musician/dashboard.html:33
|
||||||
msgid "There is no notifications at this time."
|
msgid "There is no notifications at this time."
|
||||||
msgstr "No tens cap notificació."
|
msgstr "No tens cap notificació."
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:35
|
#: templates/musician/dashboard.html:40
|
||||||
msgid "Your domains and websites"
|
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:41
|
||||||
#, 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ó "
|
||||||
"dels serveis que Pangea t’ofereix."
|
"dels serveis que Pangea t’ofereix."
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:51
|
#: templates/musician/dashboard.html:56
|
||||||
msgid "view configuration"
|
msgid "view configuration"
|
||||||
msgstr "veure la configuració"
|
msgstr "veure la configuració"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:58
|
#: templates/musician/dashboard.html:63
|
||||||
msgid "Expiration date"
|
msgid "Expiration date"
|
||||||
msgstr "Data de venciment"
|
msgstr "Data de venciment"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:65
|
#: templates/musician/dashboard.html:70
|
||||||
msgid "Mail"
|
msgid "Mail"
|
||||||
msgstr "Correu"
|
msgstr "Correu"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:68
|
#: templates/musician/dashboard.html:73
|
||||||
msgid "mail addresses created"
|
msgid "mail addresses created"
|
||||||
msgstr "adreces de correu creades"
|
msgstr "adreces de correu creades"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:71
|
#: templates/musician/dashboard.html:78
|
||||||
msgid "mail address left"
|
|
||||||
msgstr "adreces de correu per activar"
|
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:77
|
|
||||||
msgid "Mail list"
|
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:83 views.py:489
|
||||||
msgid "Software as a Service"
|
msgid "Software as a Service"
|
||||||
msgstr "Software as a Service"
|
msgstr "Software as a Service"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:84
|
#: templates/musician/dashboard.html:85
|
||||||
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:90 views.py:57
|
||||||
msgid "Disk usage"
|
msgid "Disk usage"
|
||||||
msgstr "Ús del disc"
|
msgstr "Ús del disc"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:106
|
#: templates/musician/dashboard.html:107
|
||||||
msgid "Configuration details"
|
msgid "Configuration details"
|
||||||
msgstr "Detalls de configuració"
|
msgstr "Detalls de configuració"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:113
|
#: templates/musician/dashboard.html:114
|
||||||
msgid "FTP access:"
|
msgid "FTP access:"
|
||||||
msgstr "Accés FTP:"
|
msgstr "Accés FTP:"
|
||||||
|
|
||||||
#. Translators: domain configuration detail modal
|
#. Translators: domain configuration detail modal
|
||||||
#: templates/musician/dashboard.html:115
|
#: templates/musician/dashboard.html:116
|
||||||
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 ""
|
||||||
"Contacteu-nos a <a href=“mailto:%(support_email)s”>%(support_email)s</a> per "
|
"Escriu-nos a <a href=“mailto:%(support_email)s”>%(support_email)s</a> per "
|
||||||
"saber com accedir al FTP."
|
"saber com accedir al FTP."
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:124
|
#: templates/musician/dashboard.html:125
|
||||||
msgid "No website configured."
|
msgid "No website configured."
|
||||||
msgstr "No hi ha cap web configurada."
|
msgstr "No hi ha cap web configurada."
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:126
|
#: templates/musician/dashboard.html:127
|
||||||
msgid "Root directory:"
|
msgid "Root directory:"
|
||||||
msgstr "Directori arrel:"
|
msgstr "Directori arrel:"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:127
|
#: templates/musician/dashboard.html:128
|
||||||
msgid "Type:"
|
msgid "Type:"
|
||||||
msgstr "Tipus:"
|
msgstr "Tipus:"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:132
|
#: templates/musician/dashboard.html:133
|
||||||
msgid "View DNS records"
|
msgid "View DNS records"
|
||||||
msgstr "Veure registres DNS"
|
msgstr "Veure registres DNS"
|
||||||
|
|
||||||
|
@ -279,7 +355,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 +362,66 @@ 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_change_password.html:5
|
||||||
msgid "Aliases"
|
#: templates/musician/mailbox_form.html:24
|
||||||
msgstr "Àlies"
|
msgid "Change password"
|
||||||
|
msgstr "Canvia la contrasenya"
|
||||||
|
|
||||||
#: templates/musician/mail.html:23
|
#: templates/musician/mailbox_check_delete.html:7
|
||||||
msgid "Type details"
|
#, python-format
|
||||||
msgstr "Detalls de cada tipus"
|
msgid "Are you sure that you want remove the mailbox: \"%(name)s\"?"
|
||||||
|
msgstr "Estàs segur/a que vols esborrar la bústia de correu: \"%(name)s\"?"
|
||||||
|
|
||||||
|
#: 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 límit de bústies de correu de la teva subscripció, les noves "
|
||||||
|
"bústies poden implicar <strong>costos addicionals</strong>."
|
||||||
|
|
||||||
|
#: templates/musician/mailbox_form.html:10
|
||||||
|
msgid "Close"
|
||||||
|
msgstr "Tancar"
|
||||||
|
|
||||||
|
#: templates/musician/mailboxes.html:14
|
||||||
|
msgid "Name"
|
||||||
|
msgstr "Nom"
|
||||||
|
|
||||||
|
#: templates/musician/mailboxes.html:15
|
||||||
|
msgid "Filtering"
|
||||||
|
msgstr "Filtrat"
|
||||||
|
|
||||||
|
#: templates/musician/mailboxes.html:27
|
||||||
|
msgid "Update password"
|
||||||
|
msgstr "Actualitza la contrasenya"
|
||||||
|
|
||||||
|
#: templates/musician/mailboxes.html:43
|
||||||
|
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 +432,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 +472,67 @@ 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:41
|
||||||
msgid "Dashboard"
|
msgid "Dashboard"
|
||||||
msgstr "Panell de gestió"
|
msgstr "Panell de gestió"
|
||||||
|
|
||||||
#: views.py:49
|
#: views.py:66
|
||||||
msgid "Traffic"
|
msgid "Traffic"
|
||||||
msgstr "Tràfic"
|
msgstr "Tràfic"
|
||||||
|
|
||||||
#: views.py:56
|
#: views.py:97
|
||||||
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:112
|
||||||
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:170
|
||||||
msgid "Download bill"
|
msgid "Download bill"
|
||||||
msgstr "Descarrega la factura"
|
msgstr "Descarrega la factura"
|
||||||
|
|
||||||
|
#: views.py:283
|
||||||
|
msgid "Address deleted!"
|
||||||
|
msgstr "S’ha suprimit l’adreça de correu"
|
||||||
|
|
||||||
|
#: views.py:285 views.py:422 views.py:469
|
||||||
|
msgid "Cannot process your request, please try again later."
|
||||||
|
msgstr ""
|
||||||
|
"Ara no podem processar la teva petició, torna a intentar-ho una mica més "
|
||||||
|
"tard sisplau"
|
||||||
|
|
||||||
|
#: views.py:420
|
||||||
|
msgid "Mailbox deleted!"
|
||||||
|
msgstr "S’ha suprimit la bústia de correu"
|
||||||
|
|
||||||
|
#: views.py:467
|
||||||
|
msgid "Password updated!"
|
||||||
|
msgstr "S’ha actualitzat la contrasenya"
|
||||||
|
|
||||||
#. Translators: This message appears on the page title
|
#. Translators: This message appears on the page title
|
||||||
#: views.py:272
|
#: views.py:497
|
||||||
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:523
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr "Accés"
|
msgstr "Accés"
|
||||||
|
|
||||||
|
#~ msgid "mail address left"
|
||||||
|
#~ msgstr "adreces de correu per activar"
|
||||||
|
|
||||||
|
#~ 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,8 +7,8 @@ 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-11-24 11:04+0100\n"
|
||||||
"PO-Revision-Date: 2020-01-28 17:27+0100\n"
|
"PO-Revision-Date: 2021-11-25 12:53+0100\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
"Language: es\n"
|
"Language: es\n"
|
||||||
|
@ -16,12 +16,36 @@ msgstr ""
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"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 3.0\n"
|
||||||
|
|
||||||
#: api.py:108 api.py:117
|
#: api.py:113 api.py:211
|
||||||
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:125
|
||||||
|
msgid "No object found matching the query"
|
||||||
|
msgstr "No hay objetos que coincidan con tu búsqueda"
|
||||||
|
|
||||||
|
#: api.py:178
|
||||||
|
msgid "No mailbox found matching the query"
|
||||||
|
msgstr "No hay buzones de correo que coincidan con tu búsqueda"
|
||||||
|
|
||||||
|
#: forms.py:65 forms.py:99
|
||||||
|
msgid "The two password fields didn’t match."
|
||||||
|
msgstr "Las contraseñas introducidas no coinciden."
|
||||||
|
|
||||||
|
#: forms.py:68 forms.py:103
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "Contraseña"
|
||||||
|
|
||||||
|
#: forms.py:73 forms.py:108
|
||||||
|
msgid "Password confirmation"
|
||||||
|
msgstr "Confirma la contraseña"
|
||||||
|
|
||||||
|
#: forms.py:76 forms.py:111
|
||||||
|
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:296
|
||||||
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:480
|
||||||
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:185
|
||||||
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,61 @@ 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/a 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:25
|
||||||
|
msgid "Delete"
|
||||||
|
msgstr "Borrar"
|
||||||
|
|
||||||
|
#: templates/musician/address_check_delete.html:10
|
||||||
|
#: templates/musician/address_form.html:11
|
||||||
|
#: templates/musician/mailbox_change_password.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_change_password.html:12
|
||||||
|
#: templates/musician/mailbox_form.html:21
|
||||||
|
msgid "Save"
|
||||||
|
msgstr "Guardar"
|
||||||
|
|
||||||
|
#: templates/musician/addresses.html:15
|
||||||
|
msgid "Email"
|
||||||
|
msgstr "Correo electrónico"
|
||||||
|
|
||||||
|
#: 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:325
|
||||||
|
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 +194,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:163
|
||||||
msgid "Billing"
|
msgid "Billing"
|
||||||
msgstr "Facturas"
|
msgstr "Facturas"
|
||||||
|
|
||||||
|
@ -119,7 +203,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 +215,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"
|
||||||
|
|
||||||
|
@ -165,90 +248,85 @@ msgstr "La última vez que accediste fue el día: %(last_login)s"
|
||||||
msgid "It's the first time you log into the system, welcome on board!"
|
msgid "It's the first time you log into the system, welcome on board!"
|
||||||
msgstr "Es la primera vez que accedes: ¡te damos la bienvenida!"
|
msgstr "Es la primera vez que accedes: ¡te damos la bienvenida!"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:24
|
#: templates/musician/dashboard.html:29
|
||||||
msgid "Notifications"
|
msgid "Notifications"
|
||||||
msgstr "Notificaciones"
|
msgstr "Notificaciones"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:28
|
#: templates/musician/dashboard.html:33
|
||||||
msgid "There is no notifications at this time."
|
msgid "There is no notifications at this time."
|
||||||
msgstr "No tienes ninguna notificación."
|
msgstr "No tienes ninguna notificación."
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:35
|
#: templates/musician/dashboard.html:40
|
||||||
msgid "Your domains and websites"
|
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:41
|
||||||
#, 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 "
|
||||||
"de los servicios que Pangea te ofrece."
|
"de los servicios que Pangea te ofrece."
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:51
|
#: templates/musician/dashboard.html:56
|
||||||
msgid "view configuration"
|
msgid "view configuration"
|
||||||
msgstr "ver la configuración"
|
msgstr "ver la configuración"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:58
|
#: templates/musician/dashboard.html:63
|
||||||
msgid "Expiration date"
|
msgid "Expiration date"
|
||||||
msgstr "Fecha de vencimiento"
|
msgstr "Fecha de vencimiento"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:65
|
#: templates/musician/dashboard.html:70
|
||||||
msgid "Mail"
|
msgid "Mail"
|
||||||
msgstr "Correo"
|
msgstr "Correo"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:68
|
#: templates/musician/dashboard.html:73
|
||||||
msgid "mail addresses created"
|
msgid "mail addresses created"
|
||||||
msgstr "direcciones de correo creadas"
|
msgstr "direcciones de correo creadas"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:71
|
#: templates/musician/dashboard.html:78
|
||||||
msgid "mail address left"
|
|
||||||
msgstr "direcciones de correo por activar"
|
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:77
|
|
||||||
msgid "Mail list"
|
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:83 views.py:489
|
||||||
msgid "Software as a Service"
|
msgid "Software as a Service"
|
||||||
msgstr "Software as a Service"
|
msgstr "Software as a Service"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:84
|
#: templates/musician/dashboard.html:85
|
||||||
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:90 views.py:57
|
||||||
msgid "Disk usage"
|
msgid "Disk usage"
|
||||||
msgstr "Uso del disco"
|
msgstr "Uso del disco"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:106
|
#: templates/musician/dashboard.html:107
|
||||||
msgid "Configuration details"
|
msgid "Configuration details"
|
||||||
msgstr "Detalles de configuración"
|
msgstr "Detalles de configuración"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:113
|
#: templates/musician/dashboard.html:114
|
||||||
msgid "FTP access:"
|
msgid "FTP access:"
|
||||||
msgstr "Acceso FTP:"
|
msgstr "Acceso FTP:"
|
||||||
|
|
||||||
#. Translators: domain configuration detail modal
|
#. Translators: domain configuration detail modal
|
||||||
#: templates/musician/dashboard.html:115
|
#: templates/musician/dashboard.html:116
|
||||||
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> "
|
"Escríbenos 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:125
|
||||||
msgid "No website configured."
|
msgid "No website configured."
|
||||||
msgstr "No hay ninguna web configurada."
|
msgstr "No hay ninguna web configurada."
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:126
|
#: templates/musician/dashboard.html:127
|
||||||
msgid "Root directory:"
|
msgid "Root directory:"
|
||||||
msgstr "Directorio raíz:"
|
msgstr "Directorio raíz:"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:127
|
#: templates/musician/dashboard.html:128
|
||||||
msgid "Type:"
|
msgid "Type:"
|
||||||
msgstr "Tipo:"
|
msgstr "Tipo:"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:132
|
#: templates/musician/dashboard.html:133
|
||||||
msgid "View DNS records"
|
msgid "View DNS records"
|
||||||
msgstr "Ver registros DNS"
|
msgstr "Ver registros DNS"
|
||||||
|
|
||||||
|
@ -279,7 +357,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 +364,66 @@ 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_change_password.html:5
|
||||||
msgid "Aliases"
|
#: templates/musician/mailbox_form.html:24
|
||||||
msgstr "Alias"
|
msgid "Change password"
|
||||||
|
msgstr "Cambia la contraseña"
|
||||||
|
|
||||||
#: templates/musician/mail.html:23
|
#: templates/musician/mailbox_check_delete.html:7
|
||||||
msgid "Type details"
|
#, python-format
|
||||||
msgstr "Detalles de cada tipo"
|
msgid "Are you sure that you want remove the mailbox: \"%(name)s\"?"
|
||||||
|
msgstr "¿Estás seguro/a de que quieres borrar el buzón de correo \"%(name)s\"?"
|
||||||
|
|
||||||
|
#: 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 ""
|
||||||
|
"Has alcanzado el límite de buzones de correo de tu suscripción, los nuevos "
|
||||||
|
"buzones pueden suponer <strong>costes adicionales</strong>."
|
||||||
|
|
||||||
|
#: 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:27
|
||||||
|
msgid "Update password"
|
||||||
|
msgstr "Actualiza la contraseña"
|
||||||
|
|
||||||
|
#: templates/musician/mailboxes.html:43
|
||||||
|
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 +434,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í."
|
||||||
|
|
||||||
|
@ -326,7 +443,7 @@ msgstr "Información de usuario/a"
|
||||||
|
|
||||||
#: templates/musician/profile.html:21
|
#: templates/musician/profile.html:21
|
||||||
msgid "Preferred language:"
|
msgid "Preferred language:"
|
||||||
msgstr "Lenguaje preferido:"
|
msgstr "Idioma preferido:"
|
||||||
|
|
||||||
#: templates/musician/profile.html:35
|
#: templates/musician/profile.html:35
|
||||||
msgid "Billing information"
|
msgid "Billing information"
|
||||||
|
@ -357,43 +474,67 @@ 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:41
|
||||||
msgid "Dashboard"
|
msgid "Dashboard"
|
||||||
msgstr "Panel de gestión"
|
msgstr "Panel de gestión"
|
||||||
|
|
||||||
#: views.py:49
|
#: views.py:66
|
||||||
msgid "Traffic"
|
msgid "Traffic"
|
||||||
msgstr "Tráfico"
|
msgstr "Tráfico"
|
||||||
|
|
||||||
#: views.py:56
|
#: views.py:97
|
||||||
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:112
|
||||||
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:170
|
||||||
msgid "Download bill"
|
msgid "Download bill"
|
||||||
msgstr "Descarga la factura"
|
msgstr "Descarga la factura"
|
||||||
|
|
||||||
|
#: views.py:283
|
||||||
|
msgid "Address deleted!"
|
||||||
|
msgstr "Has eliminado la dirección de correo"
|
||||||
|
|
||||||
|
#: views.py:285 views.py:422 views.py:469
|
||||||
|
msgid "Cannot process your request, please try again later."
|
||||||
|
msgstr ""
|
||||||
|
"Ahora no podemos procesar tu petición, inténtalo de nuevo un poco más tarde "
|
||||||
|
"por favor."
|
||||||
|
|
||||||
|
#: views.py:420
|
||||||
|
msgid "Mailbox deleted!"
|
||||||
|
msgstr "Has eliminado el buzón de correo"
|
||||||
|
|
||||||
|
#: views.py:467
|
||||||
|
msgid "Password updated!"
|
||||||
|
msgstr "Contraseña actualizada"
|
||||||
|
|
||||||
#. Translators: This message appears on the page title
|
#. Translators: This message appears on the page title
|
||||||
#: views.py:272
|
#: views.py:497
|
||||||
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:523
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr "Accede"
|
msgstr "Accede"
|
||||||
|
|
||||||
|
#~ msgid "mail address left"
|
||||||
|
#~ msgstr "direcciones de correo por activar"
|
||||||
|
|
||||||
|
#~ 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:"
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic.base import ContextMixin
|
from django.views.generic.base import ContextMixin
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from . import api, get_version
|
from . import api, get_version
|
||||||
from .auth import SESSION_KEY_TOKEN
|
from .auth import SESSION_KEY_TOKEN
|
||||||
|
@ -12,14 +13,15 @@ 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,
|
||||||
'version': get_version(),
|
'version': get_version(),
|
||||||
|
'languages': settings.LANGUAGES,
|
||||||
})
|
})
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
|
@ -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):
|
||||||
|
@ -34,6 +35,8 @@ class OrchestraModel:
|
||||||
Args:
|
Args:
|
||||||
data: A JSON dict, as converted from the JSON in the orchestra API.
|
data: A JSON dict, as converted from the JSON in the orchestra API.
|
||||||
"""
|
"""
|
||||||
|
if data is None:
|
||||||
|
return cls()
|
||||||
|
|
||||||
json_data = data.copy()
|
json_data = data.copy()
|
||||||
if kwargs:
|
if kwargs:
|
||||||
|
@ -126,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'
|
||||||
|
@ -159,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 = {}
|
||||||
|
@ -196,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
|
||||||
|
@ -222,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'
|
||||||
|
@ -236,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 [
|
||||||
|
@ -243,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):
|
||||||
|
@ -282,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')
|
||||||
|
@ -299,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):
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from collections import defaultdict
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,24 +6,30 @@ def getsetting(name):
|
||||||
value = getattr(settings, name, None)
|
value = getattr(settings, name, None)
|
||||||
return value or DEFAULTS.get(name)
|
return value or DEFAULTS.get(name)
|
||||||
|
|
||||||
|
# provide a default value allowing to overwrite it for each type of account
|
||||||
|
def allowed_resources_default_factory():
|
||||||
|
return {'mailbox': 2}
|
||||||
|
|
||||||
DEFAULTS = {
|
DEFAULTS = {
|
||||||
# allowed resources limit hardcoded because cannot be retrieved from the API.
|
# allowed resources limit hardcoded because cannot be retrieved from the API.
|
||||||
"ALLOWED_RESOURCES": {
|
"ALLOWED_RESOURCES": defaultdict(
|
||||||
'INDIVIDUAL':
|
allowed_resources_default_factory,
|
||||||
{
|
{
|
||||||
# 'disk': 1024,
|
'INDIVIDUAL':
|
||||||
# 'traffic': 2048,
|
{
|
||||||
'mailbox': 2,
|
# 'disk': 1024,
|
||||||
},
|
# 'traffic': 2048,
|
||||||
'ASSOCIATION': {
|
'mailbox': 2,
|
||||||
# 'disk': 5 * 1024,
|
},
|
||||||
# 'traffic': 20 * 1024,
|
'ASSOCIATION': {
|
||||||
'mailbox': 10,
|
# 'disk': 5 * 1024,
|
||||||
|
# 'traffic': 20 * 1024,
|
||||||
|
'mailbox': 10,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
),
|
||||||
"URL_DB_PHPMYADMIN": "https://phpmyadmin.pangea.org/",
|
"URL_DB_PHPMYADMIN": "https://phpmyadmin.pangea.org/",
|
||||||
"URL_MAILTRAIN": "https://mailtrain.org/",
|
"URL_MAILTRAIN": "https://grups.pangea.org/",
|
||||||
"URL_SAAS_GITLAB": "https://gitlab.pangea.org/",
|
"URL_SAAS_GITLAB": "https://gitlab.pangea.org/",
|
||||||
"URL_SAAS_OWNCLOUD": "https://nextcloud.pangea.org/",
|
"URL_SAAS_OWNCLOUD": "https://nextcloud.pangea.org/",
|
||||||
"URL_SAAS_WORDPRESS": "https://blog.pangea.org/",
|
"URL_SAAS_WORDPRESS": "https://blog.pangea.org/",
|
||||||
|
|
|
@ -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,21 @@ 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;
|
||||||
|
transition-duration: 200ms;
|
||||||
|
transition-property: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#sidebar-btn {
|
||||||
|
display: none;
|
||||||
|
width: fit-content;
|
||||||
|
transition-duration: 200ms;
|
||||||
|
transition-property: left;
|
||||||
|
}
|
||||||
|
|
||||||
#sidebar #sidebar-services {
|
#sidebar #sidebar-services {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
@ -62,20 +69,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 +96,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 +129,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 +139,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 +214,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 +226,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 +253,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 +316,69 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar-btn * {
|
||||||
|
font-size: 14.4px !important;
|
||||||
|
font-family: "Font Awesome 5 Free";
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1350px){
|
||||||
|
.card-deck .card {
|
||||||
|
flex: 1 0 0%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px){
|
||||||
|
#sidebar {
|
||||||
|
left: calc(-277px + 34px);
|
||||||
|
}
|
||||||
|
#sidebar * {
|
||||||
|
opacity: 0;
|
||||||
|
transition-delay: 150ms;
|
||||||
|
transition-duration: 300ms;
|
||||||
|
}
|
||||||
|
#sidebar-btn {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
#sidebar-btn::before {
|
||||||
|
font-family: "Font Awesome 5 Free";
|
||||||
|
content: "\f0c9";
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
#content {
|
||||||
|
margin-left: 34px;
|
||||||
|
}
|
||||||
|
#sidebar-toggle:checked {
|
||||||
|
width: 277px;
|
||||||
|
}
|
||||||
|
#sidebar-toggle:checked ~ #sidebar {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
#sidebar-toggle:checked ~ #sidebar * {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
#sidebar-toggle:checked ~ #sidebar-btn {
|
||||||
|
left: 243px;
|
||||||
|
}
|
||||||
|
#sidebar-toggle:checked ~ #sidebar-btn::before {
|
||||||
|
content: "\f00d";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 576px){
|
||||||
|
.card-deck .card {
|
||||||
|
flex: 1 0 40%;
|
||||||
|
margin: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 %}
|
|
@ -9,6 +9,7 @@
|
||||||
{% block meta %}
|
{% block meta %}
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
<meta name="robots" content="NONE,NOARCHIVE" />
|
<meta name="robots" content="NONE,NOARCHIVE" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
<title>{% block title %}{% if title %}{{ title }} – {% endif %}Django musician{% endblock %}</title>
|
<title>{% block title %}{% if title %}{{ title }} – {% endif %}Django musician{% endblock %}</title>
|
||||||
|
@ -32,6 +33,8 @@
|
||||||
|
|
||||||
<body class="{% block bodyclass %}{% endblock %}">
|
<body class="{% block bodyclass %}{% endblock %}">
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
|
<input style="display: none" type="checkbox" id="sidebar-toggle" />
|
||||||
|
<label type="button" for="sidebar-toggle" id="sidebar-btn" class="btn btn-primary fixed-top"></label>
|
||||||
<nav id="sidebar" class="bg-primary border-right pt-4">
|
<nav id="sidebar" class="bg-primary border-right pt-4">
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
<div class="sidebar-branding">
|
<div class="sidebar-branding">
|
||||||
|
@ -65,9 +68,16 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="sidebar-logout">
|
<div class="sidebar-logout">
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-row">
|
||||||
<li class="nav-item text-right">
|
<li class="nav-item btn btn-outline-primary btn-sm" data-toggle="modal" data-target="#helpModal">
|
||||||
|
<a class="nav-link text-light" href="#">
|
||||||
|
<i class="far fa-question-circle"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item text-right flex-grow-1">
|
||||||
<a class="nav-link text-light" href="{% url 'musician:logout' %}">
|
<a class="nav-link text-light" href="{% url 'musician:logout' %}">
|
||||||
{% trans 'Log out' %}
|
{% trans 'Log out' %}
|
||||||
<i class="fas fa-power-off"></i>
|
<i class="fas fa-power-off"></i>
|
||||||
|
@ -77,16 +87,51 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 pr-3 pb-2 text-light d-block text-right">
|
<div class="mt-4 pr-3 pb-2 text-light d-block text-right">
|
||||||
|
<div class="dropdown">
|
||||||
|
<a class="btn p-0 text-light" id="dropdownMenu3" data-toggle="dropdown" aria-haspopup="true"
|
||||||
|
aria-expanded="false">
|
||||||
|
<i class="fas fa-globe"></i> {% trans "Language" %}
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
{% for code, language in languages %}
|
||||||
|
<a class="dropdown-item" href="{% url 'musician:profile-set-lang' code %}">{{ language }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<small>Panel Version {{ version }}</small>
|
<small>Panel Version {{ version }}</small>
|
||||||
</div>
|
</div>
|
||||||
{% endblock sidebar %}
|
{% endblock sidebar %}
|
||||||
</nav><!-- ./sidebar -->
|
</nav><!-- ./sidebar -->
|
||||||
<div id="content" class="container-fluid pt-4">
|
<div id="content" class="container-fluid pt-4">
|
||||||
{% block content %}
|
{% 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 %}
|
{% endblock content %}
|
||||||
</div><!-- ./content -->
|
</div><!-- ./content -->
|
||||||
</div><!-- ./wrapper -->
|
</div><!-- ./wrapper -->
|
||||||
|
|
||||||
|
<!-- Help Modal -->
|
||||||
|
<div class="modal fade" id="helpModal" tabindex="-1" role="dialog" aria-labelledby="helpModal" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-body">
|
||||||
|
<span class="text-center m-auto">Do you need help? Write to <a href="mailto:suport@pangea.org" target="_blank">suport@pangea.org</a></span>
|
||||||
|
<button type="button" class="close m-0 pb-1" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script src="{% static "musician/js/jquery-3.3.1.slim.min.js" %}"></script>
|
<script src="{% static "musician/js/jquery-3.3.1.slim.min.js" %}"></script>
|
||||||
<script src="{% static "musician/js/popper.min.js" %}"></script>
|
<script src="{% static "musician/js/popper.min.js" %}"></script>
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
{% for bill in object_list %}
|
{% for bill in object_list %}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{{ bill.number }}</th>
|
<th scope="row">{{ bill.number }}</th>
|
||||||
<td>{{ bill.created_on }}</td>
|
<td>{{ bill.created_on|date:"SHORT_DATE_FORMAT" }}</td>
|
||||||
<td>{{ bill.type }}</td>
|
<td>{{ bill.type }}</td>
|
||||||
<td>{{ bill.total|floatformat:2|localize }}€</td>
|
<td>{{ bill.total|floatformat:2|localize }}€</td>
|
||||||
<td><a class="text-dark" href="{% url 'musician:bill-download' bill.id %}" target="_blank" rel="noopener noreferrer"><i class="fas fa-file-pdf"></i></a></td>
|
<td><a class="text-dark" href="{% url 'musician:bill-download' bill.id %}" target="_blank" rel="noopener noreferrer"><i class="fas fa-file-pdf"></i></a></td>
|
||||||
|
|
|
@ -11,7 +11,7 @@ Expected structure: dictionary or object with attributes:
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
{% if detail %}
|
{% if detail %}
|
||||||
{{ detail.usage }} of {{ detail.total }} {{ detail.unit }}
|
{{ detail.usage }} {{ detail.unit }}
|
||||||
{% else %}
|
{% else %}
|
||||||
N/A
|
N/A
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<h2>{% trans "Welcome back" %} <strong>{{ profile.username }}</strong></h2>
|
<h2 style="margin-top: 10px;">{% trans "Welcome back" %} <strong>{{ profile.username }}</strong></h2>
|
||||||
{% if profile.last_login %}
|
{% if profile.last_login %}
|
||||||
<p>{% blocktrans with last_login=profile.last_login|date:"SHORT_DATE_FORMAT" %}Last time you logged in was: {{ last_login }}{% endblocktrans %}</p>
|
<p>{% blocktrans with last_login=profile.last_login|date:"SHORT_DATE_FORMAT" %}Last time you logged in was: {{ last_login }}{% endblocktrans %}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -61,38 +66,34 @@
|
||||||
</div>
|
</div>
|
||||||
</div><!-- /card-header-->
|
</div><!-- /card-header-->
|
||||||
<div class="card-body row text-center">
|
<div class="card-body row text-center">
|
||||||
<div class="col-md-2 border-right">
|
<div class="col-6 col-md-3 col-lg-2 border-right">
|
||||||
<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-6 col-md-3 col-lg-2 border-right">
|
||||||
<h4>{% trans "Mail list" %}</h4>
|
<h4>{% trans "Mail list" %}</h4>
|
||||||
<p class="card-text"><i class="fas fa-mail-bulk fa-3x"></i></p>
|
<p class="card-text"><i class="fas fa-mail-bulk fa-3x"></i></p>
|
||||||
<a class="stretched-link" href="{% url 'musician:mailing-lists' %}?domain={{ domain.id }}"></a>
|
<a class="stretched-link" href="{% url 'musician:mailing-lists' %}?domain={{ domain.id }}"></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2 border-right">
|
<div class="col-6 col-md-3 col-lg-2 border-right">
|
||||||
<h4>{% trans "Software as a Service" %}</h4>
|
<h4>{% trans "Software as a Service" %}</h4>
|
||||||
<p class="card-text"><i class="fas fa-fire fa-3x"></i></p>
|
<p class="card-text"><i class="fas fa-fire fa-3x"></i></p>
|
||||||
<p class="card-text text-dark">{% 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="d-none d-lg-block col-lg-1"></div>
|
||||||
<div class="col-md-4">
|
<div class="col-6 col-md-3 col-lg-4">
|
||||||
<h4>{% trans "Disk usage" %}</h4>
|
<h4>{% trans "Disk usage" %}</h4>
|
||||||
<p class="card-text"><i class="fas fa-hdd fa-3x"></i></p>
|
<p class="card-text"><i class="fas fa-hdd fa-3x"></i></p>
|
||||||
<div class="w-75 m-auto">
|
<div class="w-75 m-auto">
|
||||||
{% include "musician/components/usage_progress_bar.html" with detail=domain.usage %}
|
{% include "musician/components/usage_progress_bar.html" with detail=domain.usage %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-1"></div>
|
<div class="d-none d-lg-block col-lg-1"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -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 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(
|
||||||
|
@ -62,6 +90,31 @@ class UserAccountTest(TestCase):
|
||||||
account = UserAccount.new_from_json(data)
|
account = UserAccount.new_from_json(data)
|
||||||
self.assertIsNone(account.last_login)
|
self.assertIsNone(account.last_login)
|
||||||
|
|
||||||
|
def test_user_without_billcontact(self):
|
||||||
|
data = {
|
||||||
|
'billcontact': None,
|
||||||
|
'date_joined': '2020-03-05T09:49:21Z',
|
||||||
|
'full_name': 'David Rock',
|
||||||
|
'id': 2,
|
||||||
|
'is_active': True,
|
||||||
|
'language': 'CA',
|
||||||
|
'last_login': '2020-03-19T10:21:49.504266Z',
|
||||||
|
'resources': [{'allocated': None,
|
||||||
|
'name': 'disk',
|
||||||
|
'unit': 'GiB',
|
||||||
|
'used': '0.000'},
|
||||||
|
{'allocated': None,
|
||||||
|
'name': 'traffic',
|
||||||
|
'unit': 'GiB',
|
||||||
|
'used': '0.000'}],
|
||||||
|
'short_name': '',
|
||||||
|
'type': 'STAFF',
|
||||||
|
'url': 'https://example.org/api/accounts/2/',
|
||||||
|
'username': 'drock'
|
||||||
|
}
|
||||||
|
account = UserAccount.new_from_json(data)
|
||||||
|
self.assertIsNotNone(account.billing)
|
||||||
|
|
||||||
|
|
||||||
class GetBootstrapedPercentTest(TestCase):
|
class GetBootstrapedPercentTest(TestCase):
|
||||||
BS_WIDTH = [0, 25, 50, 100]
|
BS_WIDTH = [0, 25, 50, 100]
|
||||||
|
@ -93,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,20 @@ 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('profile/setLang/<code>', views.profile_set_language, name='profile-set-lang'),
|
||||||
|
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,37 @@
|
||||||
|
import logging
|
||||||
|
import smtplib
|
||||||
|
import datetime
|
||||||
|
|
||||||
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.http import HttpResponse, HttpResponseRedirect
|
from django.core.mail import mail_managers
|
||||||
from django.shortcuts import render
|
from django.http import HttpResponse, HttpResponseNotFound, HttpResponseRedirect
|
||||||
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 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 +49,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 +70,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 +81,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"
|
||||||
|
@ -116,6 +125,23 @@ class ProfileView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
def profile_set_language(request, code):
|
||||||
|
# set user language as active language
|
||||||
|
|
||||||
|
if any(x[0] == code for x in settings.LANGUAGES):
|
||||||
|
# http://127.0.0.1:8080/profile/setLang/es
|
||||||
|
user_language = code
|
||||||
|
translation.activate(user_language)
|
||||||
|
|
||||||
|
response = HttpResponseRedirect('/dashboard')
|
||||||
|
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, user_language)
|
||||||
|
|
||||||
|
return response
|
||||||
|
else:
|
||||||
|
response = HttpResponseNotFound('Languague not found')
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
class ServiceListView(CustomContextMixin, ExtendedPaginationMixin, UserTokenRequiredMixin, ListView):
|
class ServiceListView(CustomContextMixin, ExtendedPaginationMixin, UserTokenRequiredMixin, ListView):
|
||||||
"""Base list view to all services"""
|
"""Base list view to all services"""
|
||||||
service_class = None
|
service_class = None
|
||||||
|
@ -153,6 +179,13 @@ class BillingView(ServiceListView):
|
||||||
'title': _('Billing'),
|
'title': _('Billing'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
qs = super().get_queryset()
|
||||||
|
qs = sorted(qs, key=lambda x: x.created_on, reverse=True)
|
||||||
|
for q in qs:
|
||||||
|
q.created_on = datetime.datetime.strptime(q.created_on, "%Y-%m-%d")
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
class BillDownloadView(CustomContextMixin, UserTokenRequiredMixin, View):
|
class BillDownloadView(CustomContextMixin, UserTokenRequiredMixin, View):
|
||||||
extra_context = {
|
extra_context = {
|
||||||
|
@ -168,8 +201,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 +231,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"
|
||||||
|
@ -209,7 +319,6 @@ class MailingListsView(ServiceListView):
|
||||||
'title': _('Mailing lists'),
|
'title': _('Mailing lists'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
domain_id = self.request.GET.get('domain')
|
domain_id = self.request.GET.get('domain')
|
||||||
|
@ -230,6 +339,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.13
|
django==2.2.27
|
||||||
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 New Issue