Compare commits

..

1 Commits

Author SHA1 Message Date
imperosol
a45f0e02b0 include only subscribed users and their direct relations in the galaxy 2025-10-11 13:16:19 +02:00
16 changed files with 90 additions and 529 deletions

View File

@@ -31,7 +31,11 @@ from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import NON_FIELD_ERRORS, PermissionDenied, ValidationError
from django.core.paginator import InvalidPage, Paginator
from django.db.models import Q, Sum
from django.http import Http404, HttpResponseRedirect, StreamingHttpResponse
from django.http import (
Http404,
HttpResponseRedirect,
StreamingHttpResponse,
)
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse, reverse_lazy
from django.utils import timezone
@@ -51,7 +55,12 @@ from club.forms import (
MailingForm,
SellingsForm,
)
from club.models import Club, Mailing, MailingSubscription, Membership
from club.models import (
Club,
Mailing,
MailingSubscription,
Membership,
)
from com.models import Poster
from com.views import (
PosterCreateBaseView,
@@ -59,7 +68,9 @@ from com.views import (
PosterEditBaseView,
PosterListBaseView,
)
from core.auth.mixins import CanEditMixin
from core.auth.mixins import (
CanEditMixin,
)
from core.models import PageRev
from core.views import DetailFormView, PageEditViewBase, UseFragmentsMixin
from core.views.mixins import FragmentMixin, FragmentRenderer, TabedViewMixin

View File

@@ -154,8 +154,10 @@ form {
margin-bottom: 1rem;
}
.row > label {
margin: unset;
.row {
label {
margin: unset;
}
}
// ------------- LABEL

View File

@@ -115,7 +115,7 @@ class SelectUser(TextInput):
def validate_future_timestamp(value: date | datetime):
if value <= now():
raise ValidationError(_("Ensure this timestamp is set in the future"))
raise ValueError(_("Ensure this timestamp is set in the future"))
class FutureDateTimeField(forms.DateTimeField):

View File

@@ -1,23 +1,13 @@
import json
import math
import uuid
from django import forms
from django.db.models import Q
from django.forms import BaseModelFormSet
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_celery_beat.models import ClockedSchedule
from phonenumber_field.widgets import RegionalPhoneNumberWidget
from club.widgets.ajax_select import AutoCompleteSelectClub
from core.models import User
from core.views.forms import (
FutureDateTimeField,
NFCTextInput,
SelectDate,
SelectDateTime,
)
from core.views.forms import NFCTextInput, SelectDate, SelectDateTime
from core.views.widgets.ajax_select import (
AutoCompleteSelect,
AutoCompleteSelectMultipleGroup,
@@ -32,9 +22,7 @@ from counter.models import (
Product,
Refilling,
ReturnableProduct,
ScheduledProductAction,
StudentCard,
get_product_actions,
)
from counter.widgets.ajax_select import (
AutoCompleteSelectMultipleCounter,
@@ -170,101 +158,7 @@ class CounterEditForm(forms.ModelForm):
}
class ScheduledProductActionForm(forms.ModelForm):
"""Form for automatic product archiving.
The `save` method will update or create tasks using celery-beat.
"""
required_css_class = "required"
prefix = "scheduled"
class Meta:
model = ScheduledProductAction
fields = ["task"]
widgets = {"task": forms.RadioSelect(choices=get_product_actions)}
labels = {"task": _("Action")}
help_texts = {"task": ""}
trigger_at = FutureDateTimeField(
label=_("Date and time of action"), widget=SelectDateTime
)
counters = forms.ModelMultipleChoiceField(
label=_("New counters"),
help_text=_("The selected counters will replace the current ones"),
required=False,
widget=AutoCompleteSelectMultipleCounter,
queryset=Counter.objects.all(),
)
def __init__(self, *args, product: Product, **kwargs):
self.product = product
super().__init__(*args, **kwargs)
if not self.instance._state.adding:
self.fields["trigger_at"].initial = self.instance.clocked.clocked_time
self.fields["counters"].initial = json.loads(self.instance.kwargs).get(
"counters"
)
def clean(self):
if not self.changed_data or "trigger_at" in self.errors:
return super().clean()
if "trigger_at" in self.changed_data:
if not self.instance.clocked_id:
self.instance.clocked = ClockedSchedule(
clocked_time=self.cleaned_data["trigger_at"]
)
else:
self.instance.clocked.clocked_time = self.cleaned_data["trigger_at"]
self.instance.clocked.save()
task_kwargs = {"product_id": self.product.id}
if (
self.cleaned_data["task"] == "counter.tasks.change_counters"
and "counters" in self.changed_data
):
task_kwargs["counters"] = [c.id for c in self.cleaned_data["counters"]]
self.instance.product = self.product
self.instance.kwargs = json.dumps(task_kwargs)
self.instance.name = (
f"{self.cleaned_data['task']} - {self.product} - {uuid.uuid4()}"
)
return super().clean()
class BaseScheduledProductActionFormSet(BaseModelFormSet):
def __init__(self, *args, product: Product, **kwargs):
if product.id:
queryset = (
product.scheduled_actions.filter(
enabled=True, clocked__clocked_time__gt=now()
)
.order_by("clocked__clocked_time")
.select_related("clocked")
)
else:
queryset = ScheduledProductAction.objects.none()
form_kwargs = {"product": product}
super().__init__(*args, queryset=queryset, form_kwargs=form_kwargs, **kwargs)
def delete_existing(self, obj: ScheduledProductAction, commit: bool = True): # noqa FBT001
clocked = obj.clocked
super().delete_existing(obj, commit=commit)
if commit:
clocked.delete()
ScheduledProductActionFormSet = forms.modelformset_factory(
ScheduledProductAction,
ScheduledProductActionForm,
formset=BaseScheduledProductActionFormSet,
absolute_max=None,
can_delete=True,
can_delete_extra=False,
extra=2,
)
class ProductForm(forms.ModelForm):
class ProductEditForm(forms.ModelForm):
error_css_class = "error"
required_css_class = "required"
@@ -305,21 +199,22 @@ class ProductForm(forms.ModelForm):
queryset=Counter.objects.all(),
)
def __init__(self, *args, instance=None, **kwargs):
super().__init__(*args, instance=instance, **kwargs)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance.id:
self.fields["counters"].initial = self.instance.counters.all()
self.action_formset = ScheduledProductActionFormSet(
*args, product=self.instance, **kwargs
)
def is_valid(self):
return super().is_valid() and self.action_formset.is_valid()
def save(self, *args, **kwargs):
ret = super().save(*args, **kwargs)
self.instance.counters.set(self.cleaned_data["counters"])
self.action_formset.save()
if self.fields["counters"].initial:
# Remove the product from all counter it was added to
# It will then only be added to selected counters
for counter in self.fields["counters"].initial:
counter.products.remove(self.instance)
counter.save()
for counter in self.cleaned_data["counters"]:
counter.products.add(self.instance)
counter.save()
return ret
@@ -371,7 +266,7 @@ class CloseCustomerAccountForm(forms.Form):
)
class BasketProductForm(forms.Form):
class ProductForm(forms.Form):
quantity = forms.IntegerField(min_value=1, required=True)
id = forms.IntegerField(min_value=0, required=True)
@@ -476,5 +371,5 @@ class BaseBasketForm(forms.BaseFormSet):
BasketForm = forms.formset_factory(
BasketProductForm, formset=BaseBasketForm, absolute_max=None, min_num=1
ProductForm, formset=BaseBasketForm, absolute_max=None, min_num=1
)

View File

@@ -1,40 +0,0 @@
# Generated by Django 5.2.3 on 2025-09-14 11:29
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("counter", "0031_alter_counter_options"),
("django_celery_beat", "0019_alter_periodictasks_options"),
]
operations = [
migrations.CreateModel(
name="ScheduledProductAction",
fields=[
(
"periodictask_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="django_celery_beat.periodictask",
),
),
(
"product",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="scheduled_actions",
to="counter.product",
),
),
],
options={"verbose_name": "Product scheduled action"},
bases=("django_celery_beat.periodictask",),
),
]

View File

@@ -34,7 +34,6 @@ from django.urls import reverse
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from django_celery_beat.models import PeriodicTask
from django_countries.fields import CountryField
from ordered_model.models import OrderedModel
from phonenumber_field.modelfields import PhoneNumberField
@@ -446,8 +445,7 @@ class Product(models.Model):
buying_groups = list(self.buying_groups.all())
if not buying_groups:
return True
res = any(user.is_in_group(pk=group.id) for group in buying_groups)
return res
return any(user.is_in_group(pk=group.id) for group in buying_groups)
@property
def profit(self):
@@ -481,7 +479,7 @@ class CounterQuerySet(models.QuerySet):
return self.annotate(has_annotated_barman=Exists(subquery))
def annotate_is_open(self) -> Self:
"""Annotate the queryset with the `is_open` field.
"""Annotate tue queryset with the `is_open` field.
For each counter, if `is_open=True`, then the counter is currently opened.
Else the counter is closed.
@@ -1359,39 +1357,3 @@ class ReturnableProductBalance(models.Model):
f"return balance of {self.customer} "
f"for {self.returnable.product_id} : {self.balance}"
)
def get_product_actions():
return [
("counter.tasks.archive_product", _("Archiving")),
("counter.tasks.change_counters", _("Counters change")),
]
class ScheduledProductAction(PeriodicTask):
"""Extension of celery-beat tasks dedicated to perform actions on Product."""
product = models.ForeignKey(
Product, related_name="scheduled_actions", on_delete=models.CASCADE
)
class Meta:
verbose_name = _("Product scheduled action")
def __init__(self, *args, **kwargs):
self._meta.get_field("task").choices = get_product_actions()
super().__init__(*args, **kwargs)
def full_clean(self, *args, **kwargs):
self.one_off = True # A product action should occur one time only
return super().full_clean(*args, **kwargs)
def clean_clocked(self):
if not self.clocked:
raise ValidationError(_("Product actions must declare a clocked schedule."))
def validate_unique(self, *args, **kwargs):
# The checks done in PeriodicTask.validate_unique aren't
# adapted in the case of scheduled product action,
# so we skip it and execute directly Model.validate_unique
return super(PeriodicTask, self).validate_unique(*args, **kwargs)

View File

@@ -1,19 +0,0 @@
# Create your tasks here
from celery import shared_task
from counter.models import Counter, Product
@shared_task
def archive_product(*, product_id: int, **kwargs):
product = Product.objects.get(id=product_id)
product.archived = True
product.save()
@shared_task
def change_counters(*, product_id: int, counters: list[int], **kwargs):
product = Product.objects.get(id=product_id)
counters = Counter.objects.filter(id__in=counters)
product.counters.set(counters)

View File

@@ -1,56 +0,0 @@
{% extends "core/base.jinja" %}
{% block content %}
{% if object %}
<h2>{% trans name=object %}Edit product {{ name }}{% endtrans %}</h2>
{% else %}
<h2>{% trans %}Product creation{% endtrans %}</h2>
{% endif %}
<form method="post">
{% csrf_token %}
{{ form.as_p() }}
<br />
<h3>{% trans %}Automatic actions{% endtrans %}</h3>
<p class="margin-bottom">
<em>
{%- trans trimmed -%}
Automatic actions allows to schedule product changes
ahead of time.
{%- endtrans -%}
</em>
</p>
{{ form.action_formset.management_form }}
{%- for action_form in form.action_formset.forms -%}
<fieldset x-data="{action: '{{ action_form.task.initial }}'}">
{{ action_form.non_field_errors() }}
<div class="row gap-2x margin-bottom">
<div>
{{ action_form.task.errors }}
{{ action_form.task.label_tag() }}
{{ action_form.task|add_attr("x-model=action") }}
</div>
<div>{{ action_form.trigger_at.as_field_group() }}</div>
</div>
<div x-show="action==='counter.tasks.change_counters'" class="margin-bottom">
{{ action_form.counters.as_field_group() }}
</div>
{%- if action_form.DELETE -%}
<div class="row gap">
{{ action_form.DELETE.as_field_group() }}
</div>
{%- endif -%}
{%- for field in action_form.hidden_fields() -%}
{{ field }}
{%- endfor -%}
</fieldset>
{%- if not loop.last -%}
<hr class="margin-bottom">
{%- endif -%}
{%- endfor -%}
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p>
</form>
{% endblock %}

View File

@@ -1,116 +0,0 @@
import json
from datetime import timedelta
import pytest
from django.conf import settings
from django.test import Client
from django.urls import reverse
from django.utils.timezone import now
from django_celery_beat.models import ClockedSchedule
from model_bakery import baker
from core.models import Group, User
from counter.baker_recipes import counter_recipe, product_recipe
from counter.forms import ScheduledProductActionForm, ScheduledProductActionFormSet
from counter.models import ScheduledProductAction
@pytest.mark.django_db
def test_edit_product(client: Client):
client.force_login(
baker.make(
User, groups=[Group.objects.get(id=settings.SITH_GROUP_COUNTER_ADMIN_ID)]
)
)
product = product_recipe.make()
url = reverse("counter:product_edit", kwargs={"product_id": product.id})
res = client.get(url)
assert res.status_code == 200
res = client.post(url, data={})
# This is actually a failure, but we just want to check that
# we don't have a 403 or a 500.
# The actual behaviour will be tested directly on the form.
assert res.status_code == 200
@pytest.mark.django_db
class TestProductActionForm:
def test_single_form_archive(self):
product = product_recipe.make()
trigger_at = now() + timedelta(minutes=10)
form = ScheduledProductActionForm(
product=product,
data={
"scheduled-task": "counter.tasks.archive_product",
"scheduled-trigger_at": trigger_at,
},
)
assert form.is_valid()
instance = form.save()
assert instance.clocked.clocked_time == trigger_at
assert instance.enabled is True
assert instance.one_off is True
assert instance.task == "counter.tasks.archive_product"
assert instance.kwargs == json.dumps({"product_id": product.id})
def test_single_form_change_counters(self):
product = product_recipe.make()
counter = counter_recipe.make()
trigger_at = now() + timedelta(minutes=10)
form = ScheduledProductActionForm(
product=product,
data={
"scheduled-task": "counter.tasks.change_counters",
"scheduled-trigger_at": trigger_at,
"scheduled-counters": [counter.id],
},
)
assert form.is_valid()
instance = form.save()
instance.refresh_from_db()
assert instance.clocked.clocked_time == trigger_at
assert instance.enabled is True
assert instance.one_off is True
assert instance.task == "counter.tasks.change_counters"
assert instance.kwargs == json.dumps(
{"product_id": product.id, "counters": [counter.id]}
)
def test_delete(self):
product = product_recipe.make()
clocked = baker.make(ClockedSchedule, clocked_time=now() + timedelta(minutes=2))
task = baker.make(
ScheduledProductAction,
product=product,
one_off=True,
clocked=clocked,
task="counter.tasks.archive_product",
)
formset = ScheduledProductActionFormSet(product=product)
formset.delete_existing(task)
assert not ScheduledProductAction.objects.filter(id=task.id).exists()
assert not ClockedSchedule.objects.filter(id=clocked.id).exists()
@pytest.mark.django_db
class TestProductActionFormSet:
def test_ok(self):
product = product_recipe.make()
counter = counter_recipe.make()
trigger_at = now() + timedelta(minutes=10)
formset = ScheduledProductActionFormSet(
product=product,
data={
"form-TOTAL_FORMS": "2",
"form-INITIAL_FORMS": "0",
"form-0-task": "counter.tasks.archive_product",
"form-0-trigger_at": trigger_at,
"form-1-task": "counter.tasks.change_counters",
"form-1-trigger_at": trigger_at,
"form-1-counters": [counter.id],
},
)
assert formset.is_valid()
formset.save()
assert ScheduledProductAction.objects.filter(product=product).count() == 2

View File

@@ -6,16 +6,14 @@ import pytest
from django.conf import settings
from django.core.cache import cache
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import Client, TestCase
from django.test import Client
from django.urls import reverse
from model_bakery import baker
from PIL import Image
from pytest_django.asserts import assertNumQueries, assertRedirects
from pytest_django.asserts import assertNumQueries
from club.models import Club
from core.baker_recipes import board_user, subscriber_user
from core.models import Group, User
from counter.forms import ProductForm
from counter.models import Product, ProductType
@@ -86,49 +84,3 @@ def test_fetch_product_nb_queries(client: Client):
# - 1 for the actual request
# - 1 to prefetch the related buying_groups
client.get(reverse("api:search_products_detailed"))
class TestCreateProduct(TestCase):
@classmethod
def setUpTestData(cls):
cls.product_type = baker.make(ProductType)
cls.club = baker.make(Club)
cls.data = {
"name": "foo",
"description": "bar",
"product_type": cls.product_type.id,
"club": cls.club.id,
"code": "FOO",
"purchase_price": 1.0,
"selling_price": 1.0,
"special_selling_price": 1.0,
"limit_age": 0,
"form-TOTAL_FORMS": 0,
"form-INITIAL_FORMS": 0,
}
def test_form(self):
form = ProductForm(data=self.data)
assert form.is_valid()
instance = form.save()
assert instance.club == self.club
assert instance.product_type == self.product_type
assert instance.name == "foo"
assert instance.selling_price == 1.0
def test_view(self):
self.client.force_login(
baker.make(
User,
groups=[Group.objects.get(id=settings.SITH_GROUP_COUNTER_ADMIN_ID)],
)
)
url = reverse("counter:new_product")
response = self.client.get(url)
assert response.status_code == 200
response = self.client.post(url, data=self.data)
assertRedirects(response, reverse("counter:product_list"))
product = Product.objects.last()
assert product.name == "foo"
assert product.club == self.club
assert product.product_type == self.product_type

View File

@@ -32,7 +32,7 @@ from core.utils import get_semester_code, get_start_of_semester
from counter.forms import (
CloseCustomerAccountForm,
CounterEditForm,
ProductForm,
ProductEditForm,
ReturnableProductForm,
)
from counter.models import (
@@ -146,8 +146,8 @@ class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
"""A create view for the admins."""
model = Product
form_class = ProductForm
template_name = "counter/product_form.jinja"
form_class = ProductEditForm
template_name = "core/create.jinja"
current_tab = "products"
@@ -155,9 +155,9 @@ class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
"""An edit view for the admins."""
model = Product
form_class = ProductForm
form_class = ProductEditForm
pk_url_kwarg = "product_id"
template_name = "counter/product_form.jinja"
template_name = "core/edit.jinja"
current_tab = "products"

View File

@@ -48,7 +48,7 @@ from django_countries.fields import Country
from core.auth.mixins import CanViewMixin
from core.views.mixins import FragmentMixin, UseFragmentsMixin
from counter.forms import BaseBasketForm, BasketProductForm, BillingInfoForm
from counter.forms import BaseBasketForm, BillingInfoForm, ProductForm
from counter.models import (
BillingInfo,
Customer,
@@ -78,7 +78,7 @@ class BaseEbouticBasketForm(BaseBasketForm):
EbouticBasketForm = forms.formset_factory(
BasketProductForm, formset=BaseEbouticBasketForm, absolute_max=None, min_num=1
ProductForm, formset=BaseEbouticBasketForm, absolute_max=None, min_num=1
)

View File

@@ -45,8 +45,9 @@ class Command(BaseCommand):
"verbosity level should be between 0 and 2 included", stacklevel=2
)
if options["verbosity"] == 2:
if options["verbosity"] >= 2:
logger.setLevel(logging.DEBUG)
logging.getLogger("django.db.backends").setLevel(logging.DEBUG)
elif options["verbosity"] == 1:
logger.setLevel(logging.INFO)
else:
@@ -59,6 +60,3 @@ class Command(BaseCommand):
Galaxy.objects.filter(state__isnull=True).delete()
logger.info("Ruled the galaxy in {} queries.".format(len(connection.queries)))
if options["verbosity"] > 2:
for q in connection.queries:
logger.debug(q)

View File

@@ -31,13 +31,14 @@ from collections import defaultdict
from typing import NamedTuple, TypedDict
from django.db import models
from django.db.models import Count, F, Q, QuerySet
from django.utils.timezone import localdate
from django.db.models import Count, Exists, F, OuterRef, Q, QuerySet
from django.utils.timezone import localdate, now
from django.utils.translation import gettext_lazy as _
from club.models import Membership
from core.models import User
from sas.models import PeoplePictureRelation, Picture
from subscription.models import Subscription
class GalaxyStar(models.Model):
@@ -198,8 +199,16 @@ class Galaxy(models.Model):
cls, picture_count_threshold: int = DEFAULT_PICTURE_COUNT_THRESHOLD
) -> QuerySet[User]:
return (
User.objects.exclude(subscriptions=None)
.annotate(pictures_count=Count("pictures"))
User.objects.filter(is_subscriber_viewable=True)
.exclude(subscriptions=None)
.annotate(
pictures_count=Count("pictures"),
is_active_in_galaxy=Exists(
Subscription.objects.filter(
member=OuterRef("id"), subscription_end__gt=now()
)
),
)
.filter(pictures_count__gt=picture_count_threshold)
.distinct()
)
@@ -290,9 +299,9 @@ class Galaxy(models.Model):
31/12/2022 (also two years, but with an offset of one year), then their
club score is 365.
"""
memberships = user.memberships.only("start_date", "end_date", "club_id")
memberships = user.memberships.values("start_date", "end_date", "club_id")
result = defaultdict(int)
now = localdate()
today = localdate()
for membership in memberships:
# This is a N+1 query, but 92% of galaxy users have less than 10 memberships.
# Only 5 users have more than 30 memberships.
@@ -300,23 +309,23 @@ class Galaxy(models.Model):
Membership.objects.exclude(user=user)
.filter(
Q( # start2 <= start1 <= end2
start_date__lte=membership.start_date,
end_date__gte=membership.start_date,
start_date__lte=membership["start_date"],
end_date__gte=membership["start_date"],
)
| Q( # start2 <= start1 <= now
start_date__lte=membership.start_date, end_date=None
| Q( # start2 <= start1 <= today
start_date__lte=membership["start_date"], end_date=None
)
| Q( # start1 <= start2 <= end2
start_date__gte=membership.start_date,
start_date__lte=membership.end_date or now,
start_date__gte=membership["start_date"],
start_date__lte=membership["end_date"] or today,
),
club_id=membership.club_id,
club_id=membership["club_id"],
)
.only("start_date", "end_date", "user_id")
)
for other in common_memberships:
start = max(membership.start_date, other.start_date)
end = min(membership.end_date or now, other.end_date or now)
start = max(membership["start_date"], other.start_date)
end = min(membership["end_date"] or today, other.end_date or today)
result[other.user_id] += (end - start).days * cls.CLUBS_POINTS
return result
@@ -382,18 +391,22 @@ class Galaxy(models.Model):
# this is memory expensive but prevents a lot of db hits, therefore
# is far more time efficient
rulable_users = list(self.get_rulable_users(picture_count_threshold))
rulable_users_count = len(rulable_users)
rulable_users_qs = self.get_rulable_users(picture_count_threshold)
active_users_count = rulable_users_qs.filter(is_active_in_galaxy=True).count()
rulable_users = list(rulable_users_qs)
user1_count = 0
self.logger.info(
f"{rulable_users_count} citizen have been listed. Starting to rule."
f" {len(rulable_users)} citizens (with {active_users_count} active ones) "
f"have been listed. Starting to rule."
)
self.logger.info("Creating stars for all citizen")
individual_scores = self.compute_individual_scores()
GalaxyStar.objects.bulk_create(
[
GalaxyStar(owner=user, galaxy=self, mass=individual_scores[user.id])
GalaxyStar(
owner_id=user.id, galaxy=self, mass=individual_scores[user.id]
)
for user in rulable_users
]
)
@@ -405,9 +418,9 @@ class Galaxy(models.Model):
t_global_start = time.time()
while len(rulable_users) > 0:
user1 = rulable_users.pop()
if not user1.is_active_in_galaxy:
continue
user1_count += 1
rulable_users_count2 = len(rulable_users)
star1 = stars[user1.id]
lanes = []
@@ -448,17 +461,20 @@ class Galaxy(models.Model):
self.logger.info("")
self.logger.info(f" Ruling of {self} ".center(60, "#"))
self.logger.info(
f"Progression: {user1_count}/{rulable_users_count} "
f"citizen -- {rulable_users_count - user1_count} remaining"
f"Progression: {user1_count}/{active_users_count} "
f"citizen -- {active_users_count - user1_count} remaining"
)
self.logger.info(f"Speed: {global_avg_speed:.2f} citizen per second")
eta = rulable_users_count2 // global_avg_speed
eta = len(rulable_users) // global_avg_speed
self.logger.info(
f"ETA: {int(eta // 60 % 60)} minutes {int(eta % 60)} seconds"
)
self.logger.info("#" * 60)
t_global_start = time.time()
count, _ = self.stars.filter(Q(lanes1=None) & Q(lanes2=None)).delete()
self.logger.info(f"{count} orphan stars have been trimmed.")
# Here, we get the IDs of the old galaxies that we'll need to delete. In normal operation, only one galaxy
# should be returned, and we can't delete it yet, as it's the one still displayed by the Sith.
old_galaxies_pks = list(

View File

@@ -122,7 +122,7 @@ class TestGalaxyModel(TestCase):
self.com,
]
with self.assertNumQueries(44):
with self.assertNumQueries(38):
while len(users) > 0:
user1 = users.pop(0)
family_scores = Galaxy.compute_user_family_score(user1)
@@ -150,7 +150,7 @@ class TestGalaxyModel(TestCase):
that the number of queries to rule the galaxy is stable.
"""
galaxy = Galaxy.objects.create()
with self.assertNumQueries(39):
with self.assertNumQueries(36):
galaxy.rule(0) # We want everybody here

View File

@@ -117,7 +117,7 @@ msgstr "S'abonner"
msgid "Remove"
msgstr "Retirer"
#: club/forms.py counter/forms.py pedagogy/templates/pedagogy/moderation.jinja
#: club/forms.py pedagogy/templates/pedagogy/moderation.jinja
msgid "Action"
msgstr "Action"
@@ -556,7 +556,6 @@ msgstr ""
#: core/templates/core/user_godfathers_tree.jinja
#: core/templates/core/user_preferences.jinja
#: counter/templates/counter/cash_register_summary.jinja
#: counter/templates/counter/product_form.jinja
#: forum/templates/forum/reply.jinja
#: subscription/templates/subscription/fragments/creation_form.jinja
#: trombi/templates/trombi/comment.jinja
@@ -2956,18 +2955,6 @@ msgstr "Cet UID est invalide"
msgid "User not found"
msgstr "Utilisateur non trouvé"
#: counter/forms.py
msgid "Date and time of action"
msgstr "Date et heure de l'action"
#: counter/forms.py
msgid "New counters"
msgstr "Nouveaux comptoirs"
#: counter/forms.py
msgid "The selected counters will replace the current ones"
msgstr "Les comptoirs sélectionnés remplaceront les comptoirs actuels"
#: counter/forms.py
msgid ""
"Describe the product. If it's an event's click, give some insights about it, "
@@ -3302,18 +3289,6 @@ msgid "The returnable product cannot be the same as the returned one"
msgstr ""
"Le produit consigné ne peut pas être le même que le produit de déconsigne"
#: counter/models.py
msgid "Archiving"
msgstr "Archivage"
#: counter/models.py
msgid "Counters change"
msgstr "Changement des comptoirs"
#: counter/models.py
msgid "Product scheduled action"
msgstr "Actions sur produit planifiées"
#: counter/templates/counter/activity.jinja
#, python-format
msgid "%(counter_name)s activity"
@@ -3632,25 +3607,6 @@ msgstr ""
"votre cotisation. Si vous ne renouvelez pas votre cotisation, il n'y aura "
"aucune conséquence autre que le retrait de l'argent de votre compte."
#: counter/templates/counter/product_form.jinja
#, python-format
msgid "Edit product %(name)s"
msgstr "Édition du produit %(name)s"
#: counter/templates/counter/product_form.jinja
msgid "Product creation"
msgstr "Création de produit"
#: counter/templates/counter/product_form.jinja
msgid "Automatic actions"
msgstr "Actions automatiques"
#: counter/templates/counter/product_form.jinja
msgid "Automatic actions allows to schedule product changes ahead of time."
msgstr ""
"Les actions automatiques vous permettent de planifier des modifications du "
"produit à l'avance."
#: counter/templates/counter/product_list.jinja
msgid "Product list"
msgstr "Liste des produits"