Merge branch 'user-profile'

This commit is contained in:
Santiago Lamora 2019-12-18 10:29:09 +01:00
commit 65322f7e52
8 changed files with 133 additions and 72 deletions

View file

@ -27,6 +27,8 @@ API_PATHS = {
'saas-list': 'saas/', 'saas-list': 'saas/',
# other # other
'bill-list': 'bills/',
'bill-document': 'bills/{pk}/document/',
'payment-source-list': 'payment-sources/', 'payment-source-list': 'payment-sources/',
} }
@ -58,7 +60,7 @@ class Orchestra(object):
return response.json().get("token", None) 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"] assert verb in ["HEAD", "GET", "POST", "PATCH", "PUT", "DELETE"]
if resource is not None: if resource is not None:
url = self.build_absolute_uri(resource) url = self.build_absolute_uri(resource)
@ -76,7 +78,10 @@ class Orchestra(object):
response.raise_for_status() response.raise_for_status()
status = response.status_code status = response.status_code
output = response.json() if render_as == "json":
output = response.json()
else:
output = response.content
return status, output return status, output
@ -93,6 +98,15 @@ class Orchestra(object):
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 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): 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})

View file

@ -1,13 +1,19 @@
import ast
import logging
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
logger = logging.getLogger(__name__)
class OrchestraModel: class OrchestraModel:
""" Base class from which all orchestra models will inherit. """ """ Base class from which all orchestra models will inherit. """
api_name = None api_name = None
verbose_name = None verbose_name = None
fields = () fields = ()
param_defaults = {} id = None
def __init__(self, **kwargs): def __init__(self, **kwargs):
if self.verbose_name is None: if self.verbose_name is None:
@ -16,9 +22,6 @@ class OrchestraModel:
for (param, default) in self.param_defaults.items(): for (param, default) in self.param_defaults.items():
setattr(self, param, kwargs.get(param, default)) 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 @classmethod
def new_from_json(cls, data, **kwargs): def new_from_json(cls, data, **kwargs):
@ -35,8 +38,7 @@ class OrchestraModel:
c = cls(**json_data) c = cls(**json_data)
c._json = data c._json = data
# TODO(@slamora) remove/replace by private variable to ovoid name collisions
c.data = data
return c return c
def __repr__(self): def __repr__(self):
@ -46,6 +48,20 @@ class OrchestraModel:
return '%s object (%s)' % (self.__class__.__name__, self.id) 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): class BillingContact(OrchestraModel):
param_defaults = { param_defaults = {
'name': None, 'name': None,
@ -65,6 +81,15 @@ class PaymentSource(OrchestraModel):
"is_active": False, "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): class UserAccount(OrchestraModel):
api_name = 'accounts' api_name = 'accounts'
@ -159,10 +184,15 @@ class MailService(OrchestraModel):
verbose_name = _('Mail addresses') 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.') 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') fields = ('mail_address', 'aliases', 'type', 'type_detail')
param_defaults = {}
FORWARD = 'forward' FORWARD = 'forward'
MAILBOX = 'mailbox' MAILBOX = 'mailbox'
def __init__(self, **kwargs):
self.data = kwargs
super().__init__(**kwargs)
@property @property
def aliases(self): def aliases(self):
return [ return [
@ -202,6 +232,10 @@ class MailinglistService(OrchestraModel):
'admin_email': None, 'admin_email': None,
} }
def __init__(self, **kwargs):
self.data = kwargs
super().__init__(**kwargs)
@property @property
def status(self): def status(self):
# TODO(@slamora): where retrieve if the list is active? # TODO(@slamora): where retrieve if the list is active?

View file

@ -247,6 +247,13 @@ h1.service-name {
font-weight: 900; font-weight: 900;
} }
.card.card-profile .card-header {
background: white;
border-bottom: none;
font-size: large;
text-transform: uppercase;
}
#configDetailsModal .modal-header { #configDetailsModal .modal-header {
border-bottom: 0; border-bottom: 0;
text-align: center; text-align: center;

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -1,5 +1,5 @@
{% extends "musician/base.html" %} {% extends "musician/base.html" %}
{% load i18n %} {% load i18n l10n %}
{% block content %} {% block content %}
@ -13,7 +13,6 @@
<col span="1" style="width: 40%;"> <col span="1" style="width: 40%;">
<col span="1" style="width: 10%;"> <col span="1" style="width: 10%;">
<col span="1" style="width: 10%;"> <col span="1" style="width: 10%;">
<col span="1" style="width: 10%;">
</colgroup> </colgroup>
<thead class="thead-dark"> <thead class="thead-dark">
<tr> <tr>
@ -21,7 +20,6 @@
<th scope="col">Bill date</th> <th scope="col">Bill date</th>
<th scope="col">Type</th> <th scope="col">Type</th>
<th scope="col">Total</th> <th scope="col">Total</th>
<th scope="col">Status</th>
<th scope="col">Download PDF</th> <th scope="col">Download PDF</th>
</tr> </tr>
</thead> </thead>
@ -29,11 +27,10 @@
{% for bill in object_list %} {% for bill in object_list %}
<tr> <tr>
<th scope="row">{{ bill.number }}</th> <th scope="row">{{ bill.number }}</th>
<td>{{ bill.date|date:"SHORT_DATE_FORMAT" }}</td> <td>{{ bill.created_on }}</td>
<td>{{ bill.type }}</td> <td>{{ bill.type }}</td>
<td>{{ bill.total_amount }}</td> <td>{{ bill.total|floatformat:2|localize }}€</td>
<td class="font-weight-bold">{{ bill.status }}</td> <td><a class="text-dark" href="{% url 'musician:bill-download' bill.id %}" target="_blank" rel="noopener noreferrer"><i class="fas fa-file-pdf"></i></a></td>
<td><a class="text-dark" href="{{ bill.pdf_url }}" target="_blank" rel="noopener noreferrer"><i class="fas fa-file-pdf"></i></a></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View file

@ -3,48 +3,60 @@
{% block content %} {% block content %}
<h1>Profile</h1> <h1 class="service-name">Profile</h1>
<p>Little description of what to be expected...</p> <p class="service-description">Little description of what to be expected...</p>
<div class="card"> <div class="card-deck">
<h5 class="card-header text-right">User information</h5> <div class="card card-profile">
<div class="card-body row"> <h5 class="card-header">User information</h5>
<div class="col-md-4"> <div class="card-body row">
<div class="m-auto border border-secondary rounded-circle rounded-lg" style="width:125px; height:125px;"> <div class="col-md">
{# <!-- <img class="" src="#" alt="User profile image" /> -->#} <div class="border-primary rounded-circle d-inline-block p-1" style="background-color: white; border: 5px solid grey">
<img id="user-avatar" width="160" height="160" src="/static/musician/images/default-profile-picture-primary-color.png" alt="user-profile-picture">
</div>
</div> </div>
</div> <div class="col-md-9">
<div class="col-md-8"> <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">Preferred language: {{ profile.language }}</p>
<p class="card-text">Preferred language: {{ profile.language }}</p> </div>
{% comment %}
<!-- disabled until set_password is implemented -->
<div class="col-md-12 text-right">
<a class="btn btn-primary pl-5 pr-5" href="#">Set new password</a>
</div>
{% endcomment %}
</div> </div>
</div> </div>
</div>
{% with profile.billing as contact %} {% with profile.billing as contact %}
<div class="card mt-4"> <div class="card card-profile">
<h5 class="card-header text-right">Billing information</h5> <h5 class="card-header">Billing information</h5>
<div class="card-body"> <div class="card-body">
<div class="form-group">{{ contact.name }}</div> <div class="form-group">{{ contact.name }}</div>
<div class="form-group">{{ contact.address }}</div> <div class="form-group">{{ contact.address }}</div>
<div class="form-group"> <div class="form-group">
{{ contact.zipcode }} {{ contact.zipcode }}
{{ contact.city }} {{ contact.city }}
{{ contact.country }} {{ contact.country }}
</div> </div>
<div class="form-group"> <div class="form-group">
{{ contact.vat }} {{ contact.vat }}
</div> </div>
<!-- payment method --> <!-- payment method -->
<div class="form-group"> <div class="form-group">
payment method: {{ payment.method }} payment method: {{ payment.method }}
</div> </div>
<div class="form-group"> <div class="form-group">
{# TODO(@slamora) format payment method details #} {% if payment.method == 'SEPADirectDebit' %}
{{ payment.data.data }} IBAN {{ payment.data.iban }}
</div> {% else %}
{# <!-- "TODO handle Credit Card" --> #}
Details: {{ payment.data }}
{% endif %}
</div>
</div>
</div> </div>
</div> </div>
{% endwith %} {% endwith %}

View file

@ -16,7 +16,8 @@ urlpatterns = [
path('auth/logout/', views.LogoutView.as_view(), name='logout'), path('auth/logout/', views.LogoutView.as_view(), name='logout'),
path('dashboard/', views.DashboardView.as_view(), name='dashboard'), path('dashboard/', views.DashboardView.as_view(), name='dashboard'),
path('domains/<int:pk>/', views.DomainDetailView.as_view(), name='domain-detail'), path('domains/<int:pk>/', 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/<int:pk>/download/', views.BillDownloadView.as_view(), name='bill-download'),
path('profile/', views.ProfileView.as_view(), name='profile'), path('profile/', views.ProfileView.as_view(), name='profile'),
path('mails/', views.MailView.as_view(), name='mails'), path('mails/', views.MailView.as_view(), name='mails'),
path('mailing-lists/', views.MailingListsView.as_view(), name='mailing-lists'), path('mailing-lists/', views.MailingListsView.as_view(), name='mailing-lists'),

View file

@ -1,11 +1,12 @@
from itertools import groupby from itertools import groupby
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponseRedirect from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render from django.shortcuts import render
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.http import is_safe_url from django.utils.http import is_safe_url
from django.utils.translation import gettext_lazy as _ 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.base import RedirectView, TemplateView
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
@ -17,7 +18,7 @@ from .auth import logout as auth_logout
from .forms import LoginForm from .forms import LoginForm
from .mixins import (CustomContextMixin, ExtendedPaginationMixin, from .mixins import (CustomContextMixin, ExtendedPaginationMixin,
UserTokenRequiredMixin) UserTokenRequiredMixin)
from .models import (DatabaseService, MailinglistService, MailService, from .models import (Bill, DatabaseService, MailinglistService, MailService,
PaymentSource, SaasService, UserAccount) PaymentSource, SaasService, UserAccount)
from .settings import ALLOWED_RESOURCES from .settings import ALLOWED_RESOURCES
@ -82,24 +83,6 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
return context 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): class ProfileView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
template_name = "musician/profile.html" template_name = "musician/profile.html"
@ -146,6 +129,19 @@ class ServiceListView(CustomContextMixin, ExtendedPaginationMixin, UserTokenRequ
return context 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): class MailView(ServiceListView):
service_class = MailService service_class = MailService
template_name = "musician/mail.html" template_name = "musician/mail.html"