diff --git a/musician/api.py b/musician/api.py index 4df4943..8b4139c 100644 --- a/musician/api.py +++ b/musician/api.py @@ -27,6 +27,8 @@ API_PATHS = { 'saas-list': 'saas/', # other + 'bill-list': 'bills/', + 'bill-document': 'bills/{pk}/document/', 'payment-source-list': 'payment-sources/', } @@ -58,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) @@ -76,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 @@ -93,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 68ef088..db3367b 100644 --- a/musician/models.py +++ b/musician/models.py @@ -1,13 +1,19 @@ +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 verbose_name = None fields = () - param_defaults = {} + id = None def __init__(self, **kwargs): if self.verbose_name is 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): @@ -46,6 +48,20 @@ class OrchestraModel: return '%s object (%s)' % (self.__class__.__name__, self.id) +class Bill(OrchestraModel): + api_name = 'bill' + param_defaults = { + "id": None, + "number": "1", + "type": "INVOICE", + "total": 0.0, + "is_sent": False, + "created_on": "", + "due_on": "", + "comments": "", + } + + class BillingContact(OrchestraModel): param_defaults = { 'name': None, @@ -65,6 +81,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' @@ -159,10 +184,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 [ @@ -202,6 +232,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? 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 0000000..feaf2c7 Binary files /dev/null and b/musician/static/musician/images/default-profile-picture-primary-color.png differ diff --git a/musician/templates/musician/billing.html b/musician/templates/musician/billing.html index f99d816..46d1396 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,11 +27,10 @@ {% 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/templates/musician/profile.html b/musician/templates/musician/profile.html index e9da7be..c820007 100644 --- a/musician/templates/musician/profile.html +++ b/musician/templates/musician/profile.html @@ -3,48 +3,60 @@ {% 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 }} +
+
+ {% if payment.method == 'SEPADirectDebit' %} + IBAN {{ payment.data.iban }} + {% else %} + {# #} + Details: {{ payment.data }} + {% endif %} +
+
{% endwith %} 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 51a5013..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 @@ -17,7 +18,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 +83,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 +129,19 @@ class ServiceListView(CustomContextMixin, ExtendedPaginationMixin, UserTokenRequ return context +class BillingView(ServiceListView): + service_class = Bill + 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"