Compare commits
4 Commits
ea632ca17f
...
fc09f776c4
Author | SHA1 | Date |
---|---|---|
Santiago L | fc09f776c4 | |
Santiago L | 2e659dbc26 | |
Santiago L | e8e1d2240e | |
Santiago L | be3a89be8a |
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 -->
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue