django-musician/musician/views.py

535 lines
17 KiB
Python
Raw Normal View History

2021-10-05 11:10:53 +00:00
import logging
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
2019-12-17 14:15:58 +00:00
from django.http import HttpResponse, HttpResponseRedirect
2019-10-10 07:18:34 +00:00
from django.shortcuts import render
from django.urls import reverse_lazy
from django.utils import translation
2019-10-31 09:46:54 +00:00
from django.utils.http import is_safe_url
from django.utils.translation import gettext_lazy as _
2019-12-17 14:15:58 +00:00
from django.views import View
from django.views.generic.base import RedirectView, TemplateView
2019-12-06 09:28:34 +00:00
from django.views.generic.detail import DetailView
2021-10-01 11:36:52 +00:00
from django.views.generic.edit import DeleteView, FormView
from django.views.generic.list import ListView
2021-06-23 11:47:27 +00:00
from requests.exceptions import HTTPError
2019-10-10 07:18:34 +00:00
from . import api, get_version
from .auth import login as auth_login
from .auth import logout as auth_logout
2021-10-07 11:51:31 +00:00
from .forms import LoginForm, MailForm, MailboxCreateForm, MailboxUpdateForm
2019-12-06 09:28:34 +00:00
from .mixins import (CustomContextMixin, ExtendedPaginationMixin,
UserTokenRequiredMixin)
2021-09-27 10:40:52 +00:00
from .models import (Address, Bill, DatabaseService, Mailbox, MailinglistService,
2019-12-06 09:28:34 +00:00
PaymentSource, SaasService, UserAccount)
from .settings import ALLOWED_RESOURCES
from .utils import get_bootstraped_percent
2019-10-25 11:33:37 +00:00
2021-10-05 11:10:53 +00:00
logger = logging.getLogger(__name__)
class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
2019-10-25 11:33:37 +00:00
template_name = "musician/dashboard.html"
2020-01-07 12:53:59 +00:00
extra_context = {
# Translators: This message appears on the page title
'title': _('Dashboard'),
}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
domains = self.orchestra.retrieve_domain_list()
# TODO(@slamora) update when backend supports notifications
notifications = []
# show resource usage based on plan definition
profile_type = context['profile'].type
total_mailboxes = 0
for domain in domains:
total_mailboxes += len(domain.mails)
2020-01-20 09:20:26 +00:00
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'
2020-01-20 09:20:26 +00:00
domain.addresses_left = {
'count': addresses_left,
'alert_level': alert_level,
}
# TODO(@slamora) update when backend provides resource usage data
resource_usage = {
'disk': {
'verbose_name': _('Disk usage'),
'data': {
# 'usage': 534,
# 'total': 1024,
# 'unit': 'MB',
# 'percent': 50,
},
},
'traffic': {
'verbose_name': _('Traffic'),
'data': {
# 'usage': 300,
# 'total': 2048,
# 'unit': 'MB/month',
# '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']),
},
},
}
context.update({
'domains': domains,
'resource_usage': resource_usage,
'notifications': notifications,
})
return context
2019-11-20 19:07:35 +00:00
class ProfileView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
template_name = "musician/profile.html"
2020-01-07 12:53:59 +00:00
extra_context = {
# Translators: This message appears on the page title
'title': _('User profile'),
}
2019-11-20 19:07:35 +00:00
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
try:
pay_source = self.orchestra.retrieve_service_list(
PaymentSource.api_name)[0]
except IndexError:
pay_source = {}
context.update({
'payment': PaymentSource.new_from_json(pay_source)
})
return context
2019-10-31 16:16:51 +00:00
class ServiceListView(CustomContextMixin, ExtendedPaginationMixin, UserTokenRequiredMixin, ListView):
"""Base list view to all services"""
service_class = None
2020-01-07 12:53:59 +00:00
template_name = "musician/service_list.html"
2019-10-31 16:16:51 +00:00
def get_queryset(self):
if self.service_class is None or self.service_class.api_name is None:
raise ImproperlyConfigured(
"ServiceListView requires a definiton of 'service'")
queryfilter = self.get_queryfilter()
2019-11-20 19:07:35 +00:00
json_qs = self.orchestra.retrieve_service_list(
self.service_class.api_name,
querystring=queryfilter,
)
return [self.service_class.new_from_json(data) for data in json_qs]
def get_queryfilter(self):
"""Does nothing by default. Should be implemented on subclasses"""
return ''
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'service': self.service_class,
})
return context
2019-12-17 13:48:21 +00:00
class BillingView(ServiceListView):
service_class = Bill
template_name = "musician/billing.html"
2020-01-07 12:53:59 +00:00
extra_context = {
# Translators: This message appears on the page title
'title': _('Billing'),
}
2019-12-17 13:48:21 +00:00
2019-12-17 14:15:58 +00:00
class BillDownloadView(CustomContextMixin, UserTokenRequiredMixin, View):
2020-01-07 12:53:59 +00:00
extra_context = {
# Translators: This message appears on the page title
'title': _('Download bill'),
}
2019-12-17 14:15:58 +00:00
def get(self, request, *args, **kwargs):
pk = self.kwargs.get('pk')
bill = self.orchestra.retrieve_bill_document(pk)
return HttpResponse(bill)
class MailView(ServiceListView):
service_class = Address
template_name = "musician/addresses.html"
2020-01-07 12:53:59 +00:00
extra_context = {
# Translators: This message appears on the page title
'title': _('Mail addresses'),
}
def get_queryset(self):
# retrieve mails applying filters (if any)
queryfilter = self.get_queryfilter()
2020-02-17 10:07:21 +00:00
addresses = self.orchestra.retrieve_mail_address_list(
querystring=queryfilter
)
return addresses
2019-10-30 13:23:46 +00:00
def get_queryfilter(self):
"""Retrieve query params (if any) to filter queryset"""
domain_id = self.request.GET.get('domain')
if domain_id:
return "domain={}".format(domain_id)
return ''
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
domain_id = self.request.GET.get('domain')
if domain_id:
context.update({
'active_domain': self.orchestra.retrieve_domain(domain_id)
})
context['mailboxes'] = self.orchestra.retrieve_mailbox_list()
return context
2019-10-30 13:23:46 +00:00
2021-06-23 11:47:27 +00:00
class MailCreateView(CustomContextMixin, UserTokenRequiredMixin, FormView):
service_class = Address
2021-10-01 11:36:52 +00:00
template_name = "musician/address_form.html"
2021-06-23 11:47:27 +00:00
form_class = MailForm
2021-09-27 11:52:27 +00:00
success_url = reverse_lazy("musician:address-list")
2021-06-24 11:19:54 +00:00
extra_context = {'service': service_class}
2021-06-23 11:47:27 +00:00
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['domains'] = self.orchestra.retrieve_domain_list()
kwargs['mailboxes'] = self.orchestra.retrieve_mailbox_list()
return kwargs
def form_valid(self, form):
# handle request errors e.g. 400 validation
try:
serialized_data = form.serialize()
self.orchestra.create_mail_address(serialized_data)
except HTTPError as e:
form.add_error(field='__all__', error=e)
return self.form_invalid(form)
return super().form_valid(form)
class MailUpdateView(CustomContextMixin, UserTokenRequiredMixin, FormView):
service_class = Address
2021-10-01 11:36:52 +00:00
template_name = "musician/address_form.html"
form_class = MailForm
2021-09-27 11:52:27 +00:00
success_url = reverse_lazy("musician:address-list")
2021-06-24 11:19:54 +00:00
extra_context = {'service': service_class}
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
instance = self.orchestra.retrieve_mail_address(self.kwargs['pk'])
kwargs.update({
'instance': instance,
'domains': self.orchestra.retrieve_domain_list(),
'mailboxes': self.orchestra.retrieve_mailbox_list(),
})
return kwargs
def form_valid(self, form):
# handle request errors e.g. 400 validation
try:
serialized_data = form.serialize()
self.orchestra.update_mail_address(self.kwargs['pk'], serialized_data)
2021-06-23 11:47:27 +00:00
except HTTPError as e:
form.add_error(field='__all__', error=e)
return self.form_invalid(form)
return super().form_valid(form)
2021-10-01 11:36:52 +00:00
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:
# TODO(@slamora): show error message to user
logger.error(e)
2021-10-01 11:36:52 +00:00
return HttpResponseRedirect(self.success_url)
2019-10-31 16:16:51 +00:00
class MailingListsView(ServiceListView):
2019-11-13 10:42:23 +00:00
service_class = MailinglistService
2019-12-04 11:37:35 +00:00
template_name = "musician/mailinglists.html"
2020-01-07 12:53:59 +00:00
extra_context = {
# Translators: This message appears on the page title
'title': _('Mailing lists'),
}
2019-10-30 13:23:46 +00:00
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
domain_id = self.request.GET.get('domain')
if domain_id:
context.update({
'active_domain': self.orchestra.retrieve_domain(domain_id)
})
return context
def get_queryfilter(self):
"""Retrieve query params (if any) to filter queryset"""
# TODO(@slamora): this is not working because backend API
# doesn't support filtering by domain
domain_id = self.request.GET.get('domain')
if domain_id:
return "domain={}".format(domain_id)
return ''
class MailboxesView(ServiceListView):
2021-09-27 10:40:52 +00:00
service_class = Mailbox
template_name = "musician/mailboxes.html"
extra_context = {
# Translators: This message appears on the page title
'title': _('Mailboxes'),
}
2021-10-05 11:10:53 +00:00
class MailboxCreateView(CustomContextMixin, UserTokenRequiredMixin, FormView):
service_class = Mailbox
template_name = "musician/mailbox_form.html"
form_class = MailboxCreateForm
success_url = reverse_lazy("musician:mailbox-list")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'extra_mailbox': self.is_extra_mailbox(context['profile']),
'service': self.service_class,
})
return context
def is_extra_mailbox(self, profile):
number_of_mailboxes = len(self.orchestra.retrieve_mailbox_list())
return number_of_mailboxes >= profile.allowed_resources('mailbox')
2021-10-05 11:10:53 +00:00
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs.update({
'addresses': self.orchestra.retrieve_mail_address_list(),
})
return kwargs
2021-10-05 11:10:53 +00:00
def form_valid(self, form):
serialized_data = form.serialize()
status, response = self.orchestra.create_mailbox(serialized_data)
if status >= 400:
if status == 400:
# handle errors & add to form (they will be rendered)
form.add_error(field=None, error=response)
else:
logger.error("{}: {}".format(status, response[:120]))
msg = "Sorry, an error occurred while processing your request ({})".format(status)
form.add_error(field='__all__', error=msg)
return self.form_invalid(form)
2021-10-05 11:10:53 +00:00
return super().form_valid(form)
2021-10-07 11:51:31 +00:00
class MailboxUpdateView(CustomContextMixin, UserTokenRequiredMixin, FormView):
service_class = Mailbox
template_name = "musician/mailbox_form.html"
form_class = MailboxUpdateForm
success_url = reverse_lazy("musician:mailbox-list")
extra_context = {'service': service_class}
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
instance = self.orchestra.retrieve_mailbox(self.kwargs['pk'])
kwargs.update({
'instance': instance,
'addresses': self.orchestra.retrieve_mail_address_list(),
})
return kwargs
def form_valid(self, form):
serialized_data = form.serialize()
status, response = self.orchestra.update_mailbox(self.kwargs['pk'], serialized_data)
if status >= 400:
if status == 400:
# handle errors & add to form (they will be rendered)
form.add_error(field=None, error=response)
else:
logger.error("{}: {}".format(status, response[:120]))
msg = "Sorry, an error occurred while processing your request ({})".format(status)
form.add_error(field='__all__', error=msg)
return self.form_invalid(form)
return super().form_valid(form)
class MailboxDeleteView(CustomContextMixin, UserTokenRequiredMixin, DeleteView):
template_name = "musician/mailbox_check_delete.html"
success_url = reverse_lazy("musician:mailbox-list")
def get_object(self, queryset=None):
obj = self.orchestra.retrieve_mailbox(self.kwargs['pk'])
return obj
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
try:
self.orchestra.delete_mailbox(self.object.id)
except HTTPError as e:
# TODO(@slamora): show error message to user
logger.error(e)
return HttpResponseRedirect(self.success_url)
2019-11-13 11:27:25 +00:00
class DatabasesView(ServiceListView):
template_name = "musician/databases.html"
2019-11-13 11:27:25 +00:00
service_class = DatabaseService
2020-01-07 12:53:59 +00:00
extra_context = {
# Translators: This message appears on the page title
'title': _('Databases'),
}
2019-10-30 13:23:46 +00:00
2019-12-06 09:28:34 +00:00
class SaasView(ServiceListView):
service_class = SaasService
2019-10-30 13:23:46 +00:00
template_name = "musician/saas.html"
2020-01-07 12:53:59 +00:00
extra_context = {
# Translators: This message appears on the page title
'title': _('Software as a Service'),
}
2019-10-30 13:23:46 +00:00
2019-12-13 14:08:01 +00:00
class DomainDetailView(CustomContextMixin, UserTokenRequiredMixin, DetailView):
template_name = "musician/domain_detail.html"
2020-01-07 12:53:59 +00:00
extra_context = {
# Translators: This message appears on the page title
'title': _('Domain details'),
}
2019-12-13 14:08:01 +00:00
def get_queryset(self):
2019-12-17 09:25:10 +00:00
# Return an empty list to avoid a request to retrieve all the
# user domains. We will get a 404 if the domain doesn't exists
# while invoking `get_object`
return []
2019-12-13 14:08:01 +00:00
def get_object(self, queryset=None):
if queryset is None:
queryset = self.get_queryset()
pk = self.kwargs.get(self.pk_url_kwarg)
2019-12-17 09:25:10 +00:00
domain = self.orchestra.retrieve_domain(pk)
2019-12-13 14:08:01 +00:00
return domain
class LoginView(FormView):
template_name = 'auth/login.html'
form_class = LoginForm
success_url = reverse_lazy('musician:dashboard')
2019-10-31 09:46:54 +00:00
redirect_field_name = 'next'
2020-01-07 12:53:59 +00:00
extra_context = {
# Translators: This message appears on the page title
'title': _('Login'),
'version': get_version(),
}
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['request'] = self.request
return kwargs
def form_valid(self, form):
"""Security check complete. Log the user in."""
auth_login(self.request, form.username, form.token)
# set user language as active language
user_language = form.user.language
translation.activate(user_language)
response = HttpResponseRedirect(self.get_success_url())
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, user_language)
return response
2019-10-31 09:46:54 +00:00
def get_success_url(self):
url = self.get_redirect_url()
return url or self.success_url
def get_redirect_url(self):
"""Return the user-originating redirect URL if it's safe."""
redirect_to = self.request.POST.get(
self.redirect_field_name,
self.request.GET.get(self.redirect_field_name, '')
)
url_is_safe = is_safe_url(
url=redirect_to,
allowed_hosts={self.request.get_host()},
require_https=self.request.is_secure(),
)
return redirect_to if url_is_safe else ''
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
self.redirect_field_name: self.get_redirect_url(),
**(self.extra_context or {})
})
return context
class LogoutView(RedirectView):
"""
Log out the user.
"""
permanent = False
pattern_name = 'musician:login'
def get_redirect_url(self, *args, **kwargs):
"""
Logs out the user.
"""
auth_logout(self.request)
return super().get_redirect_url(*args, **kwargs)
def post(self, request, *args, **kwargs):
"""Logout may be done via POST."""
return self.get(request, *args, **kwargs)