From 7c17d929fa05d75ab3a0783babeb51540c6867f7 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Tue, 17 Dec 2019 13:04:09 +0100 Subject: [PATCH 1/6] Fix OrchestraModel `__str__` method. --- musician/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/musician/models.py b/musician/models.py index 68ef088..4fae5bd 100644 --- a/musician/models.py +++ b/musician/models.py @@ -7,7 +7,7 @@ class OrchestraModel: api_name = None verbose_name = None fields = () - param_defaults = {} + id = None def __init__(self, **kwargs): if self.verbose_name is None: From ff481b6317915ecd695150d3f3b0f96333ffde7a Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Tue, 17 Dec 2019 13:04:53 +0100 Subject: [PATCH 2/6] Code styles of profile view. --- musician/static/musician/css/default.css | 7 ++ .../default-profile-picture-primary-color.png | Bin 0 -> 3037 bytes musician/templates/musician/profile.html | 80 ++++++++++-------- 3 files changed, 51 insertions(+), 36 deletions(-) create mode 100644 musician/static/musician/images/default-profile-picture-primary-color.png diff --git a/musician/static/musician/css/default.css b/musician/static/musician/css/default.css index 42972ab..a9d7da4 100644 --- a/musician/static/musician/css/default.css +++ b/musician/static/musician/css/default.css @@ -247,6 +247,13 @@ h1.service-name { font-weight: 900; } +.card.card-profile .card-header { + background: white; + border-bottom: none; + font-size: large; + text-transform: uppercase; +} + #configDetailsModal .modal-header { border-bottom: 0; text-align: center; diff --git a/musician/static/musician/images/default-profile-picture-primary-color.png b/musician/static/musician/images/default-profile-picture-primary-color.png new file mode 100644 index 0000000000000000000000000000000000000000..feaf2c7c3c686840fd87f2d6d5ae54ea7d5f888b GIT binary patch literal 3037 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 literal 0 HcmV?d00001 diff --git a/musician/templates/musician/profile.html b/musician/templates/musician/profile.html index e9da7be..fead18a 100644 --- a/musician/templates/musician/profile.html +++ b/musician/templates/musician/profile.html @@ -3,48 +3,56 @@ {% block content %} -

Profile

-

Little description of what to be expected...

+

Profile

+

Little description of what to be expected...

