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.utils.translation import gettext_lazy as _
from .models import (Address, DatabaseService, Domain, Mailbox, SaasService,
UserAccount, WebSite)
from orchestra.contrib.domains.models import Domain
from orchestra.contrib.mailboxes.models import Mailbox
from orchestra.contrib.websites.models import Website
from .models import Address, DatabaseService, SaasService, UserAccount
DOMAINS_PATH = 'domains/'
TOKEN_PATH = '/api-token-auth/'
@ -43,14 +46,6 @@ class Orchestra(object):
self.username = username
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):
user = authenticate(self.request, username=username, password=password)
@ -61,6 +56,21 @@ class Orchestra(object):
# Return an 'invalid login' error message.
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):
assert verb in ["HEAD", "GET", "POST", "PATCH", "PUT", "DELETE"]
if resource is not None:
@ -89,18 +99,24 @@ class Orchestra(object):
return status, output
def retrieve_service_list(self, service_name, querystring=None):
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_service_list(self, model_class, querystring=None):
qs = model_class.objects.filter(account=self.user)
# TODO filter by querystring
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):
status, output = self.request("GET", 'my-account')
if status >= 400:
if self.user.is_anonymous:
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):
path = API_PATHS.get('bill-document').format_map({'pk': pk})
@ -183,8 +199,8 @@ class Orchestra(object):
return status, response
def retrieve_mailbox_list(self):
mailboxes = self.retrieve_service_list(Mailbox.api_name)
return [Mailbox.new_from_json(mailbox_data) for mailbox_data in mailboxes]
qs = self.retrieve_service_list(Mailbox)
return qs
def delete_mailbox(self, pk):
path = API_PATHS.get('mailbox-detail').format_map({'pk': pk})
@ -201,6 +217,7 @@ class Orchestra(object):
def retrieve_domain(self, pk):
path = API_PATHS.get('domain-detail').format_map({'pk': pk})
url = urllib.parse.urljoin(self.base_url, path)
@ -210,46 +227,24 @@ class Orchestra(object):
return Domain.new_from_json(domain_json)
def retrieve_domain_list(self):
output = self.retrieve_service_list(Domain.api_name)
websites = self.retrieve_website_list()
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'])
domains = self.retrieve_service_list(Domain)
domains = domains.prefetch_related("addresses", "websites")
# 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,
# 'total': 650,
# 'unit': 'MB',
# 'percent': 50,
}
# append to list a Domain object
domains.append(Domain.new_from_json(domain_json))
# }
return domains
def retrieve_website_list(self):
output = self.retrieve_service_list(WebSite.api_name)
return [WebSite.new_from_json(website_data) for website_data in output]
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
qs = self.retrieve_service_list(Website)
return qs
def verify_credentials(self):
"""

View File

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

View File

@ -94,7 +94,7 @@
</a>
<div class="dropdown-menu">
{% 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 %}
</div>
</div>

View File

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

View File

@ -18,7 +18,7 @@
<div class="col-md-9">
<p class="card-text">{{ profile.username }}</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>
{% comment %}
<!-- disabled until set_password is implemented -->

View File

@ -1,4 +1,3 @@
import datetime
import logging
import smtplib
@ -8,6 +7,7 @@ from django.core.exceptions import ImproperlyConfigured
from django.core.mail import mail_managers
from django.http import (HttpResponse, HttpResponseNotFound,
HttpResponseRedirect)
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy
from django.utils import translation
from django.utils.html import format_html
@ -21,6 +21,10 @@ from django.views.generic.list import ListView
from requests.exceptions import HTTPError
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 logout as auth_logout
@ -28,8 +32,10 @@ from .forms import (LoginForm, MailboxChangePasswordForm, MailboxCreateForm,
MailboxUpdateForm, MailForm)
from .mixins import (CustomContextMixin, ExtendedPaginationMixin,
UserTokenRequiredMixin)
from .models import (Address, Bill, DatabaseService, Mailbox,
MailinglistService, PaymentSource, SaasService)
from .models import Address
from .models import Bill as BillService
from .models import (DatabaseService, Mailbox, MailinglistService,
PaymentSource, SaasService)
from .settings import ALLOWED_RESOURCES
from .utils import get_bootstraped_percent
@ -116,13 +122,10 @@ class ProfileView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
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 = {}
user = self.request.user
context.update({
'payment': PaymentSource.new_from_json(pay_source)
'payment': user.paymentsources.first(),
'preferred_language_code': user.language.lower(),
})
return context
@ -132,11 +135,19 @@ def profile_set_language(request, code):
# set user language as active language
if any(x[0] == code for x in settings.LANGUAGES):
# http://127.0.0.1:8080/profile/setLang/es
user_language = code
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)
return response
@ -147,35 +158,35 @@ def profile_set_language(request, code):
class ServiceListView(CustomContextMixin, ExtendedPaginationMixin, UserTokenRequiredMixin, ListView):
"""Base list view to all services"""
service_class = None
model = None
template_name = "musician/service_list.html"
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(
"ServiceListView requires a definiton of 'service'")
"ServiceListView requires definiton of 'model' attribute")
queryfilter = self.get_queryfilter()
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]
qs = self.model.objects.filter(account=self.request.user, **queryfilter)
return qs
def get_queryfilter(self):
"""Does nothing by default. Should be implemented on subclasses"""
return ''
return {}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'service': self.service_class,
# TODO(@slamora): check where is used on the template
'service': self.model.__name__,
})
return context
class BillingView(ServiceListView):
service_class = Bill
service_class = BillService
model = Bill
template_name = "musician/billing.html"
extra_context = {
# Translators: This message appears on the page title
@ -184,23 +195,35 @@ class BillingView(ServiceListView):
def get_queryset(self):
qs = super().get_queryset()
qs = sorted(qs, key=lambda x: x.created_on, reverse=True)
for q in qs:
q.created_on = datetime.datetime.strptime(q.created_on, "%Y-%m-%d")
qs = qs.order_by("-created_on")
return qs
class BillDownloadView(CustomContextMixin, UserTokenRequiredMixin, View):
extra_context = {
# Translators: This message appears on the page title
'title': _('Download bill'),
}
def get(self, request, *args, **kwargs):
pk = self.kwargs.get('pk')
bill = self.orchestra.retrieve_bill_document(pk)
def get_object(self):
return get_object_or_404(
Bill.objects.filter(account=self.request.user),
pk=self.kwargs.get('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):
@ -508,6 +531,7 @@ class DatabasesView(ServiceListView):
class SaasView(ServiceListView):
service_class = SaasService
model = SaaS
template_name = "musician/saas.html"
extra_context = {
# Translators: This message appears on the page title
@ -523,19 +547,7 @@ class DomainDetailView(CustomContextMixin, UserTokenRequiredMixin, DetailView):
}
def get_queryset(self):
# 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 []
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
return Domain.objects.filter(account=self.request.user)
class LoginView(FormView):