Compare commits

...

4 Commits

6 changed files with 111 additions and 104 deletions

View File

@ -6,8 +6,11 @@ from django.http import Http404
from django.urls.exceptions import NoReverseMatch from django.urls.exceptions import NoReverseMatch
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .models import (Address, DatabaseService, Domain, Mailbox, SaasService, from orchestra.contrib.domains.models import Domain
UserAccount, WebSite) from orchestra.contrib.mailboxes.models import Mailbox
from orchestra.contrib.websites.models import Website
from .models import Address, DatabaseService, SaasService, UserAccount
DOMAINS_PATH = 'domains/' DOMAINS_PATH = 'domains/'
TOKEN_PATH = '/api-token-auth/' TOKEN_PATH = '/api-token-auth/'
@ -43,14 +46,6 @@ class Orchestra(object):
self.username = username self.username = username
self.user = self.authenticate(self.username, password) self.user = self.authenticate(self.username, password)
def build_absolute_uri(self, path_name):
path = API_PATHS.get(path_name, None)
if path is None:
raise NoReverseMatch(
"Not found API path name '{}'".format(path_name))
return urllib.parse.urljoin(self.base_url, path)
def authenticate(self, username, password): def authenticate(self, username, password):
user = authenticate(self.request, username=username, password=password) user = authenticate(self.request, username=username, password=password)
@ -61,6 +56,21 @@ class Orchestra(object):
# Return an 'invalid login' error message. # Return an 'invalid login' error message.
return None return None
class OrchestraConnector:
def __init__(self, request):
self._request = request
self.user = request.user
assert not self.user.is_anonymous
def build_absolute_uri(self, path_name):
path = API_PATHS.get(path_name, None)
if path is None:
raise NoReverseMatch(
"Not found API path name '{}'".format(path_name))
return urllib.parse.urljoin(self.base_url, path)
def request(self, verb, resource=None, url=None, data=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"] assert verb in ["HEAD", "GET", "POST", "PATCH", "PUT", "DELETE"]
if resource is not None: if resource is not None:
@ -89,18 +99,24 @@ class Orchestra(object):
return status, output return status, output
def retrieve_service_list(self, service_name, querystring=None): def retrieve_service_list(self, model_class, querystring=None):
pattern_name = '{}-list'.format(service_name) qs = model_class.objects.filter(account=self.user)
if pattern_name not in API_PATHS:
raise ValueError("Unknown service {}".format(service_name)) # TODO filter by querystring
_, output = self.request("GET", pattern_name, querystring=querystring)
return output return qs
# pattern_name = '{}-list'.format(service_name)
# if pattern_name not in API_PATHS:
# raise ValueError("Unknown service {}".format(service_name))
# _, output = self.request("GET", pattern_name, querystring=querystring)
# return output
def retrieve_profile(self): def retrieve_profile(self):
status, output = self.request("GET", 'my-account') if self.user.is_anonymous:
if status >= 400:
raise PermissionError("Cannot retrieve profile of an anonymous user.") raise PermissionError("Cannot retrieve profile of an anonymous user.")
return UserAccount.new_from_json(output[0])
return self.user # return UserAccount.new_from_json(output[0])
def retrieve_bill_document(self, pk): def retrieve_bill_document(self, pk):
path = API_PATHS.get('bill-document').format_map({'pk': pk}) path = API_PATHS.get('bill-document').format_map({'pk': pk})
@ -183,8 +199,8 @@ class Orchestra(object):
return status, response return status, response
def retrieve_mailbox_list(self): def retrieve_mailbox_list(self):
mailboxes = self.retrieve_service_list(Mailbox.api_name) qs = self.retrieve_service_list(Mailbox)
return [Mailbox.new_from_json(mailbox_data) for mailbox_data in mailboxes] return qs
def delete_mailbox(self, pk): def delete_mailbox(self, pk):
path = API_PATHS.get('mailbox-detail').format_map({'pk': pk}) path = API_PATHS.get('mailbox-detail').format_map({'pk': pk})
@ -201,6 +217,7 @@ class Orchestra(object):
def retrieve_domain(self, pk): def retrieve_domain(self, pk):
path = API_PATHS.get('domain-detail').format_map({'pk': pk}) path = API_PATHS.get('domain-detail').format_map({'pk': pk})
url = urllib.parse.urljoin(self.base_url, path) url = urllib.parse.urljoin(self.base_url, path)
@ -210,46 +227,24 @@ class Orchestra(object):
return Domain.new_from_json(domain_json) return Domain.new_from_json(domain_json)
def retrieve_domain_list(self): def retrieve_domain_list(self):
output = self.retrieve_service_list(Domain.api_name) domains = self.retrieve_service_list(Domain)
websites = self.retrieve_website_list() domains = domains.prefetch_related("addresses", "websites")
domains = []
for domain_json in output:
# filter querystring
querystring = "domain={}".format(domain_json['id'])
# retrieve services associated to a domain
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)
domain_json['websites'] = self.filter_websites_by_domain(websites, domain_json['id'])
# TODO(@slamora): update when backend provides resource disk usage data # TODO(@slamora): update when backend provides resource disk usage data
domain_json['usage'] = { # initialize domain usage for every domain
# for domain in domains:
# domain.usage = {
# 'usage': 300, # 'usage': 300,
# 'total': 650, # 'total': 650,
# 'unit': 'MB', # 'unit': 'MB',
# 'percent': 50, # 'percent': 50,
} # }
# append to list a Domain object
domains.append(Domain.new_from_json(domain_json))
return domains return domains
def retrieve_website_list(self): def retrieve_website_list(self):
output = self.retrieve_service_list(WebSite.api_name) qs = self.retrieve_service_list(Website)
return [WebSite.new_from_json(website_data) for website_data in output] return qs
def filter_websites_by_domain(self, websites, domain_id):
matching = []
for website in websites:
web_domains = [web_domain.id for web_domain in website.domains]
if domain_id in web_domains:
matching.append(website)
return matching
def verify_credentials(self): def verify_credentials(self):
""" """

View File

@ -52,11 +52,11 @@ class ExtendedPaginationMixin:
class UserTokenRequiredMixin(LoginRequiredMixin): class UserTokenRequiredMixin(LoginRequiredMixin):
# TODO XXX adapt this code
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
self.orchestra = api.OrchestraConnector(self.request)
context.update({ context.update({
# TODO XXX 'profile': self.orchestra.retrieve_profile(),
# 'profile': self.orchestra.retrieve_profile(),
}) })
return context return context

View File

@ -94,7 +94,7 @@
</a> </a>
<div class="dropdown-menu"> <div class="dropdown-menu">
{% for code, language in languages %} {% for code, language in languages %}
<a class="dropdown-item" href="{% url 'musician:profile-set-lang' code %}">{{ language }}</a> <a class="dropdown-item" href="{% url 'musician:profile-set-lang' code %}?next={{ request.path }}">{{ language }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@ -19,7 +19,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for record in object.records %} {% for record in object.records.all %}
<tr> <tr>
<td>{{ record.type }}</td> <td>{{ record.type }}</td>
<td>{{ record.value }}</td> <td>{{ record.value }}</td>

View File

@ -18,7 +18,7 @@
<div class="col-md-9"> <div class="col-md-9">
<p class="card-text">{{ profile.username }}</p> <p class="card-text">{{ profile.username }}</p>
<p class="card-text">{{ profile.type }}</p> <p class="card-text">{{ profile.type }}</p>
<p class="card-text">{% trans "Preferred language:" %} {{ profile.language|language_name_local }}</p> <p class="card-text">{% trans "Preferred language:" %} {{ preferred_language_code|language_name_local }}</p>
</div> </div>
{% comment %} {% comment %}
<!-- disabled until set_password is implemented --> <!-- disabled until set_password is implemented -->

View File

@ -1,4 +1,3 @@
import datetime
import logging import logging
import smtplib import smtplib
@ -8,6 +7,7 @@ from django.core.exceptions import ImproperlyConfigured
from django.core.mail import mail_managers from django.core.mail import mail_managers
from django.http import (HttpResponse, HttpResponseNotFound, from django.http import (HttpResponse, HttpResponseNotFound,
HttpResponseRedirect) HttpResponseRedirect)
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import translation from django.utils import translation
from django.utils.html import format_html from django.utils.html import format_html
@ -21,6 +21,10 @@ 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.domains.models import Domain
from orchestra.contrib.saas.models import SaaS
from orchestra.utils.html import html_to_pdf
# from .auth import login as auth_login # from .auth import login as auth_login
from .auth import logout as auth_logout from .auth import logout as auth_logout
@ -28,8 +32,10 @@ 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, Bill, DatabaseService, Mailbox, from .models import Address
MailinglistService, PaymentSource, SaasService) from .models import Bill as BillService
from .models import (DatabaseService, Mailbox, MailinglistService,
PaymentSource, SaasService)
from .settings import ALLOWED_RESOURCES from .settings import ALLOWED_RESOURCES
from .utils import get_bootstraped_percent from .utils import get_bootstraped_percent
@ -116,13 +122,10 @@ class ProfileView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
try: user = self.request.user
pay_source = self.orchestra.retrieve_service_list(
PaymentSource.api_name)[0]
except IndexError:
pay_source = {}
context.update({ context.update({
'payment': PaymentSource.new_from_json(pay_source) 'payment': user.paymentsources.first(),
'preferred_language_code': user.language.lower(),
}) })
return context return context
@ -132,11 +135,19 @@ def profile_set_language(request, code):
# set user language as active language # set user language as active language
if any(x[0] == code for x in settings.LANGUAGES): if any(x[0] == code for x in settings.LANGUAGES):
# http://127.0.0.1:8080/profile/setLang/es
user_language = code user_language = code
translation.activate(user_language) translation.activate(user_language)
response = HttpResponseRedirect('/dashboard') redirect_to = request.GET.get('next', '')
url_is_safe = is_safe_url(
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)
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, user_language) response.set_cookie(settings.LANGUAGE_COOKIE_NAME, user_language)
return response return response
@ -147,35 +158,35 @@ def profile_set_language(request, code):
class ServiceListView(CustomContextMixin, ExtendedPaginationMixin, UserTokenRequiredMixin, ListView): class ServiceListView(CustomContextMixin, ExtendedPaginationMixin, UserTokenRequiredMixin, ListView):
"""Base list view to all services""" """Base list view to all services"""
service_class = None model = None
template_name = "musician/service_list.html" template_name = "musician/service_list.html"
def get_queryset(self): def get_queryset(self):
if self.service_class is None or self.service_class.api_name is None: if self.model is None :
raise ImproperlyConfigured( raise ImproperlyConfigured(
"ServiceListView requires a definiton of 'service'") "ServiceListView requires definiton of 'model' attribute")
queryfilter = self.get_queryfilter() queryfilter = self.get_queryfilter()
json_qs = self.orchestra.retrieve_service_list( qs = self.model.objects.filter(account=self.request.user, **queryfilter)
self.service_class.api_name,
querystring=queryfilter, return qs
)
return [self.service_class.new_from_json(data) for data in json_qs]
def get_queryfilter(self): def get_queryfilter(self):
"""Does nothing by default. Should be implemented on subclasses""" """Does nothing by default. Should be implemented on subclasses"""
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)
context.update({ context.update({
'service': self.service_class, # TODO(@slamora): check where is used on the template
'service': self.model.__name__,
}) })
return context return context
class BillingView(ServiceListView): class BillingView(ServiceListView):
service_class = Bill service_class = BillService
model = Bill
template_name = "musician/billing.html" template_name = "musician/billing.html"
extra_context = { extra_context = {
# Translators: This message appears on the page title # Translators: This message appears on the page title
@ -184,23 +195,35 @@ class BillingView(ServiceListView):
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
qs = sorted(qs, key=lambda x: x.created_on, reverse=True) qs = qs.order_by("-created_on")
for q in qs:
q.created_on = datetime.datetime.strptime(q.created_on, "%Y-%m-%d")
return qs return qs
class BillDownloadView(CustomContextMixin, UserTokenRequiredMixin, View): class BillDownloadView(CustomContextMixin, UserTokenRequiredMixin, View):
extra_context = { extra_context = {
# Translators: This message appears on the page title # Translators: This message appears on the page title
'title': _('Download bill'), 'title': _('Download bill'),
} }
def get(self, request, *args, **kwargs): def get_object(self):
return get_object_or_404(
Bill.objects.filter(account=self.request.user),
pk=self.kwargs.get('pk') pk=self.kwargs.get('pk')
bill = self.orchestra.retrieve_bill_document(pk) )
return HttpResponse(bill) 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
content_type = request.META.get('HTTP_ACCEPT')
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())
class MailView(ServiceListView): class MailView(ServiceListView):
@ -508,6 +531,7 @@ class DatabasesView(ServiceListView):
class SaasView(ServiceListView): class SaasView(ServiceListView):
service_class = SaasService service_class = SaasService
model = SaaS
template_name = "musician/saas.html" template_name = "musician/saas.html"
extra_context = { extra_context = {
# Translators: This message appears on the page title # Translators: This message appears on the page title
@ -523,19 +547,7 @@ class DomainDetailView(CustomContextMixin, UserTokenRequiredMixin, DetailView):
} }
def get_queryset(self): def get_queryset(self):
# Return an empty list to avoid a request to retrieve all the return Domain.objects.filter(account=self.request.user)
# user domains. We will get a 404 if the domain doesn't exists
# while invoking `get_object`
return []
def get_object(self, queryset=None):
if queryset is None:
queryset = self.get_queryset()
pk = self.kwargs.get(self.pk_url_kwarg)
domain = self.orchestra.retrieve_domain(pk)
return domain
class LoginView(FormView): class LoginView(FormView):