Compare commits

...

3 Commits

Author SHA1 Message Date
Santiago L f2537fac33 Add bootstrap4 to INSTALLED_APPS 2023-11-23 12:53:33 +01:00
Santiago L fa2d81dfe2 Refactor address & mailbox views 2023-11-23 12:50:55 +01:00
Santiago L 6497122024 Refactor Mailboxes list view 2023-11-23 11:18:30 +01:00
7 changed files with 84 additions and 200 deletions

View File

@ -71,6 +71,7 @@ INSTALLED_APPS = [
'passlib.ext.django', 'passlib.ext.django',
'django_countries', 'django_countries',
# 'debug_toolbar', # 'debug_toolbar',
'bootstrap4',
# Django.contrib # Django.contrib
'django.contrib.auth', 'django.contrib.auth',

View File

@ -1,9 +1,11 @@
from django import forms from django import forms
from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import AuthenticationForm
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from orchestra.contrib.domains.models import Domain
from orchestra.contrib.mailboxes.models import Address, Mailbox
from . import api from . import api
@ -25,23 +27,16 @@ class LoginForm(AuthenticationForm):
return self.cleaned_data return self.cleaned_data
class MailForm(forms.Form): class MailForm(forms.ModelForm):
name = forms.CharField() class Meta:
domain = forms.ChoiceField() model = Address
mailboxes = forms.MultipleChoiceField(required=False) fields = ("name", "domain", "mailboxes", "forward")
forward = forms.EmailField(required=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.instance = kwargs.pop('instance', None) self.user = kwargs.pop('user')
if self.instance is not None:
kwargs['initial'] = self.instance.deserialize()
domains = kwargs.pop('domains')
mailboxes = kwargs.pop('mailboxes')
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['domain'].choices = [(d.url, d.name) for d in domains] self.fields['domain'].queryset = Domain.objects.filter(account=self.user)
self.fields['mailboxes'].choices = [(m.url, m.name) for m in mailboxes] self.fields['mailboxes'].queryset = Mailbox.objects.filter(account=self.user)
def clean(self): def clean(self):
cleaned_data = super().clean() cleaned_data = super().clean()
@ -49,18 +44,15 @@ class MailForm(forms.Form):
raise ValidationError("A mailbox or forward address should be provided.") raise ValidationError("A mailbox or forward address should be provided.")
return cleaned_data return cleaned_data
def serialize(self): def save(self, commit=True):
assert hasattr(self, 'cleaned_data') instance = super().save(commit=False)
serialized_data = { instance.account = self.user
"name": self.cleaned_data["name"], if commit:
"domain": {"url": self.cleaned_data["domain"]}, super().save(commit=True)
"mailboxes": [{"url": mbox} for mbox in self.cleaned_data["mailboxes"]], return instance
"forward": self.cleaned_data["forward"],
}
return serialized_data
class MailboxChangePasswordForm(forms.Form): class MailboxChangePasswordForm(forms.ModelForm):
error_messages = { error_messages = {
'password_mismatch': _('The two password fields didnt match.'), 'password_mismatch': _('The two password fields didnt match.'),
} }
@ -76,6 +68,10 @@ class MailboxChangePasswordForm(forms.Form):
help_text=_("Enter the same password as before, for verification."), help_text=_("Enter the same password as before, for verification."),
) )
class Meta:
fields = ("password",)
model = Mailbox
def clean_password2(self): def clean_password2(self):
password = self.cleaned_data.get("password") password = self.cleaned_data.get("password")
password2 = self.cleaned_data.get("password2") password2 = self.cleaned_data.get("password2")
@ -86,15 +82,8 @@ class MailboxChangePasswordForm(forms.Form):
) )
return password2 return password2
def serialize(self):
assert self.is_valid()
serialized_data = {
"password": self.cleaned_data["password2"],
}
return serialized_data
class MailboxCreateForm(forms.ModelForm):
class MailboxCreateForm(forms.Form):
error_messages = { error_messages = {
'password_mismatch': _('The two password fields didnt match.'), 'password_mismatch': _('The two password fields didnt match.'),
} }
@ -110,12 +99,17 @@ class MailboxCreateForm(forms.Form):
strip=False, strip=False,
help_text=_("Enter the same password as before, for verification."), help_text=_("Enter the same password as before, for verification."),
) )
addresses = forms.MultipleChoiceField(required=False) addresses = forms.ModelMultipleChoiceField(queryset=Address.objects.none(), required=False)
class Meta:
fields = ("name", "password", "password2", "addresses")
model = Mailbox
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
addresses = kwargs.pop('addresses') user = kwargs.pop('user')
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['addresses'].choices = [(addr.url, addr.full_address_name) for addr in addresses] self.fields['addresses'].queryset = Address.objects.filter(account=user)
self.user = user
def clean_password2(self): def clean_password2(self):
password = self.cleaned_data.get("password") password = self.cleaned_data.get("password")
@ -127,31 +121,16 @@ class MailboxCreateForm(forms.Form):
) )
return password2 return password2
def serialize(self): def save(self, commit=True):
assert self.is_valid() instance = super().save(commit=False)
serialized_data = { instance.account = self.user
"name": self.cleaned_data["name"], if commit:
"password": self.cleaned_data["password2"], super().save(commit=True)
"addresses": self.cleaned_data["addresses"], return instance
}
return serialized_data
class MailboxUpdateForm(forms.Form): class MailboxUpdateForm(forms.ModelForm):
addresses = forms.MultipleChoiceField(required=False) addresses = forms.MultipleChoiceField(required=False)
class Meta:
def __init__(self, *args, **kwargs): fields = ('addresses',)
self.instance = kwargs.pop('instance', None) model = Mailbox
if self.instance is not None:
kwargs['initial'] = self.instance.deserialize()
addresses = kwargs.pop('addresses')
super().__init__(*args, **kwargs)
self.fields['addresses'].choices = [(addr.url, addr.full_address_name) for addr in addresses]
def serialize(self):
assert self.is_valid()
serialized_data = {
"addresses": self.cleaned_data["addresses"],
}
return serialized_data

View File

@ -10,7 +10,7 @@
{% buttons %} {% buttons %}
<a class="btn btn-light mr-2" href="{% url 'musician:address-list' %}">{% trans "Cancel" %}</a> <a class="btn btn-light mr-2" href="{% url 'musician:address-list' %}">{% trans "Cancel" %}</a>
<button type="submit" class="btn btn-secondary">{% trans "Save" %}</button> <button type="submit" class="btn btn-secondary">{% trans "Save" %}</button>
{% if form.instance %} {% if form.instance.pk %}
<div class="float-right"> <div class="float-right">
<a class="btn btn-danger" href="{% url 'musician:address-delete' view.kwargs.pk %}">{% trans "Delete" %}</a> <a class="btn btn-danger" href="{% url 'musician:address-delete' view.kwargs.pk %}">{% trans "Delete" %}</a>
</div> </div>

View File

@ -21,10 +21,10 @@
<tbody> <tbody>
{% for obj in object_list %} {% for obj in object_list %}
<tr> <tr>
<td><a href="{% url 'musician:address-update' obj.id %}">{{ obj.full_address_name }}</a></td> <td><a href="{% url 'musician:address-update' obj.id %}">{{ obj.email }}</a></td>
<td>{{ obj.domain.name }}</td> <td>{{ obj.domain.name }}</td>
<td> <td>
{% for mailbox in obj.mailboxes %} {% for mailbox in obj.mailboxes.all %}
<a href="{% url 'musician:mailbox-update' mailbox.id %}">{{ mailbox.name }}</a> <a href="{% url 'musician:mailbox-update' mailbox.id %}">{{ mailbox.name }}</a>
{% if not forloop.last %}<br/> {% endif %} {% if not forloop.last %}<br/> {% endif %}
{% endfor %} {% endfor %}

View File

@ -19,10 +19,10 @@
{% buttons %} {% buttons %}
<a class="btn btn-light mr-2" href="{% url 'musician:mailbox-list' %}">{% trans "Cancel" %}</a> <a class="btn btn-light mr-2" href="{% url 'musician:mailbox-list' %}">{% trans "Cancel" %}</a>
<button type="submit" class="btn btn-secondary">{% trans "Save" %}</button> <button type="submit" class="btn btn-secondary">{% trans "Save" %}</button>
{% if form.instance %} {% if form.instance.pk %}
<div class="float-right"> <div class="float-right">
<a class="btn btn-outline-warning" href="{% url 'musician:mailbox-password' view.kwargs.pk %}"><i class="fas fa-key"></i> {% trans "Change password" %}</a> <a class="btn btn-outline-warning" href="{% url 'musician:mailbox-password' form.instance.pk %}"><i class="fas fa-key"></i> {% trans "Change password" %}</a>
<a class="btn btn-danger" href="{% url 'musician:mailbox-delete' view.kwargs.pk %}">{% trans "Delete" %}</a> <a class="btn btn-danger" href="{% url 'musician:mailbox-delete' form.instance.pk %}">{% trans "Delete" %}</a>
</div> </div>
{% endif %} {% endif %}
{% endbuttons %} {% endbuttons %}

View File

@ -28,7 +28,7 @@
</td> </td>
<td>{{ mailbox.filtering }}</td> <td>{{ mailbox.filtering }}</td>
<td> <td>
{% for addr in mailbox.addresses %} {% for addr in mailbox.addresses.all %}
<a href="{% url 'musician:address-update' addr.data.id %}"> <a href="{% url 'musician:address-update' addr.data.id %}">
{{ addr.full_address_name }} {{ addr.full_address_name }}
</a><br/> </a><br/>

View File

@ -16,13 +16,15 @@ from django.utils.translation import gettext_lazy as _
from django.views import View from django.views import View
from django.views.generic.base import RedirectView, TemplateView from django.views.generic.base import RedirectView, TemplateView
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
from django.views.generic.edit import DeleteView, FormView from django.views.generic.edit import (CreateView, DeleteView, FormView,
UpdateView)
from django.views.generic.list import ListView from django.views.generic.list import ListView
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
from orchestra import get_version from orchestra import get_version
from orchestra.contrib.bills.models import Bill from orchestra.contrib.bills.models import Bill
from orchestra.contrib.domains.models import Domain from orchestra.contrib.domains.models import Domain
from orchestra.contrib.mailboxes.models import Address, Mailbox
from orchestra.contrib.saas.models import SaaS from orchestra.contrib.saas.models import SaaS
from orchestra.utils.html import html_to_pdf from orchestra.utils.html import html_to_pdf
@ -32,10 +34,11 @@ from .forms import (LoginForm, MailboxChangePasswordForm, MailboxCreateForm,
MailboxUpdateForm, MailForm) MailboxUpdateForm, MailForm)
from .mixins import (CustomContextMixin, ExtendedPaginationMixin, from .mixins import (CustomContextMixin, ExtendedPaginationMixin,
UserTokenRequiredMixin) UserTokenRequiredMixin)
from .models import Address from .models import Address as AddressService
from .models import Bill as BillService from .models import Bill as BillService
from .models import (DatabaseService, Mailbox, MailinglistService, from .models import DatabaseService
PaymentSource, SaasService) from .models import Mailbox as MailboxService
from .models import MailinglistService, SaasService
from .settings import ALLOWED_RESOURCES from .settings import ALLOWED_RESOURCES
from .utils import get_bootstraped_percent from .utils import get_bootstraped_percent
@ -227,28 +230,21 @@ class BillDownloadView(CustomContextMixin, UserTokenRequiredMixin, View):
class MailView(ServiceListView): class MailView(ServiceListView):
service_class = Address service_class = AddressService
model = Address
template_name = "musician/addresses.html" template_name = "musician/addresses.html"
extra_context = { extra_context = {
# Translators: This message appears on the page title # Translators: This message appears on the page title
'title': _('Mail addresses'), 'title': _('Mail addresses'),
} }
def get_queryset(self):
# retrieve mails applying filters (if any)
queryfilter = self.get_queryfilter()
addresses = self.orchestra.retrieve_mail_address_list(
querystring=queryfilter
)
return addresses
def get_queryfilter(self): def get_queryfilter(self):
"""Retrieve query params (if any) to filter queryset""" """Retrieve query params (if any) to filter queryset"""
domain_id = self.request.GET.get('domain') domain_id = self.request.GET.get('domain')
if domain_id: if domain_id:
return "domain={}".format(domain_id) return {"domain": domain_id}
return '' return {}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -261,8 +257,9 @@ class MailView(ServiceListView):
return context return context
class MailCreateView(CustomContextMixin, UserTokenRequiredMixin, FormView): class MailCreateView(CustomContextMixin, UserTokenRequiredMixin, CreateView):
service_class = Address service_class = AddressService
model = Address
template_name = "musician/address_form.html" template_name = "musician/address_form.html"
form_class = MailForm form_class = MailForm
success_url = reverse_lazy("musician:address-list") success_url = reverse_lazy("musician:address-list")
@ -270,24 +267,13 @@ class MailCreateView(CustomContextMixin, UserTokenRequiredMixin, FormView):
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs['domains'] = self.orchestra.retrieve_domain_list() kwargs['user'] = self.request.user
kwargs['mailboxes'] = self.orchestra.retrieve_mailbox_list()
return kwargs return kwargs
def form_valid(self, form):
# handle request errors e.g. 400 validation
try:
serialized_data = form.serialize()
self.orchestra.create_mail_address(serialized_data)
except HTTPError as e:
form.add_error(field='__all__', error=e)
return self.form_invalid(form)
return super().form_valid(form) class MailUpdateView(CustomContextMixin, UserTokenRequiredMixin, UpdateView):
service_class = AddressService
model = Address
class MailUpdateView(CustomContextMixin, UserTokenRequiredMixin, FormView):
service_class = Address
template_name = "musician/address_form.html" template_name = "musician/address_form.html"
form_class = MailForm form_class = MailForm
success_url = reverse_lazy("musician:address-list") success_url = reverse_lazy("musician:address-list")
@ -295,27 +281,9 @@ class MailUpdateView(CustomContextMixin, UserTokenRequiredMixin, FormView):
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
instance = self.orchestra.retrieve_mail_address(self.kwargs['pk']) kwargs["user"] = self.request.user
kwargs.update({
'instance': instance,
'domains': self.orchestra.retrieve_domain_list(),
'mailboxes': self.orchestra.retrieve_mailbox_list(),
})
return kwargs return kwargs
def form_valid(self, form):
# handle request errors e.g. 400 validation
try:
serialized_data = form.serialize()
self.orchestra.update_mail_address(self.kwargs['pk'], serialized_data)
except HTTPError as e:
form.add_error(field='__all__', error=e)
return self.form_invalid(form)
return super().form_valid(form)
class AddressDeleteView(CustomContextMixin, UserTokenRequiredMixin, DeleteView): class AddressDeleteView(CustomContextMixin, UserTokenRequiredMixin, DeleteView):
template_name = "musician/address_check_delete.html" template_name = "musician/address_check_delete.html"
@ -360,13 +328,14 @@ class MailingListsView(ServiceListView):
# doesn't support filtering by domain # doesn't support filtering by domain
domain_id = self.request.GET.get('domain') domain_id = self.request.GET.get('domain')
if domain_id: if domain_id:
return "domain={}".format(domain_id) return {"domain": domain_id}
return '' return {}
class MailboxesView(ServiceListView): class MailboxesView(ServiceListView):
service_class = Mailbox service_class = MailboxService
model = Mailbox
template_name = "musician/mailboxes.html" template_name = "musician/mailboxes.html"
extra_context = { extra_context = {
# Translators: This message appears on the page title # Translators: This message appears on the page title
@ -374,8 +343,9 @@ class MailboxesView(ServiceListView):
} }
class MailboxCreateView(CustomContextMixin, UserTokenRequiredMixin, FormView): class MailboxCreateView(CustomContextMixin, UserTokenRequiredMixin, CreateView):
service_class = Mailbox service_class = MailboxService
model = Mailbox
template_name = "musician/mailbox_form.html" template_name = "musician/mailbox_form.html"
form_class = MailboxCreateForm form_class = MailboxCreateForm
success_url = reverse_lazy("musician:mailbox-list") success_url = reverse_lazy("musician:mailbox-list")
@ -390,68 +360,27 @@ class MailboxCreateView(CustomContextMixin, UserTokenRequiredMixin, FormView):
def is_extra_mailbox(self, profile): def is_extra_mailbox(self, profile):
number_of_mailboxes = len(self.orchestra.retrieve_mailbox_list()) number_of_mailboxes = len(self.orchestra.retrieve_mailbox_list())
return number_of_mailboxes >= profile.allowed_resources('mailbox') # TODO(@slamora): how to retrieve allowed mailboxes?
allowed_mailboxes = 2 # TODO(@slamora): harcoded value
return number_of_mailboxes >= allowed_mailboxes
# return number_of_mailboxes >= profile.allowed_resources('mailbox')
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs.update({ kwargs.update({
'addresses': self.orchestra.retrieve_mail_address_list(), 'user': self.request.user,
}) })
return kwargs return kwargs
def form_valid(self, form): class MailboxUpdateView(CustomContextMixin, UserTokenRequiredMixin, UpdateView):
serialized_data = form.serialize() service_class = MailboxService
status, response = self.orchestra.create_mailbox(serialized_data) model = Mailbox
if status >= 400:
if status == 400:
# handle errors & add to form (they will be rendered)
form.add_error(field=None, error=response)
else:
logger.error("{}: {}".format(status, response[:120]))
msg = "Sorry, an error occurred while processing your request ({})".format(status)
form.add_error(field='__all__', error=msg)
return self.form_invalid(form)
return super().form_valid(form)
class MailboxUpdateView(CustomContextMixin, UserTokenRequiredMixin, FormView):
service_class = Mailbox
template_name = "musician/mailbox_form.html" template_name = "musician/mailbox_form.html"
form_class = MailboxUpdateForm form_class = MailboxUpdateForm
success_url = reverse_lazy("musician:mailbox-list") success_url = reverse_lazy("musician:mailbox-list")
extra_context = {'service': service_class} 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): class MailboxDeleteView(CustomContextMixin, UserTokenRequiredMixin, DeleteView):
template_name = "musician/mailbox_check_delete.html" template_name = "musician/mailbox_check_delete.html"
@ -488,37 +417,12 @@ class MailboxDeleteView(CustomContextMixin, UserTokenRequiredMixin, DeleteView):
logger.error("Error sending email to managers", exc_info=True) logger.error("Error sending email to managers", exc_info=True)
class MailboxChangePasswordView(CustomContextMixin, UserTokenRequiredMixin, FormView): class MailboxChangePasswordView(CustomContextMixin, UserTokenRequiredMixin, UpdateView):
template_name = "musician/mailbox_change_password.html" template_name = "musician/mailbox_change_password.html"
model = Mailbox
form_class = MailboxChangePasswordForm form_class = MailboxChangePasswordForm
success_url = reverse_lazy("musician:mailbox-list") success_url = reverse_lazy("musician:mailbox-list")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
self.object = self.get_object()
context.update({
'object': self.object,
})
return context
def get_object(self, queryset=None):
obj = self.orchestra.retrieve_mailbox(self.kwargs['pk'])
return obj
def form_valid(self, form):
data = {
'password': form.cleaned_data['password2']
}
status, response = self.orchestra.set_password_mailbox(self.kwargs['pk'], data)
if status < 400:
messages.success(self.request, _('Password updated!'))
else:
messages.error(self.request, _('Cannot process your request, please try again later.'))
logger.error("{}: {}".format(status, str(response)[:100]))
return super().form_valid(form)
class DatabasesView(ServiceListView): class DatabasesView(ServiceListView):
template_name = "musician/databases.html" template_name = "musician/databases.html"