Compare commits

..

76 Commits

Author SHA1 Message Date
Santiago L 2062c0c519
Merge pull request #22 from RubenPX/docker-musician
add compatibility with docker
2022-03-17 22:52:35 +01:00
Santiago L fc55c33c90 Add reference to docker-compose (quickstart) 2022-03-17 22:52:05 +01:00
Santiago L 1e3a919390 Rename dockerfile -> Dockerfile
*nix filesystem is case sensitive
2022-03-17 22:49:46 +01:00
Santiago L 9b4f2ba3da
Merge pull request #21 from RubenPX/PR-5
format date using ES Format
2022-03-17 22:35:14 +01:00
Santiago L 41f5493368
Format date using SHORT_DATE_FORMAT
Support automatically i18n on date format
2022-03-17 22:34:54 +01:00
RubenPX c1f25a73da refactor to use django template string 2022-03-17 19:23:36 +01:00
RubenPX ca3a8c4639 add docker files 2022-03-17 01:36:46 +01:00
RubenPX ed3ad7cda0 format date using ES Format 2022-03-11 17:36:51 +01:00
Santiago L 49d49a3044 Release version 0.2.0 2022-03-11 14:07:06 +01:00
Santiago L 4911cf4226
Merge pull request #19 from ribaguifi/feature/order-bills
#4 Order bills by creation date DESC
2022-03-11 12:18:18 +01:00
Santiago L 9e193107cd Order bills by creation date DESC 2022-03-11 12:16:29 +01:00
Santiago L 872243a8c6
Merge pull request #17 from ribaguifi/feature/language-selector
Update UI of language selector
2022-02-28 19:05:45 +01:00
Santiago L 19f5229536 Fix flake8 issues 2022-02-28 19:05:06 +01:00
Santiago L 66530351ad Move language selector to bottom of the sidebar 2022-02-28 19:01:32 +01:00
Santiago L 249a1182d4
Merge pull request #14 from RubenPX/PR-4
#2 add language selector to web page
2022-02-28 18:55:41 +01:00
RubenPX 1816301952 aded full functionality to lang menu 2022-02-26 19:54:27 +01:00
RubenPX feb591ea79 Add URL and detect if languague exist 2022-02-24 22:09:17 +01:00
Santiago L 179918bd62
Merge pull request #16 from ribaguifi/bugfix/9-mailtrain-url
Fix mailtrain URL of Pangea
2022-02-18 07:56:12 +01:00
Santiago L 57db1eed80 Fix mailtrain URL of Pangea 2022-02-18 07:55:30 +01:00
Santiago L afce4a5527
Merge pull request #13 from RubenPX/PR-3
#3 Add "Help" link
2022-02-18 07:49:51 +01:00
Santiago L 62a1d57f7d
Merge pull request #12 from ribaguifi/dependabot/pip/django-2.2.27
Bump django from 2.2.24 to 2.2.27
2022-02-16 17:02:55 +01:00
RubenPX 6b7cad86f2 add launguage selector to web page (only visual) 2022-02-12 03:29:42 +01:00
RubenPX 80a93ea8c0 fix hover help button 2022-02-12 02:22:23 +01:00
RubenPX 90b0956f71 add help link to vertical panel 2022-02-12 01:53:48 +01:00
dependabot[bot] b6b980ebaa
Bump django from 2.2.24 to 2.2.27
Bumps [django](https://github.com/django/django) from 2.2.24 to 2.2.27.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/2.2.24...2.2.27)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-10 10:27:47 +00:00
Santiago L bb70914066 Bump to version 0.2.0 beta 1 2022-02-05 13:14:15 +01:00
Santiago L bc84603b5e Fix dashboard layout on tablet & mobile devices 2022-02-05 13:10:25 +01:00
Santiago L 70b256d1ed
Merge pull request #10 from KryptoPX/master
fix dashboard deck css and add responisve feature to web
2022-02-05 13:07:03 +01:00
KryptoPX 7d799092cd sidebar icons switch if is collapsed 2022-01-28 22:36:29 +00:00
KryptoPX a7d025fc01 modify design of sidebar 2022-01-28 22:15:55 +00:00
KryptoPX e203b43a69 fix style order (media query's) 2022-01-28 21:58:41 +00:00
KryptoPX 828bb5f0de remove unnecesary styles 2022-01-27 23:30:09 +00:00
KryptoPX 95d2998d05 remove extra whitespaces 2022-01-27 18:04:08 +00:00
KryptoPX a0cc4d0a41 Fix: Remove duplicated h1 (my fault) 2022-01-27 17:58:39 +00:00
KryptoPX 560c48ddaa FIX: add responsive sidebarand content 2022-01-27 13:04:06 +00:00
Santiago L 74bcbb43a1 Drop duplicated resource total message 2021-11-26 22:30:05 +01:00
Santiago L 3a7d920611
Merge pull request #9 from ribaguifi/i18n/translate-v0.2
Update new messages of locales Catalan (ca) & Spanish (es)
2021-11-26 22:26:16 +01:00
Santiago L dd8e7f1f52 Update Catalan & Spanish translations (by Merce)
Reviewed & completed v0.2 i18n strings
2021-11-26 22:23:40 +01:00
Santiago L 3f46809620 Extract new messages of local ca & es
Run `manage.py makemessages -l ca -l es`
2021-11-24 11:05:21 +01:00
Santiago L 44f9390bee
Merge pull request #8 from ribaguifi/dev/api-writable
Write operations for addresses and mailboxes
2021-11-24 11:02:44 +01:00
Santiago L bd42b83ea3 Handle total=None on get_bootstraped_percent 2021-10-22 12:10:00 +02:00
Santiago L aee0267f17 Fix mailbox resource usage on dashboard.
Mail addresses are not limited, only mailboxes.
2021-10-14 12:56:50 +02:00
Santiago L d7bd21d865 Fix broken links 2021-10-14 12:08:22 +02:00
Santiago L d77b876a54 Show success & error messages 2021-10-14 12:05:42 +02:00
Santiago L b171cbf641 Use django.contrib.messages to show alerts 2021-10-14 11:59:59 +02:00
Santiago L 33e68b5d07 Add view to change mailbox password 2021-10-14 11:09:59 +02:00
Santiago L 6c773893f7 Notify managers via email on mailbox deletion 2021-10-08 13:33:09 +02:00
Santiago L 2aab4a666f Add warning message on check_delete pages 2021-10-08 11:56:46 +02:00
Santiago L a13bdeac56 Translate strings to es (spanish) and ca (catalan) 2021-10-08 11:42:33 +02:00
Santiago L 056f472ee0 Set mailbox related addresses on creation 2021-10-07 14:10:25 +02:00
Santiago L ddd8ecf634 Allow updating mailbox addresses 2021-10-07 13:51:31 +02:00
Santiago L a0808896b4 Allow deleting a mailbox (mark as inactive) 2021-10-06 11:07:22 +02:00
Santiago L 9b52bc4b92 Show warning when extra fees may be applied on mailbox creation 2021-10-05 13:31:09 +02:00
Santiago L 9e51457069 Add view to create mailbox 2021-10-05 13:10:53 +02:00
Santiago L ed5460c4b1 Fix api.retrieve_mail_address_list() 2021-10-05 10:04:41 +02:00
Santiago L b0366ff1d0 Implement address delete 2021-10-01 13:36:52 +02:00
Santiago L 98dfa7a9f4 Make URLs patterns homogeneus 2021-09-27 13:52:27 +02:00
Santiago L 6d7ee0b76a Refactor Addresses list view 2021-09-27 13:37:11 +02:00
Santiago L a9c59edbf2 Fix MailForm after encapsulate Mailbox service 2021-09-27 13:17:49 +02:00
Santiago L 0246d0a22e Encapsulate Mailbox as a service 2021-09-27 12:40:52 +02:00
Santiago L 9ba1d0a23c Split mail view into addresses & mailboxes 2021-09-24 14:31:29 +02:00
Santiago L bb07bcd126 Merge branch 'master' into dev/api-writable 2021-07-02 13:10:12 +02:00
Santiago L 2a1a82f271
Merge pull request #7 from ribaguifi/dependabot/pip/django-2.2.24
Bump django from 2.2.21 to 2.2.24
2021-07-02 13:09:35 +02:00
Santiago L 0d327127f5 Rename class `MailService` to `Address` 2021-07-02 13:08:06 +02:00
Santiago L 77577a67da Disable hacky patch for issue #4 because breaks code 2021-07-02 12:57:55 +02:00
Santiago L 29c752e572 Lay out mail form using bootstrap4 2021-06-24 13:19:54 +02:00
Santiago L 4d5497f2fa Add django-bootstrap4 to requirements 2021-06-24 13:19:22 +02:00
Santiago L 7ff01d60ef (Draft) Add view to update existing addresses 2021-06-24 13:08:16 +02:00
Santiago L f635721831 (Draft) Add view to create addresses 2021-06-23 13:47:27 +02:00
dependabot[bot] 3061ab34d3
Bump django from 2.2.21 to 2.2.24
Bumps [django](https://github.com/django/django) from 2.2.21 to 2.2.24.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/2.2.21...2.2.24)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-10 20:20:11 +00:00
Santiago L 30bb572589 Handle Mailinglist without domain address 2021-06-08 10:43:49 +02:00
Santiago L 5281e9595e Merge branch '7-login-error' 2021-06-08 10:39:29 +02:00
Santiago L db6715808b
Merge pull request #5 from ribaguifi/dependabot/pip/django-2.2.21
Bump django from 2.2.13 to 2.2.21
2021-06-08 10:33:39 +02:00
dependabot[bot] 13ac215973
Bump django from 2.2.13 to 2.2.21
Bumps [django](https://github.com/django/django) from 2.2.13 to 2.2.21.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/2.2.13...2.2.21)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-04 23:50:36 +00:00
Santiago Lamora 1d5d3a5ed3 Provide default value to ALLOWED_RESOURCES. 2020-04-02 08:15:36 +02:00
Santiago Lamora c68aec5dc3 Handle accounts without billing contact data.
Fixes #7
2020-03-30 15:26:01 +02:00
33 changed files with 1539 additions and 358 deletions

View File

@ -1,12 +0,0 @@
{
"scanSettings": {
"baseBranches": []
},
"checkRunSettings": {
"vulnerableCheckRunConclusionLevel": "failure",
"displayMode": "diff"
},
"issueSettings": {
"minSeverityLevel": "LOW"
}
}

View File

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

14
Dockerfile Normal file
View File

@ -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" ]

View File

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

8
docker-compose.yml Normal file
View File

@ -0,0 +1,8 @@
version: "3.9"
services:
web:
build: .
ports:
- "8080:8080"
volumes:
- .:/home

View File

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

View File

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

View File

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

View File

@ -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 didnt 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 dusuari/a i texplicarem què fer." "indicant el teu nom dusuari/a i texplicarem 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 ladreç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 don podràs consultar la configuració " "Aquest és el teu panell de gestió, des don podràs consultar la configuració "
"dels serveis que Pangea tofereix." "dels serveis que Pangea tofereix."
#: 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 daccés i opcions de perfil des daquí." msgstr "Canvia les teves dades daccés i opcions de perfil des daquí."
@ -357,43 +472,67 @@ msgid "Open service admin panel"
msgstr "Obre el panell dadministració del servei" msgstr "Obre el panell dadministració 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 despai a la bústia de correu" msgstr "Ús despai 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 "Sha suprimit ladreç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 "Sha suprimit la bústia de correu"
#: views.py:467
msgid "Password updated!"
msgstr "Sha 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 dusuari/a" #~ msgstr "Nom dusuari/a"
#~ msgid "Password:"
#~ msgstr "Contrasenya:"

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
from collections import defaultdict
from django.conf import settings from django.conf import settings
@ -5,10 +6,15 @@ 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(
allowed_resources_default_factory,
{
'INDIVIDUAL': 'INDIVIDUAL':
{ {
# 'disk': 1024, # 'disk': 1024,
@ -20,9 +26,10 @@ DEFAULTS = {
# 'traffic': 20 * 1024, # 'traffic': 20 * 1024,
'mailbox': 10, '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/",

View File

@ -12,16 +12,15 @@ a:hover {
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;
@ -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,6 +69,7 @@ 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;
@ -75,7 +83,6 @@ a:hover {
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,14 +96,16 @@ 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;
} }
@ -106,8 +115,7 @@ a:hover {
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,7 +214,6 @@ 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 {
@ -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;
}
}

View File

@ -0,0 +1,12 @@
{% extends "musician/base.html" %}
{% load i18n %}
{% block content %}
<form method="post">
{% csrf_token %}
<p>{% blocktrans with address_name=object.full_address_name %}Are you sure that you want remove the address: "{{ address_name }}"?{% endblocktrans %}</p>
<p class="alert alert-warning"><strong>{% trans 'WARNING: This action cannot be undone.' %}</strong></p>
<input class="btn btn-danger" type="submit" value="{% trans 'Delete' %}">
<a class="btn btn-secondary" href="{% url 'musician:address-update' view.kwargs.pk %}">{% trans 'Cancel' %}</a>
</form>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends "musician/base.html" %}
{% load bootstrap4 i18n %}
{% block content %}
<h1 class="service-name">{{ service.verbose_name }}</h1>
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<a class="btn btn-light mr-2" href="{% url 'musician:address-list' %}">{% trans "Cancel" %}</a>
<button type="submit" class="btn btn-secondary">{% trans "Save" %}</button>
{% if form.instance %}
<div class="float-right">
<a class="btn btn-danger" href="{% url 'musician:address-delete' view.kwargs.pk %}">{% trans "Delete" %}</a>
</div>
{% endif %}
{% endbuttons %}
</form>
{% endblock %}

View File

@ -0,0 +1,41 @@
{% extends "musician/mail_base.html" %}
{% load i18n %}
{% block tabcontent %}
<div class="tab-pane fade show active" id="addresses" role="tabpanel" aria-labelledby="addresses-tab">
<table class="table service-list">
<colgroup>
<col span="1" style="width: 25%;">
<col span="1" style="width: 25%;">
<col span="1" style="width: 25%;">
<col span="1" style="width: 25%;">
</colgroup>
<thead class="thead-dark">
<tr>
<th scope="col">{% trans "Email" %}</th>
<th scope="col">{% trans "Domain" %}</th>
<th scope="col">{% trans "Mailboxes" %}</th>
<th scope="col">{% trans "Forward" %}</th>
</tr>
</thead>
<tbody>
{% for obj in object_list %}
<tr>
<td><a href="{% url 'musician:address-update' obj.id %}">{{ obj.full_address_name }}</a></td>
<td>{{ obj.domain.name }}</td>
<td>
{% for mailbox in obj.mailboxes %}
<a href="{% url 'musician:mailbox-update' mailbox.id %}">{{ mailbox.name }}</a>
{% if not forloop.last %}<br/> {% endif %}
{% endfor %}
</td>
<td>{{ obj.forward }}</td>
</tr>
{% endfor %}
</tbody>
{% include "musician/components/table_paginator.html" %}
</table>
<a class="btn btn-primary mt-4 mb-4" href="{% url 'musician:address-create' %}">{% trans "New mail address" %}</a>
</div>
{% endblock %}

View File

@ -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 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">&times;</span>
</button>
</div>
{% endfor %}
{% endblock messages %}
{% block content %} {% 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">&times;</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>

View File

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

View File

@ -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 %}

View File

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

View File

@ -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 %}

View File

@ -0,0 +1,32 @@
{% extends "musician/base.html" %}
{% load i18n %}
{% block content %}
{% if active_domain %}
<a class="btn-arrow-left" href="{% url 'musician:address-list' %}">{% trans "Go to global" %}</a>
{% endif %}
<h1 class="service-name">{{ service.verbose_name }}
{% if active_domain %}<span class="font-weight-light">{% trans "for" %} {{ active_domain.name }}</span>{% endif %}
</h1>
<p class="service-description">{{ service.description }}</p>
{% with request.resolver_match.url_name as url_name %}
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item">
<a class="nav-link {% if url_name == 'address-list' %}active{% endif %}" href="{% url 'musician:address-list' %}" role="tab"
aria-selected="{% if url_name == 'address-list' %}true{% else %}false{% endif %}">{% trans "Addresses" %}</a>
</li>
<li class="nav-item">
<a class="nav-link {% if url_name == 'mailbox-list' %}active{% endif %}" href="{% url 'musician:mailbox-list' %}" role="tab"
aria-selected="{% if url_name == 'mailbox-list' %}true{% else %}false{% endif %}">{% trans "Mailboxes" %}</a>
</li>
</ul>
{% endwith %}
<div class="tab-content" id="myTabContent">
{% block tabcontent %}
{% endblock %}
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends "musician/base.html" %}
{% load bootstrap4 i18n %}
{% block content %}
<h1 class="service-name">{% trans "Change password" %}: <span class="font-weight-light">{{ object.name }}</span></h1>
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<a class="btn btn-light mr-2" href="{% url 'musician:mailbox-list' %}">{% trans "Cancel" %}</a>
<button type="submit" class="btn btn-secondary">{% trans "Save" %}</button>
{% endbuttons %}
</form>
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends "musician/base.html" %}
{% load i18n %}
{% block content %}
<form method="post">
{% csrf_token %}
<p>{% blocktrans with name=object.name %}Are you sure that you want remove the mailbox: "{{ name }}"?{% endblocktrans %}</p>
<div class="alert alert-danger" role="alert">
{% trans "All mailbox's messages will be <strong>deleted and cannot be recovered</strong>." %}
</div>
<p class="alert alert-warning"><strong>{% trans 'WARNING: This action cannot be undone.' %}</strong></p>
<input class="btn btn-danger" type="submit" value="{% trans 'Delete' %}">
<a class="btn btn-secondary" href="{% url 'musician:mailbox-list' %}">{% trans 'Cancel' %}</a>
</form>
{% endblock %}

View File

@ -0,0 +1,30 @@
{% extends "musician/base.html" %}
{% load bootstrap4 i18n %}
{% block content %}
<h1 class="service-name">{{ service.verbose_name }}</h1>
{% if extra_mailbox %}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<strong>{% trans "Warning!" %}</strong> {% trans "You have reached the limit of mailboxes of your subscription so <strong>extra fees</strong> may apply." %}
<button type="button" class="close" data-dismiss="alert" aria-label="{% trans 'Close' %}">
<span aria-hidden="true">&times;</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 %}

View File

@ -0,0 +1,46 @@
{% extends "musician/mail_base.html" %}
{% load i18n %}
{% block tabcontent %}
<div class="tab-pane fade show active" id="mailboxes" role="tabpanel" aria-labelledby="mailboxes-tab">
<table class="table service-list">
<colgroup>
<col span="1" style="width: 25%;">
<col span="1" style="width: 10%;">
<col span="1" style="width: 65%;">
</colgroup>
<thead class="thead-dark">
<tr>
<th scope="col">{% trans "Name" %}</th>
<th scope="col">{% trans "Filtering" %}</th>
<th scope="col">{% trans "Addresses" %}</th>
</tr>
</thead>
<tbody>
{% for mailbox in object_list %}
{# <!-- Exclude (don't render) inactive mailboxes -->#}
{% if mailbox.is_active %}
<tr>
<td>
<a href="{% url 'musician:mailbox-update' mailbox.id %}">{{ mailbox.name }}</a>
<a class="roll-hover btn btn-outline-warning" href="{% url 'musician:mailbox-password' mailbox.id %}">
<i class="fas fa-key"></i> {% trans "Update password" %}</a>
</td>
<td>{{ mailbox.filtering }}</td>
<td>
{% for addr in mailbox.addresses %}
<a href="{% url 'musician:address-update' addr.data.id %}">
{{ addr.full_address_name }}
</a><br/>
{% endfor %}
</td>
</tr>
{% endif %}{# <!-- /is_active --> #}
{% endfor %}
</tbody>
{% include "musician/components/table_paginator.html" %}
</table>
<a class="btn btn-primary mt-4 mb-4" href="{% url 'musician:mailbox-create' %}">{% trans "New mailbox" %}</a>
</div>
{% endblock %}

View File

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

View File

@ -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'),
] ]

View File

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

View File

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

View File

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

View File

@ -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',
}