2023-11-21 11:48:39 +00:00
|
|
|
import logging
|
|
|
|
import smtplib
|
2024-01-25 12:59:14 +00:00
|
|
|
from typing import Any
|
2023-11-21 11:48:39 +00:00
|
|
|
|
|
|
|
from django.conf import settings
|
2024-01-30 12:47:22 +00:00
|
|
|
from django.contrib.contenttypes.models import ContentType
|
2023-11-21 11:48:39 +00:00
|
|
|
from django.core.exceptions import ImproperlyConfigured
|
|
|
|
from django.core.mail import mail_managers
|
2024-01-30 10:31:24 +00:00
|
|
|
from django.db.models import Value
|
|
|
|
from django.db.models.functions import Concat
|
2023-11-21 12:56:09 +00:00
|
|
|
from django.http import (HttpResponse, HttpResponseNotFound,
|
|
|
|
HttpResponseRedirect)
|
2024-08-09 11:21:01 +00:00
|
|
|
from django.shortcuts import get_object_or_404, render
|
2023-11-21 11:48:39 +00:00
|
|
|
from django.urls import reverse_lazy
|
|
|
|
from django.utils import translation
|
|
|
|
from django.utils.html import format_html
|
2024-01-29 16:26:26 +00:00
|
|
|
from django.utils.http import is_safe_url
|
2023-11-21 11:48:39 +00:00
|
|
|
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
|
2023-11-23 11:50:55 +00:00
|
|
|
from django.views.generic.edit import (CreateView, DeleteView, FormView,
|
|
|
|
UpdateView)
|
2023-11-21 11:48:39 +00:00
|
|
|
from django.views.generic.list import ListView
|
|
|
|
from requests.exceptions import HTTPError
|
|
|
|
|
2024-06-19 11:41:34 +00:00
|
|
|
from django.urls import reverse
|
|
|
|
from django.db.models import Q
|
|
|
|
|
2023-11-21 11:48:39 +00:00
|
|
|
from orchestra import get_version
|
2023-11-23 09:46:56 +00:00
|
|
|
from orchestra.contrib.bills.models import Bill
|
2023-11-23 17:51:12 +00:00
|
|
|
from orchestra.contrib.databases.models import Database
|
2023-11-29 11:16:17 +00:00
|
|
|
from orchestra.contrib.domains.models import Domain, Record
|
2023-11-23 17:48:47 +00:00
|
|
|
from orchestra.contrib.lists.models import List
|
2023-11-23 10:18:30 +00:00
|
|
|
from orchestra.contrib.mailboxes.models import Address, Mailbox
|
2024-01-30 12:47:22 +00:00
|
|
|
from orchestra.contrib.resources.models import Resource, ResourceData
|
2023-11-23 09:46:56 +00:00
|
|
|
from orchestra.contrib.saas.models import SaaS
|
2024-04-16 18:49:11 +00:00
|
|
|
from orchestra.contrib.systemusers.models import WebappUsers, SystemUser
|
2023-11-23 09:46:56 +00:00
|
|
|
from orchestra.utils.html import html_to_pdf
|
2023-11-21 12:56:09 +00:00
|
|
|
|
2023-11-21 11:48:39 +00:00
|
|
|
from .auth import logout as auth_logout
|
2023-11-21 12:56:09 +00:00
|
|
|
from .forms import (LoginForm, MailboxChangePasswordForm, MailboxCreateForm,
|
2024-01-30 10:31:24 +00:00
|
|
|
MailboxSearchForm, MailboxUpdateForm, MailForm,
|
2024-04-16 18:49:11 +00:00
|
|
|
RecordCreateForm, RecordUpdateForm, WebappUsersChangePasswordForm,
|
2024-05-18 12:27:03 +00:00
|
|
|
SystemUsersChangePasswordForm)
|
2023-11-21 11:48:39 +00:00
|
|
|
from .mixins import (CustomContextMixin, ExtendedPaginationMixin,
|
|
|
|
UserTokenRequiredMixin)
|
2023-11-23 10:18:30 +00:00
|
|
|
from .models import Address as AddressService
|
2023-11-23 09:46:56 +00:00
|
|
|
from .models import Bill as BillService
|
2023-11-23 10:18:30 +00:00
|
|
|
from .models import DatabaseService
|
|
|
|
from .models import Mailbox as MailboxService
|
|
|
|
from .models import MailinglistService, SaasService
|
2024-04-29 18:06:20 +00:00
|
|
|
from .settings import ALLOWED_RESOURCES, MUSICIAN_EDIT_ENABLE_PHP_OPTIONS
|
2024-06-17 18:54:23 +00:00
|
|
|
from .utils import get_bootstraped_percent, get_bootstraped_percent_exact
|
2023-11-21 11:48:39 +00:00
|
|
|
|
2024-05-29 15:55:01 +00:00
|
|
|
from .webapps.views import *
|
|
|
|
from .websites.views import *
|
2024-05-29 18:03:25 +00:00
|
|
|
from .lists.views import *
|
2024-05-18 12:27:03 +00:00
|
|
|
|
2023-11-21 11:48:39 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2024-08-09 11:21:01 +00:00
|
|
|
import json
|
|
|
|
from urllib.parse import parse_qs
|
|
|
|
from orchestra.contrib.resources.helpers import get_history_data
|
2024-08-14 11:04:26 +00:00
|
|
|
from django.http import HttpResponseNotFound, Http404
|
|
|
|
|
|
|
|
class HistoryView(CustomContextMixin, UserTokenRequiredMixin, View):
|
|
|
|
|
2024-08-20 08:46:04 +00:00
|
|
|
def check_resource(self, pk):
|
2024-08-14 11:04:26 +00:00
|
|
|
related_resources = self.get_all_resources()
|
|
|
|
|
|
|
|
account = related_resources.filter(resource_id__verbose_name='account-disk').first()
|
|
|
|
account_trafic = related_resources.filter(resource_id__verbose_name='account-traffic').first()
|
|
|
|
account = getattr(account, "id", False) == pk
|
|
|
|
account_trafic = getattr(account_trafic, "id", False) == pk
|
|
|
|
if account == False and account_trafic == False:
|
|
|
|
raise Http404(f"Resource with id {pk} does not exist")
|
2024-08-09 11:21:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
def get(self, request, pk, *args, **kwargs):
|
|
|
|
context = {
|
|
|
|
'ids': pk
|
|
|
|
}
|
2024-08-20 08:46:04 +00:00
|
|
|
self.check_resource(pk)
|
2024-08-09 11:21:01 +00:00
|
|
|
return render(request, "musician/history.html", context)
|
|
|
|
|
2024-08-14 11:04:26 +00:00
|
|
|
# TODO: funcion de dashborad, mirar como no repetir esta funcion
|
|
|
|
def get_all_resources(self):
|
|
|
|
user = self.request.user
|
|
|
|
resources = Resource.objects.select_related('content_type')
|
|
|
|
resource_models = {r.content_type.model_class(): r.content_type_id for r in resources}
|
|
|
|
ct_id = resource_models[user._meta.model]
|
|
|
|
qset = Q(content_type_id=ct_id, object_id=user.id, resource__is_active=True)
|
|
|
|
for field, rel in user._meta.fields_map.items():
|
|
|
|
try:
|
|
|
|
ct_id = resource_models[rel.related_model]
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
manager = getattr(user, field)
|
|
|
|
ids = manager.values_list('id', flat=True)
|
|
|
|
qset = Q(qset) | Q(content_type_id=ct_id, object_id__in=ids, resource__is_active=True)
|
|
|
|
return ResourceData.objects.filter(qset)
|
|
|
|
|
2024-08-09 11:21:01 +00:00
|
|
|
|
2024-08-14 11:04:26 +00:00
|
|
|
class HistoryDataView(CustomContextMixin, UserTokenRequiredMixin, View):
|
2024-08-09 11:21:01 +00:00
|
|
|
def get(self, request, pk, *args, **kwargs):
|
|
|
|
ids = [pk]
|
|
|
|
queryset = ResourceData.objects.filter(id__in=ids)
|
|
|
|
history = get_history_data(queryset)
|
|
|
|
response = json.dumps(history, indent=4)
|
|
|
|
return HttpResponse(response, content_type="application/json")
|
2023-11-21 11:48:39 +00:00
|
|
|
|
2024-06-19 11:41:34 +00:00
|
|
|
|
|
|
|
class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
|
|
|
template_name = "musician/dashboard.html"
|
2024-06-10 20:26:55 +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)
|
|
|
|
|
|
|
|
related_resources = self.get_all_resources()
|
2024-06-13 18:42:49 +00:00
|
|
|
|
2024-06-18 17:38:36 +00:00
|
|
|
account = related_resources.filter(resource_id__verbose_name='account-disk').first()
|
|
|
|
account_trafic = related_resources.filter(resource_id__verbose_name='account-traffic').first()
|
|
|
|
|
2024-06-13 18:42:49 +00:00
|
|
|
mailboxes = related_resources.filter(resource_id__verbose_name='mailbox-disk')
|
|
|
|
lists = related_resources.filter(resource_id__verbose_name='list-traffic')
|
|
|
|
databases = related_resources.filter(resource_id__verbose_name='database-disk')
|
|
|
|
nextcloud = related_resources.filter(resource_id__verbose_name='nextcloud-disk')
|
|
|
|
domains = Domain.objects.filter(account_id=self.request.user)
|
|
|
|
|
2024-06-10 20:26:55 +00:00
|
|
|
|
|
|
|
# TODO(@slamora) update when backend supports notifications
|
|
|
|
notifications = []
|
|
|
|
|
|
|
|
# show resource usage based on plan definition
|
|
|
|
profile_type = context['profile'].type
|
|
|
|
|
|
|
|
# TODO(@slamora) update when backend provides resource usage data
|
|
|
|
resource_usage = {
|
2024-06-13 18:42:49 +00:00
|
|
|
'mailbox': self.get_resource_usage(profile_type, mailboxes, 'mailbox'),
|
|
|
|
'database': self.get_resource_usage(profile_type, databases, 'database'),
|
|
|
|
'nextcloud': self.get_resource_usage(profile_type, nextcloud, 'nextcloud'),
|
|
|
|
'list': self.get_resource_usage(profile_type, lists, 'Mailman list Traffic'),
|
2024-06-10 20:26:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
support_email = getattr(settings, "USER_SUPPORT_EMAIL", "suport@pangea.org")
|
|
|
|
support_email_anchor = format_html(
|
|
|
|
"<a href='mailto:{}'>{}</a>",
|
|
|
|
support_email,
|
|
|
|
support_email,
|
|
|
|
)
|
|
|
|
context.update({
|
2024-06-13 18:42:49 +00:00
|
|
|
'domains': domains,
|
2024-06-10 20:26:55 +00:00
|
|
|
'resource_usage': resource_usage,
|
|
|
|
'notifications': notifications,
|
|
|
|
"support_email_anchor": support_email_anchor,
|
2024-06-18 17:38:36 +00:00
|
|
|
'account': self.get_account_usage(profile_type, account, account_trafic),
|
2024-06-10 20:26:55 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
return context
|
|
|
|
|
|
|
|
def get_all_resources(self):
|
|
|
|
user = self.request.user
|
|
|
|
resources = Resource.objects.select_related('content_type')
|
|
|
|
resource_models = {r.content_type.model_class(): r.content_type_id for r in resources}
|
|
|
|
ct_id = resource_models[user._meta.model]
|
|
|
|
qset = Q(content_type_id=ct_id, object_id=user.id, resource__is_active=True)
|
|
|
|
for field, rel in user._meta.fields_map.items():
|
|
|
|
try:
|
|
|
|
ct_id = resource_models[rel.related_model]
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
manager = getattr(user, field)
|
|
|
|
ids = manager.values_list('id', flat=True)
|
|
|
|
qset = Q(qset) | Q(content_type_id=ct_id, object_id__in=ids, resource__is_active=True)
|
|
|
|
return ResourceData.objects.filter(qset)
|
|
|
|
|
2024-06-13 18:42:49 +00:00
|
|
|
def get_resource_usage(self, profile_type, resource_data, name_resource):
|
|
|
|
limit_rs = 0
|
|
|
|
total_rs = len(resource_data)
|
|
|
|
rs_left = 0
|
|
|
|
alert = ''
|
|
|
|
progres_bar = False
|
2024-08-14 11:04:26 +00:00
|
|
|
|
2024-06-13 18:42:49 +00:00
|
|
|
if ALLOWED_RESOURCES[profile_type].get(name_resource):
|
|
|
|
progres_bar = True
|
|
|
|
limit_rs = ALLOWED_RESOURCES[profile_type][name_resource]
|
|
|
|
rs_left = limit_rs - total_rs
|
2024-06-10 20:26:55 +00:00
|
|
|
|
2024-06-13 18:42:49 +00:00
|
|
|
alert = ''
|
|
|
|
if rs_left < 0:
|
|
|
|
alert = format_html(f"<span class='text-danger'>{rs_left * -1} extra {name_resource}</span>")
|
|
|
|
elif rs_left <= 1:
|
2024-09-12 10:42:43 +00:00
|
|
|
alert = format_html("<span class='text-warning'>{} {} {}</span>".format(rs_left, name_resource, _('available')))
|
2024-06-18 17:38:36 +00:00
|
|
|
elif rs_left > 1:
|
2024-09-12 10:42:43 +00:00
|
|
|
alert = format_html("<span class='text-secondary'>{} {} {}</span>".format(rs_left, name_resource, _('available')))
|
2024-10-11 09:38:54 +00:00
|
|
|
|
|
|
|
# porcentage de uso en los recursos
|
|
|
|
for x in resource_data:
|
|
|
|
if getattr(x, 'used', False) and getattr(x, 'allocated', False):
|
|
|
|
rs_percent = getattr(x, 'used') / getattr(x, 'allocated') * 100
|
|
|
|
x.rs_percent = int(rs_percent)
|
|
|
|
|
|
|
|
|
2024-06-13 18:42:49 +00:00
|
|
|
return {
|
|
|
|
'verbose_name': _(name_resource.capitalize()),
|
|
|
|
'data': {
|
|
|
|
'progres_bar': progres_bar,
|
|
|
|
'used': total_rs,
|
|
|
|
'total': limit_rs,
|
|
|
|
'alert': alert,
|
|
|
|
'unit': name_resource.capitalize(),
|
2024-06-17 18:54:23 +00:00
|
|
|
'percent': get_bootstraped_percent_exact(total_rs, limit_rs),
|
2024-06-13 18:42:49 +00:00
|
|
|
},
|
|
|
|
'objects': resource_data,
|
|
|
|
}
|
|
|
|
|
2024-06-18 17:38:36 +00:00
|
|
|
def get_account_usage(self, profile_type, account, account_trafic):
|
2024-08-14 11:04:26 +00:00
|
|
|
total_size = 0
|
|
|
|
if account != None and getattr(account, "used") != None:
|
|
|
|
total_size = account.used
|
|
|
|
|
2024-06-13 18:42:49 +00:00
|
|
|
allowed_size = ALLOWED_RESOURCES[profile_type]['account']
|
|
|
|
size_left = allowed_size - total_size
|
2024-08-14 11:04:26 +00:00
|
|
|
unit = account.unit if account != None else "GiB"
|
2024-06-10 20:26:55 +00:00
|
|
|
|
2024-06-13 18:42:49 +00:00
|
|
|
alert = ''
|
|
|
|
if size_left < 0:
|
2024-08-14 11:04:26 +00:00
|
|
|
alert = format_html(f"<span class='text-danger'>{size_left * -1} {unit} extra</span>")
|
2024-06-13 18:42:49 +00:00
|
|
|
elif size_left <= 1:
|
2024-09-12 10:42:43 +00:00
|
|
|
alert = format_html("<span class='text-warning'>{} {} {}</span>".format(size_left, unit, _('available')))
|
2024-06-18 17:38:36 +00:00
|
|
|
|
2024-06-10 20:26:55 +00:00
|
|
|
return {
|
2024-06-13 18:42:49 +00:00
|
|
|
'verbose_name': _('Account'),
|
2024-06-10 20:26:55 +00:00
|
|
|
'data': {
|
2024-06-13 18:42:49 +00:00
|
|
|
'progres_bar': True,
|
|
|
|
'used': total_size,
|
|
|
|
'total': allowed_size,
|
2024-06-10 20:26:55 +00:00
|
|
|
'alert': alert,
|
2024-06-13 18:42:49 +00:00
|
|
|
'unit': 'GiB Size',
|
2024-06-17 18:54:23 +00:00
|
|
|
'percent': get_bootstraped_percent_exact(total_size, allowed_size),
|
2024-06-10 20:26:55 +00:00
|
|
|
},
|
2024-06-18 17:38:36 +00:00
|
|
|
'objects': {
|
|
|
|
'size': {'ac': account},
|
|
|
|
'traffic': {'ac': account_trafic}
|
|
|
|
},
|
2024-06-10 20:26:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2024-06-19 11:41:34 +00:00
|
|
|
class DomainListView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
|
|
|
template_name = "musician/domain_list.html"
|
2023-11-21 11:48:39 +00:00
|
|
|
extra_context = {
|
|
|
|
# Translators: This message appears on the page title
|
2024-06-19 11:41:34 +00:00
|
|
|
'title': _('Domains'),
|
2023-11-21 11:48:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
domains = self.orchestra.retrieve_domain_list()
|
|
|
|
|
2023-11-23 18:59:14 +00:00
|
|
|
support_email = getattr(settings, "USER_SUPPORT_EMAIL", "suport@pangea.org")
|
|
|
|
support_email_anchor = format_html(
|
|
|
|
"<a href='mailto:{}'>{}</a>",
|
|
|
|
support_email,
|
|
|
|
support_email,
|
|
|
|
)
|
2023-11-21 11:48:39 +00:00
|
|
|
context.update({
|
|
|
|
'domains': domains,
|
2023-11-23 18:59:14 +00:00
|
|
|
"support_email_anchor": support_email_anchor,
|
2023-11-21 11:48:39 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
return context
|
|
|
|
|
|
|
|
|
|
|
|
class ProfileView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
|
|
|
template_name = "musician/profile.html"
|
|
|
|
extra_context = {
|
|
|
|
# Translators: This message appears on the page title
|
|
|
|
'title': _('User profile'),
|
|
|
|
}
|
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
context = super().get_context_data(**kwargs)
|
2023-11-23 10:09:45 +00:00
|
|
|
user = self.request.user
|
2023-11-21 11:48:39 +00:00
|
|
|
context.update({
|
2023-11-23 10:09:45 +00:00
|
|
|
'payment': user.paymentsources.first(),
|
|
|
|
'preferred_language_code': user.language.lower(),
|
2023-11-21 11:48:39 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
return context
|
|
|
|
|
|
|
|
|
|
|
|
def profile_set_language(request, code):
|
|
|
|
# set user language as active language
|
|
|
|
|
|
|
|
if any(x[0] == code for x in settings.LANGUAGES):
|
|
|
|
user_language = code
|
|
|
|
translation.activate(user_language)
|
|
|
|
|
2023-11-23 10:09:45 +00:00
|
|
|
redirect_to = request.GET.get('next', '')
|
2024-01-29 16:26:26 +00:00
|
|
|
url_is_safe = is_safe_url(
|
2023-11-23 10:09:45 +00:00
|
|
|
url=redirect_to,
|
|
|
|
allowed_hosts={request.get_host()},
|
|
|
|
require_https=request.is_secure(),
|
|
|
|
)
|
|
|
|
if not url_is_safe:
|
|
|
|
redirect_to = reverse_lazy(settings.LOGIN_REDIRECT_URL)
|
|
|
|
|
|
|
|
response = HttpResponseRedirect(redirect_to)
|
2023-11-21 11:48:39 +00:00
|
|
|
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, user_language)
|
|
|
|
|
|
|
|
return response
|
|
|
|
else:
|
|
|
|
response = HttpResponseNotFound('Languague not found')
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
class ServiceListView(CustomContextMixin, ExtendedPaginationMixin, UserTokenRequiredMixin, ListView):
|
|
|
|
"""Base list view to all services"""
|
2023-11-23 09:46:56 +00:00
|
|
|
model = None
|
2023-11-21 11:48:39 +00:00
|
|
|
template_name = "musician/service_list.html"
|
|
|
|
|
|
|
|
def get_queryset(self):
|
2023-11-23 09:46:56 +00:00
|
|
|
if self.model is None :
|
2023-11-21 11:48:39 +00:00
|
|
|
raise ImproperlyConfigured(
|
2023-11-23 09:46:56 +00:00
|
|
|
"ServiceListView requires definiton of 'model' attribute")
|
2023-11-21 11:48:39 +00:00
|
|
|
|
|
|
|
queryfilter = self.get_queryfilter()
|
2023-11-23 09:46:56 +00:00
|
|
|
qs = self.model.objects.filter(account=self.request.user, **queryfilter)
|
|
|
|
|
|
|
|
return qs
|
2023-11-21 11:48:39 +00:00
|
|
|
|
|
|
|
def get_queryfilter(self):
|
|
|
|
"""Does nothing by default. Should be implemented on subclasses"""
|
2023-11-23 09:46:56 +00:00
|
|
|
return {}
|
2023-11-21 11:48:39 +00:00
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
context.update({
|
2023-11-23 09:46:56 +00:00
|
|
|
# TODO(@slamora): check where is used on the template
|
|
|
|
'service': self.model.__name__,
|
2023-11-21 11:48:39 +00:00
|
|
|
})
|
|
|
|
return context
|
|
|
|
|
|
|
|
|
|
|
|
class BillingView(ServiceListView):
|
2023-11-23 09:46:56 +00:00
|
|
|
service_class = BillService
|
|
|
|
model = Bill
|
2023-11-21 11:48:39 +00:00
|
|
|
template_name = "musician/billing.html"
|
|
|
|
extra_context = {
|
|
|
|
# Translators: This message appears on the page title
|
|
|
|
'title': _('Billing'),
|
|
|
|
}
|
|
|
|
|
|
|
|
def get_queryset(self):
|
|
|
|
qs = super().get_queryset()
|
2023-11-23 09:46:56 +00:00
|
|
|
qs = qs.order_by("-created_on")
|
2023-11-21 11:48:39 +00:00
|
|
|
return qs
|
|
|
|
|
|
|
|
|
2023-11-23 09:46:56 +00:00
|
|
|
|
|
|
|
|
2023-11-21 11:48:39 +00:00
|
|
|
class BillDownloadView(CustomContextMixin, UserTokenRequiredMixin, View):
|
|
|
|
extra_context = {
|
|
|
|
# Translators: This message appears on the page title
|
|
|
|
'title': _('Download bill'),
|
|
|
|
}
|
|
|
|
|
2023-11-23 09:46:56 +00:00
|
|
|
def get_object(self):
|
|
|
|
return get_object_or_404(
|
|
|
|
Bill.objects.filter(account=self.request.user),
|
|
|
|
pk=self.kwargs.get('pk')
|
|
|
|
)
|
2023-11-21 11:48:39 +00:00
|
|
|
|
2023-11-23 09:46:56 +00:00
|
|
|
def get(self, request, *args, **kwargs):
|
|
|
|
# NOTE: this is a copy of method document() on orchestra.contrib.bills.api.BillViewSet
|
|
|
|
bill = self.get_object()
|
|
|
|
|
|
|
|
# TODO(@slamora): implement download as PDF, now only HTML is reachable via link
|
2024-01-26 12:58:51 +00:00
|
|
|
content_type = request.headers.get('accept')
|
2023-11-23 09:46:56 +00:00
|
|
|
if content_type == 'application/pdf':
|
|
|
|
pdf = html_to_pdf(bill.html or bill.render())
|
|
|
|
return HttpResponse(pdf, content_type='application/pdf')
|
|
|
|
else:
|
|
|
|
return HttpResponse(bill.html or bill.render())
|
2023-11-21 11:48:39 +00:00
|
|
|
|
|
|
|
|
2024-01-26 12:51:08 +00:00
|
|
|
class AddressListView(ServiceListView):
|
2023-11-23 10:18:30 +00:00
|
|
|
service_class = AddressService
|
|
|
|
model = Address
|
2024-01-26 13:22:18 +00:00
|
|
|
template_name = "musician/address_list.html"
|
2023-11-21 11:48:39 +00:00
|
|
|
extra_context = {
|
|
|
|
# Translators: This message appears on the page title
|
|
|
|
'title': _('Mail addresses'),
|
|
|
|
}
|
|
|
|
|
2024-01-26 12:51:08 +00:00
|
|
|
def get_queryset(self):
|
|
|
|
qs = super().get_queryset()
|
|
|
|
qs = qs.order_by("domain", "name")
|
|
|
|
return qs
|
|
|
|
|
2023-11-21 11:48:39 +00:00
|
|
|
def get_queryfilter(self):
|
|
|
|
"""Retrieve query params (if any) to filter queryset"""
|
2024-01-29 17:32:44 +00:00
|
|
|
queryfilter = {}
|
|
|
|
|
|
|
|
domain_id = self.clean_domain_id()
|
2023-11-21 11:48:39 +00:00
|
|
|
if domain_id:
|
2024-01-29 17:32:44 +00:00
|
|
|
queryfilter.update({"domain": domain_id})
|
2023-11-21 11:48:39 +00:00
|
|
|
|
2024-01-29 17:32:44 +00:00
|
|
|
else:
|
|
|
|
domain_name = self.request.GET.get('domain__name')
|
|
|
|
if domain_name:
|
|
|
|
queryfilter.update({"domain__name__icontains": domain_name})
|
|
|
|
|
|
|
|
return queryfilter
|
2023-11-21 11:48:39 +00:00
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
context = super().get_context_data(**kwargs)
|
2024-01-29 17:32:44 +00:00
|
|
|
domain_id = self.clean_domain_id()
|
2023-11-21 11:48:39 +00:00
|
|
|
if domain_id:
|
2023-11-23 18:59:14 +00:00
|
|
|
qs = Domain.objects.filter(account=self.request.user)
|
2023-11-21 11:48:39 +00:00
|
|
|
context.update({
|
2023-11-23 18:59:14 +00:00
|
|
|
'active_domain': get_object_or_404(qs, pk=domain_id)
|
2023-11-21 11:48:39 +00:00
|
|
|
})
|
2023-11-23 18:59:14 +00:00
|
|
|
context['mailboxes'] = Mailbox.objects.filter(account=self.request.user)
|
2023-11-21 11:48:39 +00:00
|
|
|
return context
|
|
|
|
|
2024-01-29 17:32:44 +00:00
|
|
|
def clean_domain_id(self):
|
|
|
|
try:
|
|
|
|
return int(self.request.GET.get('domain', ''))
|
|
|
|
except ValueError:
|
|
|
|
return None
|
2023-11-21 11:48:39 +00:00
|
|
|
|
2023-11-23 11:50:55 +00:00
|
|
|
class MailCreateView(CustomContextMixin, UserTokenRequiredMixin, CreateView):
|
|
|
|
service_class = AddressService
|
|
|
|
model = Address
|
2023-11-21 11:48:39 +00:00
|
|
|
template_name = "musician/address_form.html"
|
|
|
|
form_class = MailForm
|
|
|
|
success_url = reverse_lazy("musician:address-list")
|
|
|
|
extra_context = {'service': service_class}
|
|
|
|
|
|
|
|
def get_form_kwargs(self):
|
|
|
|
kwargs = super().get_form_kwargs()
|
2023-11-23 11:50:55 +00:00
|
|
|
kwargs['user'] = self.request.user
|
2023-11-21 11:48:39 +00:00
|
|
|
return kwargs
|
|
|
|
|
|
|
|
|
2023-11-23 11:50:55 +00:00
|
|
|
class MailUpdateView(CustomContextMixin, UserTokenRequiredMixin, UpdateView):
|
|
|
|
service_class = AddressService
|
|
|
|
model = Address
|
2023-11-21 11:48:39 +00:00
|
|
|
template_name = "musician/address_form.html"
|
|
|
|
form_class = MailForm
|
|
|
|
success_url = reverse_lazy("musician:address-list")
|
|
|
|
extra_context = {'service': service_class}
|
|
|
|
|
2024-01-26 11:56:12 +00:00
|
|
|
def get_queryset(self):
|
2024-01-25 12:59:14 +00:00
|
|
|
return self.model.objects.filter(account=self.request.user)
|
|
|
|
|
2023-11-21 11:48:39 +00:00
|
|
|
def get_form_kwargs(self):
|
|
|
|
kwargs = super().get_form_kwargs()
|
2023-11-23 11:50:55 +00:00
|
|
|
kwargs["user"] = self.request.user
|
2023-11-21 11:48:39 +00:00
|
|
|
return kwargs
|
|
|
|
|
|
|
|
|
|
|
|
class AddressDeleteView(CustomContextMixin, UserTokenRequiredMixin, DeleteView):
|
|
|
|
template_name = "musician/address_check_delete.html"
|
2024-01-25 12:59:14 +00:00
|
|
|
model = Address
|
2023-11-21 11:48:39 +00:00
|
|
|
success_url = reverse_lazy("musician:address-list")
|
|
|
|
|
2024-01-26 11:56:12 +00:00
|
|
|
def get_queryset(self):
|
2024-01-25 12:59:14 +00:00
|
|
|
return self.model.objects.filter(account=self.request.user)
|
2023-11-21 11:48:39 +00:00
|
|
|
|
|
|
|
|
2024-01-26 12:35:10 +00:00
|
|
|
class MailboxListView(ServiceListView):
|
2023-11-23 10:18:30 +00:00
|
|
|
service_class = MailboxService
|
|
|
|
model = Mailbox
|
2024-01-26 13:22:18 +00:00
|
|
|
template_name = "musician/mailbox_list.html"
|
2023-11-21 11:48:39 +00:00
|
|
|
extra_context = {
|
|
|
|
# Translators: This message appears on the page title
|
|
|
|
'title': _('Mailboxes'),
|
|
|
|
}
|
2024-01-30 10:31:24 +00:00
|
|
|
search_form_class = MailboxSearchForm
|
|
|
|
|
|
|
|
def get_queryset(self):
|
|
|
|
qs = super().get_queryset()
|
|
|
|
|
|
|
|
search_form = self.search_form_class(self.request.GET)
|
|
|
|
cleaned_data = search_form.cleaned_data if search_form.is_valid() else {}
|
|
|
|
|
|
|
|
if "address" in cleaned_data:
|
|
|
|
qs = qs.annotate(
|
|
|
|
full_address=Concat("addresses__name", Value("@"), "addresses__domain__name")
|
|
|
|
).filter(
|
|
|
|
full_address__icontains=cleaned_data["address"]
|
|
|
|
)
|
|
|
|
|
|
|
|
if "name" in cleaned_data:
|
|
|
|
qs = qs.filter(name__icontains=cleaned_data["name"])
|
|
|
|
|
|
|
|
return qs
|
|
|
|
|
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
context['form'] = self.search_form_class()#self.request.GET)
|
|
|
|
return context
|
2023-11-21 11:48:39 +00:00
|
|
|
|
|
|
|
|
2023-11-23 11:50:55 +00:00
|
|
|
class MailboxCreateView(CustomContextMixin, UserTokenRequiredMixin, CreateView):
|
2023-11-23 10:18:30 +00:00
|
|
|
service_class = MailboxService
|
|
|
|
model = Mailbox
|
2023-11-21 11:48:39 +00:00
|
|
|
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())
|
2023-11-23 11:50:55 +00:00
|
|
|
# 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')
|
2023-11-21 11:48:39 +00:00
|
|
|
|
|
|
|
def get_form_kwargs(self):
|
|
|
|
kwargs = super().get_form_kwargs()
|
|
|
|
kwargs.update({
|
2023-11-23 11:50:55 +00:00
|
|
|
'user': self.request.user,
|
2023-11-21 11:48:39 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
return kwargs
|
|
|
|
|
2023-11-23 11:50:55 +00:00
|
|
|
class MailboxUpdateView(CustomContextMixin, UserTokenRequiredMixin, UpdateView):
|
|
|
|
service_class = MailboxService
|
|
|
|
model = Mailbox
|
2023-11-21 11:48:39 +00:00
|
|
|
template_name = "musician/mailbox_form.html"
|
|
|
|
form_class = MailboxUpdateForm
|
|
|
|
success_url = reverse_lazy("musician:mailbox-list")
|
|
|
|
extra_context = {'service': service_class}
|
|
|
|
|
2024-01-26 12:35:10 +00:00
|
|
|
def get_queryset(self):
|
|
|
|
return self.model.objects.filter(account=self.request.user)
|
|
|
|
|
2023-11-21 11:48:39 +00:00
|
|
|
|
|
|
|
class MailboxDeleteView(CustomContextMixin, UserTokenRequiredMixin, DeleteView):
|
2024-01-25 13:19:58 +00:00
|
|
|
model = Mailbox
|
2023-11-21 11:48:39 +00:00
|
|
|
template_name = "musician/mailbox_check_delete.html"
|
|
|
|
success_url = reverse_lazy("musician:mailbox-list")
|
|
|
|
|
2024-01-26 11:56:12 +00:00
|
|
|
def get_queryset(self):
|
2024-01-25 13:19:58 +00:00
|
|
|
return self.model.objects.filter(account=self.request.user)
|
2023-11-21 11:48:39 +00:00
|
|
|
|
|
|
|
def delete(self, request, *args, **kwargs):
|
2024-01-25 13:19:58 +00:00
|
|
|
response = super().delete(request, *args, **kwargs)
|
2023-11-21 11:48:39 +00:00
|
|
|
self.notify_managers(self.object)
|
2024-01-25 13:19:58 +00:00
|
|
|
return response
|
2023-11-21 11:48:39 +00:00
|
|
|
|
|
|
|
def notify_managers(self, mailbox):
|
2024-01-25 13:19:58 +00:00
|
|
|
user = self.request.user
|
|
|
|
subject = f"Mailbox '{mailbox.name}' ({mailbox.id}) deleted | Musician"
|
2023-11-21 11:48:39 +00:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2023-11-23 11:50:55 +00:00
|
|
|
class MailboxChangePasswordView(CustomContextMixin, UserTokenRequiredMixin, UpdateView):
|
2023-11-21 11:48:39 +00:00
|
|
|
template_name = "musician/mailbox_change_password.html"
|
2023-11-23 11:50:55 +00:00
|
|
|
model = Mailbox
|
2023-11-21 11:48:39 +00:00
|
|
|
form_class = MailboxChangePasswordForm
|
|
|
|
success_url = reverse_lazy("musician:mailbox-list")
|
|
|
|
|
2024-01-26 12:35:10 +00:00
|
|
|
def get_queryset(self):
|
|
|
|
return self.model.objects.filter(account=self.request.user)
|
|
|
|
|
2023-11-21 11:48:39 +00:00
|
|
|
|
2024-01-30 12:47:22 +00:00
|
|
|
class DatabaseListView(ServiceListView):
|
2024-01-30 11:58:53 +00:00
|
|
|
template_name = "musician/database_list.html"
|
2023-11-23 17:51:12 +00:00
|
|
|
model = Database
|
2023-11-21 11:48:39 +00:00
|
|
|
service_class = DatabaseService
|
|
|
|
extra_context = {
|
|
|
|
# Translators: This message appears on the page title
|
|
|
|
'title': _('Databases'),
|
|
|
|
}
|
|
|
|
|
2024-01-29 16:59:38 +00:00
|
|
|
def get_queryset(self):
|
2024-01-30 12:47:22 +00:00
|
|
|
qs = super().get_queryset().order_by("name")
|
2024-01-29 16:59:38 +00:00
|
|
|
|
2024-01-30 12:47:22 +00:00
|
|
|
# TODO(@slamora): optimize query
|
2024-01-29 16:59:38 +00:00
|
|
|
ctype = ContentType.objects.get_for_model(self.model)
|
|
|
|
disk_resource = Resource.objects.get(name='disk', content_type=ctype)
|
|
|
|
for db in qs:
|
|
|
|
try:
|
|
|
|
db.usage = db.resource_set.get(resource=disk_resource)
|
|
|
|
except ResourceData.DoesNotExist:
|
|
|
|
db.usage = ResourceData(resource=disk_resource)
|
|
|
|
return qs
|
|
|
|
|
2023-11-21 11:48:39 +00:00
|
|
|
|
2024-01-26 12:35:10 +00:00
|
|
|
class SaasListView(ServiceListView):
|
2023-11-21 11:48:39 +00:00
|
|
|
service_class = SaasService
|
2023-11-23 09:46:56 +00:00
|
|
|
model = SaaS
|
2024-01-26 13:22:18 +00:00
|
|
|
template_name = "musician/saas_list.html"
|
2023-11-21 11:48:39 +00:00
|
|
|
extra_context = {
|
|
|
|
# Translators: This message appears on the page title
|
|
|
|
'title': _('Software as a Service'),
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class DomainDetailView(CustomContextMixin, UserTokenRequiredMixin, DetailView):
|
|
|
|
template_name = "musician/domain_detail.html"
|
|
|
|
extra_context = {
|
|
|
|
# Translators: This message appears on the page title
|
|
|
|
'title': _('Domain details'),
|
|
|
|
}
|
|
|
|
|
|
|
|
def get_queryset(self):
|
2023-11-23 09:19:34 +00:00
|
|
|
return Domain.objects.filter(account=self.request.user)
|
2023-11-21 11:48:39 +00:00
|
|
|
|
|
|
|
|
2023-11-29 11:16:17 +00:00
|
|
|
class DomainAddRecordView(CustomContextMixin, UserTokenRequiredMixin, CreateView):
|
|
|
|
model = Record
|
|
|
|
form_class = RecordCreateForm
|
|
|
|
template_name = "musician/record_form.html"
|
|
|
|
|
|
|
|
def get_form_kwargs(self):
|
|
|
|
kwargs = super().get_form_kwargs()
|
|
|
|
domain = get_object_or_404(Domain, account=self.request.user, pk=self.kwargs["pk"])
|
|
|
|
kwargs['domain'] = domain
|
|
|
|
return kwargs
|
|
|
|
|
|
|
|
def get_success_url(self):
|
|
|
|
return reverse_lazy("musician:domain-detail", kwargs={"pk": self.kwargs["pk"]})
|
|
|
|
|
|
|
|
|
2023-11-29 11:42:54 +00:00
|
|
|
class DomainUpdateRecordView(CustomContextMixin, UserTokenRequiredMixin, UpdateView):
|
|
|
|
model = Record
|
|
|
|
form_class = RecordUpdateForm
|
|
|
|
template_name = "musician/record_form.html"
|
|
|
|
pk_url_kwarg = "record_pk"
|
|
|
|
|
|
|
|
def get_queryset(self):
|
|
|
|
qs = Record.objects.filter(domain__account=self.request.user, domain=self.kwargs["pk"])
|
|
|
|
return qs
|
|
|
|
|
|
|
|
def get_success_url(self):
|
|
|
|
return reverse_lazy("musician:domain-detail", kwargs={"pk": self.kwargs["pk"]})
|
|
|
|
|
|
|
|
|
|
|
|
class DomainDeleteRecordView(CustomContextMixin, UserTokenRequiredMixin, DeleteView):
|
|
|
|
model = Record
|
2024-01-26 13:22:18 +00:00
|
|
|
template_name = "musician/record_check_delete.html"
|
2023-11-29 11:42:54 +00:00
|
|
|
pk_url_kwarg = "record_pk"
|
|
|
|
|
|
|
|
def get_queryset(self):
|
|
|
|
qs = Record.objects.filter(domain__account=self.request.user, domain=self.kwargs["pk"])
|
|
|
|
return qs
|
|
|
|
|
|
|
|
def get_success_url(self):
|
|
|
|
return reverse_lazy("musician:domain-detail", kwargs={"pk": self.kwargs["pk"]})
|
|
|
|
|
|
|
|
|
2023-11-21 11:48:39 +00:00
|
|
|
class LoginView(FormView):
|
|
|
|
template_name = 'auth/login.html'
|
|
|
|
form_class = LoginForm
|
|
|
|
success_url = reverse_lazy('musician:dashboard')
|
|
|
|
redirect_field_name = 'next'
|
|
|
|
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."""
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
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, '')
|
|
|
|
)
|
2024-01-29 16:26:26 +00:00
|
|
|
url_is_safe = is_safe_url(
|
2023-11-21 11:48:39 +00:00
|
|
|
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)
|
2024-04-16 18:49:11 +00:00
|
|
|
|
|
|
|
|
|
|
|
class WebappUserListView(ServiceListView):
|
|
|
|
model = WebappUsers
|
2024-05-18 12:27:03 +00:00
|
|
|
template_name = "musician/webapps/webappuser_list.html"
|
2024-04-16 18:49:11 +00:00
|
|
|
extra_context = {
|
|
|
|
# Translators: This message appears on the page title
|
|
|
|
'title': _('Webapp users'),
|
|
|
|
}
|
|
|
|
|
|
|
|
class WebappUserChangePasswordView(CustomContextMixin, UserTokenRequiredMixin, UpdateView):
|
2024-05-18 12:27:03 +00:00
|
|
|
template_name = "musician/webapps/webappuser_change_password.html"
|
2024-04-16 18:49:11 +00:00
|
|
|
model = WebappUsers
|
|
|
|
form_class = WebappUsersChangePasswordForm
|
|
|
|
success_url = reverse_lazy("musician:webappuser-list")
|
|
|
|
|
|
|
|
def get_queryset(self):
|
|
|
|
return self.model.objects.filter(account=self.request.user)
|
|
|
|
|
|
|
|
|
|
|
|
class SystemUserListView(ServiceListView):
|
|
|
|
model = SystemUser
|
|
|
|
template_name = "musician/systemuser_list.html"
|
|
|
|
extra_context = {
|
|
|
|
# Translators: This message appears on the page title
|
|
|
|
'title': _('Main users'),
|
|
|
|
}
|
|
|
|
|
|
|
|
class SystemUserChangePasswordView(CustomContextMixin, UserTokenRequiredMixin, UpdateView):
|
|
|
|
template_name = "musician/systemuser_change_password.html"
|
|
|
|
model = SystemUser
|
|
|
|
form_class = SystemUsersChangePasswordForm
|
|
|
|
success_url = reverse_lazy("musician:systemuser-list")
|
|
|
|
|
|
|
|
def get_queryset(self):
|
2024-04-23 15:14:39 +00:00
|
|
|
return self.model.objects.filter(account=self.request.user)
|
|
|
|
|