Merge branch 'user-profile'
This commit is contained in:
commit
65322f7e52
|
@ -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})
|
||||||
|
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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.0 KiB |
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue