4 Commits

Author SHA1 Message Date
imperosol
7ee645c5ee remove services from navbar 2025-09-13 20:00:41 +02:00
thomas girod
b767079c5a Merge pull request #1167 from ae-utbm/page-n+1
Page n+1
2025-09-08 11:28:55 +02:00
imperosol
37961e437b fix: N+1 queries on PageListView 2025-09-04 17:39:17 +02:00
imperosol
b97a1a2e56 improve User.can_view and User.can_edit 2025-09-04 17:38:58 +02:00
10 changed files with 95 additions and 277 deletions

View File

@@ -560,7 +560,7 @@ class User(AbstractUser):
"""Determine if the object is owned by the user."""
if hasattr(obj, "is_owned_by") and obj.is_owned_by(self):
return True
if hasattr(obj, "owner_group") and self.is_in_group(pk=obj.owner_group.id):
if hasattr(obj, "owner_group") and self.is_in_group(pk=obj.owner_group_id):
return True
return self.is_root
@@ -569,9 +569,15 @@ class User(AbstractUser):
if hasattr(obj, "can_be_edited_by") and obj.can_be_edited_by(self):
return True
if hasattr(obj, "edit_groups"):
for pk in obj.edit_groups.values_list("pk", flat=True):
if self.is_in_group(pk=pk):
return True
if (
hasattr(obj, "_prefetched_objects_cache")
and "edit_groups" in obj._prefetched_objects_cache
):
pks = [g.id for g in obj.edit_groups.all()]
else:
pks = list(obj.edit_groups.values_list("id", flat=True))
if any(self.is_in_group(pk=pk) for pk in pks):
return True
if isinstance(obj, User) and obj == self:
return True
return self.is_owner(obj)
@@ -581,9 +587,18 @@ class User(AbstractUser):
if hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self):
return True
if hasattr(obj, "view_groups"):
for pk in obj.view_groups.values_list("pk", flat=True):
if self.is_in_group(pk=pk):
return True
# if "view_groups" has already been prefetched, use
# the prefetch cache, else fetch only the ids, to make
# the query lighter.
if (
hasattr(obj, "_prefetched_objects_cache")
and "view_groups" in obj._prefetched_objects_cache
):
pks = [g.id for g in obj.view_groups.all()]
else:
pks = list(obj.view_groups.values_list("id", flat=True))
if any(self.is_in_group(pk=pk) for pk in pks):
return True
return self.can_edit(obj)
def can_be_edited_by(self, user):
@@ -1384,9 +1399,9 @@ class Page(models.Model):
@cached_property
def is_club_page(self):
club_root_page = Page.objects.filter(name=settings.SITH_CLUB_ROOT_PAGE).first()
return club_root_page is not None and (
self == club_root_page or club_root_page in self.get_parent_list()
return (
self.name == settings.SITH_CLUB_ROOT_PAGE
or settings.SITH_CLUB_ROOT_PAGE in [p.name for p in self.get_parent_list()]
)
@cached_property

View File

@@ -514,10 +514,6 @@ th {
text-align: center;
padding: 5px 10px;
>input[type="checkbox"] {
padding: unset;
}
>ul {
margin-top: 0;
}

View File

@@ -20,14 +20,6 @@
<a class="link" href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a>
<a class="link" href="{{ url('sas:main') }}">{% trans %}Gallery{% endtrans %}</a>
<a class="link" href="{{ url('eboutic:main') }}">{% trans %}Eboutic{% endtrans %}</a>
<details name="navbar" class="menu">
<summary class="head">{% trans %}Services{% endtrans %}</summary>
<ul class="content">
<li><a href="{{ url('matmat:search_clear') }}">{% trans %}Matmatronch{% endtrans %}</a></li>
<li><a href="{{ url('core:file_list') }}">{% trans %}Files{% endtrans %}</a></li>
<li><a href="{{ url('pedagogy:guide') }}">{% trans %}Pedagogy{% endtrans %}</a></li>
</ul>
</details>
<details name="navbar" class="menu">
<summary class="head">{% trans %}My Benefits{% endtrans %}</summary>
<ul class="content">

View File

@@ -5,16 +5,12 @@
{% endblock %}
{% block content %}
{% if page_list %}
<h3>{% trans %}Page list{% endtrans %}</h3>
<ul>
{% for p in page_list %}
<li><a href="{{ p.get_absolute_url() }}">{{ p.get_display_name() }}</a></li>
{% endfor %}
</ul>
{% else %}
{% trans %}There is no page in this website.{% endtrans %}
{% endif %}
<h3>{% trans %}Page list{% endtrans %}</h3>
<ul>
{% for p in page_list %}
<li><a href="{{ p.get_absolute_url() }}">{{ p.display_name }}</a></li>
{% endfor %}
</ul>
{% endblock %}

View File

@@ -12,7 +12,10 @@
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import F, OuterRef, Subquery
from django.db.models.functions import Coalesce
# This file contains all the views that concern the page model
from django.forms.models import modelform_factory
@@ -43,6 +46,20 @@ class CanEditPagePropMixin(CanEditPropMixin):
class PageListView(CanViewMixin, ListView):
model = Page
template_name = "core/page_list.jinja"
queryset = (
Page.objects.annotate(
display_name=Coalesce(
Subquery(
PageRev.objects.filter(page=OuterRef("id"))
.order_by("-date")
.values("title")[:1]
),
F("name"),
)
)
.prefetch_related("view_groups")
.select_related("parent")
)
class PageView(CanViewMixin, DetailView):

View File

@@ -5,7 +5,6 @@ from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from phonenumber_field.widgets import RegionalPhoneNumberWidget
from club.models import Club
from club.widgets.ajax_select import AutoCompleteSelectClub
from core.models import User
from core.views.forms import NFCTextInput, SelectDate, SelectDateTime
@@ -20,7 +19,6 @@ from counter.models import (
Counter,
Customer,
Eticket,
InvoiceCall,
Product,
Refilling,
ReturnableProduct,
@@ -375,39 +373,3 @@ class BaseBasketForm(forms.BaseFormSet):
BasketForm = forms.formset_factory(
ProductForm, formset=BaseBasketForm, absolute_max=None, min_num=1
)
class InvoiceCallForm(forms.Form):
def __init__(self, *args, month, clubs: list[Club] | None = None, **kwargs):
super().__init__(*args, **kwargs)
self.month = month
self.clubs = clubs
if self.clubs is None:
self.clubs = []
invoices = {
i["club_id"]: i["is_validated"]
for i in InvoiceCall.objects.filter(
club__in=self.clubs, month=self.month
).values("club_id", "is_validated")
}
for club in self.clubs:
is_validated = invoices.get(club.id, False)
self.fields[f"club_{club.id}"] = forms.BooleanField(
required=False, initial=is_validated
)
def save(self):
for club in self.clubs:
field_name = f"club_{club.id}"
is_validated = self.cleaned_data.get(field_name, False)
InvoiceCall.objects.update_or_create(
month=self.month, club=club, defaults={"is_validated": is_validated}
)
def get_club_name(self, club_id):
return f"club_{club_id}"

View File

@@ -1,45 +0,0 @@
# Generated by Django 5.2.3 on 2025-09-09 10:24
import django.db.models.deletion
from django.db import migrations, models
import counter.models
class Migration(migrations.Migration):
dependencies = [
("club", "0014_alter_club_options_rename_unix_name_club_slug_name_and_more"),
("counter", "0031_alter_counter_options"),
]
operations = [
migrations.CreateModel(
name="InvoiceCall",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"is_validated",
models.BooleanField(default=False, verbose_name="is validated"),
),
("month", counter.models.MonthField(verbose_name="invoice date")),
(
"club",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="club.club"
),
),
],
options={
"verbose_name": "Invoice call",
"verbose_name_plural": "Invoice calls",
},
),
]