-
-
User information
-
-
-
- {# #} +
+
+
User information
+
+
+
+ user-profile-picture +
-
-
-

{{ profile.username }}

-

{{ profile.type }}

-

Preferred language: {{ profile.language }}

+
+

{{ profile.username }}

+

{{ profile.type }}

+

Preferred language: {{ profile.language }}

+
+ {% comment %} + + + {% endcomment %}
-
-{% with profile.billing as contact %} -
-
Billing information
-
-
{{ contact.name }}
-
{{ contact.address }}
-
- {{ contact.zipcode }} - {{ contact.city }} - {{ contact.country }} -
-
- {{ contact.vat }} -
- -
- payment method: {{ payment.method }} -
-
- {# TODO(@slamora) format payment method details #} - {{ payment.data.data }} -
+ {% with profile.billing as contact %} +
+
Billing information
+
+
{{ contact.name }}
+
{{ contact.address }}
+
+ {{ contact.zipcode }} + {{ contact.city }} + {{ contact.country }} +
+
+ {{ contact.vat }} +
+ +
+ payment method: {{ payment.method }} +
+
+ {# TODO(@slamora) format payment method details #} + {{ payment.data.data }} +
+
{% endwith %} From 37c9183c1eb40acfcee5462a9cf9974b864ede6c Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Tue, 17 Dec 2019 13:50:53 +0100 Subject: [PATCH 3/6] Render payment method details. --- musician/models.py | 21 ++++++++++++++++----- musician/templates/musician/profile.html | 8 ++++++-- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/musician/models.py b/musician/models.py index 4fae5bd..0f924c4 100644 --- a/musician/models.py +++ b/musician/models.py @@ -1,7 +1,13 @@ +import ast +import logging + from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ +logger = logging.getLogger(__name__) + + class OrchestraModel: """ Base class from which all orchestra models will inherit. """ api_name = None @@ -16,9 +22,6 @@ class OrchestraModel: for (param, default) in self.param_defaults.items(): setattr(self, param, kwargs.get(param, default)) - # def get(self, key): - # # retrieve attr of the object and if undefined get raw data - # return getattr(self, key, self.data.get(key)) @classmethod def new_from_json(cls, data, **kwargs): @@ -35,8 +38,7 @@ class OrchestraModel: c = cls(**json_data) c._json = data - # TODO(@slamora) remove/replace by private variable to ovoid name collisions - c.data = data + return c def __repr__(self): @@ -65,6 +67,15 @@ class PaymentSource(OrchestraModel): "is_active": False, } + def __init__(self, **kwargs): + super().__init__(**kwargs) + # payment details are passed as a plain string + # try to convert to a python structure + try: + self.data = ast.literal_eval(self.data) + except (ValueError, SyntaxError) as e: + logger.error(e) + class UserAccount(OrchestraModel): api_name = 'accounts' diff --git a/musician/templates/musician/profile.html b/musician/templates/musician/profile.html index fead18a..c820007 100644 --- a/musician/templates/musician/profile.html +++ b/musician/templates/musician/profile.html @@ -48,8 +48,12 @@ payment method: {{ payment.method }}
- {# TODO(@slamora) format payment method details #} - {{ payment.data.data }} + {% if payment.method == 'SEPADirectDebit' %} + IBAN {{ payment.data.iban }} + {% else %} + {# #} + Details: {{ payment.data }} + {% endif %}
From 770c6c9c9b7e19ab345585353af0e2e237c7007f Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Tue, 17 Dec 2019 14:10:59 +0100 Subject: [PATCH 4/6] Fix Mail and Mailinglist `__init__` --- musician/models.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/musician/models.py b/musician/models.py index 0f924c4..477a371 100644 --- a/musician/models.py +++ b/musician/models.py @@ -170,10 +170,15 @@ class MailService(OrchestraModel): verbose_name = _('Mail addresses') description = _('Litle description of what to be expected in this section to aid the user. Even a link to more help if there is one available.') fields = ('mail_address', 'aliases', 'type', 'type_detail') + param_defaults = {} FORWARD = 'forward' MAILBOX = 'mailbox' + def __init__(self, **kwargs): + self.data = kwargs + super().__init__(**kwargs) + @property def aliases(self): return [ @@ -213,6 +218,10 @@ class MailinglistService(OrchestraModel): 'admin_email': None, } + def __init__(self, **kwargs): + self.data = kwargs + super().__init__(**kwargs) + @property def status(self): # TODO(@slamora): where retrieve if the list is active? From 186d20ec20cdd157ee0035053a97eb7581783cb7 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Tue, 17 Dec 2019 14:48:21 +0100 Subject: [PATCH 5/6] Retrieve user bills. --- musician/api.py | 1 + musician/models.py | 19 ++++++++++++++++++ musician/templates/musician/billing.html | 9 +++------ musician/views.py | 25 ++++++------------------ 4 files changed, 29 insertions(+), 25 deletions(-) diff --git a/musician/api.py b/musician/api.py index 4df4943..78964bd 100644 --- a/musician/api.py +++ b/musician/api.py @@ -27,6 +27,7 @@ API_PATHS = { 'saas-list': 'saas/', # other + 'bill-list': 'bills/', 'payment-source-list': 'payment-sources/', } diff --git a/musician/models.py b/musician/models.py index 477a371..b0faab8 100644 --- a/musician/models.py +++ b/musician/models.py @@ -48,6 +48,25 @@ class OrchestraModel: return '%s object (%s)' % (self.__class__.__name__, self.id) +class Bill(OrchestraModel): + api_name = 'bill' + param_defaults = { + "number": "1", + "type": "INVOICE", + "total": 0.0, + "is_sent": False, + "created_on": "", + "due_on": "", + "comments": "", + } + + def pdf_url(self): + # TODO (@slamora) create a view that exposes & downloads backend PDF + import urllib.parse + bill_url = self._json.get('url') + return urllib.parse.urljoin(bill_url, 'document/') + + class BillingContact(OrchestraModel): param_defaults = { 'name': None, diff --git a/musician/templates/musician/billing.html b/musician/templates/musician/billing.html index f99d816..c374dc0 100644 --- a/musician/templates/musician/billing.html +++ b/musician/templates/musician/billing.html @@ -1,5 +1,5 @@ {% extends "musician/base.html" %} -{% load i18n %} +{% load i18n l10n %} {% block content %} @@ -13,7 +13,6 @@ - @@ -21,7 +20,6 @@ Bill date Type Total - Status Download PDF @@ -29,10 +27,9 @@ {% for bill in object_list %} {{ bill.number }} - {{ bill.date|date:"SHORT_DATE_FORMAT" }} + {{ bill.created_on }} {{ bill.type }} - {{ bill.total_amount }} - {{ bill.status }} + {{ bill.total|floatformat:2|localize }}€ {% endfor %} diff --git a/musician/views.py b/musician/views.py index 51a5013..b0271b1 100644 --- a/musician/views.py +++ b/musician/views.py @@ -17,7 +17,7 @@ from .auth import logout as auth_logout from .forms import LoginForm from .mixins import (CustomContextMixin, ExtendedPaginationMixin, UserTokenRequiredMixin) -from .models import (DatabaseService, MailinglistService, MailService, +from .models import (Bill, DatabaseService, MailinglistService, MailService, PaymentSource, SaasService, UserAccount) from .settings import ALLOWED_RESOURCES @@ -82,24 +82,6 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): return context -class BillingView(CustomContextMixin, ExtendedPaginationMixin, UserTokenRequiredMixin, ListView): - template_name = "musician/billing.html" - - def get_queryset(self): - # TODO (@slamora) retrieve user bills - from django.utils import timezone - return [ - { - 'number': 24, - 'date': timezone.now(), - 'type': 'subscription', - 'total_amount': '25,00 €', - 'status': 'paid', - 'pdf_url': 'https://example.org/bill.pdf' - }, - ] - - class ProfileView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): template_name = "musician/profile.html" @@ -146,6 +128,11 @@ class ServiceListView(CustomContextMixin, ExtendedPaginationMixin, UserTokenRequ return context +class BillingView(ServiceListView): + service_class = Bill + template_name = "musician/billing.html" + + class MailView(ServiceListView): service_class = MailService template_name = "musician/mail.html" From c2d865d664a8a372a902c6039333529e9d540b48 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Tue, 17 Dec 2019 15:15:58 +0100 Subject: [PATCH 6/6] Add view to retrieve bill document. --- musician/api.py | 17 +++++++++++++++-- musician/models.py | 7 +------ musician/templates/musician/billing.html | 2 +- musician/urls.py | 3 ++- musician/views.py | 11 ++++++++++- 5 files changed, 29 insertions(+), 11 deletions(-) diff --git a/musician/api.py b/musician/api.py index 78964bd..8b4139c 100644 --- a/musician/api.py +++ b/musician/api.py @@ -28,6 +28,7 @@ API_PATHS = { # other 'bill-list': 'bills/', + 'bill-document': 'bills/{pk}/document/', 'payment-source-list': 'payment-sources/', } @@ -59,7 +60,7 @@ class Orchestra(object): return response.json().get("token", None) - def request(self, verb, resource=None, querystring=None, url=None, raise_exception=True): + def request(self, verb, resource=None, url=None, render_as="json", querystring=None, raise_exception=True): assert verb in ["HEAD", "GET", "POST", "PATCH", "PUT", "DELETE"] if resource is not None: url = self.build_absolute_uri(resource) @@ -77,7 +78,10 @@ class Orchestra(object): response.raise_for_status() status = response.status_code - output = response.json() + if render_as == "json": + output = response.json() + else: + output = response.content return status, output @@ -94,6 +98,15 @@ class Orchestra(object): raise PermissionError("Cannot retrieve profile of an anonymous user.") return UserAccount.new_from_json(output[0]) + def retrieve_bill_document(self, pk): + path = API_PATHS.get('bill-document').format_map({'pk': pk}) + + url = urllib.parse.urljoin(self.base_url, path) + status, bill_pdf = self.request("GET", render_as="html", url=url, raise_exception=False) + if status == 404: + raise Http404(_("No domain found matching the query")) + return bill_pdf + def retrieve_domain(self, pk): path = API_PATHS.get('domain-detail').format_map({'pk': pk}) diff --git a/musician/models.py b/musician/models.py index b0faab8..db3367b 100644 --- a/musician/models.py +++ b/musician/models.py @@ -51,6 +51,7 @@ class OrchestraModel: class Bill(OrchestraModel): api_name = 'bill' param_defaults = { + "id": None, "number": "1", "type": "INVOICE", "total": 0.0, @@ -60,12 +61,6 @@ class Bill(OrchestraModel): "comments": "", } - def pdf_url(self): - # TODO (@slamora) create a view that exposes & downloads backend PDF - import urllib.parse - bill_url = self._json.get('url') - return urllib.parse.urljoin(bill_url, 'document/') - class BillingContact(OrchestraModel): param_defaults = { diff --git a/musician/templates/musician/billing.html b/musician/templates/musician/billing.html index c374dc0..46d1396 100644 --- a/musician/templates/musician/billing.html +++ b/musician/templates/musician/billing.html @@ -30,7 +30,7 @@ {{ bill.created_on }} {{ bill.type }} {{ bill.total|floatformat:2|localize }}€ - + {% endfor %} diff --git a/musician/urls.py b/musician/urls.py index 6bae570..9139f35 100644 --- a/musician/urls.py +++ b/musician/urls.py @@ -16,7 +16,8 @@ urlpatterns = [ path('auth/logout/', views.LogoutView.as_view(), name='logout'), path('dashboard/', views.DashboardView.as_view(), name='dashboard'), path('domains//', views.DomainDetailView.as_view(), name='domain-detail'), - path('billing/', views.BillingView.as_view(), name='billing'), + path('bills/', views.BillingView.as_view(), name='billing'), + path('bills//download/', views.BillDownloadView.as_view(), name='bill-download'), path('profile/', views.ProfileView.as_view(), name='profile'), path('mails/', views.MailView.as_view(), name='mails'), path('mailing-lists/', views.MailingListsView.as_view(), name='mailing-lists'), diff --git a/musician/views.py b/musician/views.py index b0271b1..7a8a2d1 100644 --- a/musician/views.py +++ b/musician/views.py @@ -1,11 +1,12 @@ from itertools import groupby from django.core.exceptions import ImproperlyConfigured -from django.http import HttpResponseRedirect +from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import render from django.urls import reverse_lazy from django.utils.http import is_safe_url 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 from django.views.generic.edit import FormView @@ -133,6 +134,14 @@ class BillingView(ServiceListView): template_name = "musician/billing.html" +class BillDownloadView(CustomContextMixin, UserTokenRequiredMixin, View): + 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 = MailService template_name = "musician/mail.html"