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 %}
+ {% trans "Change password" %} {% trans "Delete" %}
{% 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 %} - {{ 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