mirror of
https://github.com/ae-utbm/sith.git
synced 2025-11-25 06:06:56 +00:00
Merge pull request #1262 from ae-utbm/refactor/userstats
Refactor/userstats
This commit is contained in:
@@ -11,32 +11,35 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% if profile.permanencies %}
|
{% if total_perm_time %}
|
||||||
<div>
|
<div>
|
||||||
<h3>{% trans %}Permanencies{% endtrans %}</h3>
|
<h3>{% trans %}Permanencies{% endtrans %}</h3>
|
||||||
<div class="flexed">
|
<div class="flexed">
|
||||||
<div><span>Foyer :</span><span>{{ total_foyer_time }}</span></div>
|
{% for perm in perm_time %}
|
||||||
<div><span>Gommette :</span><span>{{ total_gommette_time }}</span></div>
|
<div>
|
||||||
<div><span>MDE :</span><span>{{ total_mde_time }}</span></div>
|
<span>{{ perm["counter__name"] }} :</span>
|
||||||
<div><b>Total :</b><b>{{ total_perm_time }}</b></div>
|
<span>{{ perm["total"]|format_timedelta }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<div><b>Total :</b><b>{{ total_perm_time|format_timedelta }}</b></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3>{% trans %}Buyings{% endtrans %}</h3>
|
<h3>{% trans %}Buyings{% endtrans %}</h3>
|
||||||
<div class="flexed">
|
<div class="flexed">
|
||||||
<div><span>Foyer :</span><span>{{ total_foyer_buyings }} €</span></div>
|
{% for sum in purchase_sums %}
|
||||||
<div><span>Gommette :</span><span>{{ total_gommette_buyings }} €</span></div>
|
|
||||||
<div><span>MDE :</span><span>{{ total_mde_buyings }} €</span></div>
|
|
||||||
<div><b>Total :</b><b>{{ total_foyer_buyings + total_gommette_buyings + total_mde_buyings }} €</b>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3>{% trans %}Product top 10{% endtrans %}</h3>
|
<span>{{ sum["counter__name"] }}</span>
|
||||||
|
<span>{{ sum["total"] }} €</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<div><b>Total : </b><b>{{ total_purchases }} €</b></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>{% trans %}Product top 15{% endtrans %}</h3>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -55,31 +55,17 @@ def phonenumber(
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="truncate_time")
|
|
||||||
def truncate_time(value, time_unit):
|
|
||||||
"""Remove everything in the time format lower than the specified unit.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
value: the value to truncate
|
|
||||||
time_unit: the lowest unit to display
|
|
||||||
"""
|
|
||||||
value = str(value)
|
|
||||||
return {
|
|
||||||
"millis": lambda: value.split(".")[0],
|
|
||||||
"seconds": lambda: value.rsplit(":", maxsplit=1)[0],
|
|
||||||
"minutes": lambda: value.split(":", maxsplit=1)[0],
|
|
||||||
"hours": lambda: value.rsplit(" ")[0],
|
|
||||||
}[time_unit]()
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="format_timedelta")
|
@register.filter(name="format_timedelta")
|
||||||
def format_timedelta(value: datetime.timedelta) -> str:
|
def format_timedelta(value: datetime.timedelta) -> str:
|
||||||
|
value = value - datetime.timedelta(microseconds=value.microseconds)
|
||||||
days = value.days
|
days = value.days
|
||||||
if days == 0:
|
if days == 0:
|
||||||
return str(value)
|
return str(value)
|
||||||
remainder = value - datetime.timedelta(days=days)
|
remainder = value - datetime.timedelta(days=days)
|
||||||
return ngettext(
|
return ngettext(
|
||||||
"%(nb_days)d day, %(remainder)s", "%(nb_days)d days, %(remainder)s", days
|
"%(nb_days)d day, %(remainder)s",
|
||||||
|
"%(nb_days)d days, %(remainder)s",
|
||||||
|
days,
|
||||||
) % {"nb_days": days, "remainder": str(remainder)}
|
) % {"nb_days": days, "remainder": str(remainder)}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import itertools
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ from core.baker_recipes import (
|
|||||||
from core.models import AnonymousUser, Group, User
|
from core.models import AnonymousUser, Group, User
|
||||||
from core.views import UserTabsMixin
|
from core.views import UserTabsMixin
|
||||||
from counter.baker_recipes import sale_recipe
|
from counter.baker_recipes import sale_recipe
|
||||||
from counter.models import Counter, Customer, Refilling, Selling
|
from counter.models import Counter, Customer, Permanency, Refilling, Selling
|
||||||
from counter.utils import is_logged_in_counter
|
from counter.utils import is_logged_in_counter
|
||||||
from eboutic.models import Invoice, InvoiceItem
|
from eboutic.models import Invoice, InvoiceItem
|
||||||
|
|
||||||
@@ -424,3 +425,28 @@ class TestUserQuerySetViewableBy:
|
|||||||
user = user_factory()
|
user = user_factory()
|
||||||
viewable = User.objects.filter(id__in=[u.id for u in users]).viewable_by(user)
|
viewable = User.objects.filter(id__in=[u.id for u in users]).viewable_by(user)
|
||||||
assert not viewable.exists()
|
assert not viewable.exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_user_stats(client: Client):
|
||||||
|
user = subscriber_user.make()
|
||||||
|
baker.make(Refilling, customer=user.customer, amount=99999)
|
||||||
|
bars = [b[0] for b in settings.SITH_COUNTER_BARS]
|
||||||
|
baker.make(
|
||||||
|
Permanency,
|
||||||
|
end=now() - timedelta(days=5),
|
||||||
|
start=now() - timedelta(days=5, hours=3),
|
||||||
|
counter_id=itertools.cycle(bars),
|
||||||
|
_quantity=5,
|
||||||
|
_bulk_create=True,
|
||||||
|
)
|
||||||
|
sale_recipe.make(
|
||||||
|
counter_id=itertools.cycle(bars),
|
||||||
|
customer=user.customer,
|
||||||
|
unit_price=1,
|
||||||
|
quantity=1,
|
||||||
|
_quantity=5,
|
||||||
|
)
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.get(reverse("core:user_stats", kwargs={"user_id": user.id}))
|
||||||
|
assert response.status_code == 200
|
||||||
|
|||||||
@@ -22,9 +22,9 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
import itertools
|
import itertools
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
# This file contains all the views that concern the user model
|
# This file contains all the views that concern the user model
|
||||||
from datetime import date, timedelta
|
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from smtplib import SMTPException
|
from smtplib import SMTPException
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ from django.contrib.auth import login, views
|
|||||||
from django.contrib.auth.forms import PasswordChangeForm
|
from django.contrib.auth.forms import PasswordChangeForm
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db.models import DateField, QuerySet
|
from django.db.models import DateField, F, QuerySet, Sum
|
||||||
from django.db.models.functions import Trunc
|
from django.db.models.functions import Trunc
|
||||||
from django.forms.models import modelform_factory
|
from django.forms.models import modelform_factory
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
@@ -66,9 +66,8 @@ from core.views.forms import (
|
|||||||
UserProfileForm,
|
UserProfileForm,
|
||||||
)
|
)
|
||||||
from core.views.mixins import TabedViewMixin, UseFragmentsMixin
|
from core.views.mixins import TabedViewMixin, UseFragmentsMixin
|
||||||
from counter.models import Counter, Refilling, Selling
|
from counter.models import Refilling, Selling
|
||||||
from eboutic.models import Invoice
|
from eboutic.models import Invoice
|
||||||
from subscription.models import Subscription
|
|
||||||
from trombi.views import UserTrombiForm
|
from trombi.views import UserTrombiForm
|
||||||
|
|
||||||
|
|
||||||
@@ -353,87 +352,40 @@ class UserStatsView(UserTabsMixin, CanViewMixin, DetailView):
|
|||||||
context_object_name = "profile"
|
context_object_name = "profile"
|
||||||
template_name = "core/user_stats.jinja"
|
template_name = "core/user_stats.jinja"
|
||||||
current_tab = "stats"
|
current_tab = "stats"
|
||||||
|
queryset = User.objects.exclude(customer=None).select_related("customer")
|
||||||
|
|
||||||
def dispatch(self, request, *arg, **kwargs):
|
def dispatch(self, request, *arg, **kwargs):
|
||||||
profile = self.get_object()
|
profile = self.get_object()
|
||||||
|
|
||||||
if not hasattr(profile, "customer"):
|
|
||||||
raise Http404
|
|
||||||
|
|
||||||
if not (
|
if not (
|
||||||
profile == request.user or request.user.has_perm("counter.view_customer")
|
profile == request.user or request.user.has_perm("counter.view_customer")
|
||||||
):
|
):
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
|
||||||
return super().dispatch(request, *arg, **kwargs)
|
return super().dispatch(request, *arg, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
from django.db.models import Sum
|
|
||||||
|
|
||||||
foyer = Counter.objects.filter(name="Foyer").first()
|
kwargs["perm_time"] = list(
|
||||||
mde = Counter.objects.filter(name="MDE").first()
|
self.object.permanencies.filter(end__isnull=False, counter__type="BAR")
|
||||||
gommette = Counter.objects.filter(name="La Gommette").first()
|
.values("counter", "counter__name")
|
||||||
semester_start = Subscription.compute_start(d=date.today(), duration=3)
|
.annotate(total=Sum(F("end") - F("start"), default=timedelta(seconds=0)))
|
||||||
|
.order_by("-total")
|
||||||
|
)
|
||||||
kwargs["total_perm_time"] = sum(
|
kwargs["total_perm_time"] = sum(
|
||||||
[p.end - p.start for p in self.object.permanencies.exclude(end=None)],
|
[perm["total"] for perm in kwargs["perm_time"]], start=timedelta(seconds=0)
|
||||||
timedelta(),
|
|
||||||
)
|
)
|
||||||
kwargs["total_foyer_time"] = sum(
|
kwargs["purchase_sums"] = list(
|
||||||
[
|
self.object.customer.buyings.filter(counter__type="BAR")
|
||||||
p.end - p.start
|
.values("counter", "counter__name")
|
||||||
for p in self.object.permanencies.filter(counter=foyer).exclude(
|
.annotate(total=Sum(F("unit_price") * F("quantity")))
|
||||||
end=None
|
.order_by("-total")
|
||||||
)
|
|
||||||
],
|
|
||||||
timedelta(),
|
|
||||||
)
|
|
||||||
kwargs["total_mde_time"] = sum(
|
|
||||||
[
|
|
||||||
p.end - p.start
|
|
||||||
for p in self.object.permanencies.filter(counter=mde).exclude(end=None)
|
|
||||||
],
|
|
||||||
timedelta(),
|
|
||||||
)
|
|
||||||
kwargs["total_gommette_time"] = sum(
|
|
||||||
[
|
|
||||||
p.end - p.start
|
|
||||||
for p in self.object.permanencies.filter(counter=gommette).exclude(
|
|
||||||
end=None
|
|
||||||
)
|
|
||||||
],
|
|
||||||
timedelta(),
|
|
||||||
)
|
|
||||||
kwargs["total_foyer_buyings"] = sum(
|
|
||||||
[
|
|
||||||
b.unit_price * b.quantity
|
|
||||||
for b in self.object.customer.buyings.filter(
|
|
||||||
counter=foyer, date__gte=semester_start
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
kwargs["total_mde_buyings"] = sum(
|
|
||||||
[
|
|
||||||
b.unit_price * b.quantity
|
|
||||||
for b in self.object.customer.buyings.filter(
|
|
||||||
counter=mde, date__gte=semester_start
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
kwargs["total_gommette_buyings"] = sum(
|
|
||||||
[
|
|
||||||
b.unit_price * b.quantity
|
|
||||||
for b in self.object.customer.buyings.filter(
|
|
||||||
counter=gommette, date__gte=semester_start
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
kwargs["total_purchases"] = sum(s["total"] for s in kwargs["purchase_sums"])
|
||||||
kwargs["top_product"] = (
|
kwargs["top_product"] = (
|
||||||
self.object.customer.buyings.values("product__name")
|
self.object.customer.buyings.values("product__name")
|
||||||
.annotate(product_sum=Sum("quantity"))
|
.annotate(product_sum=Sum("quantity"))
|
||||||
.exclude(product_sum=None)
|
|
||||||
.order_by("-product_sum")
|
.order_by("-product_sum")
|
||||||
.all()[:10]
|
.all()[:15]
|
||||||
)
|
)
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
<td>{{ loop.index }}</td>
|
<td>{{ loop.index }}</td>
|
||||||
<td>{{ barman.name }} {% if barman.nickname %}({{ barman.nickname }}){% endif %}</td>
|
<td>{{ barman.name }} {% if barman.nickname %}({{ barman.nickname }}){% endif %}</td>
|
||||||
<td>{{ barman.promo or '' }}</td>
|
<td>{{ barman.promo or '' }}</td>
|
||||||
<td>{{ barman.perm_sum|format_timedelta|truncate_time("millis") }}</td>
|
<td>{{ barman.perm_sum|format_timedelta }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
<td>{{ loop.index }}</td>
|
<td>{{ loop.index }}</td>
|
||||||
<td>{{ barman.name }} {% if barman.nickname %}({{ barman.nickname }}){% endif %}</td>
|
<td>{{ barman.name }} {% if barman.nickname %}({{ barman.nickname }}){% endif %}</td>
|
||||||
<td>{{ barman.promo or '' }}</td>
|
<td>{{ barman.promo or '' }}</td>
|
||||||
<td>{{ barman.perm_sum|format_timedelta|truncate_time("millis") }}</td>
|
<td>{{ barman.perm_sum|format_timedelta }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-19 21:00+0100\n"
|
"POT-Creation-Date: 2025-11-24 11:05+0100\n"
|
||||||
"PO-Revision-Date: 2016-07-18\n"
|
"PO-Revision-Date: 2016-07-18\n"
|
||||||
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
||||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
||||||
@@ -2658,8 +2658,8 @@ msgid "Buyings"
|
|||||||
msgstr "Achats"
|
msgstr "Achats"
|
||||||
|
|
||||||
#: core/templates/core/user_stats.jinja
|
#: core/templates/core/user_stats.jinja
|
||||||
msgid "Product top 10"
|
msgid "Product top 15"
|
||||||
msgstr "Top 10 produits"
|
msgstr "Top 15 produits"
|
||||||
|
|
||||||
#: core/templates/core/user_stats.jinja
|
#: core/templates/core/user_stats.jinja
|
||||||
msgid "Product"
|
msgid "Product"
|
||||||
@@ -2819,8 +2819,8 @@ msgstr "Outils Trombi"
|
|||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(nb_days)d day, %(remainder)s"
|
msgid "%(nb_days)d day, %(remainder)s"
|
||||||
msgid_plural "%(nb_days)d days, %(remainder)s"
|
msgid_plural "%(nb_days)d days, %(remainder)s"
|
||||||
msgstr[0] ""
|
msgstr[0] "%(nb_days)d jour, %(remainder)s"
|
||||||
msgstr[1] ""
|
msgstr[1] "%(nb_days)d jours, %(remainder)s"
|
||||||
|
|
||||||
#: core/views/files.py
|
#: core/views/files.py
|
||||||
msgid "Add a new folder"
|
msgid "Add a new folder"
|
||||||
|
|||||||
@@ -177,7 +177,6 @@ TEMPLATES = [
|
|||||||
"filters": {
|
"filters": {
|
||||||
"markdown": "core.templatetags.renderer.markdown",
|
"markdown": "core.templatetags.renderer.markdown",
|
||||||
"phonenumber": "core.templatetags.renderer.phonenumber",
|
"phonenumber": "core.templatetags.renderer.phonenumber",
|
||||||
"truncate_time": "core.templatetags.renderer.truncate_time",
|
|
||||||
"format_timedelta": "core.templatetags.renderer.format_timedelta",
|
"format_timedelta": "core.templatetags.renderer.format_timedelta",
|
||||||
"add_attr": "core.templatetags.renderer.add_attr",
|
"add_attr": "core.templatetags.renderer.add_attr",
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user