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/',
# 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})

View File

@ -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?

View File

@ -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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

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

View File

@ -3,48 +3,60 @@
{% block content %}
<h1>Profile</h1>
<p>Little description of what to be expected...</p>
<h1 class="service-name">Profile</h1>
<p class="service-description">Little description of what to be expected...</p>
<div class="card">
<h5 class="card-header text-right">User information</h5>
<div class="card-body row">
<div class="col-md-4">
<div class="m-auto border border-secondary rounded-circle rounded-lg" style="width:125px; height:125px;">
{# <!-- <img class="" src="#" alt="User profile image" /> -->#}
<div class="card-deck">
<div class="card card-profile">
<h5 class="card-header">User information</h5>
<div class="card-body row">
<div class="col-md">
<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 class="col-md-8">
<p class="card-text">{{ profile.username }}</p>
<p class="card-text">{{ profile.type }}</p>
<p class="card-text">Preferred language: {{ profile.language }}</p>
<div class="col-md-9">
<p class="card-text">{{ profile.username }}</p>
<p class="card-text">{{ profile.type }}</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>
{% with profile.billing as contact %}
<div class="card mt-4">
<h5 class="card-header text-right">Billing information</h5>
<div class="card-body">
<div class="form-group">{{ contact.name }}</div>
<div class="form-group">{{ contact.address }}</div>
<div class="form-group">
{{ contact.zipcode }}
{{ contact.city }}
{{ contact.country }}
</div>
<div class="form-group">
{{ contact.vat }}
</div>
<!-- payment method -->
<div class="form-group">
payment method: {{ payment.method }}
</div>
<div class="form-group">
{# TODO(@slamora) format payment method details #}
{{ payment.data.data }}
</div>
{% with profile.billing as contact %}
<div class="card card-profile">
<h5 class="card-header">Billing information</h5>
<div class="card-body">
<div class="form-group">{{ contact.name }}</div>
<div class="form-group">{{ contact.address }}</div>
<div class="form-group">
{{ contact.zipcode }}
{{ contact.city }}
{{ contact.country }}
</div>
<div class="form-group">
{{ contact.vat }}
</div>
<!-- payment method -->
<div class="form-group">
payment method: {{ payment.method }}
</div>
<div class="form-group">
{% if payment.method == 'SEPADirectDebit' %}
IBAN {{ payment.data.iban }}
{% else %}
{# <!-- "TODO handle Credit Card" --> #}
Details: {{ payment.data }}
{% endif %}
</div>
</div>
</div>
</div>
{% endwith %}

View File

@ -16,7 +16,8 @@ urlpatterns = [
path('auth/logout/', views.LogoutView.as_view(), name='logout'),
path('dashboard/', views.DashboardView.as_view(), name='dashboard'),
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('mails/', views.MailView.as_view(), name='mails'),
path('mailing-lists/', views.MailingListsView.as_view(), name='mailing-lists'),

View File

@ -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"