View File

@@ -1362,45 +1362,3 @@ class ReturnableProductBalance(models.Model):
f"return balance of {self.customer} "
f"for {self.returnable.product_id} : {self.balance}"
)
class MonthField(models.DateField):
description = _("Year + month field (day forced to 1)")
default_error_messages = {
"invalid": _(
"%(value)s” value has an invalid date format. It must be "
"in YYYY-MM format."
),
"invalid_date": _(
"%(value)s” value has the correct format (YYYY-MM) "
"but it is an invalid date."
),
}
def to_python(self, value):
if isinstance(value, date):
return value.replace(day=1)
if isinstance(value, str):
try:
year, month = map(int, value.split("-"))
return date(year, month, 1)
except (ValueError, TypeError) as err:
raise ValueError(
self.error_messages["invalid"] % {"value": value}
) from err
return super().to_python(value)
class InvoiceCall(models.Model):
is_validated = models.BooleanField(verbose_name=_("is validated"), default=False)
club = models.ForeignKey(Club, on_delete=models.CASCADE)
month = MonthField(verbose_name=_("invoice date"))
class Meta:
verbose_name = _("Invoice call")
verbose_name_plural = _("Invoice calls")
def __str__(self):
return f"invoice call of {self.month} made by {self.club}"

View File

@@ -15,32 +15,24 @@
</select>
<input type="submit" value="{% trans %}Go{% endtrans %}" />
</form>
<br>
<p>{% trans %}CB Payments{% endtrans %} : {{ sum_cb }} €</p>
<br>
<table>
<thead>
<td>{% trans %}Club{% endtrans %}</td>
<td>{% trans %}Sum{% endtrans %}</td>
</thead>
<tbody>
{% for i in sums %}
<tr>
<td>{{ i['club__name'] }}</td>
<td>{{ i['selling_sum'] }} €</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
<form method="post" action="">
{% csrf_token %}
<br>
<p>{% trans %}CB Payments{% endtrans %} : {{ sum_cb }} €</p>
<br>
<table>
<thead>
<td>{% trans %}Club{% endtrans %}</td>
<td>{% trans %}Sum{% endtrans %}</td>
<td>{% trans %}Validated{% endtrans %}</td>
</thead>
<tbody>
{% for data in club_data %}
<tr>
<td>{{ data.club.name }}</td>
<td>{{"%.2f"|format(data.sum)}} €</td>
<td>
{{ form[form.get_club_name(data.club.id)] }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<input type="hidden" name="month" value="{{ start_date|date('Y-m') }}">
<button type="submit">{% trans %}Save validation{% endtrans %}</button>
</form>
{% endblock %}

View File

@@ -12,17 +12,15 @@
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
from datetime import date, datetime, timedelta
from datetime import datetime, timedelta
from datetime import timezone as tz
from django.db.models import Exists, F, OuterRef
from django.shortcuts import redirect
from django.db.models import F
from django.utils import timezone
from django.views.generic import TemplateView
from counter.fields import CurrencyField
from counter.forms import InvoiceCallForm
from counter.models import Club, InvoiceCall, Refilling, Selling
from counter.models import Refilling, Selling
from counter.views.mixins import CounterAdminMixin, CounterAdminTabsMixin
@@ -30,30 +28,12 @@ class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView):
template_name = "counter/invoices_call.jinja"
current_tab = "invoices_call"
def get(self, request, *args, **kwargs):
month_str = request.GET.get("month")
if month_str:
try:
start_date = datetime.strptime(month_str, "%Y-%m").date()
today = timezone.now().date().replace(day=1)
if start_date > today:
return redirect("counter:invoices_call")
except ValueError:
return redirect("counter:invoices_call")
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
"""Add sums to the context."""
kwargs = super().get_context_data(**kwargs)
kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC")
month_str = self.request.GET.get("month")
if month_str:
try:
start_date = datetime.strptime(self.request.GET["month"], "%Y-%m")
except ValueError:
return redirect("counter:invoices_call")
if "month" in self.request.GET:
start_date = datetime.strptime(self.request.GET["month"], "%Y-%m")
else:
start_date = datetime(
year=timezone.now().year,
@@ -66,23 +46,30 @@ class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView):
)
from django.db.models import Case, Sum, When
kwargs["sum_cb"] = Refilling.objects.filter(
payment_method="CARD",
is_validated=True,
date__gte=start_date,
date__lte=end_date,
).aggregate(amount=Sum(F("amount"), default=0))["amount"]
kwargs["sum_cb"] += Selling.objects.filter(
payment_method="CARD",
is_validated=True,
date__gte=start_date,
date__lte=end_date,
).aggregate(amount=Sum(F("quantity") * F("unit_price"), default=0))["amount"]
kwargs["sum_cb"] = sum(
[
r.amount
for r in Refilling.objects.filter(
payment_method="CARD",
is_validated=True,
date__gte=start_date,
date__lte=end_date,
)
]
)
kwargs["sum_cb"] += sum(
[
s.quantity * s.unit_price
for s in Selling.objects.filter(
payment_method="CARD",
is_validated=True,
date__gte=start_date,
date__lte=end_date,
)
]
)
kwargs["start_date"] = start_date
kwargs["sums"] = list(
kwargs["sums"] = (
Selling.objects.values("club__name")
.annotate(
selling_sum=Sum(
@@ -99,56 +86,4 @@ class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView):
.exclude(selling_sum=None)
.order_by("-selling_sum")
)
club_names = [i["club__name"] for i in kwargs["sums"]]
clubs = Club.objects.filter(name__in=club_names)
invoice_calls = InvoiceCall.objects.filter(month=month_str, club__in=clubs)
invoice_statuses = {ic.club.name: ic.is_validated for ic in invoice_calls}
kwargs["form"] = InvoiceCallForm(clubs=clubs, month=month_str)
kwargs["club_data"] = []
for club in clubs:
selling_sum = next(
(
item["selling_sum"]
for item in kwargs["sums"]
if item["club__name"] == club.name
),
0,
)
kwargs["club_data"].append(
{
"club": club,
"sum": selling_sum,
"validated": invoice_statuses.get(club.name, False),
}
)
return kwargs
def post(self, request, *args, **kwargs):
month_str = request.POST.get("month")
if not month_str:
return self.get(request, *args, **kwargs)
try:
start_date = datetime.strptime(month_str, "%Y-%m")
start_date = date(start_date.year, start_date.month, 1)
except ValueError:
return redirect(request.path)
selling_subquery = Selling.objects.filter(
club=OuterRef("pk"),
date__year=start_date.year,
date__month=start_date.month,
)
clubs = Club.objects.filter(Exists(selling_subquery))
form = InvoiceCallForm(request.POST, clubs=clubs, month=month_str)
if form.is_valid():
form.save()
return redirect(f"{request.path}?month={request.POST.get('month', '')}")