Merge pull request #859 from ae-utbm/account-pages

Optimize user account pages
This commit is contained in:
thomas girod
2024-10-08 19:55:45 +02:00
committed by GitHub
6 changed files with 226 additions and 138 deletions

View File

@ -1,6 +1,6 @@
{% extends "core/base.jinja" %}
{% macro monthly(obj) %}
{% macro monthly(objects) %}
<div>
<table>
<thead>
@ -11,17 +11,18 @@
</tr>
</thead>
<tbody>
{% for array in obj %}
{% for dict in array %}
{% if dict['sum'] != 0 %}
{% set link=url('core:user_account_detail', user_id=profile.id, year=dict['date'].year, month=dict['date'].month) %}
<tr>
<td><a href="{{ link }}">{{ dict['date'].year }}</a></td>
<td><a href="{{ link }}">{{ dict['date']|date("E") }}</a></td>
<td><a href="{{ link }}">{{ dict['sum'] }} €</a></td>
</tr>
{% endif %}
{% endfor %}
{% for object in objects %}
{% set link=url(
'core:user_account_detail',
user_id=profile.id,
year=object['grouped_date'].year,
month=object['grouped_date'].month
) %}
<tr>
<td><a href="{{ link }}">{{ object["grouped_date"]|date("Y") }}</a></td>
<td><a href="{{ link }}">{{ object["grouped_date"]|date("E") }}</a></td>
<td><a href="{{ link }}">{{ "%.2f"|format(object["total"]) }} €</a></td>
</tr>
{% endfor %}
</tbody>
</table>
@ -37,19 +38,15 @@
<h3>{% trans %}User account{% endtrans %}</h3>
<p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p>
<div id="drop">
{% set bought = customer.buyings.exists() %}
{% set refilled = customer.refillings.exists() %}
{% if bought or refilled %}
{% if bought %}
<h5>{% trans %}Account purchases{% endtrans %}</h5>
{{ monthly(buyings_month) }}
{% endif %}
{% if refilled %}
<h5>{% trans %}Reloads{% endtrans %}</h5>
{{ monthly(refilling_month) }}
{% endif %}
{% if buyings_month %}
<h5>{% trans %}Account purchases{% endtrans %}</h5>
{{ monthly(buyings_month) }}
{% endif %}
{% if customer.user.invoices.exists() %}
{% if refilling_month %}
<h5>{% trans %}Reloads{% endtrans %}</h5>
{{ monthly(refilling_month) }}
{% endif %}
{% if invoices_month %}
<h5>{% trans %}Eboutic invoices{% endtrans %}</h5>
{{ monthly(invoices_month) }}
{% endif %}
@ -58,7 +55,11 @@
<div>
<ul>
{% for s in etickets %}
<li><a href="{{ url('counter:eticket_pdf', selling_id=s.id) }}">{{ s.quantity }} x {{ s.product.eticket }}</a></li>
<li>
<a href="{{ url('counter:eticket_pdf', selling_id=s.id) }}">
{{ s.quantity }} x {{ s.product.eticket }}
</a>
</li>
{% endfor %}
</ul>
</div>

View File

@ -5,44 +5,49 @@
{% endblock %}
{% block content %}
{% if customer %}
<h3>{% trans %}User account{% endtrans %}</h3>
<p>{% trans %}Amount: {% endtrans %}{{ customer.amount }}</p>
<p><a href="{{ url('core:user_account', user_id=profile.id) }}">{% trans %}Back{% endtrans %}</a></p>
{% if customer.buyings.exists() %}
<h4>{% trans %}Account purchases{% endtrans %}</h4>
<table>
<thead>
<h3>{% trans %}User account{% endtrans %}</h3>
<p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p>
<p><a href="{{ url('core:user_account', user_id=profile.id) }}">{% trans %}Back{% endtrans %}</a></p>
{% if purchases %}
<h4>{% trans %}Account purchases{% endtrans %}</h4>
<table>
<thead>
<tr>
<td>{% trans %}Date{% endtrans %}</td>
<td>{% trans %}Counter{% endtrans %}</td>
<td>{% trans %}Barman{% endtrans %}</td>
<td>{% trans %}Label{% endtrans %}</td>
<td>{% trans %}Quantity{% endtrans %}</td>
<td>{% trans %}Total{% endtrans %}</td>
<td>{% trans %}Payment method{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% for purchase in purchases %}
<tr>
<td>{% trans %}Date{% endtrans %}</td>
<td>{% trans %}Counter{% endtrans %}</td>
<td>{% trans %}Barman{% endtrans %}</td>
<td>{% trans %}Label{% endtrans %}</td>
<td>{% trans %}Quantity{% endtrans %}</td>
<td>{% trans %}Total{% endtrans %}</td>
<td>{% trans %}Payment method{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% for i in customer.buyings.order_by('-date').all().filter(
date__year=year, date__month=month) %}
<tr>
<td>{{ i.date|localtime|date(DATETIME_FORMAT) }} - {{ i.date|localtime|time(DATETIME_FORMAT) }}</td>
<td>{{ i.counter }}</td>
<td><a href="{{ i.seller.get_absolute_url() }}">{{ i.seller.get_display_name() }}</a></td>
<td>{{ i.label }}</td>
<td>{{ i.quantity }}</td>
<td>{{ i.quantity * i.unit_price }} €</td>
<td>{{ i.get_payment_method_display() }}</td>
{% if i.is_owned_by(user) %}
<td><a href="{{ url('counter:selling_delete', selling_id=i.id) }}">{% trans %}Delete{% endtrans %}</a></td>
<td>
{{ purchase.date|localtime|date(DATETIME_FORMAT) }}
- {{ purchase.date|localtime|time(DATETIME_FORMAT) }}
</td>
<td>{{ purchase.counter }}</td>
<td><a href="{{ purchase.seller.get_absolute_url() }}">{{ purchase.seller.get_display_name() }}</a></td>
<td>{{ purchase.label }}</td>
<td>{{ purchase.quantity }}</td>
<td>{{ purchase.quantity * purchase.unit_price }}</td>
<td>{{ purchase.get_payment_method_display() }}</td>
{% if purchase.is_owned_by(user) %}
<td>
<a href="{{ url('counter:selling_delete', selling_id=purchase.id) }}">
{% trans %}Delete{% endtrans %}
</a>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if customer.refillings.exists() %}
{% if refills %}
<h4>{% trans %}Reloads{% endtrans %}</h4>
<table>
<thead>
@ -55,22 +60,30 @@
</tr>
</thead>
<tbody>
{% for i in customer.refillings.order_by('-date').filter( date__year=year, date__month=month) %}
{% for refill in refills %}
<tr>
<td>{{ i.date|localtime|date(DATETIME_FORMAT) }} - {{ i.date|localtime|time(DATETIME_FORMAT) }}</td>
<td>{{ i.counter }}</td>
<td><a href="{{ i.operator.get_absolute_url() }}">{{ i.operator.get_display_name() }}</a></td>
<td>{{ i.amount }} €</td>
<td>{{ i.get_payment_method_display() }}</td>
{% if i.is_owned_by(user) %}
<td><a href="{{ url('counter:refilling_delete', refilling_id=i.id) }}">{% trans %}Delete{% endtrans %}</a></td>
<td>{{ refill.date|localtime|date(DATETIME_FORMAT) }} - {{ refill.date|localtime|time(DATETIME_FORMAT) }}</td>
<td>{{ refill.counter }}</td>
<td>
<a href="{{ refill.operator.get_absolute_url() }}">
{{ refill.operator.get_display_name() }}
</a>
</td>
<td>{{ refill.amount }} €</td>
<td>{{ refill.get_payment_method_display() }}</td>
{% if refill.is_owned_by(user) %}
<td>
<a href="{{ url('counter:refilling_delete', refilling_id=refill.id) }}">
{% trans %}Delete{% endtrans %}
</a>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if customer.user.invoices.exists() %}
{% if invoices %}
<h4>{% trans %}Eboutic invoices{% endtrans %}</h4>
<table>
<thead>
@ -81,25 +94,24 @@
</tr>
</thead>
<tbody>
{% for i in customer.user.invoices.order_by('-date').all().filter(
date__year=year, date__month=month) %}
<tr>
<td>{{ i.date|localtime|date(DATETIME_FORMAT) }} - {{ i.date|localtime|time(DATETIME_FORMAT) }}</td>
<td>
<ul>
{% for it in i.items.all() %}
<li>{{ it.quantity }} x {{ it.product_name }} - {{ it.product_unit_price }} €</li>
{% endfor %}
</ul>
</td>
<td>{{ i.get_total() }} €</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% else %}
<p>{% trans %}User has no account{% endtrans %}</p>
{% endif %}
<p><a href="{{ url('core:user_account', user_id=profile.id) }}">{% trans %}Back{% endtrans %}</a></p>
{% for invoice in invoices %}
<tr>
<td>
{{ invoice.date|localtime|date(DATETIME_FORMAT) }}
- {{ invoice.date|localtime|time(DATETIME_FORMAT) }}
</td>
<td>
<ul>
{% for it in invoice.items.all() %}
<li>{{ it.quantity }} x {{ it.product_name }} - {{ it.product_unit_price }} €</li>
{% endfor %}
</ul>
</td>
<td>{{ invoice.total }} €</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<p><a href="{{ url('core:user_account', user_id=profile.id) }}">{% trans %}Back{% endtrans %}</a></p>
{% endblock %}

View File

@ -1,7 +1,8 @@
from datetime import timedelta
import pytest
from django.core.management import call_command
from django.test import TestCase
from django.test import Client, TestCase
from django.urls import reverse
from django.utils.timezone import now
from model_bakery import baker, seq
@ -95,3 +96,18 @@ class TestSearchUsersView(TestSearchUsers):
self.client.force_login(subscriber_user.make())
response = self.client.get(reverse("core:search"))
assert response.status_code == 200
@pytest.mark.django_db
def test_user_account_not_found(client: Client):
client.force_login(baker.make(User, is_superuser=True))
user = baker.make(User)
res = client.get(reverse("core:user_account", kwargs={"user_id": user.id}))
assert res.status_code == 404
res = client.get(
reverse(
"core:user_account_detail",
kwargs={"user_id": user.id, "year": 2024, "month": 10},
)
)
assert res.status_code == 404

View File

@ -21,7 +21,6 @@
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
import logging
# This file contains all the views that concern the user model
from datetime import date, timedelta
@ -32,6 +31,8 @@ from django.contrib.auth import login, views
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied
from django.db.models import DateField, QuerySet
from django.db.models.functions import Trunc
from django.forms import CheckboxSelectMultiple
from django.forms.models import modelform_factory
from django.http import Http404
@ -68,6 +69,8 @@ from core.views.forms import (
UserProfileForm,
)
from counter.forms import StudentCardForm
from counter.models import Refilling, Selling
from eboutic.models import Invoice
from subscription.models import Subscription
from trombi.views import UserTrombiForm
@ -615,65 +618,57 @@ class UserAccountBase(UserTabsMixin, DetailView):
model = User
pk_url_kwarg = "user_id"
current_tab = "account"
queryset = User.objects.select_related("customer")
def dispatch(self, request, *arg, **kwargs): # Manually validates the rights
res = super().dispatch(request, *arg, **kwargs)
if (
self.object == request.user
kwargs.get("user_id") == request.user.id
or request.user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
or request.user.is_in_group(
name=settings.SITH_BAR_MANAGER["unix_name"] + settings.SITH_BOARD_SUFFIX
)
or request.user.is_root
):
return res
return super().dispatch(request, *arg, **kwargs)
raise PermissionDenied
def get_object(self, queryset=None):
obj = super().get_object(queryset)
if not hasattr(obj, "customer"):
raise Http404(_("User has no account"))
return obj
class UserAccountView(UserAccountBase):
"""Display a user's account."""
template_name = "core/user_account.jinja"
def expense_by_month(self, obj, calc):
stats = []
for year in obj.datetimes("date", "year", order="DESC"):
stats.append([])
i = 0
for month in obj.filter(date__year=year.year).datetimes(
"date", "month", order="DESC"
):
q = obj.filter(date__year=month.year, date__month=month.month)
stats[i].append({"sum": sum([calc(p) for p in q]), "date": month})
i += 1
return stats
def invoices_calc(self, query):
t = 0
for it in query.items.all():
t += it.quantity * it.product_unit_price
return t
@staticmethod
def expense_by_month[T](qs: QuerySet[T]) -> QuerySet[T]:
month_trunc = Trunc("date", "month", output_field=DateField())
return (
qs.annotate(grouped_date=month_trunc)
.values("grouped_date")
.annotate_total()
.exclude(total=0)
.order_by("-grouped_date")
)
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
kwargs["profile"] = self.object
try:
kwargs["customer"] = self.object.customer
kwargs["buyings_month"] = self.expense_by_month(
self.object.customer.buyings, (lambda q: q.unit_price * q.quantity)
)
kwargs["invoices_month"] = self.expense_by_month(
self.object.customer.user.invoices, self.invoices_calc
)
kwargs["refilling_month"] = self.expense_by_month(
self.object.customer.refillings, (lambda q: q.amount)
)
kwargs["etickets"] = self.object.customer.buyings.exclude(
product__eticket=None
).all()
except Exception as e:
logging.error(e)
kwargs["customer"] = self.object.customer
kwargs["buyings_month"] = self.expense_by_month(
Selling.objects.filter(customer=self.object.customer)
)
kwargs["refilling_month"] = self.expense_by_month(
Refilling.objects.filter(customer=self.object.customer)
)
kwargs["invoices_month"] = self.expense_by_month(
Invoice.objects.filter(user=self.object)
)
kwargs["etickets"] = self.object.customer.buyings.exclude(product__eticket=None)
return kwargs
@ -685,13 +680,29 @@ class UserAccountDetailView(UserAccountBase, YearMixin, MonthMixin):
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
kwargs["profile"] = self.object
kwargs["year"] = self.get_year()
kwargs["month"] = self.get_month()
try:
kwargs["customer"] = self.object.customer
except:
pass
kwargs["tab"] = "account"
kwargs["customer"] = self.object.customer
year, month = self.get_year(), self.get_month()
filters = {
"customer": self.object.customer,
"date__year": year,
"date__month": month,
}
kwargs["purchases"] = list(
Selling.objects.filter(**filters)
.select_related("counter", "counter__club", "seller")
.order_by("-date")
)
kwargs["refills"] = list(
Refilling.objects.filter(**filters)
.select_related("counter", "counter__club", "operator")
.order_by("-date")
)
kwargs["invoices"] = list(
Invoice.objects.filter(user=self.object, date__year=year, date__month=month)
.annotate_total()
.prefetch_related("items")
.order_by("-date")
)
return kwargs