From 30bb572589fcd9fdc0e48be612cea0b3ea73b693 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Tue, 8 Jun 2021 10:43:49 +0200 Subject: [PATCH 01/28] Handle Mailinglist without domain address --- musician/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/musician/models.py b/musician/models.py index 3239829..93e3d29 100644 --- a/musician/models.py +++ b/musician/models.py @@ -301,7 +301,10 @@ class MailinglistService(OrchestraModel): @property def address_name(self): - return "{}@{}".format(self.data['address_name'], self.data['address_domain']['name']) + address_domain = self.data['address_domain'] + if address_domain is None: + return self.data['address_name'] + return "{}@{}".format(self.data['address_name'], address_domain['name']) @property def manager_url(self): From f635721831f35786250ae413f6007810347328f8 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Wed, 23 Jun 2021 13:47:27 +0200 Subject: [PATCH 02/28] (Draft) Add view to create addresses --- musician/api.py | 25 +++++++++++++++++---- musician/forms.py | 22 ++++++++++++++++++ musician/models.py | 3 ++- musician/templates/musician/mail.html | 1 + musician/templates/musician/mail_form.html | 10 +++++++++ musician/urls.py | 1 + musician/views.py | 26 +++++++++++++++++++++- 7 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 musician/templates/musician/mail_form.html diff --git a/musician/api.py b/musician/api.py index 7bb6ee0..4913a23 100644 --- a/musician/api.py +++ b/musician/api.py @@ -62,7 +62,7 @@ class Orchestra(object): return response.json().get("token", None) - def request(self, verb, resource=None, url=None, render_as="json", querystring=None, raise_exception=True): + def request(self, verb, resource=None, url=None, data=None, render_as="json", querystring=None, raise_exception=True): assert verb in ["HEAD", "GET", "POST", "PATCH", "PUT", "DELETE"] if resource is not None: url = self.build_absolute_uri(resource) @@ -73,8 +73,11 @@ class Orchestra(object): url = "{}?{}".format(url, querystring) verb = getattr(self.session, verb.lower()) - response = verb(url, headers={"Authorization": "Token {}".format( - self.auth_token)}, allow_redirects=False) + headers = { + "Authorization": "Token {}".format(self.auth_token), + "Content-Type": "application/json", + } + response = verb(url, json=data, headers=headers, allow_redirects=False) if raise_exception: response.raise_for_status() @@ -109,6 +112,15 @@ class Orchestra(object): raise Http404(_("No domain found matching the query")) return bill_pdf + def create_mail_address(self, data): + resource = '{}-list'.format(MailService.api_name) + + # transform form data to expected format + data["domain"] = {"url": data["domain"]} + data["mailboxes"] = [{"url": mbox} for mbox in data["mailboxes"]] + + return self.request("POST", resource=resource, data=data) + def retrieve_mail_address_list(self, querystring=None): def get_mailbox_id(value): mailboxes = value.get('mailboxes') @@ -139,7 +151,7 @@ class Orchestra(object): # PATCH to include Pangea addresses not shown by orchestra # described on issue #4 - raw_mailboxes = self.retrieve_service_list('mailbox') + raw_mailboxes = self.retrieve_mailbox_list() for mailbox in raw_mailboxes: if mailbox['addresses'] == []: address_data = { @@ -155,6 +167,11 @@ class Orchestra(object): return addresses + def retrieve_mailbox_list(self): + # TODO(@slamora) encapsulate as a Service class + raw_mailboxes = self.retrieve_service_list('mailbox') + return raw_mailboxes + def retrieve_domain(self, pk): path = API_PATHS.get('domain-detail').format_map({'pk': pk}) diff --git a/musician/forms.py b/musician/forms.py index 7a66a00..bbfd2de 100644 --- a/musician/forms.py +++ b/musician/forms.py @@ -1,5 +1,7 @@ +from django import forms from django.contrib.auth.forms import AuthenticationForm +from django.core.exceptions import ValidationError from . import api @@ -20,3 +22,23 @@ class LoginForm(AuthenticationForm): self.user = orchestra.retrieve_profile() return self.cleaned_data + + +class MailForm(forms.Form): + name = forms.CharField() + domain = forms.ChoiceField() + mailboxes = forms.MultipleChoiceField(required=False) + forward = forms.EmailField(required=False) + + def __init__(self, *args, **kwargs): + 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 diff --git a/musician/models.py b/musician/models.py index 93e3d29..978a0c8 100644 --- a/musician/models.py +++ b/musician/models.py @@ -161,7 +161,7 @@ class DatabaseService(OrchestraModel): return super().new_from_json(data=data, users=users, usage=usage) @classmethod - def get_usage(self, data): + def get_usage(cls, data): try: resources = data['resources'] resource_disk = {} @@ -201,6 +201,7 @@ class Domain(OrchestraModel): "mails": [], "usage": {}, "websites": [], + "url": None, } @classmethod diff --git a/musician/templates/musician/mail.html b/musician/templates/musician/mail.html index e8ae8be..f1fcf14 100644 --- a/musician/templates/musician/mail.html +++ b/musician/templates/musician/mail.html @@ -41,4 +41,5 @@ {% include "musician/components/table_paginator.html" %} +{% trans "New mail address" %} {% endblock %} diff --git a/musician/templates/musician/mail_form.html b/musician/templates/musician/mail_form.html new file mode 100644 index 0000000..8451647 --- /dev/null +++ b/musician/templates/musician/mail_form.html @@ -0,0 +1,10 @@ +{% extends "musician/base.html" %} +{% load i18n %} + +{% block content %} +
+ {% csrf_token %} + {{ form }} + +
+{% endblock %} diff --git a/musician/urls.py b/musician/urls.py index 9139f35..f1f3c40 100644 --- a/musician/urls.py +++ b/musician/urls.py @@ -20,6 +20,7 @@ urlpatterns = [ path('bills//download/', views.BillDownloadView.as_view(), name='bill-download'), path('profile/', views.ProfileView.as_view(), name='profile'), path('mails/', views.MailView.as_view(), name='mails'), + path('mails/new/', views.MailCreateView.as_view(), name='mail-create'), path('mailing-lists/', views.MailingListsView.as_view(), name='mailing-lists'), path('databases/', views.DatabasesView.as_view(), name='databases'), path('software-as-a-service/', views.SaasView.as_view(), name='saas'), diff --git a/musician/views.py b/musician/views.py index b70fbf5..b039b26 100644 --- a/musician/views.py +++ b/musician/views.py @@ -11,11 +11,12 @@ from django.views.generic.base import RedirectView, TemplateView from django.views.generic.detail import DetailView from django.views.generic.edit import FormView from django.views.generic.list import ListView +from requests.exceptions import HTTPError from . import api, get_version from .auth import login as auth_login from .auth import logout as auth_logout -from .forms import LoginForm +from .forms import LoginForm, MailForm from .mixins import (CustomContextMixin, ExtendedPaginationMixin, UserTokenRequiredMixin) from .models import (Bill, DatabaseService, MailinglistService, MailService, @@ -201,6 +202,29 @@ class MailView(ServiceListView): return context +class MailCreateView(CustomContextMixin, UserTokenRequiredMixin, FormView): + service_class = MailService + template_name = "musician/mail_form.html" + form_class = MailForm + success_url = reverse_lazy("musician:mails") + + 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: + self.orchestra.create_mail_address(form.cleaned_data) + except HTTPError as e: + form.add_error(field='__all__', error=e) + return self.form_invalid(form) + + return super().form_valid(form) + + class MailingListsView(ServiceListView): service_class = MailinglistService template_name = "musician/mailinglists.html" From 7ff01d60ef75d8fe34987a0f9a996617060faa78 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Thu, 24 Jun 2021 13:08:16 +0200 Subject: [PATCH 03/28] (Draft) Add view to update existing addresses --- musician/api.py | 20 ++++++++++++---- musician/forms.py | 15 ++++++++++++ musician/models.py | 12 +++++++++- musician/templates/musician/mail.html | 2 +- musician/urls.py | 1 + musician/views.py | 33 ++++++++++++++++++++++++++- 6 files changed, 75 insertions(+), 8 deletions(-) diff --git a/musician/api.py b/musician/api.py index 4913a23..9a38e01 100644 --- a/musician/api.py +++ b/musician/api.py @@ -23,6 +23,7 @@ API_PATHS = { 'domain-list': 'domains/', 'domain-detail': 'domains/{pk}/', 'address-list': 'addresses/', + 'address-detail': 'addresses/{pk}/', 'mailbox-list': 'mailboxes/', 'mailinglist-list': 'lists/', 'saas-list': 'saas/', @@ -114,13 +115,22 @@ class Orchestra(object): def create_mail_address(self, data): resource = '{}-list'.format(MailService.api_name) - - # transform form data to expected format - data["domain"] = {"url": data["domain"]} - data["mailboxes"] = [{"url": mbox} for mbox in data["mailboxes"]] - 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 MailService.new_from_json(data) + + def update_mail_address(self, pk, data): + path = API_PATHS.get('address-detail').format_map({'pk': pk}) + url = urllib.parse.urljoin(self.base_url, path) + return self.request("PUT", url=url, data=data) + def retrieve_mail_address_list(self, querystring=None): def get_mailbox_id(value): mailboxes = value.get('mailboxes') diff --git a/musician/forms.py b/musician/forms.py index bbfd2de..1302fb2 100644 --- a/musician/forms.py +++ b/musician/forms.py @@ -31,8 +31,13 @@ class MailForm(forms.Form): forward = forms.EmailField(required=False) def __init__(self, *args, **kwargs): + instance = kwargs.pop('instance', None) + if instance is not None: + kwargs['initial'] = 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] @@ -42,3 +47,13 @@ class MailForm(forms.Form): 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 diff --git a/musician/models.py b/musician/models.py index 978a0c8..7ac681f 100644 --- a/musician/models.py +++ b/musician/models.py @@ -225,12 +225,13 @@ class DomainRecord(OrchestraModel): return '<%s: %s>' % (self.type, self.value) +# TODO(@slamora) rename to Address class MailService(OrchestraModel): api_name = 'address' verbose_name = _('Mail addresses') description = _('Description details for mail addresses page.') fields = ('mail_address', 'aliases', 'type', 'type_detail') - param_defaults = {} + param_defaults = {"id": None,} FORWARD = 'forward' MAILBOX = 'mailbox' @@ -239,6 +240,15 @@ class MailService(OrchestraModel): self.data = kwargs super().__init__(**kwargs) + def deserialize(self): + data = { + 'name': self.data['name'], + 'domain': self.data['domain']['url'], + 'mailboxes': [mbox['url'] for mbox in self.data['mailboxes']], + 'forward': self.data['forward'], + } + return data + @property def aliases(self): return [ diff --git a/musician/templates/musician/mail.html b/musician/templates/musician/mail.html index f1fcf14..38f0fe3 100644 --- a/musician/templates/musician/mail.html +++ b/musician/templates/musician/mail.html @@ -26,7 +26,7 @@ {% for obj in object_list %} - {{ obj.mail_address }} + {{ obj.mail_address }} {{ obj.aliases|join:" , " }} {{ obj.type|capfirst }} diff --git a/musician/urls.py b/musician/urls.py index f1f3c40..ae2676b 100644 --- a/musician/urls.py +++ b/musician/urls.py @@ -21,6 +21,7 @@ urlpatterns = [ path('profile/', views.ProfileView.as_view(), name='profile'), path('mails/', views.MailView.as_view(), name='mails'), path('mails/new/', views.MailCreateView.as_view(), name='mail-create'), + path('mails//', views.MailUpdateView.as_view(), name='mail-update'), path('mailing-lists/', views.MailingListsView.as_view(), name='mailing-lists'), path('databases/', views.DatabasesView.as_view(), name='databases'), path('software-as-a-service/', views.SaasView.as_view(), name='saas'), diff --git a/musician/views.py b/musician/views.py index b039b26..387c4e6 100644 --- a/musician/views.py +++ b/musician/views.py @@ -217,7 +217,38 @@ class MailCreateView(CustomContextMixin, UserTokenRequiredMixin, FormView): def form_valid(self, form): # handle request errors e.g. 400 validation try: - self.orchestra.create_mail_address(form.cleaned_data) + 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 = MailService + template_name = "musician/mail_form.html" + form_class = MailForm + success_url = reverse_lazy("musician:mails") + + 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) From 4d5497f2fa5877957f2b7204cdb13be067b455f3 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Thu, 24 Jun 2021 13:19:22 +0200 Subject: [PATCH 04/28] Add django-bootstrap4 to requirements --- requirements.txt | 1 + userpanel/settings.py | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 692e60e..5c28a4e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ django==2.2.21 python-decouple==3.1 +django-bootstrap4 django-extensions dj_database_url==0.5.0 requests==2.22.0 diff --git a/userpanel/settings.py b/userpanel/settings.py index 5d0ee92..f288ba6 100644 --- a/userpanel/settings.py +++ b/userpanel/settings.py @@ -53,6 +53,7 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'django_extensions', + 'bootstrap4', 'musician', ] From 29c752e57221e0ff1c83a776580672f7c65657b2 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Thu, 24 Jun 2021 13:19:54 +0200 Subject: [PATCH 05/28] Lay out mail form using bootstrap4 --- musician/templates/musician/mail_form.html | 11 ++++++++--- musician/views.py | 2 ++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/musician/templates/musician/mail_form.html b/musician/templates/musician/mail_form.html index 8451647..c6bb3ec 100644 --- a/musician/templates/musician/mail_form.html +++ b/musician/templates/musician/mail_form.html @@ -1,10 +1,15 @@ {% extends "musician/base.html" %} -{% load i18n %} +{% load bootstrap4 i18n %} {% block content %} +

{{ service.verbose_name }}

+
{% csrf_token %} - {{ form }} - + {% bootstrap_form form %} + {% buttons %} + {% trans "Cancel" %} + + {% endbuttons %}
{% endblock %} diff --git a/musician/views.py b/musician/views.py index 387c4e6..196b688 100644 --- a/musician/views.py +++ b/musician/views.py @@ -207,6 +207,7 @@ class MailCreateView(CustomContextMixin, UserTokenRequiredMixin, FormView): template_name = "musician/mail_form.html" form_class = MailForm success_url = reverse_lazy("musician:mails") + extra_context = {'service': service_class} def get_form_kwargs(self): kwargs = super().get_form_kwargs() @@ -231,6 +232,7 @@ class MailUpdateView(CustomContextMixin, UserTokenRequiredMixin, FormView): template_name = "musician/mail_form.html" form_class = MailForm success_url = reverse_lazy("musician:mails") + extra_context = {'service': service_class} def get_form_kwargs(self): kwargs = super().get_form_kwargs() From 77577a67dadced1a9d77f411c91e9d4b351bcbc7 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Fri, 2 Jul 2021 12:57:55 +0200 Subject: [PATCH 06/28] Disable hacky patch for issue #4 because breaks code --- musician/api.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/musician/api.py b/musician/api.py index 9a38e01..3d9755c 100644 --- a/musician/api.py +++ b/musician/api.py @@ -161,19 +161,21 @@ class Orchestra(object): # PATCH to include Pangea addresses not shown by orchestra # described on issue #4 - raw_mailboxes = self.retrieve_mailbox_list() - for mailbox in raw_mailboxes: - if mailbox['addresses'] == []: - address_data = { - 'names': [mailbox['name']], - 'forward': '', - 'domain': { - 'name': 'pangea.org.', - }, - 'mailboxes': [mailbox], - } - pangea_address = MailService.new_from_json(address_data) - addresses.append(pangea_address) + # TODO(@slamora) disabled hacky patch because breaks another funtionalities + # XXX Fix it on orchestra instead of here??? + # raw_mailboxes = self.retrieve_mailbox_list() + # for mailbox in raw_mailboxes: + # if mailbox['addresses'] == []: + # address_data = { + # 'names': [mailbox['name']], + # 'forward': '', + # 'domain': { + # 'name': 'pangea.org.', + # }, + # 'mailboxes': [mailbox], + # } + # pangea_address = MailService.new_from_json(address_data) + # addresses.append(pangea_address) return addresses From 0d327127f5db33bf1f02382173406649f86b3812 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Fri, 2 Jul 2021 13:08:06 +0200 Subject: [PATCH 07/28] Rename class `MailService` to `Address` --- musician/api.py | 19 +++++++++---------- musician/models.py | 3 +-- musician/views.py | 8 ++++---- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/musician/api.py b/musician/api.py index 3d9755c..bffaa3e 100644 --- a/musician/api.py +++ b/musician/api.py @@ -1,14 +1,13 @@ -import requests import urllib.parse - from itertools import groupby + +import requests from django.conf import settings from django.http import Http404 from django.urls.exceptions import NoReverseMatch from django.utils.translation import gettext_lazy as _ -from .models import Domain, DatabaseService, MailService, SaasService, UserAccount, WebSite - +from .models import Address, DatabaseService, Domain, SaasService, UserAccount, WebSite DOMAINS_PATH = 'domains/' TOKEN_PATH = '/api-token-auth/' @@ -114,7 +113,7 @@ class Orchestra(object): return bill_pdf def create_mail_address(self, data): - resource = '{}-list'.format(MailService.api_name) + resource = '{}-list'.format(Address.api_name) return self.request("POST", resource=resource, data=data) def retrieve_mail_address(self, pk): @@ -124,7 +123,7 @@ class Orchestra(object): if status == 404: raise Http404(_("No object found matching the query")) - return MailService.new_from_json(data) + return Address.new_from_json(data) def update_mail_address(self, pk, data): path = API_PATHS.get('address-detail').format_map({'pk': pk}) @@ -143,7 +142,7 @@ class Orchestra(object): # retrieve mails applying filters (if any) raw_data = self.retrieve_service_list( - MailService.api_name, + Address.api_name, querystring=querystring, ) @@ -157,7 +156,7 @@ class Orchestra(object): data = thing data['names'] = aliases - addresses.append(MailService.new_from_json(data)) + addresses.append(Address.new_from_json(data)) # PATCH to include Pangea addresses not shown by orchestra # described on issue #4 @@ -174,7 +173,7 @@ class Orchestra(object): # }, # 'mailboxes': [mailbox], # } - # pangea_address = MailService.new_from_json(address_data) + # pangea_address = Address.new_from_json(address_data) # addresses.append(pangea_address) return addresses @@ -204,7 +203,7 @@ class Orchestra(object): # retrieve services associated to a domain domain_json['mails'] = 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) domain_json['websites'] = self.filter_websites_by_domain(websites, domain_json['id']) diff --git a/musician/models.py b/musician/models.py index 7ac681f..b0094b3 100644 --- a/musician/models.py +++ b/musician/models.py @@ -225,8 +225,7 @@ class DomainRecord(OrchestraModel): return '<%s: %s>' % (self.type, self.value) -# TODO(@slamora) rename to Address -class MailService(OrchestraModel): +class Address(OrchestraModel): api_name = 'address' verbose_name = _('Mail addresses') description = _('Description details for mail addresses page.') diff --git a/musician/views.py b/musician/views.py index 196b688..bd147a4 100644 --- a/musician/views.py +++ b/musician/views.py @@ -19,7 +19,7 @@ from .auth import logout as auth_logout from .forms import LoginForm, MailForm from .mixins import (CustomContextMixin, ExtendedPaginationMixin, UserTokenRequiredMixin) -from .models import (Bill, DatabaseService, MailinglistService, MailService, +from .models import (Address, Bill, DatabaseService, MailinglistService, PaymentSource, SaasService, UserAccount) from .settings import ALLOWED_RESOURCES from .utils import get_bootstraped_percent @@ -169,7 +169,7 @@ class BillDownloadView(CustomContextMixin, UserTokenRequiredMixin, View): class MailView(ServiceListView): - service_class = MailService + service_class = Address template_name = "musician/mail.html" extra_context = { # Translators: This message appears on the page title @@ -203,7 +203,7 @@ class MailView(ServiceListView): class MailCreateView(CustomContextMixin, UserTokenRequiredMixin, FormView): - service_class = MailService + service_class = Address template_name = "musician/mail_form.html" form_class = MailForm success_url = reverse_lazy("musician:mails") @@ -228,7 +228,7 @@ class MailCreateView(CustomContextMixin, UserTokenRequiredMixin, FormView): class MailUpdateView(CustomContextMixin, UserTokenRequiredMixin, FormView): - service_class = MailService + service_class = Address template_name = "musician/mail_form.html" form_class = MailForm success_url = reverse_lazy("musician:mails") From 9ba1d0a23c49718a8d24c477208bae0208050a88 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Fri, 24 Sep 2021 14:31:29 +0200 Subject: [PATCH 08/28] Split mail view into addresses & mailboxes --- musician/templates/musician/addresses.html | 42 ++++++++++++++++++++ musician/templates/musician/mail.html | 45 ---------------------- musician/templates/musician/mail_base.html | 30 +++++++++++++++ musician/templates/musician/mailboxes.html | 34 ++++++++++++++++ musician/urls.py | 1 + musician/views.py | 23 ++++++++++- 6 files changed, 129 insertions(+), 46 deletions(-) create mode 100644 musician/templates/musician/addresses.html delete mode 100644 musician/templates/musician/mail.html create mode 100644 musician/templates/musician/mail_base.html create mode 100644 musician/templates/musician/mailboxes.html diff --git a/musician/templates/musician/addresses.html b/musician/templates/musician/addresses.html new file mode 100644 index 0000000..3c2e6f5 --- /dev/null +++ b/musician/templates/musician/addresses.html @@ -0,0 +1,42 @@ +{% extends "musician/mail_base.html" %} +{% load i18n %} + +{% block tabcontent %} +
+ + + + + + + + + + + + + + + + + {% for obj in object_list %} + + + + + + + {% endfor %} + + {% include "musician/components/table_paginator.html" %} +
{% trans "Mail address" %}{% trans "Aliases" %}{% trans "Type" %}{% trans "Type details" %}
{{ obj.mail_address }}{{ obj.aliases|join:" , " }}{{ obj.type|capfirst }} + {% if obj.type == 'mailbox' %} + {% include "musician/components/usage_progress_bar.html" with detail=obj.type_detail %} + {% else %} + {{ obj.type_detail }} + {% endif %} +
+ {% trans "New mail address" %} + +
+{% endblock %} diff --git a/musician/templates/musician/mail.html b/musician/templates/musician/mail.html deleted file mode 100644 index 38f0fe3..0000000 --- a/musician/templates/musician/mail.html +++ /dev/null @@ -1,45 +0,0 @@ -{% extends "musician/base.html" %} -{% load i18n %} - -{% block content %} -{% if active_domain %} -{% trans "Go to global" %} -{% endif %} - -

{{ service.verbose_name }}{% if active_domain %} {% trans "for" %} {{ active_domain.name }}{% endif %}

-

{{ service.description }}

- - - - - - - - - - - - - - - - - {% for obj in object_list %} - - - - - - - {% endfor %} - - {% include "musician/components/table_paginator.html" %} -
{% trans "Mail address" %}{% trans "Aliases" %}{% trans "Type" %}{% trans "Type details" %}
{{ obj.mail_address }}{{ obj.aliases|join:" , " }}{{ obj.type|capfirst }} - {% if obj.type == 'mailbox' %} - {% include "musician/components/usage_progress_bar.html" with detail=obj.type_detail %} - {% else %} - {{ obj.type_detail }} - {% endif %} -
-{% trans "New mail address" %} -{% endblock %} diff --git a/musician/templates/musician/mail_base.html b/musician/templates/musician/mail_base.html new file mode 100644 index 0000000..1529874 --- /dev/null +++ b/musician/templates/musician/mail_base.html @@ -0,0 +1,30 @@ +{% extends "musician/base.html" %} +{% load i18n %} + +{% block content %} +{% if active_domain %} +{% trans "Go to global" %} +{% endif %} + +

{{ service.verbose_name }}{% if active_domain %} {% trans "for" + %} {{ active_domain.name }}{% endif %}

+

{{ service.description }}

+ +{% with request.resolver_match.url_name as url_name %} + + +{% endwith %} + +
+ {% block tabcontent %} + {% endblock %} + +{% endblock %} diff --git a/musician/templates/musician/mailboxes.html b/musician/templates/musician/mailboxes.html new file mode 100644 index 0000000..2c731b5 --- /dev/null +++ b/musician/templates/musician/mailboxes.html @@ -0,0 +1,34 @@ +{% extends "musician/mail_base.html" %} +{% load i18n %} + +{% block tabcontent %} +
+ + + + + + + + + + + + + + + + + {% for mailbox in mailboxes %} + + + + + + + {% endfor %} + + {% include "musician/components/table_paginator.html" %} + + +{% endblock %} diff --git a/musician/urls.py b/musician/urls.py index ae2676b..d699832 100644 --- a/musician/urls.py +++ b/musician/urls.py @@ -22,6 +22,7 @@ urlpatterns = [ path('mails/', views.MailView.as_view(), name='mails'), path('mails/new/', views.MailCreateView.as_view(), name='mail-create'), path('mails//', views.MailUpdateView.as_view(), name='mail-update'), + path('mailboxes/', views.MailboxesView.as_view(), name='mailboxes'), path('mailing-lists/', views.MailingListsView.as_view(), name='mailing-lists'), path('databases/', views.DatabasesView.as_view(), name='databases'), path('software-as-a-service/', views.SaasView.as_view(), name='saas'), diff --git a/musician/views.py b/musician/views.py index bd147a4..412f0e5 100644 --- a/musician/views.py +++ b/musician/views.py @@ -170,7 +170,7 @@ class BillDownloadView(CustomContextMixin, UserTokenRequiredMixin, View): class MailView(ServiceListView): service_class = Address - template_name = "musician/mail.html" + template_name = "musician/addresses.html" extra_context = { # Translators: This message appears on the page title 'title': _('Mail addresses'), @@ -199,6 +199,7 @@ class MailView(ServiceListView): context.update({ 'active_domain': self.orchestra.retrieve_domain(domain_id) }) + context['mailboxes'] = self.orchestra.retrieve_mailbox_list() return context @@ -287,6 +288,26 @@ class MailingListsView(ServiceListView): return '' +class MailboxesView(ServiceListView): + # TODO (@slamora) refactor after encapsulating Mailbox as a service + # service_class = Mailbox + template_name = "musician/mailboxes.html" + extra_context = { + # Translators: This message appears on the page title + 'title': _('Mailboxes'), + } + + def get_queryset(self): + # TODO (@slamora) refactor after encapsulating Mailbox as a service + return self.orchestra.retrieve_mailbox_list() + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + # TODO (@slamora) refactor after encapsulating Mailbox as a service + context['mailboxes'] = context['object_list'] + return context + + class DatabasesView(ServiceListView): template_name = "musician/databases.html" service_class = DatabaseService From 0246d0a22ec5154bf25625d8c4238d1a2cc144ca Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Mon, 27 Sep 2021 12:40:52 +0200 Subject: [PATCH 09/28] Encapsulate Mailbox as a service --- musician/api.py | 7 +++---- musician/models.py | 19 +++++++++++++++++++ musician/templates/musician/mail_base.html | 4 ++-- musician/templates/musician/mailboxes.html | 10 ++++++++-- musician/views.py | 15 ++------------- 5 files changed, 34 insertions(+), 21 deletions(-) diff --git a/musician/api.py b/musician/api.py index bffaa3e..8e38e7c 100644 --- a/musician/api.py +++ b/musician/api.py @@ -7,7 +7,7 @@ from django.http import Http404 from django.urls.exceptions import NoReverseMatch from django.utils.translation import gettext_lazy as _ -from .models import Address, DatabaseService, Domain, SaasService, UserAccount, WebSite +from .models import Address, DatabaseService, Domain, Mailbox, SaasService, UserAccount, WebSite DOMAINS_PATH = 'domains/' TOKEN_PATH = '/api-token-auth/' @@ -179,9 +179,8 @@ class Orchestra(object): return addresses def retrieve_mailbox_list(self): - # TODO(@slamora) encapsulate as a Service class - raw_mailboxes = self.retrieve_service_list('mailbox') - return raw_mailboxes + mailboxes = self.retrieve_service_list(Mailbox.api_name) + return [Mailbox.new_from_json(mailbox_data) for mailbox_data in mailboxes] def retrieve_domain(self, pk): path = API_PATHS.get('domain-detail').format_map({'pk': pk}) diff --git a/musician/models.py b/musician/models.py index b0094b3..9e62d51 100644 --- a/musician/models.py +++ b/musician/models.py @@ -17,6 +17,7 @@ class OrchestraModel: api_name = None verbose_name = None fields = () + param_defaults = {} id = None def __init__(self, **kwargs): @@ -294,6 +295,24 @@ class Address(OrchestraModel): return mailbox_details +class Mailbox(OrchestraModel): + api_name = 'mailbox' + verbose_name = _('Mailbox') + description = _('Description details for mailbox page.') + fields = ('name', 'filtering', 'addresses', 'active') + param_defaults = { + 'name': None, + 'filtering': None, + 'is_active': True, + 'addresses': [], + } + + @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) + + class MailinglistService(OrchestraModel): api_name = 'mailinglist' verbose_name = _('Mailing list') diff --git a/musician/templates/musician/mail_base.html b/musician/templates/musician/mail_base.html index 1529874..1790e90 100644 --- a/musician/templates/musician/mail_base.html +++ b/musician/templates/musician/mail_base.html @@ -16,8 +16,8 @@ {% trans "Addresses" %} - diff --git a/musician/templates/musician/mailboxes.html b/musician/templates/musician/mailboxes.html index 2c731b5..a9e0764 100644 --- a/musician/templates/musician/mailboxes.html +++ b/musician/templates/musician/mailboxes.html @@ -19,11 +19,17 @@ - {% for mailbox in mailboxes %} + {% for mailbox in object_list %} - + {% endfor %} diff --git a/musician/views.py b/musician/views.py index 412f0e5..336f4ea 100644 --- a/musician/views.py +++ b/musician/views.py @@ -19,7 +19,7 @@ from .auth import logout as auth_logout from .forms import LoginForm, MailForm from .mixins import (CustomContextMixin, ExtendedPaginationMixin, UserTokenRequiredMixin) -from .models import (Address, Bill, DatabaseService, MailinglistService, +from .models import (Address, Bill, DatabaseService, Mailbox, MailinglistService, PaymentSource, SaasService, UserAccount) from .settings import ALLOWED_RESOURCES from .utils import get_bootstraped_percent @@ -289,24 +289,13 @@ class MailingListsView(ServiceListView): class MailboxesView(ServiceListView): - # TODO (@slamora) refactor after encapsulating Mailbox as a service - # service_class = Mailbox + service_class = Mailbox template_name = "musician/mailboxes.html" extra_context = { # Translators: This message appears on the page title 'title': _('Mailboxes'), } - def get_queryset(self): - # TODO (@slamora) refactor after encapsulating Mailbox as a service - return self.orchestra.retrieve_mailbox_list() - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - # TODO (@slamora) refactor after encapsulating Mailbox as a service - context['mailboxes'] = context['object_list'] - return context - class DatabasesView(ServiceListView): template_name = "musician/databases.html" From a9c59edbf2454317210fa14c245662845425c26d Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Mon, 27 Sep 2021 13:17:49 +0200 Subject: [PATCH 10/28] Fix MailForm after encapsulate Mailbox service --- musician/forms.py | 2 +- musician/models.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/musician/forms.py b/musician/forms.py index 1302fb2..076898d 100644 --- a/musician/forms.py +++ b/musician/forms.py @@ -40,7 +40,7 @@ class MailForm(forms.Form): 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] + self.fields['mailboxes'].choices = [(m.url, m.name) for m in mailboxes] def clean(self): cleaned_data = super().clean() diff --git a/musician/models.py b/musician/models.py index 9e62d51..a470a65 100644 --- a/musician/models.py +++ b/musician/models.py @@ -301,10 +301,12 @@ class Mailbox(OrchestraModel): 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 From 6d7ee0b76a9798d9dbe1d21243eb2822e5a7566b Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Mon, 27 Sep 2021 13:37:11 +0200 Subject: [PATCH 11/28] Refactor Addresses list view --- musician/models.py | 7 +++++- musician/templates/musician/addresses.html | 26 ++++++++++------------ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/musician/models.py b/musician/models.py index a470a65..9742f8c 100644 --- a/musician/models.py +++ b/musician/models.py @@ -231,7 +231,12 @@ class Address(OrchestraModel): verbose_name = _('Mail addresses') description = _('Description details for mail addresses page.') fields = ('mail_address', 'aliases', 'type', 'type_detail') - param_defaults = {"id": None,} + param_defaults = { + "id": None, + "domain": None, + "mailboxes": [], + "forward": None, + } FORWARD = 'forward' MAILBOX = 'mailbox' diff --git a/musician/templates/musician/addresses.html b/musician/templates/musician/addresses.html index 3c2e6f5..5c0c757 100644 --- a/musician/templates/musician/addresses.html +++ b/musician/templates/musician/addresses.html @@ -6,31 +6,29 @@
{% trans "Name" %}{% trans "Filtering" %}{% trans "Addresses" %}{% trans "Active" %}
{{ mailbox.name }}{{ mailbox.filtering }}{{ mailbox.addresses }}
{{ mailbox.name }} {{ mailbox.filtering }}{{ mailbox.addresses }} + {% for addr in mailbox.addresses %} + + {{ addr.data.name }}@{{ addr.data.domain.name }} +
+ {% endfor %} +
- - - + + + - - - - + + + + {% for obj in object_list %} - - + + {% endfor %} From 98dfa7a9f41eff60f282ca1f05f39ed9b282acf8 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Mon, 27 Sep 2021 13:52:27 +0200 Subject: [PATCH 12/28] Make URLs patterns homogeneus --- musician/mixins.py | 6 +++--- musician/templates/musician/addresses.html | 4 ++-- musician/templates/musician/dashboard.html | 4 ++-- musician/templates/musician/mail_base.html | 9 +++++---- musician/templates/musician/mail_form.html | 2 +- musician/templates/musician/mailboxes.html | 2 +- musician/urls.py | 14 +++++++------- musician/views.py | 4 ++-- 8 files changed, 23 insertions(+), 22 deletions(-) diff --git a/musician/mixins.py b/musician/mixins.py index 3cc463a..ba54587 100644 --- a/musician/mixins.py +++ b/musician/mixins.py @@ -12,10 +12,10 @@ class CustomContextMixin(ContextMixin): # generate services menu items services_menu = [ {'icon': 'globe-europe', 'pattern_name': 'musician:dashboard', 'title': _('Domains & websites')}, - {'icon': 'envelope', 'pattern_name': 'musician:mails', 'title': _('Mails')}, + {'icon': 'envelope', 'pattern_name': 'musician:address-list', 'title': _('Mails')}, {'icon': 'mail-bulk', 'pattern_name': 'musician:mailing-lists', 'title': _('Mailing lists')}, - {'icon': 'database', 'pattern_name': 'musician:databases', 'title': _('Databases')}, - {'icon': 'fire', 'pattern_name': 'musician:saas', 'title': _('SaaS')}, + {'icon': 'database', 'pattern_name': 'musician:database-list', 'title': _('Databases')}, + {'icon': 'fire', 'pattern_name': 'musician:saas-list', 'title': _('SaaS')}, ] context.update({ 'services_menu': services_menu, diff --git a/musician/templates/musician/addresses.html b/musician/templates/musician/addresses.html index 5c0c757..4f6bf5c 100644 --- a/musician/templates/musician/addresses.html +++ b/musician/templates/musician/addresses.html @@ -21,7 +21,7 @@ {% for obj in object_list %} - + {% include "musician/components/table_paginator.html" %}
{% trans "Mail address" %}{% trans "Aliases" %}{% trans "Type" %}{% trans "Type details" %}{% trans "Email" %}{% trans "Domain" %}{% trans "Mailboxes" %}{% trans "Forward" %}
{{ obj.mail_address }}{{ obj.aliases|join:" , " }}{{ obj.type|capfirst }}{{ obj.domain.name }} - {% if obj.type == 'mailbox' %} - {% include "musician/components/usage_progress_bar.html" with detail=obj.type_detail %} - {% else %} - {{ obj.type_detail }} - {% endif %} + {% for mailbox in obj.mailboxes %} + {{ mailbox.name }} + {% endfor %} {{ obj.forward }}
{{ obj.mail_address }}{{ obj.mail_address }} {{ obj.domain.name }} {% for mailbox in obj.mailboxes %} @@ -34,7 +34,7 @@
- {% trans "New mail address" %} + {% trans "New mail address" %}
{% endblock %} diff --git a/musician/templates/musician/dashboard.html b/musician/templates/musician/dashboard.html index addca3d..8226bd1 100644 --- a/musician/templates/musician/dashboard.html +++ b/musician/templates/musician/dashboard.html @@ -71,7 +71,7 @@ {{ domain.addresses_left.count }} {% trans "mail address left" %} {% endif %}

- +

{% trans "Mail list" %}

@@ -82,7 +82,7 @@

{% trans "Software as a Service" %}

{% trans "Nothing installed" %}

- +
diff --git a/musician/templates/musician/mail_base.html b/musician/templates/musician/mail_base.html index 1790e90..4d15891 100644 --- a/musician/templates/musician/mail_base.html +++ b/musician/templates/musician/mail_base.html @@ -3,7 +3,7 @@ {% block content %} {% if active_domain %} -{% trans "Go to global" %} +{% trans "Go to global" %} {% endif %}

{{ service.verbose_name }}{% if active_domain %} {% trans "for" @@ -13,11 +13,12 @@ {% with request.resolver_match.url_name as url_name %} diff --git a/musician/templates/musician/mail_form.html b/musician/templates/musician/mail_form.html index c6bb3ec..2f0aa07 100644 --- a/musician/templates/musician/mail_form.html +++ b/musician/templates/musician/mail_form.html @@ -8,7 +8,7 @@ {% csrf_token %} {% bootstrap_form form %} {% buttons %} - {% trans "Cancel" %} + {% trans "Cancel" %} {% endbuttons %} diff --git a/musician/templates/musician/mailboxes.html b/musician/templates/musician/mailboxes.html index a9e0764..666c94b 100644 --- a/musician/templates/musician/mailboxes.html +++ b/musician/templates/musician/mailboxes.html @@ -25,7 +25,7 @@ {{ mailbox.filtering }} {% for addr in mailbox.addresses %} - + {{ addr.data.name }}@{{ addr.data.domain.name }}
{% endfor %} diff --git a/musician/urls.py b/musician/urls.py index d699832..308bf06 100644 --- a/musician/urls.py +++ b/musician/urls.py @@ -16,14 +16,14 @@ urlpatterns = [ path('auth/logout/', views.LogoutView.as_view(), name='logout'), path('dashboard/', views.DashboardView.as_view(), name='dashboard'), path('domains//', 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//download/', views.BillDownloadView.as_view(), name='bill-download'), path('profile/', views.ProfileView.as_view(), name='profile'), - path('mails/', views.MailView.as_view(), name='mails'), - path('mails/new/', views.MailCreateView.as_view(), name='mail-create'), - path('mails//', views.MailUpdateView.as_view(), name='mail-update'), - path('mailboxes/', views.MailboxesView.as_view(), name='mailboxes'), + path('address/', views.MailView.as_view(), name='address-list'), + path('address/new/', views.MailCreateView.as_view(), name='address-create'), + path('address//', views.MailUpdateView.as_view(), name='address-update'), + path('mailboxes/', views.MailboxesView.as_view(), name='mailbox-list'), path('mailing-lists/', views.MailingListsView.as_view(), name='mailing-lists'), - path('databases/', views.DatabasesView.as_view(), name='databases'), - path('software-as-a-service/', views.SaasView.as_view(), name='saas'), + path('databases/', views.DatabasesView.as_view(), name='database-list'), + path('saas/', views.SaasView.as_view(), name='saas-list'), ] diff --git a/musician/views.py b/musician/views.py index 336f4ea..4e524ac 100644 --- a/musician/views.py +++ b/musician/views.py @@ -207,7 +207,7 @@ class MailCreateView(CustomContextMixin, UserTokenRequiredMixin, FormView): service_class = Address template_name = "musician/mail_form.html" form_class = MailForm - success_url = reverse_lazy("musician:mails") + success_url = reverse_lazy("musician:address-list") extra_context = {'service': service_class} def get_form_kwargs(self): @@ -232,7 +232,7 @@ class MailUpdateView(CustomContextMixin, UserTokenRequiredMixin, FormView): service_class = Address template_name = "musician/mail_form.html" form_class = MailForm - success_url = reverse_lazy("musician:mails") + success_url = reverse_lazy("musician:address-list") extra_context = {'service': service_class} def get_form_kwargs(self): From b0366ff1d047bc441451537b4215be15175a8233 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Fri, 1 Oct 2021 13:36:52 +0200 Subject: [PATCH 13/28] Implement address delete --- musician/api.py | 5 ++++ musician/forms.py | 6 ++--- musician/models.py | 5 ++++ .../musician/address_check_delete.html | 12 ++++++++++ .../{mail_form.html => address_form.html} | 5 ++++ musician/urls.py | 1 + musician/views.py | 24 ++++++++++++++++--- 7 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 musician/templates/musician/address_check_delete.html rename musician/templates/musician/{mail_form.html => address_form.html} (70%) diff --git a/musician/api.py b/musician/api.py index 8e38e7c..178012e 100644 --- a/musician/api.py +++ b/musician/api.py @@ -178,6 +178,11 @@ class Orchestra(object): 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 retrieve_mailbox_list(self): mailboxes = self.retrieve_service_list(Mailbox.api_name) return [Mailbox.new_from_json(mailbox_data) for mailbox_data in mailboxes] diff --git a/musician/forms.py b/musician/forms.py index 076898d..3f90a74 100644 --- a/musician/forms.py +++ b/musician/forms.py @@ -31,9 +31,9 @@ class MailForm(forms.Form): forward = forms.EmailField(required=False) def __init__(self, *args, **kwargs): - instance = kwargs.pop('instance', None) - if instance is not None: - kwargs['initial'] = instance.deserialize() + 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') diff --git a/musician/models.py b/musician/models.py index 9742f8c..9e9a31e 100644 --- a/musician/models.py +++ b/musician/models.py @@ -233,6 +233,7 @@ class Address(OrchestraModel): fields = ('mail_address', 'aliases', 'type', 'type_detail') param_defaults = { "id": None, + "name": None, "domain": None, "mailboxes": [], "forward": None, @@ -260,6 +261,10 @@ class Address(OrchestraModel): name + '@' + self.data['domain']['name'] for name in self.data['names'][1:] ] + @property + def full_address_name(self): + return "{}@{}".format(self.name, self.domain['name']) + @property def mail_address(self): return self.data['names'][0] + '@' + self.data['domain']['name'] diff --git a/musician/templates/musician/address_check_delete.html b/musician/templates/musician/address_check_delete.html new file mode 100644 index 0000000..651b365 --- /dev/null +++ b/musician/templates/musician/address_check_delete.html @@ -0,0 +1,12 @@ +{% extends "musician/base.html" %} +{% load i18n %} + +{% block content %} +
+ {% csrf_token %} +

{% blocktrans with address_name=object.full_address_name %}Are you sure that you want remove the address: "{{ address_name }}"?{% endblocktrans %}

+

{% trans 'NOTE: This action cannot be undone.' %}

+ + {% trans 'Cancel' %} +
+{% endblock %} diff --git a/musician/templates/musician/mail_form.html b/musician/templates/musician/address_form.html similarity index 70% rename from musician/templates/musician/mail_form.html rename to musician/templates/musician/address_form.html index 2f0aa07..de21067 100644 --- a/musician/templates/musician/mail_form.html +++ b/musician/templates/musician/address_form.html @@ -10,6 +10,11 @@ {% buttons %} {% trans "Cancel" %} + {% if form.instance %} + + {% endif %} {% endbuttons %} {% endblock %} diff --git a/musician/urls.py b/musician/urls.py index 308bf06..caa6799 100644 --- a/musician/urls.py +++ b/musician/urls.py @@ -22,6 +22,7 @@ urlpatterns = [ path('address/', views.MailView.as_view(), name='address-list'), path('address/new/', views.MailCreateView.as_view(), name='address-create'), path('address//', views.MailUpdateView.as_view(), name='address-update'), + path('address//delete/', views.AddressDeleteView.as_view(), name='address-delete'), path('mailboxes/', views.MailboxesView.as_view(), name='mailbox-list'), path('mailing-lists/', views.MailingListsView.as_view(), name='mailing-lists'), path('databases/', views.DatabasesView.as_view(), name='database-list'), diff --git a/musician/views.py b/musician/views.py index 4e524ac..6a1efc1 100644 --- a/musician/views.py +++ b/musician/views.py @@ -9,7 +9,7 @@ from django.utils.translation import gettext_lazy as _ from django.views import View from django.views.generic.base import RedirectView, TemplateView from django.views.generic.detail import DetailView -from django.views.generic.edit import FormView +from django.views.generic.edit import DeleteView, FormView from django.views.generic.list import ListView from requests.exceptions import HTTPError @@ -205,7 +205,7 @@ class MailView(ServiceListView): class MailCreateView(CustomContextMixin, UserTokenRequiredMixin, FormView): service_class = Address - template_name = "musician/mail_form.html" + template_name = "musician/address_form.html" form_class = MailForm success_url = reverse_lazy("musician:address-list") extra_context = {'service': service_class} @@ -230,7 +230,7 @@ class MailCreateView(CustomContextMixin, UserTokenRequiredMixin, FormView): class MailUpdateView(CustomContextMixin, UserTokenRequiredMixin, FormView): service_class = Address - template_name = "musician/mail_form.html" + template_name = "musician/address_form.html" form_class = MailForm success_url = reverse_lazy("musician:address-list") extra_context = {'service': service_class} @@ -259,6 +259,24 @@ class MailUpdateView(CustomContextMixin, UserTokenRequiredMixin, FormView): 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) + except HTTPError as e: + print(e) + + return HttpResponseRedirect(self.success_url) + + class MailingListsView(ServiceListView): service_class = MailinglistService template_name = "musician/mailinglists.html" From ed5460c4b1118cae15d618b8cfc67412c6697e23 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Tue, 5 Oct 2021 10:04:41 +0200 Subject: [PATCH 14/28] Fix api.retrieve_mail_address_list() --- musician/api.py | 21 +-------------------- musician/models.py | 4 ---- musician/templates/musician/addresses.html | 3 ++- musician/templates/musician/mail_base.html | 5 +++-- 4 files changed, 6 insertions(+), 27 deletions(-) diff --git a/musician/api.py b/musician/api.py index 178012e..1149eaa 100644 --- a/musician/api.py +++ b/musician/api.py @@ -131,32 +131,13 @@ class Orchestra(object): return self.request("PUT", url=url, data=data) def retrieve_mail_address_list(self, querystring=None): - def get_mailbox_id(value): - mailboxes = value.get('mailboxes') - - # forwarded address should not grouped - if len(mailboxes) == 0: - return value.get('name') - - return mailboxes[0]['id'] - # retrieve mails applying filters (if any) raw_data = self.retrieve_service_list( Address.api_name, querystring=querystring, ) - # group addresses with the same mailbox - addresses = [] - for key, group in groupby(raw_data, get_mailbox_id): - aliases = [] - data = {} - for thing in group: - aliases.append(thing.pop('name')) - data = thing - - data['names'] = aliases - addresses.append(Address.new_from_json(data)) + addresses = [Address.new_from_json(data) for data in raw_data] # PATCH to include Pangea addresses not shown by orchestra # described on issue #4 diff --git a/musician/models.py b/musician/models.py index 9e9a31e..3c86283 100644 --- a/musician/models.py +++ b/musician/models.py @@ -265,10 +265,6 @@ class Address(OrchestraModel): def full_address_name(self): return "{}@{}".format(self.name, self.domain['name']) - @property - def mail_address(self): - return self.data['names'][0] + '@' + self.data['domain']['name'] - @property def type(self): if self.data['forward']: diff --git a/musician/templates/musician/addresses.html b/musician/templates/musician/addresses.html index 4f6bf5c..8d52e06 100644 --- a/musician/templates/musician/addresses.html +++ b/musician/templates/musician/addresses.html @@ -21,11 +21,12 @@ {% for obj in object_list %} - {{ obj.mail_address }} + {{ obj.full_address_name }} {{ obj.domain.name }} {% for mailbox in obj.mailboxes %} {{ mailbox.name }} + {% if not forloop.last %}
{% endif %} {% endfor %} {{ obj.forward }} diff --git a/musician/templates/musician/mail_base.html b/musician/templates/musician/mail_base.html index 4d15891..9445f7f 100644 --- a/musician/templates/musician/mail_base.html +++ b/musician/templates/musician/mail_base.html @@ -6,8 +6,9 @@ {% trans "Go to global" %} {% endif %} -

{{ service.verbose_name }}{% if active_domain %} {% trans "for" - %} {{ active_domain.name }}{% endif %}

+

{{ service.verbose_name }} + {% if active_domain %}{% trans "for" %} {{ active_domain.name }}{% endif %} +

{{ service.description }}

{% with request.resolver_match.url_name as url_name %} From 9e5145706973030ec5b259c07737a85c5cb8a48e Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Tue, 5 Oct 2021 13:10:53 +0200 Subject: [PATCH 15/28] Add view to create mailbox --- musician/api.py | 6 ++- musician/forms.py | 37 +++++++++++++++++++ musician/templates/musician/mailbox_form.html | 20 ++++++++++ musician/templates/musician/mailboxes.html | 4 +- musician/urls.py | 1 + musician/views.py | 31 +++++++++++++++- 6 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 musician/templates/musician/mailbox_form.html diff --git a/musician/api.py b/musician/api.py index 1149eaa..1365b91 100644 --- a/musician/api.py +++ b/musician/api.py @@ -83,7 +83,7 @@ class Orchestra(object): response.raise_for_status() status = response.status_code - if render_as == "json": + if status < 500 and render_as == "json": output = response.json() else: output = response.content @@ -164,6 +164,10 @@ class Orchestra(object): 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_list(self): mailboxes = self.retrieve_service_list(Mailbox.api_name) return [Mailbox.new_from_json(mailbox_data) for mailbox_data in mailboxes] diff --git a/musician/forms.py b/musician/forms.py index 3f90a74..4823fd7 100644 --- a/musician/forms.py +++ b/musician/forms.py @@ -2,6 +2,7 @@ from django import forms from django.contrib.auth.forms import AuthenticationForm from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ from . import api @@ -57,3 +58,39 @@ class MailForm(forms.Form): "forward": self.cleaned_data["forward"], } return serialized_data + + +class MailboxCreateForm(forms.Form): + error_messages = { + 'password_mismatch': _('The two password fields didn’t match.'), + } + name = forms.CharField() + password = forms.CharField( + label=_("Password"), + strip=False, + widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}), + ) + password2 = forms.CharField( + label=_("Password confirmation"), + widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}), + strip=False, + help_text=_("Enter the same password as before, for verification."), + ) + + 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"], + } + return serialized_data diff --git a/musician/templates/musician/mailbox_form.html b/musician/templates/musician/mailbox_form.html new file mode 100644 index 0000000..bafff86 --- /dev/null +++ b/musician/templates/musician/mailbox_form.html @@ -0,0 +1,20 @@ +{% extends "musician/base.html" %} +{% load bootstrap4 i18n %} + +{% block content %} +

{{ service.verbose_name }}

+ +
+ {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + {% trans "Cancel" %} + + {% if form.instance %} + + {% endif %} + {% endbuttons %} +
+{% endblock %} diff --git a/musician/templates/musician/mailboxes.html b/musician/templates/musician/mailboxes.html index 666c94b..0b1f8b0 100644 --- a/musician/templates/musician/mailboxes.html +++ b/musician/templates/musician/mailboxes.html @@ -35,6 +35,8 @@ {% endfor %} {% include "musician/components/table_paginator.html" %} -

+ + {% trans "New mailbox" %} + {% endblock %} diff --git a/musician/urls.py b/musician/urls.py index caa6799..a3740ab 100644 --- a/musician/urls.py +++ b/musician/urls.py @@ -24,6 +24,7 @@ urlpatterns = [ path('address//', views.MailUpdateView.as_view(), name='address-update'), path('address//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('mailing-lists/', views.MailingListsView.as_view(), name='mailing-lists'), path('databases/', views.DatabasesView.as_view(), name='database-list'), path('saas/', views.SaasView.as_view(), name='saas-list'), diff --git a/musician/views.py b/musician/views.py index 6a1efc1..487434f 100644 --- a/musician/views.py +++ b/musician/views.py @@ -1,3 +1,5 @@ +import logging + from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponse, HttpResponseRedirect @@ -16,7 +18,7 @@ from requests.exceptions import HTTPError from . import api, get_version from .auth import login as auth_login from .auth import logout as auth_logout -from .forms import LoginForm, MailForm +from .forms import LoginForm, MailForm, MailboxCreateForm from .mixins import (CustomContextMixin, ExtendedPaginationMixin, UserTokenRequiredMixin) from .models import (Address, Bill, DatabaseService, Mailbox, MailinglistService, @@ -25,6 +27,9 @@ from .settings import ALLOWED_RESOURCES from .utils import get_bootstraped_percent +logger = logging.getLogger(__name__) + + class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): template_name = "musician/dashboard.html" extra_context = { @@ -315,6 +320,30 @@ class MailboxesView(ServiceListView): } +class MailboxCreateView(CustomContextMixin, UserTokenRequiredMixin, FormView): + service_class = Mailbox + template_name = "musician/mailbox_form.html" + form_class = MailboxCreateForm + success_url = reverse_lazy("musician:mailbox-list") + extra_context = {'service': service_class} + + 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) + return self.form_invalid(form) + 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 super().form_valid(form) + + class DatabasesView(ServiceListView): template_name = "musician/databases.html" service_class = DatabaseService From 9b52bc4b924402ae98d1ab744861524e7105d879 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Tue, 5 Oct 2021 13:31:09 +0200 Subject: [PATCH 16/28] Show warning when extra fees may be applied on mailbox creation --- musician/models.py | 4 ++++ musician/templates/musician/mailbox_form.html | 9 +++++++++ musician/views.py | 13 ++++++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/musician/models.py b/musician/models.py index 3c86283..ffd594e 100644 --- a/musician/models.py +++ b/musician/models.py @@ -129,6 +129,10 @@ class UserAccount(OrchestraModel): return super().new_from_json(data=data, billing=billing, language=language, last_login=last_login) + def allowed_resources(self, resource): + allowed_by_type = musician_settings.ALLOWED_RESOURCES[self.type] + return allowed_by_type[resource] + class DatabaseUser(OrchestraModel): api_name = 'databaseusers' diff --git a/musician/templates/musician/mailbox_form.html b/musician/templates/musician/mailbox_form.html index bafff86..b7484bc 100644 --- a/musician/templates/musician/mailbox_form.html +++ b/musician/templates/musician/mailbox_form.html @@ -4,6 +4,15 @@ {% block content %}

{{ service.verbose_name }}

+{% if extra_mailbox %} + +{% endif %} +
{% csrf_token %} {% bootstrap_form form %} diff --git a/musician/views.py b/musician/views.py index 487434f..a6e2542 100644 --- a/musician/views.py +++ b/musician/views.py @@ -325,7 +325,18 @@ class MailboxCreateView(CustomContextMixin, UserTokenRequiredMixin, FormView): template_name = "musician/mailbox_form.html" form_class = MailboxCreateForm success_url = reverse_lazy("musician:mailbox-list") - extra_context = {'service': service_class} + + 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 form_valid(self, form): serialized_data = form.serialize() From a0808896b4ebf8fa6b05d2816b40c0401b81fe8b Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Wed, 6 Oct 2021 11:07:22 +0200 Subject: [PATCH 17/28] Allow deleting a mailbox (mark as inactive) --- musician/api.py | 17 ++++++++++++++ .../musician/mailbox_check_delete.html | 12 ++++++++++ musician/templates/musician/mailboxes.html | 10 +++++++-- musician/urls.py | 1 + musician/views.py | 22 ++++++++++++++++++- 5 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 musician/templates/musician/mailbox_check_delete.html diff --git a/musician/api.py b/musician/api.py index 1365b91..a106040 100644 --- a/musician/api.py +++ b/musician/api.py @@ -24,6 +24,7 @@ API_PATHS = { 'address-list': 'addresses/', 'address-detail': 'addresses/{pk}/', 'mailbox-list': 'mailboxes/', + 'mailbox-detail': 'mailboxes/{pk}/', 'mailinglist-list': 'lists/', 'saas-list': 'saas/', 'website-list': 'websites/', @@ -168,10 +169,26 @@ class Orchestra(object): 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 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 retrieve_domain(self, pk): path = API_PATHS.get('domain-detail').format_map({'pk': pk}) diff --git a/musician/templates/musician/mailbox_check_delete.html b/musician/templates/musician/mailbox_check_delete.html new file mode 100644 index 0000000..e0fa701 --- /dev/null +++ b/musician/templates/musician/mailbox_check_delete.html @@ -0,0 +1,12 @@ +{% extends "musician/base.html" %} +{% load i18n %} + +{% block content %} + + {% csrf_token %} +

{% blocktrans with name=object.name %}Are you sure that you want remove the mailbox: "{{ name }}"?{% endblocktrans %}

+

{% trans 'NOTE: This action cannot be undone.' %}

+ + {% trans 'Cancel' %} + +{% endblock %} diff --git a/musician/templates/musician/mailboxes.html b/musician/templates/musician/mailboxes.html index 0b1f8b0..8260b75 100644 --- a/musician/templates/musician/mailboxes.html +++ b/musician/templates/musician/mailboxes.html @@ -15,11 +15,13 @@ {% trans "Name" %} {% trans "Filtering" %} {% trans "Addresses" %} - {% trans "Active" %} + {% for mailbox in object_list %} + {# #} + {% if mailbox.is_active %} {{ mailbox.name }} {{ mailbox.filtering }} @@ -30,8 +32,12 @@
{% endfor %} - + + + + + {% endif %}{# #} {% endfor %} {% include "musician/components/table_paginator.html" %} diff --git a/musician/urls.py b/musician/urls.py index a3740ab..0902ae6 100644 --- a/musician/urls.py +++ b/musician/urls.py @@ -25,6 +25,7 @@ urlpatterns = [ path('address//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//delete/', views.MailboxDeleteView.as_view(), name='mailbox-delete'), path('mailing-lists/', views.MailingListsView.as_view(), name='mailing-lists'), path('databases/', views.DatabasesView.as_view(), name='database-list'), path('saas/', views.SaasView.as_view(), name='saas-list'), diff --git a/musician/views.py b/musician/views.py index a6e2542..8683261 100644 --- a/musician/views.py +++ b/musician/views.py @@ -277,7 +277,8 @@ class AddressDeleteView(CustomContextMixin, UserTokenRequiredMixin, DeleteView): try: self.orchestra.delete_mail_address(self.object.id) except HTTPError as e: - print(e) + # TODO(@slamora): show error message to user + logger.error(e) return HttpResponseRedirect(self.success_url) @@ -355,6 +356,25 @@ class MailboxCreateView(CustomContextMixin, UserTokenRequiredMixin, FormView): 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) + except HTTPError as e: + # TODO(@slamora): show error message to user + logger.error(e) + + return HttpResponseRedirect(self.success_url) + + class DatabasesView(ServiceListView): template_name = "musician/databases.html" service_class = DatabaseService From ddd8ecf634289f68e705107f2b94fd0fe9f98b5b Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Thu, 7 Oct 2021 13:51:31 +0200 Subject: [PATCH 18/28] Allow updating mailbox addresses --- musician/api.py | 7 +++- musician/forms.py | 20 ++++++++++ musician/models.py | 7 ++++ musician/templates/musician/mailbox_form.html | 2 +- musician/templates/musician/mailboxes.html | 14 ++----- musician/urls.py | 1 + musician/views.py | 38 ++++++++++++++++++- 7 files changed, 76 insertions(+), 13 deletions(-) diff --git a/musician/api.py b/musician/api.py index a106040..15ca269 100644 --- a/musician/api.py +++ b/musician/api.py @@ -1,5 +1,4 @@ import urllib.parse -from itertools import groupby import requests from django.conf import settings @@ -178,6 +177,12 @@ class Orchestra(object): 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] diff --git a/musician/forms.py b/musician/forms.py index 4823fd7..cd3b74d 100644 --- a/musician/forms.py +++ b/musician/forms.py @@ -94,3 +94,23 @@ class MailboxCreateForm(forms.Form): "password": self.cleaned_data["password2"], } 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 diff --git a/musician/models.py b/musician/models.py index ffd594e..146f7c3 100644 --- a/musician/models.py +++ b/musician/models.py @@ -241,6 +241,7 @@ class Address(OrchestraModel): "domain": None, "mailboxes": [], "forward": None, + 'url': None, } FORWARD = 'forward' @@ -324,6 +325,12 @@ class Mailbox(OrchestraModel): addresses = [Address.new_from_json(addr) for addr in data.get('addresses', [])] return super().new_from_json(data=data, addresses=addresses) + def deserialize(self): + data = { + 'addresses': [addr.url for addr in self.addresses], + } + return data + class MailinglistService(OrchestraModel): api_name = 'mailinglist' diff --git a/musician/templates/musician/mailbox_form.html b/musician/templates/musician/mailbox_form.html index b7484bc..af51a04 100644 --- a/musician/templates/musician/mailbox_form.html +++ b/musician/templates/musician/mailbox_form.html @@ -21,7 +21,7 @@ {% if form.instance %} {% endif %} {% endbuttons %} diff --git a/musician/templates/musician/mailboxes.html b/musician/templates/musician/mailboxes.html index 8260b75..961dd74 100644 --- a/musician/templates/musician/mailboxes.html +++ b/musician/templates/musician/mailboxes.html @@ -5,17 +5,15 @@
- - - + + - @@ -23,19 +21,15 @@ {# #} {% if mailbox.is_active %} - + - {% endif %}{# #} {% endfor %} diff --git a/musician/urls.py b/musician/urls.py index 0902ae6..62a3794 100644 --- a/musician/urls.py +++ b/musician/urls.py @@ -25,6 +25,7 @@ urlpatterns = [ path('address//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//', views.MailboxUpdateView.as_view(), name='mailbox-update'), path('mailboxes//delete/', views.MailboxDeleteView.as_view(), name='mailbox-delete'), path('mailing-lists/', views.MailingListsView.as_view(), name='mailing-lists'), path('databases/', views.DatabasesView.as_view(), name='database-list'), diff --git a/musician/views.py b/musician/views.py index 8683261..4cc0b40 100644 --- a/musician/views.py +++ b/musician/views.py @@ -18,7 +18,7 @@ from requests.exceptions import HTTPError from . import api, get_version from .auth import login as auth_login from .auth import logout as auth_logout -from .forms import LoginForm, MailForm, MailboxCreateForm +from .forms import LoginForm, MailForm, MailboxCreateForm, MailboxUpdateForm from .mixins import (CustomContextMixin, ExtendedPaginationMixin, UserTokenRequiredMixin) from .models import (Address, Bill, DatabaseService, Mailbox, MailinglistService, @@ -356,6 +356,42 @@ class MailboxCreateView(CustomContextMixin, UserTokenRequiredMixin, FormView): 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") From 056f472ee078e49c344755a62e7dc30f6ac891e7 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Thu, 7 Oct 2021 14:10:25 +0200 Subject: [PATCH 19/28] Set mailbox related addresses on creation --- musician/forms.py | 7 +++++++ musician/views.py | 10 +++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/musician/forms.py b/musician/forms.py index cd3b74d..62ea0ae 100644 --- a/musician/forms.py +++ b/musician/forms.py @@ -76,6 +76,12 @@ class MailboxCreateForm(forms.Form): 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") @@ -92,6 +98,7 @@ class MailboxCreateForm(forms.Form): serialized_data = { "name": self.cleaned_data["name"], "password": self.cleaned_data["password2"], + "addresses": self.cleaned_data["addresses"], } return serialized_data diff --git a/musician/views.py b/musician/views.py index 4cc0b40..56bc28f 100644 --- a/musician/views.py +++ b/musician/views.py @@ -339,6 +339,14 @@ class MailboxCreateView(CustomContextMixin, UserTokenRequiredMixin, FormView): 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) @@ -347,11 +355,11 @@ class MailboxCreateView(CustomContextMixin, UserTokenRequiredMixin, FormView): if status == 400: # handle errors & add to form (they will be rendered) form.add_error(field=None, error=response) - return self.form_invalid(form) 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) From a13bdeac566529fa754a90191861a640b93833ed Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Fri, 8 Oct 2021 11:42:33 +0200 Subject: [PATCH 20/28] Translate strings to es (spanish) and ca (catalan) --- musician/locale/ca/LC_MESSAGES/django.po | 195 +++++++++++++++++----- musician/locale/es/LC_MESSAGES/django.po | 198 +++++++++++++++++------ 2 files changed, 303 insertions(+), 90 deletions(-) diff --git a/musician/locale/ca/LC_MESSAGES/django.po b/musician/locale/ca/LC_MESSAGES/django.po index e5e7bb4..5ae1265 100644 --- a/musician/locale/ca/LC_MESSAGES/django.po +++ b/musician/locale/ca/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-01-23 17:49+0100\n" +"POT-Creation-Date: 2021-10-08 11:14+0200\n" "PO-Revision-Date: 2020-01-28 17:27+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -18,10 +18,35 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 2.2.4\n" -#: api.py:108 api.py:117 +#: api.py:112 api.py:203 msgid "No domain found matching the query" msgstr "No trobem cap domini que coincideixi amb la teva consulta" +#: api.py:124 +msgid "No object found matching the query" +msgstr "No trobem cap objecte que coincideixi amb la teva consulta" + +#: api.py:177 +msgid "No mailbox found matching the query" +msgstr "No trobem cap bústia que coincideixi amb la teva consulta" + +#: forms.py:65 +msgid "The two password fields didn’t match." +msgstr "Les contrasenyes introduides no coincideixen." + +#: forms.py:69 +msgid "Password" +msgstr "Contrasenya" + +#: forms.py:74 +#| msgid "view configuration" +msgid "Password confirmation" +msgstr "Verificació de la contrasenya" + +#: forms.py:77 +msgid "Enter the same password as before, for verification." +msgstr "Introdueix la mateixa contrasenya per verificar." + #: mixins.py:14 msgid "Domains & websites" msgstr "Dominis i llocs web" @@ -31,12 +56,12 @@ msgid "Mails" msgstr "Correus" #. Translators: This message appears on the page title -#: mixins.py:16 views.py:226 +#: mixins.py:16 views.py:291 msgid "Mailing lists" msgstr "Llistes de correu" #. Translators: This message appears on the page title -#: mixins.py:17 models.py:138 views.py:255 +#: mixins.py:17 models.py:147 views.py:427 msgid "Databases" msgstr "Bases de dades" @@ -44,36 +69,42 @@ msgstr "Bases de dades" msgid "SaaS" msgstr "SaaS" -#: models.py:139 -#, fuzzy +#: models.py:148 msgid "Description details for databases page." msgstr "Consulta la configuració de les teves bases de dades." #. Translators: This message appears on the page title -#: models.py:200 views.py:169 +#: models.py:235 views.py:181 msgid "Mail addresses" msgstr "Adreces de correu" -#: models.py:201 -#, fuzzy +#: models.py:236 msgid "Description details for mail addresses page." msgstr "Consulta aquí totes les adreces de correu que tens actives." -#: models.py:243 +#: models.py:311 +msgid "Mailbox" +msgstr "Bústia de correu" + +#: models.py:312 +msgid "Description details for mailbox page." +msgstr "Aquí trobaràs les teves bústies de correu i els seus detalls de " +"configuració." + + +#: models.py:337 msgid "Mailing list" msgstr "Llista de correu" -#: models.py:244 -#, fuzzy +#: models.py:338 msgid "Description details for mailinglist page." msgstr "Consulta aquí els detalls de les teves llistes de correu." -#: models.py:267 +#: models.py:364 msgid "Software as a Service (SaaS)" msgstr "Software as a Service (SaaS)" -#: models.py:268 -#, fuzzy +#: models.py:365 msgid "Description details for SaaS page." msgstr "" "Si tens algun servei SaaS (Software as a Service) contractat, aquí trobaràs " @@ -100,6 +131,57 @@ msgstr "" "Envia un correu a %(support_email)s " "indicant el teu nom d’usuari/a i t’explicarem què fer." +#: templates/musician/address_check_delete.html:7 +#, python-format +msgid "Are you sure that you want remove the address: \"%(address_name)s\"?" +msgstr "Estàs segur de que vols esborrar la adreça de correu: \"%(address_name)s\"?" + +#: templates/musician/address_check_delete.html:8 +#: templates/musician/mailbox_check_delete.html:8 +msgid "NOTE: This action cannot be undone." +msgstr "NOTA: Aquesta acció es irreversible." + +#: templates/musician/address_check_delete.html:9 +#: templates/musician/address_form.html:15 +#: templates/musician/mailbox_check_delete.html:9 +#: templates/musician/mailbox_form.html:24 +msgid "Delete" +msgstr "Esborrar" + +#: templates/musician/address_check_delete.html:10 +#: templates/musician/address_form.html:11 +#: templates/musician/mailbox_check_delete.html:10 +#: templates/musician/mailbox_form.html:20 +msgid "Cancel" +msgstr "Cancel·lar" + +#: templates/musician/address_form.html:12 +#: templates/musician/mailbox_form.html:21 +msgid "Save" +msgstr "Desar" + +#: templates/musician/addresses.html:15 +msgid "Email" +msgstr "E-mail" + +#: templates/musician/addresses.html:16 +msgid "Domain" +msgstr "Domini" + +#. Translators: This message appears on the page title +#: templates/musician/addresses.html:17 templates/musician/mail_base.html:22 +#: views.py:320 +msgid "Mailboxes" +msgstr "Bústia de correu" + +#: templates/musician/addresses.html:18 +msgid "Forward" +msgstr "Redirecció" + +#: templates/musician/addresses.html:38 +msgid "New mail address" +msgstr "Nova adreça de correu" + #: templates/musician/base.html:60 msgid "Settings" msgstr "Configuració" @@ -110,7 +192,7 @@ msgstr "Perfil" #. Translators: This message appears on the page title #: templates/musician/base.html:64 templates/musician/billing.html:6 -#: views.py:147 +#: views.py:159 msgid "Billing" msgstr "Factures" @@ -119,7 +201,6 @@ msgid "Log out" msgstr "Surt" #: templates/musician/billing.html:7 -#, fuzzy msgid "Billing page description." msgstr "Consulta i descarrega les teves factures." @@ -132,7 +213,7 @@ msgid "Bill date" msgstr "Data de la factura" #: templates/musician/billing.html:21 templates/musician/databases.html:17 -#: templates/musician/domain_detail.html:17 templates/musician/mail.html:22 +#: templates/musician/domain_detail.html:17 msgid "Type" msgstr "Tipus" @@ -178,7 +259,6 @@ msgid "Your domains and websites" msgstr "Els teus dominis i llocs web" #: templates/musician/dashboard.html:36 -#, fuzzy msgid "Dashboard page description." msgstr "" "Aquest és el teu panell de gestió, des d’on podràs consultar la configuració " @@ -209,7 +289,7 @@ msgid "Mail list" msgstr "Llista de correu" #. Translators: This message appears on the page title -#: templates/musician/dashboard.html:82 views.py:264 +#: templates/musician/dashboard.html:82 views.py:436 msgid "Software as a Service" msgstr "Software as a Service" @@ -217,7 +297,7 @@ msgstr "Software as a Service" msgid "Nothing installed" msgstr "No tens res instal·lat" -#: templates/musician/dashboard.html:89 views.py:42 +#: templates/musician/dashboard.html:89 views.py:67 msgid "Disk usage" msgstr "Ús del disc" @@ -279,7 +359,6 @@ msgid "DNS settings for" msgstr "Configuració DNS per a" #: templates/musician/domain_detail.html:8 -#, fuzzy msgid "DNS settings page description." msgstr "Consulta aquí la teva configuració DNS." @@ -287,25 +366,51 @@ msgstr "Consulta aquí la teva configuració DNS." msgid "Value" msgstr "Valor" -#: templates/musician/mail.html:6 templates/musician/mailinglists.html:6 +#: templates/musician/mail_base.html:6 templates/musician/mailinglists.html:6 msgid "Go to global" msgstr "Totes les adreces" -#: templates/musician/mail.html:9 templates/musician/mailinglists.html:9 +#: templates/musician/mail_base.html:10 templates/musician/mailinglists.html:9 msgid "for" msgstr "per a" -#: templates/musician/mail.html:20 -msgid "Mail address" -msgstr "Adreça de correu" +#: templates/musician/mail_base.html:18 templates/musician/mailboxes.html:16 +#| msgid "Mail addresses" +msgid "Addresses" +msgstr "Adreces de correu" -#: templates/musician/mail.html:21 -msgid "Aliases" -msgstr "Àlies" +#: templates/musician/mailbox_check_delete.html:7 +#, python-format +msgid "Are you sure that you want remove the mailbox: \"%(name)s\"?" +msgstr "Estàs segur de que vols esborrar la bústia de correu: \"%(name)s\"?" -#: templates/musician/mail.html:23 -msgid "Type details" -msgstr "Detalls de cada tipus" +#: templates/musician/mailbox_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 " +"extra fees may apply." +msgstr "" +"Has assolit el llímit de bústies de correu de la teva suscripció, " +"les noves bústies poden implicar costos adicionals." + +#: templates/musician/mailbox_form.html:10 +msgid "Close" +msgstr "Tancar" + +#: templates/musician/mailboxes.html:14 +msgid "Name" +msgstr "Nombre" + +#: templates/musician/mailboxes.html:15 +msgid "Filtering" +msgstr "Filtrat" + +#: templates/musician/mailboxes.html:39 +msgid "New mailbox" +msgstr "Nova bústia de correu" #: templates/musician/mailinglists.html:34 msgid "Active" @@ -316,7 +421,6 @@ msgid "Inactive" msgstr "Inactiu" #: templates/musician/profile.html:7 -#, fuzzy msgid "Little description on profile page." msgstr "Canvia les teves dades d’accés i opcions de perfil des d’aquí." @@ -357,43 +461,46 @@ msgid "Open service admin panel" msgstr "Obre el panell d’administració del servei" #. Translators: This message appears on the page title -#: views.py:32 +#: views.py:37 msgid "Dashboard" msgstr "Panell de gestió" -#: views.py:49 +#: views.py:76 msgid "Traffic" msgstr "Tràfic" -#: views.py:56 +#: views.py:85 msgid "Mailbox usage" msgstr "Ús d’espai a la bústia de correu" #. Translators: This message appears on the page title -#: views.py:96 +#: views.py:108 msgid "User profile" msgstr "El teu perfil" #. Translators: This message appears on the page title -#: views.py:154 +#: views.py:166 msgid "Download bill" msgstr "Descarrega la factura" #. Translators: This message appears on the page title -#: views.py:272 +#: views.py:444 msgid "Domain details" msgstr "Detalls del domini" #. Translators: This message appears on the page title -#: views.py:298 +#: views.py:470 msgid "Login" msgstr "Accés" +#~ msgid "Aliases" +#~ msgstr "Àlies" + +#~ msgid "Type details" +#~ msgstr "Detalls de cada tipus" + #~ msgid "databases created" #~ msgstr "bases de dades creades" #~ msgid "Username" #~ msgstr "Nom d’usuari/a" - -#~ msgid "Password:" -#~ msgstr "Contrasenya:" diff --git a/musician/locale/es/LC_MESSAGES/django.po b/musician/locale/es/LC_MESSAGES/django.po index 7ff015b..e8105e9 100644 --- a/musician/locale/es/LC_MESSAGES/django.po +++ b/musician/locale/es/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-01-23 17:49+0100\n" +"POT-Creation-Date: 2021-10-08 11:14+0200\n" "PO-Revision-Date: 2020-01-28 17:27+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -18,10 +18,34 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 2.2.4\n" -#: api.py:108 api.py:117 +#: api.py:112 api.py:203 msgid "No domain found matching the query" msgstr "No hay dominios que coincidan con tu búsqueda" +#: api.py:124 +msgid "No object found matching the query" +msgstr "No hay objetos que coincidan con tu búsqueda" + +#: api.py:177 +msgid "No mailbox found matching the query" +msgstr "No hay buzones de correo que coincidan con tu búsqueda" + +#: forms.py:65 +msgid "The two password fields didn’t match." +msgstr "Las contraseñas introducidas no coinciden." + +#: forms.py:69 +msgid "Password" +msgstr "Contraseña" + +#: forms.py:74 +msgid "Password confirmation" +msgstr "Confirma la contraseña" + +#: forms.py:77 +msgid "Enter the same password as before, for verification." +msgstr "Introduce la misma contraseña para verificarla" + #: mixins.py:14 msgid "Domains & websites" msgstr "Dominios y sitios web" @@ -31,12 +55,12 @@ msgid "Mails" msgstr "Correos" #. Translators: This message appears on the page title -#: mixins.py:16 views.py:226 +#: mixins.py:16 views.py:291 msgid "Mailing lists" msgstr "Listas de correo" #. Translators: This message appears on the page title -#: mixins.py:17 models.py:138 views.py:255 +#: mixins.py:17 models.py:147 views.py:427 msgid "Databases" msgstr "Bases de datos" @@ -44,36 +68,41 @@ msgstr "Bases de datos" msgid "SaaS" msgstr "SaaS" -#: models.py:139 -#, fuzzy +#: models.py:148 msgid "Description details for databases page." msgstr "Consulta la configuración de tus bases de datos." #. Translators: This message appears on the page title -#: models.py:200 views.py:169 +#: models.py:235 views.py:181 msgid "Mail addresses" msgstr "Direcciones de correo" -#: models.py:201 -#, fuzzy +#: models.py:236 msgid "Description details for mail addresses page." msgstr "Consulta aquí todas las direcciones de correo que tienes activas." -#: models.py:243 +#: models.py:311 +msgid "Mailbox" +msgstr "Buzón de correo" + +#: models.py:312 +msgid "Description details for mailbox page." +msgstr "" +"Aquí encontrarás tus buzones de correo y sus detalles de configuración." + +#: models.py:337 msgid "Mailing list" msgstr "Lista de correo" -#: models.py:244 -#, fuzzy +#: models.py:338 msgid "Description details for mailinglist page." msgstr "Consulta aquí los detalles de tus listas de correo." -#: models.py:267 +#: models.py:364 msgid "Software as a Service (SaaS)" msgstr "Software as a Service (SaaS)" -#: models.py:268 -#, fuzzy +#: models.py:365 msgid "Description details for SaaS page." msgstr "" "Si tienes algún servicio SaaS (Software as a Service) contratado, aquí " @@ -100,6 +129,59 @@ msgstr "" "Envía un correo a %(support_email)s " "indicando tu nombre de usuaria/o y te explicaremos qué hacer." +#: templates/musician/address_check_delete.html:7 +#, python-format +msgid "Are you sure that you want remove the address: \"%(address_name)s\"?" +msgstr "" +"¿Estás seguro de que quieres borrar la dirección de correo " +"\"%(address_name)s\"?" + +#: templates/musician/address_check_delete.html:8 +#: templates/musician/mailbox_check_delete.html:8 +msgid "NOTE: This action cannot be undone." +msgstr "NOTA: Esta acción es irreversible." + +#: templates/musician/address_check_delete.html:9 +#: templates/musician/address_form.html:15 +#: templates/musician/mailbox_check_delete.html:9 +#: templates/musician/mailbox_form.html:24 +msgid "Delete" +msgstr "Borrar" + +#: templates/musician/address_check_delete.html:10 +#: templates/musician/address_form.html:11 +#: templates/musician/mailbox_check_delete.html:10 +#: templates/musician/mailbox_form.html:20 +msgid "Cancel" +msgstr "Cancelar" + +#: templates/musician/address_form.html:12 +#: templates/musician/mailbox_form.html:21 +msgid "Save" +msgstr "Guardar" + +#: templates/musician/addresses.html:15 +msgid "Email" +msgstr "E-mail" + +#: templates/musician/addresses.html:16 +msgid "Domain" +msgstr "Dominio" + +#. Translators: This message appears on the page title +#: templates/musician/addresses.html:17 templates/musician/mail_base.html:22 +#: views.py:320 +msgid "Mailboxes" +msgstr "Buzones de correo" + +#: templates/musician/addresses.html:18 +msgid "Forward" +msgstr "Redirección" + +#: templates/musician/addresses.html:38 +msgid "New mail address" +msgstr "Nueva dirección de correo" + #: templates/musician/base.html:60 msgid "Settings" msgstr "Configuración" @@ -110,7 +192,7 @@ msgstr "Perfil" #. Translators: This message appears on the page title #: templates/musician/base.html:64 templates/musician/billing.html:6 -#: views.py:147 +#: views.py:159 msgid "Billing" msgstr "Facturas" @@ -119,7 +201,6 @@ msgid "Log out" msgstr "Desconéctate" #: templates/musician/billing.html:7 -#, fuzzy msgid "Billing page description." msgstr "Consulta y descarga tus facturas." @@ -132,7 +213,7 @@ msgid "Bill date" msgstr "Fecha de la factura" #: templates/musician/billing.html:21 templates/musician/databases.html:17 -#: templates/musician/domain_detail.html:17 templates/musician/mail.html:22 +#: templates/musician/domain_detail.html:17 msgid "Type" msgstr "Tipo" @@ -178,7 +259,6 @@ msgid "Your domains and websites" msgstr "Tus dominios y sitios web" #: templates/musician/dashboard.html:36 -#, fuzzy msgid "Dashboard page description." msgstr "" "Este es tu panel de gestión, desde donde podrás consultar la configuración " @@ -209,7 +289,7 @@ msgid "Mail list" msgstr "Lista de correo" #. Translators: This message appears on the page title -#: templates/musician/dashboard.html:82 views.py:264 +#: templates/musician/dashboard.html:82 views.py:436 msgid "Software as a Service" msgstr "Software as a Service" @@ -217,7 +297,7 @@ msgstr "Software as a Service" msgid "Nothing installed" msgstr "No tienes nada instalado" -#: templates/musician/dashboard.html:89 views.py:42 +#: templates/musician/dashboard.html:89 views.py:67 msgid "Disk usage" msgstr "Uso del disco" @@ -233,8 +313,8 @@ msgstr "Acceso FTP:" #: templates/musician/dashboard.html:115 msgid "Contact with the support team to get details concerning FTP access." msgstr "" -"Contactadnos a %(support_email)s " -"para saber cómo acceder al FTP." +"Contactadnos a %(support_email)s para " +"saber cómo acceder al FTP." #: templates/musician/dashboard.html:124 msgid "No website configured." @@ -279,7 +359,6 @@ msgid "DNS settings for" msgstr "Configuración DNS para" #: templates/musician/domain_detail.html:8 -#, fuzzy msgid "DNS settings page description." msgstr "Consulta aquí tu configuración DNS." @@ -287,25 +366,50 @@ msgstr "Consulta aquí tu configuración DNS." msgid "Value" msgstr "Valor" -#: templates/musician/mail.html:6 templates/musician/mailinglists.html:6 +#: templates/musician/mail_base.html:6 templates/musician/mailinglists.html:6 msgid "Go to global" msgstr "Todas las direcciones" -#: templates/musician/mail.html:9 templates/musician/mailinglists.html:9 +#: templates/musician/mail_base.html:10 templates/musician/mailinglists.html:9 msgid "for" msgstr "para" -#: templates/musician/mail.html:20 -msgid "Mail address" -msgstr "Dirección de correo" +#: templates/musician/mail_base.html:18 templates/musician/mailboxes.html:16 +#| msgid "Mail addresses" +msgid "Addresses" +msgstr "Direcciones de correo" -#: templates/musician/mail.html:21 -msgid "Aliases" -msgstr "Alias" +#: templates/musician/mailbox_check_delete.html:7 +#, python-format +msgid "Are you sure that you want remove the mailbox: \"%(name)s\"?" +msgstr "" +"¿Estás seguro de que quieres borrar el buzón de correo \"%(name)s\"?" -#: templates/musician/mail.html:23 -msgid "Type details" -msgstr "Detalles de cada tipo" +#: templates/musician/mailbox_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 " +"extra fees may apply." +msgstr "" + +#: templates/musician/mailbox_form.html:10 +msgid "Close" +msgstr "Cerrar" + +#: templates/musician/mailboxes.html:14 +msgid "Name" +msgstr "Nombre" + +#: templates/musician/mailboxes.html:15 +msgid "Filtering" +msgstr "Filtrado" + +#: templates/musician/mailboxes.html:39 +msgid "New mailbox" +msgstr "Nuevo buzón de correo" #: templates/musician/mailinglists.html:34 msgid "Active" @@ -316,7 +420,6 @@ msgid "Inactive" msgstr "Inactivo" #: templates/musician/profile.html:7 -#, fuzzy msgid "Little description on profile page." msgstr "Cambia tus datos de acceso y opciones de perfil desde aquí." @@ -357,43 +460,46 @@ msgid "Open service admin panel" msgstr "Abre el panel de administración del servicio" #. Translators: This message appears on the page title -#: views.py:32 +#: views.py:37 msgid "Dashboard" msgstr "Panel de gestión" -#: views.py:49 +#: views.py:76 msgid "Traffic" msgstr "Tráfico" -#: views.py:56 +#: views.py:85 msgid "Mailbox usage" msgstr "Uso de espacio en tu buzón de correo" #. Translators: This message appears on the page title -#: views.py:96 +#: views.py:108 msgid "User profile" msgstr "Tu perfil" #. Translators: This message appears on the page title -#: views.py:154 +#: views.py:166 msgid "Download bill" msgstr "Descarga la factura" #. Translators: This message appears on the page title -#: views.py:272 +#: views.py:444 msgid "Domain details" msgstr "Detalles del dominio" #. Translators: This message appears on the page title -#: views.py:298 +#: views.py:470 msgid "Login" msgstr "Accede" +#~ msgid "Aliases" +#~ msgstr "Alias" + +#~ msgid "Type details" +#~ msgstr "Detalles de cada tipo" + #~ msgid "databases created" #~ msgstr "bases de datos creadas" #~ msgid "Username" #~ msgstr "Nombre de usuario/a" - -#~ msgid "Password:" -#~ msgstr "Contraseña:" From 2aab4a666fc23813d0e6ea8883963b097e15d948 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Fri, 8 Oct 2021 11:56:46 +0200 Subject: [PATCH 21/28] Add warning message on check_delete pages --- musician/locale/ca/LC_MESSAGES/django.po | 33 +++++++++++-------- musician/locale/es/LC_MESSAGES/django.po | 25 ++++++++------ .../musician/address_check_delete.html | 2 +- .../musician/mailbox_check_delete.html | 5 ++- 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/musician/locale/ca/LC_MESSAGES/django.po b/musician/locale/ca/LC_MESSAGES/django.po index 5ae1265..50660b4 100644 --- a/musician/locale/ca/LC_MESSAGES/django.po +++ b/musician/locale/ca/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-10-08 11:14+0200\n" +"POT-Creation-Date: 2021-10-08 11:49+0200\n" "PO-Revision-Date: 2020-01-28 17:27+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -39,7 +39,6 @@ msgid "Password" msgstr "Contrasenya" #: forms.py:74 -#| msgid "view configuration" msgid "Password confirmation" msgstr "Verificació de la contrasenya" @@ -88,9 +87,8 @@ msgstr "Bústia de correu" #: models.py:312 msgid "Description details for mailbox page." -msgstr "Aquí trobaràs les teves bústies de correu i els seus detalls de " -"configuració." - +msgstr "" +"Aquí trobaràs les teves bústies de correu i els seus detalls de configuració." #: models.py:337 msgid "Mailing list" @@ -134,23 +132,24 @@ msgstr "" #: templates/musician/address_check_delete.html:7 #, python-format msgid "Are you sure that you want remove the address: \"%(address_name)s\"?" -msgstr "Estàs segur de que vols esborrar la adreça de correu: \"%(address_name)s\"?" +msgstr "" +"Estàs segur de que vols esborrar la adreça de correu: \"%(address_name)s\"?" #: templates/musician/address_check_delete.html:8 -#: templates/musician/mailbox_check_delete.html:8 -msgid "NOTE: This action cannot be undone." -msgstr "NOTA: Aquesta acció es irreversible." +#: 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:9 +#: templates/musician/mailbox_check_delete.html:12 #: templates/musician/mailbox_form.html:24 msgid "Delete" msgstr "Esborrar" #: templates/musician/address_check_delete.html:10 #: templates/musician/address_form.html:11 -#: templates/musician/mailbox_check_delete.html:10 +#: templates/musician/mailbox_check_delete.html:13 #: templates/musician/mailbox_form.html:20 msgid "Cancel" msgstr "Cancel·lar" @@ -375,7 +374,6 @@ msgid "for" msgstr "per a" #: templates/musician/mail_base.html:18 templates/musician/mailboxes.html:16 -#| msgid "Mail addresses" msgid "Addresses" msgstr "Adreces de correu" @@ -384,6 +382,13 @@ msgstr "Adreces de correu" msgid "Are you sure that you want remove the mailbox: \"%(name)s\"?" msgstr "Estàs segur de que vols esborrar la bústia de correu: \"%(name)s\"?" +#: templates/musician/mailbox_check_delete.html:9 +msgid "" +"All mailbox's messages will be deleted and cannot be recovered" +"." +msgstr "" +"Tots els missatges s'esborraran i no es podran recuperar" + #: templates/musician/mailbox_form.html:9 msgid "Warning!" msgstr "Atenció!" @@ -393,8 +398,8 @@ msgid "" "You have reached the limit of mailboxes of your subscription so " "extra fees may apply." msgstr "" -"Has assolit el llímit de bústies de correu de la teva suscripció, " -"les noves bústies poden implicar costos adicionals." +"Has assolit el llímit de bústies de correu de la teva suscripció, les noves " +"bústies poden implicar costos adicionals." #: templates/musician/mailbox_form.html:10 msgid "Close" diff --git a/musician/locale/es/LC_MESSAGES/django.po b/musician/locale/es/LC_MESSAGES/django.po index e8105e9..e58aa03 100644 --- a/musician/locale/es/LC_MESSAGES/django.po +++ b/musician/locale/es/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-10-08 11:14+0200\n" +"POT-Creation-Date: 2021-10-08 11:49+0200\n" "PO-Revision-Date: 2020-01-28 17:27+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -133,24 +133,24 @@ msgstr "" #, python-format msgid "Are you sure that you want remove the address: \"%(address_name)s\"?" msgstr "" -"¿Estás seguro de que quieres borrar la dirección de correo " -"\"%(address_name)s\"?" +"¿Estás seguro de que quieres borrar la dirección de correo \"%(address_name)s" +"\"?" #: templates/musician/address_check_delete.html:8 -#: templates/musician/mailbox_check_delete.html:8 -msgid "NOTE: This action cannot be undone." -msgstr "NOTA: Esta acción es irreversible." +#: 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:9 +#: templates/musician/mailbox_check_delete.html:12 #: templates/musician/mailbox_form.html:24 msgid "Delete" msgstr "Borrar" #: templates/musician/address_check_delete.html:10 #: templates/musician/address_form.html:11 -#: templates/musician/mailbox_check_delete.html:10 +#: templates/musician/mailbox_check_delete.html:13 #: templates/musician/mailbox_form.html:20 msgid "Cancel" msgstr "Cancelar" @@ -375,15 +375,20 @@ msgid "for" msgstr "para" #: templates/musician/mail_base.html:18 templates/musician/mailboxes.html:16 -#| msgid "Mail addresses" msgid "Addresses" msgstr "Direcciones de correo" #: templates/musician/mailbox_check_delete.html:7 #, python-format msgid "Are you sure that you want remove the mailbox: \"%(name)s\"?" +msgstr "¿Estás seguro de que quieres borrar el buzón de correo \"%(name)s\"?" + +#: templates/musician/mailbox_check_delete.html:9 +msgid "" +"All mailbox's messages will be deleted and cannot be recovered." msgstr "" -"¿Estás seguro de que quieres borrar el buzón de correo \"%(name)s\"?" +"Todos los mensajes se borrarán y no se podrán recuperar" #: templates/musician/mailbox_form.html:9 msgid "Warning!" diff --git a/musician/templates/musician/address_check_delete.html b/musician/templates/musician/address_check_delete.html index 651b365..27981d4 100644 --- a/musician/templates/musician/address_check_delete.html +++ b/musician/templates/musician/address_check_delete.html @@ -5,7 +5,7 @@
{% csrf_token %}

{% blocktrans with address_name=object.full_address_name %}Are you sure that you want remove the address: "{{ address_name }}"?{% endblocktrans %}

-

{% trans 'NOTE: This action cannot be undone.' %}

+

{% trans 'WARNING: This action cannot be undone.' %}

{% trans 'Cancel' %} diff --git a/musician/templates/musician/mailbox_check_delete.html b/musician/templates/musician/mailbox_check_delete.html index e0fa701..18b9249 100644 --- a/musician/templates/musician/mailbox_check_delete.html +++ b/musician/templates/musician/mailbox_check_delete.html @@ -5,7 +5,10 @@
{% csrf_token %}

{% blocktrans with name=object.name %}Are you sure that you want remove the mailbox: "{{ name }}"?{% endblocktrans %}

-

{% trans 'NOTE: This action cannot be undone.' %}

+ +

{% trans 'WARNING: This action cannot be undone.' %}

{% trans 'Cancel' %} From 6c773893f7887e8d6a2d59ddc20a61c38365cd64 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Fri, 8 Oct 2021 13:33:09 +0200 Subject: [PATCH 22/28] Notify managers via email on mailbox deletion --- musician/views.py | 24 ++++++++++++++++++++---- userpanel/settings.py | 13 +++++++------ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/musician/views.py b/musician/views.py index 56bc28f..878a60b 100644 --- a/musician/views.py +++ b/musician/views.py @@ -1,7 +1,9 @@ import logging +import smtplib from django.conf import settings from django.core.exceptions import ImproperlyConfigured +from django.core.mail import mail_managers from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import render from django.urls import reverse_lazy @@ -18,15 +20,14 @@ from requests.exceptions import HTTPError from . import api, get_version from .auth import login as auth_login from .auth import logout as auth_logout -from .forms import LoginForm, MailForm, MailboxCreateForm, MailboxUpdateForm +from .forms import LoginForm, MailboxCreateForm, MailboxUpdateForm, MailForm from .mixins import (CustomContextMixin, ExtendedPaginationMixin, UserTokenRequiredMixin) -from .models import (Address, Bill, DatabaseService, Mailbox, MailinglistService, - PaymentSource, SaasService, UserAccount) +from .models import (Address, Bill, DatabaseService, Mailbox, + MailinglistService, PaymentSource, SaasService) from .settings import ALLOWED_RESOURCES from .utils import get_bootstraped_percent - logger = logging.getLogger(__name__) @@ -416,8 +417,23 @@ class MailboxDeleteView(CustomContextMixin, UserTokenRequiredMixin, DeleteView): # TODO(@slamora): show error message to user 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 DatabasesView(ServiceListView): template_name = "musician/databases.html" diff --git a/userpanel/settings.py b/userpanel/settings.py index f288ba6..937642e 100644 --- a/userpanel/settings.py +++ b/userpanel/settings.py @@ -41,6 +41,8 @@ EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='') EMAIL_PORT = config('EMAIL_PORT', default=25, cast=int) +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' + ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=[], cast=Csv()) @@ -149,12 +151,6 @@ USE_L10N = True USE_TZ = True -LANGUAGES = ( - ('ca', _('Catalan')), - ('es', _('Spanish')), - ('en', _('English')), -) - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.2/howto/static-files/ @@ -177,3 +173,8 @@ URL_SAAS_GITLAB = config('URL_SAAS_GITLAB', None) URL_SAAS_OWNCLOUD = config('URL_SAAS_OWNCLOUD', None) URL_SAAS_WORDPRESS = config('URL_SAAS_WORDPRESS', None) + + +# Managers: who should get notifications about services changes that +# may require human actions (e.g. deleted mailboxes) +MANAGERS = [] From 33e68b5d07dfb950b1778dc244bab4693530138a Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Thu, 14 Oct 2021 11:09:59 +0200 Subject: [PATCH 23/28] Add view to change mailbox password --- musician/api.py | 8 +++ musician/forms.py | 34 +++++++++ musician/static/musician/css/default.css | 71 +++++++++++-------- .../musician/mailbox_change_password.html | 15 ++++ musician/templates/musician/mailbox_form.html | 1 + musician/templates/musician/mailboxes.html | 6 +- musician/urls.py | 1 + musician/views.py | 27 ++++++- 8 files changed, 130 insertions(+), 33 deletions(-) create mode 100644 musician/templates/musician/mailbox_change_password.html diff --git a/musician/api.py b/musician/api.py index 15ca269..2b33dee 100644 --- a/musician/api.py +++ b/musician/api.py @@ -24,6 +24,7 @@ API_PATHS = { 'address-detail': 'addresses/{pk}/', 'mailbox-list': 'mailboxes/', 'mailbox-detail': 'mailboxes/{pk}/', + 'mailbox-password': 'mailboxes/{pk}/set_password/', 'mailinglist-list': 'lists/', 'saas-list': 'saas/', 'website-list': 'websites/', @@ -194,6 +195,13 @@ class Orchestra(object): # return self.request("DELETE", url=url, render_as=None) return self.request("PATCH", url=url, data={"is_active": False}) + def set_password_mailbox(self, pk, data): + path = API_PATHS.get('mailbox-password').format_map({'pk': pk}) + url = urllib.parse.urljoin(self.base_url, path) + status, response = self.request("POST", url=url, data=data, raise_exception=False) + return status, response + + def retrieve_domain(self, pk): path = API_PATHS.get('domain-detail').format_map({'pk': pk}) diff --git a/musician/forms.py b/musician/forms.py index 62ea0ae..98e23c4 100644 --- a/musician/forms.py +++ b/musician/forms.py @@ -60,6 +60,40 @@ class MailForm(forms.Form): return serialized_data +class MailboxChangePasswordForm(forms.Form): + error_messages = { + 'password_mismatch': _('The two password fields didn’t match.'), + } + password = forms.CharField( + label=_("Password"), + strip=False, + widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}), + ) + password2 = forms.CharField( + label=_("Password confirmation"), + widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}), + strip=False, + help_text=_("Enter the same password as before, for verification."), + ) + + def clean_password2(self): + password = self.cleaned_data.get("password") + password2 = self.cleaned_data.get("password2") + if password and password2 and password != password2: + raise ValidationError( + self.error_messages['password_mismatch'], + code='password_mismatch', + ) + return password2 + + def serialize(self): + assert self.is_valid() + serialized_data = { + "password": self.cleaned_data["password2"], + } + return serialized_data + + class MailboxCreateForm(forms.Form): error_messages = { 'password_mismatch': _('The two password fields didn’t match.'), diff --git a/musician/static/musician/css/default.css b/musician/static/musician/css/default.css index 3fbe7da..0bbfe4b 100644 --- a/musician/static/musician/css/default.css +++ b/musician/static/musician/css/default.css @@ -4,31 +4,30 @@ a, a:hover, a:focus { } a:hover { - color: rgba(0,0,0,.7); + color: rgba(0, 0, 0, .7); } -.btn-arrow-left{ +.btn-arrow-left { color: #eee; background: #D3D0DA; position: relative; padding: 8px 20px 8px 30px; - margin-left: 1em; /** equal value than arrow.left **/ + margin-left: 1em; + /** equal value than arrow.left **/ } -.btn-arrow-left::after, -.btn-arrow-left::before{ +.btn-arrow-left::after, .btn-arrow-left::before { content: ""; position: absolute; top: 50%; left: -1em; - margin-top: -19px; border-top: 19px solid transparent; border-bottom: 19px solid transparent; border-right: 1em solid; } -.btn-arrow-left::after{ +.btn-arrow-left::after { border-right-color: #D3D0DA; z-index: 2; } @@ -43,13 +42,12 @@ a:hover { min-width: 280px; max-width: 280px; min-height: 100vh; - position: fixed; z-index: 999; - display: flex; flex-direction: column; } + #sidebar #sidebar-services { flex-grow: 1; } @@ -62,20 +60,20 @@ a:hover { padding-left: 2rem; padding-right: 2rem; } + #sidebar #sidebar-services { padding-left: 1rem; padding-right: 1rem; } #sidebar #user-profile-menu { - background:rgba(254, 251, 242, 0.25); + background: rgba(254, 251, 242, 0.25); } #sidebar ul.components { padding: 20px 0; } - #sidebar ul li a { padding: 10px; font-size: 1.1em; @@ -89,25 +87,26 @@ a:hover { } .vertical-center { - min-height: 100%; /* Fallback for browsers do NOT support vh unit */ - min-height: 100vh; /* These two lines are counted as one :-) */ - + min-height: 100%; + /* Fallback for browsers do NOT support vh unit */ + min-height: 100vh; + /* These two lines are counted as one :-) */ display: flex; align-items: center; - } +} /** login **/ + #body-login .jumbotron { background: #282532 no-repeat url("../images/logo-pangea-lilla-bg.svg") right; } #login-content { - background:white; + background: white; padding: 2rem; } -#login-content input[type="text"].form-control, -#login-content input[type="password"].form-control { +#login-content input[type="text"].form-control, #login-content input[type="password"].form-control { border-radius: 0; border: 0; border-bottom: 2px solid #8E8E8E; @@ -121,6 +120,7 @@ a:hover { margin-top: 1.5rem; text-align: center; } + #login-footer a { color: #FEFBF2; } @@ -130,34 +130,37 @@ a:hover { background-position: right 5% top 10%; color: #343434; padding-left: 2rem; - margin-left: 280px; /** sidebar width **/ + margin-left: 280px; + /** sidebar width **/ } /** services **/ + h1.service-name { - font: Bold 26px/34px Roboto; margin-top: 3rem; } .service-description { - font: 16px/21px Roboto; } + .table.service-list { margin-top: 2rem; table-layout: fixed; } + /** TODO update theme instead of overriding **/ -.service-list thead.thead-dark th, -.service-card .card-header { + +.service-list thead.thead-dark th, .service-card .card-header { background: rgba(80, 70, 110, 0.25); color: #50466E; border-color: transparent; } + /** /TODO **/ -.table.service-list td, -.table.service-list th { + +.table.service-list td, .table.service-list th { vertical-align: middle; } @@ -202,11 +205,10 @@ h1.service-name { .service-card .card-body { color: #787878; - } .service-card .card-body i.fas { - color:#9C9AA7; + color: #9C9AA7; } .service-manager-link { @@ -215,8 +217,7 @@ h1.service-name { right: 15px; } -.service-card .service-manager-link a, -.service-card .service-manager-link a i.fas { +.service-card .service-manager-link a, .service-card .service-manager-link a i.fas { color: white; } @@ -243,11 +244,9 @@ h1.service-name { font-variant: normal; text-rendering: auto; -webkit-font-smoothing: antialiased; - position: absolute; top: 0; right: 10px; - color: #E8E7EB; font-size: 2em; } @@ -308,3 +307,13 @@ h1.service-name { border-top: 0; justify-content: center; } + +.roll-hover { + visibility: hidden; + display: inline-block; + margin-left: 2rem; +} + +td:hover .roll-hover { + visibility: visible; +} diff --git a/musician/templates/musician/mailbox_change_password.html b/musician/templates/musician/mailbox_change_password.html new file mode 100644 index 0000000..e18b95a --- /dev/null +++ b/musician/templates/musician/mailbox_change_password.html @@ -0,0 +1,15 @@ +{% extends "musician/base.html" %} +{% load bootstrap4 i18n %} + +{% block content %} +

{% trans "Change password" %}: {{ object.name }}

+ +
+ {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + {% trans "Cancel" %} + + {% endbuttons %} + +{% endblock %} diff --git a/musician/templates/musician/mailbox_form.html b/musician/templates/musician/mailbox_form.html index af51a04..13eb3c3 100644 --- a/musician/templates/musician/mailbox_form.html +++ b/musician/templates/musician/mailbox_form.html @@ -21,6 +21,7 @@ {% if form.instance %} {% endif %} diff --git a/musician/templates/musician/mailboxes.html b/musician/templates/musician/mailboxes.html index 961dd74..a9f3700 100644 --- a/musician/templates/musician/mailboxes.html +++ b/musician/templates/musician/mailboxes.html @@ -21,7 +21,11 @@ {# #} {% if mailbox.is_active %}
- + diff --git a/musician/templates/musician/mailbox_form.html b/musician/templates/musician/mailbox_form.html index 13eb3c3..5fb9465 100644 --- a/musician/templates/musician/mailbox_form.html +++ b/musician/templates/musician/mailbox_form.html @@ -21,7 +21,7 @@ {% if form.instance %} {% endif %} From aee0267f17c0dc8ab8d2bf6500ed46edcdca9d8e Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Thu, 14 Oct 2021 12:56:50 +0200 Subject: [PATCH 27/28] Fix mailbox resource usage on dashboard. Mail addresses are not limited, only mailboxes. --- musician/api.py | 2 +- musician/models.py | 2 +- musician/templates/musician/dashboard.html | 11 ++--- musician/views.py | 47 +++++++++++----------- 4 files changed, 32 insertions(+), 30 deletions(-) diff --git a/musician/api.py b/musician/api.py index 2b33dee..6b60c0c 100644 --- a/musician/api.py +++ b/musician/api.py @@ -221,7 +221,7 @@ class Orchestra(object): querystring = "domain={}".format(domain_json['id']) # retrieve services associated to a domain - domain_json['mails'] = self.retrieve_service_list( + domain_json['addresses'] = self.retrieve_service_list( Address.api_name, querystring) # retrieve websites (as they cannot be filtered by domain on the API we should do it here) diff --git a/musician/models.py b/musician/models.py index 146f7c3..8aa4e77 100644 --- a/musician/models.py +++ b/musician/models.py @@ -203,7 +203,7 @@ class Domain(OrchestraModel): "id": None, "name": None, "records": [], - "mails": [], + "addresses": [], "usage": {}, "websites": [], "url": None, diff --git a/musician/templates/musician/dashboard.html b/musician/templates/musician/dashboard.html index 8226bd1..591b26e 100644 --- a/musician/templates/musician/dashboard.html +++ b/musician/templates/musician/dashboard.html @@ -16,6 +16,11 @@
{{ usage.verbose_name }}
{% include "musician/components/usage_progress_bar.html" with detail=usage.data %} + {% if usage.data.alert %} +
+ {{ usage.data.alert }} +
+ {% endif %}
{% endfor %} @@ -65,11 +70,7 @@

{% trans "Mail" %}

- {{ domain.mails|length }} {% trans "mail addresses created" %} - {% if domain.addresses_left.alert_level %} -
- {{ domain.addresses_left.count }} {% trans "mail address left" %} - {% endif %} + {{ domain.addresses|length }} {% trans "mail addresses created" %}

diff --git a/musician/views.py b/musician/views.py index 2daab06..663f498 100644 --- a/musician/views.py +++ b/musician/views.py @@ -10,6 +10,7 @@ from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import render from django.urls import reverse_lazy from django.utils import translation +from django.utils.html import format_html from django.utils.http import is_safe_url from django.utils.translation import gettext_lazy as _ from django.views import View @@ -49,20 +50,6 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): # show resource usage based on plan definition profile_type = context['profile'].type - total_mailboxes = 0 - for domain in domains: - total_mailboxes += len(domain.mails) - addresses_left = ALLOWED_RESOURCES[profile_type]['mailbox'] - len(domain.mails) - alert_level = None - if addresses_left == 1: - alert_level = 'warning' - elif addresses_left < 1: - alert_level = 'danger' - - domain.addresses_left = { - 'count': addresses_left, - 'alert_level': alert_level, - } # TODO(@slamora) update when backend provides resource usage data resource_usage = { @@ -84,15 +71,7 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): # 'percent': 25, }, }, - 'mailbox': { - 'verbose_name': _('Mailbox usage'), - 'data': { - 'usage': total_mailboxes, - 'total': ALLOWED_RESOURCES[profile_type]['mailbox'], - 'unit': 'accounts', - 'percent': get_bootstraped_percent(total_mailboxes, ALLOWED_RESOURCES[profile_type]['mailbox']), - }, - }, + 'mailbox': self.get_mailbox_usage(profile_type), } context.update({ @@ -103,6 +82,28 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): return context + def get_mailbox_usage(self, profile_type): + allowed_mailboxes = ALLOWED_RESOURCES[profile_type]['mailbox'] + total_mailboxes = len(self.orchestra.retrieve_mailbox_list()) + mailboxes_left = allowed_mailboxes - total_mailboxes + + alert = '' + if mailboxes_left < 0: + alert = format_html("{} extra mailboxes", mailboxes_left * -1) + elif mailboxes_left <= 1: + alert = format_html("{} mailbox left", mailboxes_left) + + return { + 'verbose_name': _('Mailbox usage'), + 'data': { + 'usage': total_mailboxes, + 'total': allowed_mailboxes, + 'alert': alert, + 'unit': 'mailboxes', + 'percent': get_bootstraped_percent(total_mailboxes, allowed_mailboxes), + }, + } + class ProfileView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): template_name = "musician/profile.html" From bd42b83ea34bac311f51aa025dfe19257f0fd2e1 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Fri, 22 Oct 2021 12:10:00 +0200 Subject: [PATCH 28/28] Handle total=None on get_bootstraped_percent --- musician/tests.py | 35 ++++++++++++++++++++++++++++++++++- musician/utils.py | 2 +- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/musician/tests.py b/musician/tests.py index 8becf4d..4927c4a 100644 --- a/musician/tests.py +++ b/musician/tests.py @@ -1,9 +1,37 @@ from django.test import TestCase -from .models import UserAccount +from .models import DatabaseService, UserAccount from .utils import get_bootstraped_percent +class DatabaseTest(TestCase): + def test_database_from_json(self): + data = { + "url": "https://example.org/api/databases/1/", + "id": 1, + "name": "bluebird", + "type": "mysql", + "users": [ + { + "url": "https://example.org/api/databaseusers/2/", + "id": 2, + "username": "bluebird" + } + ], + "resources": [ + { + "name": "disk", + "used": "1.798", + "allocated": None, + "unit": "MiB" + } + ] + } + + database = DatabaseService.new_from_json(data) + self.assertEqual(0, database.usage['percent']) + + class DomainsTestCase(TestCase): def test_domain_not_found(self): response = self.client.post( @@ -118,3 +146,8 @@ class GetBootstrapedPercentTest(TestCase): def test_invalid_total_is_zero(self): value = get_bootstraped_percent(25, 0) + self.assertEqual(value, 0) + + def test_invalid_total_is_none(self): + value = get_bootstraped_percent(25, None) + self.assertEqual(value, 0) diff --git a/musician/utils.py b/musician/utils.py index affc93f..8dea94e 100644 --- a/musician/utils.py +++ b/musician/utils.py @@ -6,7 +6,7 @@ def get_bootstraped_percent(value, total): """ try: percent = value / total - except ZeroDivisionError: + except (TypeError, ZeroDivisionError): return 0 bootstraped = round(percent * 4) * 100 // 4
{% trans "Name" %} {% trans "Filtering" %} {% trans "Addresses" %}
{{ mailbox.name }}{{ mailbox.name }} {{ mailbox.filtering }} {% for addr in mailbox.addresses %} - {{ addr.data.name }}@{{ addr.data.domain.name }} + {{ addr.full_address_name }}
{% endfor %}
- - -
{{ mailbox.name }} + {{ mailbox.name }} + + {% trans "Update password" %} + {{ mailbox.filtering }} {% for addr in mailbox.addresses %} diff --git a/musician/urls.py b/musician/urls.py index 62a3794..c4402e2 100644 --- a/musician/urls.py +++ b/musician/urls.py @@ -27,6 +27,7 @@ urlpatterns = [ path('mailboxes/new/', views.MailboxCreateView.as_view(), name='mailbox-create'), path('mailboxes//', views.MailboxUpdateView.as_view(), name='mailbox-update'), path('mailboxes//delete/', views.MailboxDeleteView.as_view(), name='mailbox-delete'), + path('mailboxes//change-password/', views.MailboxChangePasswordView.as_view(), name='mailbox-password'), path('mailing-lists/', views.MailingListsView.as_view(), name='mailing-lists'), path('databases/', views.DatabasesView.as_view(), name='database-list'), path('saas/', views.SaasView.as_view(), name='saas-list'), diff --git a/musician/views.py b/musician/views.py index 878a60b..7347327 100644 --- a/musician/views.py +++ b/musician/views.py @@ -20,7 +20,7 @@ from requests.exceptions import HTTPError from . import api, get_version from .auth import login as auth_login from .auth import logout as auth_logout -from .forms import LoginForm, MailboxCreateForm, MailboxUpdateForm, MailForm +from .forms import LoginForm, MailboxChangePasswordForm, MailboxCreateForm, MailboxUpdateForm, MailForm from .mixins import (CustomContextMixin, ExtendedPaginationMixin, UserTokenRequiredMixin) from .models import (Address, Bill, DatabaseService, Mailbox, @@ -435,6 +435,31 @@ class MailboxDeleteView(CustomContextMixin, UserTokenRequiredMixin, DeleteView): 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) + return super().form_valid(form) + + class DatabasesView(ServiceListView): template_name = "musician/databases.html" service_class = DatabaseService From b171cbf641b8f29a487626b7ff812b72d1d9abbb Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Thu, 14 Oct 2021 11:59:59 +0200 Subject: [PATCH 24/28] Use django.contrib.messages to show alerts --- musician/templates/musician/base.html | 10 ++++++++++ musician/views.py | 10 ++++++++++ userpanel/settings.py | 11 +++++++++++ 3 files changed, 31 insertions(+) diff --git a/musician/templates/musician/base.html b/musician/templates/musician/base.html index 27a1eb8..7b56ab9 100644 --- a/musician/templates/musician/base.html +++ b/musician/templates/musician/base.html @@ -82,6 +82,16 @@ {% endblock sidebar %}
+ {% block messages %} + {% for message in messages %} + + {% endfor %} + {% endblock messages %} {% block content %} {% endblock content %}
diff --git a/musician/views.py b/musician/views.py index 7347327..bf5319c 100644 --- a/musician/views.py +++ b/musician/views.py @@ -1,7 +1,9 @@ import logging +from os import stat import smtplib from django.conf import settings +from django.contrib import messages from django.core.exceptions import ImproperlyConfigured from django.core.mail import mail_managers from django.http import HttpResponse, HttpResponseRedirect @@ -457,6 +459,14 @@ class MailboxChangePasswordView(CustomContextMixin, UserTokenRequiredMixin, Form 'password': form.cleaned_data['password2'] } status, response = self.orchestra.set_password_mailbox(self.kwargs['pk'], data) + + if status < 400: + messages.add_message(self.request, messages.SUCCESS, _('Password updated!')) + else: + messages.add_message(self.request, messages.ERROR, _( + 'Cannot process your request, please try again later.')) + logger.error("{}: {}".format(status, str(response)[:100])) + return super().form_valid(form) diff --git a/userpanel/settings.py b/userpanel/settings.py index 937642e..08121c2 100644 --- a/userpanel/settings.py +++ b/userpanel/settings.py @@ -13,6 +13,7 @@ https://docs.djangoproject.com/en/2.2/ref/settings/ import os from decouple import config, Csv +from django.contrib.messages import constants as messages from django.utils.translation import gettext_lazy as _ from dj_database_url import parse as db_url @@ -178,3 +179,13 @@ 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', +} From d77b876a54b7b412f005010b1521ab71b8cd0b4e Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Thu, 14 Oct 2021 12:05:42 +0200 Subject: [PATCH 25/28] Show success & error messages --- musician/views.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/musician/views.py b/musician/views.py index bf5319c..2daab06 100644 --- a/musician/views.py +++ b/musician/views.py @@ -279,8 +279,9 @@ class AddressDeleteView(CustomContextMixin, UserTokenRequiredMixin, DeleteView): self.object = self.get_object() try: self.orchestra.delete_mail_address(self.object.id) + messages.success(self.request, _('Address deleted!')) except HTTPError as e: - # TODO(@slamora): show error message to user + messages.error(self.request, _('Cannot process your request, please try again later.')) logger.error(e) return HttpResponseRedirect(self.success_url) @@ -415,8 +416,9 @@ class MailboxDeleteView(CustomContextMixin, UserTokenRequiredMixin, DeleteView): self.object = self.get_object() try: self.orchestra.delete_mailbox(self.object.id) + messages.success(self.request, _('Mailbox deleted!')) except HTTPError as e: - # TODO(@slamora): show error message to user + messages.error(self.request, _('Cannot process your request, please try again later.')) logger.error(e) self.notify_managers(self.object) @@ -461,10 +463,9 @@ class MailboxChangePasswordView(CustomContextMixin, UserTokenRequiredMixin, Form status, response = self.orchestra.set_password_mailbox(self.kwargs['pk'], data) if status < 400: - messages.add_message(self.request, messages.SUCCESS, _('Password updated!')) + messages.success(self.request, _('Password updated!')) else: - messages.add_message(self.request, messages.ERROR, _( - 'Cannot process your request, please try again later.')) + messages.error(self.request, _('Cannot process your request, please try again later.')) logger.error("{}: {}".format(status, str(response)[:100])) return super().form_valid(form) From d7bd21d865842079cd64d50e991a57f42dd16ec1 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Thu, 14 Oct 2021 12:08:22 +0200 Subject: [PATCH 26/28] Fix broken links --- musician/templates/musician/addresses.html | 2 +- musician/templates/musician/mailbox_form.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/musician/templates/musician/addresses.html b/musician/templates/musician/addresses.html index 8d52e06..1ebc8b7 100644 --- a/musician/templates/musician/addresses.html +++ b/musician/templates/musician/addresses.html @@ -25,7 +25,7 @@
{{ obj.domain.name }} {% for mailbox in obj.mailboxes %} - {{ mailbox.name }} + {{ mailbox.name }} {% if not forloop.last %}
{% endif %} {% endfor %}