Merge branch 'taiste' into tinople/css-rewrite-taiste

This commit is contained in:
Julien Constant
2023-03-14 12:27:41 +01:00
committed by GitHub
15 changed files with 602 additions and 255 deletions

View File

@ -2,7 +2,9 @@
{% block content %}
<h3>{% trans %}404, Not Found{% endtrans %}</h3>
<div id="page">
<h3>{% trans %}404, Not Found{% endtrans %}</h3>
</div>
{% endblock %}

View File

@ -72,7 +72,9 @@ def forbidden(request, exception):
def not_found(request, exception):
return HttpResponseNotFound(render(request, "core/404.jinja"))
return HttpResponseNotFound(
render(request, "core/404.jinja", context={"exception": exception})
)
def internal_servor_error(request):

View File

@ -207,7 +207,7 @@ class UserTabsMixin(TabedViewMixin):
"name": _("Pictures"),
},
]
if self.request.user.was_subscribed:
if settings.SITH_ENABLE_GALAXY and self.request.user.was_subscribed:
tab_list.append(
{
"url": reverse("galaxy:user", kwargs={"user_id": user.id}),

View File

@ -22,6 +22,7 @@
#
#
from __future__ import annotations
from django.db.models import Sum, F
from typing import Tuple
@ -90,12 +91,9 @@ class Customer(models.Model):
about the relation between a User (not a Customer,
don't mix them) and a Product.
"""
return self.user.subscriptions.last() and (
date.today()
- self.user.subscriptions.order_by("subscription_end")
.last()
.subscription_end
) < timedelta(days=90)
subscription = self.user.subscriptions.order_by("subscription_end").last()
time_diff = date.today() - subscription.subscription_end
return subscription is not None and time_diff < timedelta(days=90)
@classmethod
def get_or_create(cls, user: User) -> Tuple[Customer, bool]:
@ -151,11 +149,15 @@ class Customer(models.Model):
super(Customer, self).save(*args, **kwargs)
def recompute_amount(self):
self.amount = 0
for r in self.refillings.all():
self.amount += r.amount
for s in self.buyings.filter(payment_method="SITH_ACCOUNT"):
self.amount -= s.quantity * s.unit_price
refillings = self.refillings.aggregate(sum=Sum(F("amount")))["sum"]
self.amount = refillings if refillings is not None else 0
purchases = (
self.buyings.filter(payment_method="SITH_ACCOUNT")
.annotate(amount=F("quantity") * F("unit_price"))
.aggregate(sum=Sum(F("amount")))
)["sum"]
if purchases is not None:
self.amount -= purchases
self.save()
def get_absolute_url(self):

View File

@ -45,7 +45,6 @@ $(function () {
const code_field = $("#code_field");
let quantity = "";
let search = "";
code_field.autocomplete({
select: function (event, ui) {
event.preventDefault();
@ -56,13 +55,13 @@ $(function () {
code_field.val(quantity + ui.item.value);
},
source: function (request, response) {
// by the dark magic of JS, parseInt("123abc") === 123
quantity = parseInt(request.term);
search = request.term.slice(quantity.toString().length)
let matcher = new RegExp($.ui.autocomplete.escapeRegex(search), "i");
response($.grep(products_autocomplete, function (value) {
const res = /^(\d+x)?(.*)/i.exec(request.term);
quantity = res[1] || "";
const search = res[2];
const matcher = new RegExp($.ui.autocomplete.escapeRegex(search), "i" );
response($.grep(products_autocomplete, function(value) {
value = value.tags;
return matcher.test(value);
return matcher.test( value );
}));
},
});

View File

@ -123,6 +123,19 @@
{% else %}
<p>{% trans %}There are no items available for sale{% endtrans %}</p>
{% endfor %}
<h3>{% trans %}Partnership Eurockéennes 2023{% endtrans %}</h3>
{% if user.is_subscribed %}
<a title="Logiciel billetterie en ligne"
href="https://widget.weezevent.com/ticket/a203b986-73b0-4e63-b18a-b7c8bad986b7?id_evenement=915745&locale=fr-FR&code=71348"
class="weezevent-widget-integration" target="_blank"
data-src="https://widget.weezevent.com/ticket/a203b986-73b0-4e63-b18a-b7c8bad986b7?id_evenement=915745&locale=fr-FR&code=71348"
data-width="650" data-height="600" data-resize="1" data-nopb="0" data-type="neo" data-width_auto="1"
data-noscroll="0" data-id="915745">Billetterie Weezevent</a>
<script type="text/javascript" src="https://widget.weezevent.com/weez.js" async defer></script>
{% else %}
<div>{% trans %}You must be a contributor to access the Eurockéennes ticketing service.{% endtrans %}</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -1,4 +1,4 @@
# Generated by Django 3.2.16 on 2023-02-03 10:31
# Generated by Django 3.2.16 on 2023-03-02 10:07
from django.conf import settings
from django.db import migrations, models
@ -51,7 +51,7 @@ class Migration(migrations.Migration):
on_delete=django.db.models.deletion.CASCADE,
related_name="galaxy_user",
to=settings.AUTH_USER_MODEL,
verbose_name="galaxy user",
verbose_name="star owner",
),
),
],
@ -96,7 +96,7 @@ class Migration(migrations.Migration):
on_delete=django.db.models.deletion.CASCADE,
related_name="lanes1",
to="galaxy.galaxystar",
verbose_name="galaxy lanes 1",
verbose_name="galaxy star 1",
),
),
(
@ -105,7 +105,7 @@ class Migration(migrations.Migration):
on_delete=django.db.models.deletion.CASCADE,
related_name="lanes2",
to="galaxy.galaxystar",
verbose_name="galaxy lanes 2",
verbose_name="galaxy star 2",
),
),
],

View File

@ -25,6 +25,7 @@
import math
import logging
from typing import Tuple
from django.db import models
from django.db.models import Q, Case, F, Value, When, Count
from django.db.models.functions import Concat
@ -47,7 +48,7 @@ class GalaxyStar(models.Model):
owner = models.OneToOneField(
User,
verbose_name=_("galaxy user"),
verbose_name=_("star owner"),
related_name="galaxy_user",
on_delete=models.CASCADE,
)
@ -69,13 +70,13 @@ class GalaxyLane(models.Model):
star1 = models.ForeignKey(
GalaxyStar,
verbose_name=_("galaxy lanes 1"),
verbose_name=_("galaxy star 1"),
related_name="lanes1",
on_delete=models.CASCADE,
)
star2 = models.ForeignKey(
GalaxyStar,
verbose_name=_("galaxy lanes 2"),
verbose_name=_("galaxy star 2"),
related_name="lanes2",
on_delete=models.CASCADE,
)
@ -120,7 +121,7 @@ class Galaxy(models.Model):
state = models.JSONField("current state")
@staticmethod
def make_state() -> GalaxyDict:
def make_state() -> None:
"""
Compute JSON structure to send to 3d-force-graph: https://github.com/vasturiano/3d-force-graph/
"""
@ -177,7 +178,7 @@ class Galaxy(models.Model):
###################
@classmethod
def compute_user_score(cls, user):
def compute_user_score(cls, user) -> int:
"""
This compute an individual score for each citizen. It will later be used by the graph algorithm to push
higher scores towards the center of the galaxy.
@ -202,7 +203,7 @@ class Galaxy(models.Model):
return user_score
@classmethod
def query_user_score(cls, user):
def query_user_score(cls, user) -> int:
score_query = (
User.objects.filter(id=user.id)
.annotate(
@ -229,7 +230,7 @@ class Galaxy(models.Model):
####################
@classmethod
def compute_users_score(cls, user1, user2):
def compute_users_score(cls, user1, user2) -> Tuple[int, int, int, int]:
family = cls.compute_users_family_score(user1, user2)
pictures = cls.compute_users_pictures_score(user1, user2)
clubs = cls.compute_users_clubs_score(user1, user2)
@ -237,7 +238,7 @@ class Galaxy(models.Model):
return score, family, pictures, clubs
@classmethod
def compute_users_family_score(cls, user1, user2):
def compute_users_family_score(cls, user1, user2) -> int:
link_count = User.objects.filter(
Q(id=user1.id, godfathers=user2) | Q(id=user2.id, godfathers=user1)
).count()
@ -248,7 +249,7 @@ class Galaxy(models.Model):
return link_count * cls.FAMILY_LINK_POINTS
@classmethod
def compute_users_pictures_score(cls, user1, user2):
def compute_users_pictures_score(cls, user1, user2) -> int:
picture_count = (
Picture.objects.filter(people__user__in=(user1,))
.filter(people__user__in=(user2,))
@ -261,7 +262,7 @@ class Galaxy(models.Model):
return picture_count * cls.PICTURE_POINTS
@classmethod
def compute_users_clubs_score(cls, user1, user2):
def compute_users_clubs_score(cls, user1, user2) -> int:
common_clubs = Club.objects.filter(members__in=user1.memberships.all()).filter(
members__in=user2.memberships.all()
)
@ -311,7 +312,7 @@ class Galaxy(models.Model):
###################
@classmethod
def rule(cls):
def rule(cls) -> None:
GalaxyStar.objects.all().delete()
# The following is a no-op thanks to cascading, but in case that changes in the future, better keep it anyway.
GalaxyLane.objects.all().delete()
@ -357,7 +358,7 @@ class Galaxy(models.Model):
).save()
@classmethod
def scale_distance(cls, value):
def scale_distance(cls, value) -> int:
# TODO: this will need adjustements with the real, typical data on Taiste
cls.logger.debug(f"\t\t> Score: {value}")

View File

@ -127,3 +127,19 @@ class GalaxyTest(TestCase):
self.maxDiff = None # Yes, we want to see the diff if any
self.assertDictEqual(expected_scores, computed_scores)
def test_page_is_citizen(self):
Galaxy.rule()
self.client.login(username="root", password="plop")
response = self.client.get("/galaxy/1/")
self.assertContains(
response,
'<a onclick="focus_node(get_node_from_id(8))">Locate</a>',
status_code=200,
)
def test_page_not_citizen(self):
Galaxy.rule()
self.client.login(username="root", password="plop")
response = self.client.get("/galaxy/2/")
self.assertEquals(response.status_code, 404)

View File

@ -23,9 +23,10 @@
#
from django.views.generic import DetailView, View
from django.http import JsonResponse
from django.http import JsonResponse, Http404
from django.db.models import Q, Case, F, When, Value
from django.db.models.functions import Concat
from django.utils.translation import gettext_lazy as _
from core.views import (
CanViewMixin,
@ -42,6 +43,12 @@ class GalaxyUserView(CanViewMixin, UserTabsMixin, DetailView):
template_name = "galaxy/user.jinja"
current_tab = "galaxy"
def get_object(self, *args, **kwargs):
user: User = super(GalaxyUserView, self).get_object(*args, **kwargs)
if not hasattr(user, "galaxy_user"):
raise Http404(_("This citizen has not yet joined the galaxy"))
return user
def get_queryset(self):
return super(GalaxyUserView, self).get_queryset().select_related("galaxy_user")

View File

@ -6,7 +6,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-11-28 16:54+0100\n"
"POT-Creation-Date: 2023-03-02 11:02+0100\n"
"PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Skia <skia@libskia.so>\n"
"Language-Team: AE info <ae.info@utbm.fr>\n"
@ -18,8 +18,8 @@ msgstr ""
#: accounting/models.py:61 accounting/models.py:110 accounting/models.py:143
#: accounting/models.py:216 club/models.py:48 com/models.py:279
#: com/models.py:296 counter/models.py:199 counter/models.py:232
#: counter/models.py:316 forum/models.py:58 launderette/models.py:38
#: com/models.py:296 counter/models.py:196 counter/models.py:229
#: counter/models.py:317 forum/models.py:58 launderette/models.py:38
#: launderette/models.py:93 launderette/models.py:131 stock/models.py:40
#: stock/models.py:63 stock/models.py:105 stock/models.py:133
msgid "name"
@ -66,8 +66,8 @@ msgid "account number"
msgstr "numero de compte"
#: accounting/models.py:116 accounting/models.py:147 club/models.py:275
#: com/models.py:75 com/models.py:266 com/models.py:302 counter/models.py:250
#: counter/models.py:318 trombi/models.py:217
#: com/models.py:75 com/models.py:266 com/models.py:302 counter/models.py:247
#: counter/models.py:319 trombi/models.py:217
msgid "club"
msgstr "club"
@ -88,12 +88,12 @@ msgstr "Compte club"
msgid "%(club_account)s on %(bank_account)s"
msgstr "%(club_account)s sur %(bank_account)s"
#: accounting/models.py:214 club/models.py:281 counter/models.py:752
#: accounting/models.py:214 club/models.py:281 counter/models.py:753
#: election/models.py:18 launderette/models.py:194
msgid "start date"
msgstr "date de début"
#: accounting/models.py:215 club/models.py:282 counter/models.py:753
#: accounting/models.py:215 club/models.py:282 counter/models.py:754
#: election/models.py:19
msgid "end date"
msgstr "date de fin"
@ -107,7 +107,7 @@ msgid "club account"
msgstr "compte club"
#: accounting/models.py:225 accounting/models.py:289 counter/models.py:60
#: counter/models.py:474
#: counter/models.py:475
msgid "amount"
msgstr "montant"
@ -129,18 +129,18 @@ msgstr "classeur"
#: accounting/models.py:290 core/models.py:862 core/models.py:1400
#: core/models.py:1448 core/models.py:1477 core/models.py:1501
#: counter/models.py:484 counter/models.py:577 counter/models.py:782
#: eboutic/models.py:66 eboutic/models.py:240 forum/models.py:311
#: counter/models.py:485 counter/models.py:578 counter/models.py:789
#: eboutic/models.py:67 eboutic/models.py:236 forum/models.py:311
#: forum/models.py:408 stock/models.py:104
msgid "date"
msgstr "date"
#: accounting/models.py:291 counter/models.py:201 counter/models.py:783
#: accounting/models.py:291 counter/models.py:198 counter/models.py:790
#: pedagogy/models.py:219 stock/models.py:107
msgid "comment"
msgstr "commentaire"
#: accounting/models.py:293 counter/models.py:486 counter/models.py:579
#: accounting/models.py:293 counter/models.py:487 counter/models.py:580
#: subscription/models.py:66
msgid "payment method"
msgstr "méthode de paiement"
@ -149,7 +149,7 @@ msgstr "méthode de paiement"
msgid "cheque number"
msgstr "numéro de chèque"
#: accounting/models.py:303 eboutic/models.py:332
#: accounting/models.py:303 eboutic/models.py:328
msgid "invoice"
msgstr "facture"
@ -167,7 +167,7 @@ msgstr "type comptable"
#: accounting/models.py:328 accounting/models.py:475 accounting/models.py:510
#: accounting/models.py:545 core/models.py:1476 core/models.py:1502
#: counter/models.py:543
#: counter/models.py:544
msgid "label"
msgstr "étiquette"
@ -211,7 +211,7 @@ msgstr "Utilisateur"
msgid "Club"
msgstr "Club"
#: accounting/models.py:339 core/views/user.py:287
#: accounting/models.py:339 core/views/user.py:279
msgid "Account"
msgstr "Compte"
@ -219,7 +219,7 @@ msgstr "Compte"
msgid "Company"
msgstr "Entreprise"
#: accounting/models.py:341 core/models.py:230 sith/settings.py:367
#: accounting/models.py:341 core/models.py:230 sith/settings.py:391
#: stock/templates/stock/shopping_list_items.jinja:37
msgid "Other"
msgstr "Autre"
@ -266,7 +266,7 @@ msgstr ""
"Vous devez fournir soit un type comptable simplifié ou un type comptable "
"standard"
#: accounting/models.py:467 counter/models.py:242 pedagogy/models.py:46
#: accounting/models.py:467 counter/models.py:239 pedagogy/models.py:46
msgid "code"
msgstr "code"
@ -530,7 +530,7 @@ msgid "Effective amount"
msgstr "Montant effectif"
#: accounting/templates/accounting/club_account_details.jinja:36
#: sith/settings.py:413
#: sith/settings.py:437
msgid "Closed"
msgstr "Fermé"
@ -670,7 +670,7 @@ msgid "Done"
msgstr "Effectuées"
#: accounting/templates/accounting/journal_details.jinja:41
#: counter/templates/counter/cash_summary_list.jinja:37 counter/views.py:1064
#: counter/templates/counter/cash_summary_list.jinja:37 counter/views.py:1072
#: pedagogy/templates/pedagogy/moderation.jinja:13
#: pedagogy/templates/pedagogy/uv_detail.jinja:138
#: trombi/templates/trombi/comment.jinja:4
@ -935,15 +935,15 @@ msgstr "Retirer"
msgid "Action"
msgstr "Action"
#: club/forms.py:116 club/tests.py:576
#: club/forms.py:116 club/tests.py:578
msgid "This field is required"
msgstr "Ce champ est obligatoire"
#: club/forms.py:128 club/forms.py:256 club/tests.py:588
#: club/forms.py:128 club/forms.py:256 club/tests.py:590
msgid "One of the selected users doesn't exist"
msgstr "Un des utilisateurs sélectionné n'existe pas"
#: club/forms.py:132 club/tests.py:606
#: club/forms.py:132 club/tests.py:608
msgid "One of the selected users doesn't have an email address"
msgstr "Un des utilisateurs sélectionnés n'a pas d'adresse email"
@ -951,15 +951,15 @@ msgstr "Un des utilisateurs sélectionnés n'a pas d'adresse email"
msgid "An action is required"
msgstr "Une action est requise"
#: club/forms.py:154 club/tests.py:565
#: club/forms.py:154 club/tests.py:567
msgid "You must specify at least an user or an email address"
msgstr "vous devez spécifier au moins un utilisateur ou une adresse email"
#: club/forms.py:162 counter/forms.py:157
#: club/forms.py:162 counter/forms.py:165
msgid "Begin date"
msgstr "Date de début"
#: club/forms.py:163 com/views.py:84 com/views.py:199 counter/forms.py:158
#: club/forms.py:163 com/views.py:84 com/views.py:199 counter/forms.py:166
#: election/views.py:172 subscription/views.py:49
msgid "End date"
msgstr "Date de fin"
@ -967,15 +967,15 @@ msgstr "Date de fin"
#: club/forms.py:166 club/templates/club/club_sellings.jinja:21
#: core/templates/core/user_account_detail.jinja:18
#: core/templates/core/user_account_detail.jinja:51
#: counter/templates/counter/cash_summary_list.jinja:33 counter/views.py:148
#: counter/templates/counter/cash_summary_list.jinja:33 counter/views.py:156
msgid "Counter"
msgstr "Comptoir"
#: club/forms.py:174 counter/views.py:762
#: club/forms.py:174 counter/views.py:770
msgid "Products"
msgstr "Produits"
#: club/forms.py:179 counter/views.py:767
#: club/forms.py:179 counter/views.py:775
msgid "Archived products"
msgstr "Produits archivés"
@ -1045,8 +1045,8 @@ msgstr "Vous ne pouvez pas faire de boucles dans les clubs"
msgid "A club with that unix_name already exists"
msgstr "Un club avec ce nom UNIX existe déjà."
#: club/models.py:267 counter/models.py:743 counter/models.py:773
#: eboutic/models.py:62 eboutic/models.py:236 election/models.py:192
#: club/models.py:267 counter/models.py:744 counter/models.py:780
#: eboutic/models.py:63 eboutic/models.py:232 election/models.py:192
#: launderette/models.py:145 launderette/models.py:213 sas/models.py:244
#: trombi/models.py:213
msgid "user"
@ -1057,8 +1057,8 @@ msgstr "nom d'utilisateur"
msgid "role"
msgstr "rôle"
#: club/models.py:289 core/models.py:81 counter/models.py:200
#: counter/models.py:233 election/models.py:15 election/models.py:120
#: club/models.py:289 core/models.py:81 counter/models.py:197
#: counter/models.py:230 election/models.py:15 election/models.py:120
#: election/models.py:197 forum/models.py:59 forum/models.py:240
msgid "description"
msgstr "description"
@ -1096,7 +1096,7 @@ msgstr "Liste de diffusion"
msgid "At least user or email is required"
msgstr "Au moins un utilisateur ou un email est nécessaire"
#: club/models.py:459 club/tests.py:634
#: club/models.py:459 club/tests.py:636
msgid "This email is already suscribed in this mailing"
msgstr "Cet email est déjà abonné à cette mailing"
@ -1362,7 +1362,7 @@ msgstr "Anciens membres"
msgid "History"
msgstr "Historique"
#: club/views.py:125 core/templates/core/base.jinja:123 core/views/user.py:220
#: club/views.py:125 core/templates/core/base.jinja:123 core/views/user.py:222
#: sas/templates/sas/picture.jinja:95 trombi/views.py:63
msgid "Tools"
msgstr "Outils"
@ -1860,7 +1860,7 @@ msgstr "Retour"
#: com/templates/com/weekmail_preview.jinja:13
msgid "The following recipients were refused by the SMTP:"
msgstr ""
msgstr "Les destinataires suivants ont été refusé par le SMTP :"
#: com/templates/com/weekmail_preview.jinja:24
msgid "Clean subscribers"
@ -2379,7 +2379,7 @@ msgstr "type d'opération"
msgid "403, Forbidden"
msgstr "403, Non autorisé"
#: core/templates/core/404.jinja:5
#: core/templates/core/404.jinja:6
msgid "404, Not Found"
msgstr "404. Non trouvé"
@ -2488,13 +2488,13 @@ msgstr "Forum"
msgid "Gallery"
msgstr "Photos"
#: core/templates/core/base.jinja:187 counter/models.py:326
#: core/templates/core/base.jinja:187 counter/models.py:327
#: counter/templates/counter/counter_list.jinja:11
#: eboutic/templates/eboutic/eboutic_main.jinja:4
#: eboutic/templates/eboutic/eboutic_main.jinja:23
#: eboutic/templates/eboutic/eboutic_makecommand.jinja:17
#: eboutic/templates/eboutic/eboutic_payment_result.jinja:4
#: sith/settings.py:366 sith/settings.py:374
#: sith/settings.py:390 sith/settings.py:398
msgid "Eboutic"
msgstr "Eboutic"
@ -3021,7 +3021,7 @@ msgstr "Résultat de la recherche"
msgid "Users"
msgstr "Utilisateurs"
#: core/templates/core/search.jinja:18 core/views/user.py:248
#: core/templates/core/search.jinja:18 core/views/user.py:244
#: counter/templates/counter/stats.jinja:17
msgid "Clubs"
msgstr "Clubs"
@ -3268,7 +3268,7 @@ msgstr "Voir l'arbre des ancêtres"
msgid "No godfathers / godmothers"
msgstr "Pas de famille"
#: core/templates/core/user_godfathers.jinja:25 core/views/user.py:472
#: core/templates/core/user_godfathers.jinja:25 core/views/user.py:464
msgid "Godchildren"
msgstr "Fillots / Fillotes"
@ -3341,7 +3341,7 @@ msgid "Picture Unavailable"
msgstr "Photo Indisponible"
#: core/templates/core/user_preferences.jinja:4
#: core/templates/core/user_preferences.jinja:8 core/views/user.py:238
#: core/templates/core/user_preferences.jinja:8 core/views/user.py:236
msgid "Preferences"
msgstr "Préférences"
@ -3411,7 +3411,7 @@ msgstr "Outils utilisateurs"
msgid "Sith management"
msgstr "Gestion de Sith"
#: core/templates/core/user_tools.jinja:14 core/views/user.py:258
#: core/templates/core/user_tools.jinja:14 core/views/user.py:252
msgid "Groups"
msgstr "Groupes"
@ -3439,8 +3439,8 @@ msgstr "Cotisations"
msgid "Subscription stats"
msgstr "Statistiques de cotisation"
#: core/templates/core/user_tools.jinja:29 counter/forms.py:131
#: counter/views.py:757
#: core/templates/core/user_tools.jinja:29 counter/forms.py:139
#: counter/views.py:765
msgid "Counters"
msgstr "Comptoirs"
@ -3457,16 +3457,16 @@ msgid "Product types management"
msgstr "Gestion des types de produit"
#: core/templates/core/user_tools.jinja:35
#: counter/templates/counter/cash_summary_list.jinja:23 counter/views.py:777
#: counter/templates/counter/cash_summary_list.jinja:23 counter/views.py:785
msgid "Cash register summaries"
msgstr "Relevés de caisse"
#: core/templates/core/user_tools.jinja:36
#: counter/templates/counter/invoices_call.jinja:4 counter/views.py:782
#: counter/templates/counter/invoices_call.jinja:4 counter/views.py:790
msgid "Invoices call"
msgstr "Appels à facture"
#: core/templates/core/user_tools.jinja:44 core/views/user.py:278
#: core/templates/core/user_tools.jinja:44 core/views/user.py:270
#: counter/templates/counter/counter_list.jinja:18
#: counter/templates/counter/counter_list.jinja:34
#: counter/templates/counter/counter_list.jinja:56
@ -3691,7 +3691,7 @@ msgstr "Parrain / Marraine"
msgid "Godchild"
msgstr "Fillot / Fillote"
#: core/views/forms.py:348 counter/forms.py:47 trombi/views.py:158
#: core/views/forms.py:348 counter/forms.py:55 trombi/views.py:158
msgid "Select user"
msgstr "Choisir un utilisateur"
@ -3713,16 +3713,20 @@ msgstr "Utilisateurs à retirer du groupe"
msgid "Users to add to group"
msgstr "Utilisateurs à ajouter au groupe"
#: core/views/user.py:206 core/views/user.py:474 core/views/user.py:476
#: core/views/user.py:202 core/views/user.py:466 core/views/user.py:468
msgid "Family"
msgstr "Famille"
#: core/views/user.py:215 trombi/templates/trombi/export.jinja:25
#: core/views/user.py:207 trombi/templates/trombi/export.jinja:25
#: trombi/templates/trombi/user_profile.jinja:11
msgid "Pictures"
msgstr "Photos"
#: core/views/user.py:618
#: core/views/user.py:217
msgid "Galaxy"
msgstr "Galaxie"
#: core/views/user.py:610
msgid "User already has a profile picture"
msgstr "L'utilisateur a déjà une photo de profil"
@ -4359,23 +4363,23 @@ msgstr "Nombre de chèque"
msgid "people(s)"
msgstr "personne(s)"
#: eboutic/forms.py:102
#: eboutic/forms.py:107
msgid "You have no basket."
msgstr "Vous n'avez pas de panier."
#: eboutic/forms.py:107
#: eboutic/forms.py:120
msgid "The request was badly formatted."
msgstr "La requête a été mal formatée."
#: eboutic/forms.py:112
#: eboutic/forms.py:126
msgid "The basket cookie was badly formatted."
msgstr "Le cookie du panier a été mal formaté."
#: eboutic/forms.py:115
#: eboutic/forms.py:130
msgid "Your basket is empty."
msgstr "Votre panier est vide"
#: eboutic/forms.py:125
#: eboutic/forms.py:141
#, python-format
msgid "%(name)s : this product does not exist."
msgstr "%(name)s : ce produit n'existe pas."
@ -4602,11 +4606,11 @@ msgstr "votes"
#: election/templates/election/election_detail.jinja:146
msgid "✏️"
msgstr ""
msgstr "✏️"
#: election/templates/election/election_detail.jinja:147
msgid ""
msgstr ""
msgstr ""
#: election/templates/election/election_detail.jinja:178
msgid "Add a new list"
@ -4824,6 +4828,50 @@ msgstr "Appliquer les droits et le club propriétaire récursivement"
msgid "%(author)s said"
msgstr "Citation de %(author)s"
#: galaxy/models.py:52
msgid "star owner"
msgstr "propriétaire de l'étoile"
#: galaxy/models.py:57
msgid "star mass"
msgstr "masse de l'étoile"
#: galaxy/models.py:74
msgid "galaxy star 1"
msgstr "étoile 1"
#: galaxy/models.py:80
msgid "galaxy star 2"
msgstr "étoile 2"
#: galaxy/models.py:85
msgid "distance"
msgstr "distance"
#: galaxy/models.py:87
msgid "Distance separating star1 and star2"
msgstr "Distance séparant étoile 1 et étoile 2"
#: galaxy/models.py:90
msgid "family score"
msgstr "score de famille"
#: galaxy/models.py:94
msgid "pictures score"
msgstr "score de photos"
#: galaxy/models.py:98
msgid "clubs score"
msgstr "score de club"
#: galaxy/templates/galaxy/user.jinja:4
msgid "%(user_name)s's Galaxy"
msgstr "Galaxie de %(user_name)s"
#: galaxy/views.py:49
msgid "This citizen has not yet joined the galaxy"
msgstr "Ce citoyen n'a pas encore rejoint la galaxie"
#: launderette/models.py:97 launderette/models.py:135
msgid "launderette"
msgstr "laverie"
@ -4877,12 +4925,12 @@ msgid "Washing and drying"
msgstr "Lavage et séchage"
#: launderette/templates/launderette/launderette_book.jinja:27
#: sith/settings.py:596
#: sith/settings.py:620
msgid "Washing"
msgstr "Lavage"
#: launderette/templates/launderette/launderette_book.jinja:31
#: sith/settings.py:596
#: sith/settings.py:620
msgid "Drying"
msgstr "Séchage"
@ -5374,356 +5422,356 @@ msgstr "Erreur de création de l'album %(album)s : %(msg)s"
msgid "Add user"
msgstr "Ajouter une personne"
#: sith/settings.py:218 sith/settings.py:421
#: sith/settings.py:242 sith/settings.py:445
msgid "English"
msgstr "Anglais"
#: sith/settings.py:218 sith/settings.py:420
#: sith/settings.py:242 sith/settings.py:444
msgid "French"
msgstr "Français"
#: sith/settings.py:340
#: sith/settings.py:364
msgid "TC"
msgstr "TC"
#: sith/settings.py:341
#: sith/settings.py:365
msgid "IMSI"
msgstr "IMSI"
#: sith/settings.py:342
#: sith/settings.py:366
msgid "IMAP"
msgstr "IMAP"
#: sith/settings.py:343
#: sith/settings.py:367
msgid "INFO"
msgstr "INFO"
#: sith/settings.py:344
#: sith/settings.py:368
msgid "GI"
msgstr "GI"
#: sith/settings.py:345 sith/settings.py:431
#: sith/settings.py:369 sith/settings.py:455
msgid "E"
msgstr "E"
#: sith/settings.py:346
#: sith/settings.py:370
msgid "EE"
msgstr "EE"
#: sith/settings.py:347
#: sith/settings.py:371
msgid "GESC"
msgstr "GESC"
#: sith/settings.py:348
#: sith/settings.py:372
msgid "GMC"
msgstr "GMC"
#: sith/settings.py:349
#: sith/settings.py:373
msgid "MC"
msgstr "MC"
#: sith/settings.py:350
#: sith/settings.py:374
msgid "EDIM"
msgstr "EDIM"
#: sith/settings.py:351
#: sith/settings.py:375
msgid "Humanities"
msgstr "Humanités"
#: sith/settings.py:352
#: sith/settings.py:376
msgid "N/A"
msgstr "N/A"
#: sith/settings.py:356 sith/settings.py:363 sith/settings.py:382
#: sith/settings.py:380 sith/settings.py:387 sith/settings.py:406
msgid "Check"
msgstr "Chèque"
#: sith/settings.py:357 sith/settings.py:365 sith/settings.py:383
#: sith/settings.py:381 sith/settings.py:389 sith/settings.py:407
msgid "Cash"
msgstr "Espèces"
#: sith/settings.py:358
#: sith/settings.py:382
msgid "Transfert"
msgstr "Virement"
#: sith/settings.py:371
#: sith/settings.py:395
msgid "Belfort"
msgstr "Belfort"
#: sith/settings.py:372
#: sith/settings.py:396
msgid "Sevenans"
msgstr "Sevenans"
#: sith/settings.py:373
#: sith/settings.py:397
msgid "Montbéliard"
msgstr "Montbéliard"
#: sith/settings.py:401
#: sith/settings.py:425
msgid "Free"
msgstr "Libre"
#: sith/settings.py:402
#: sith/settings.py:426
msgid "CS"
msgstr "CS"
#: sith/settings.py:403
#: sith/settings.py:427
msgid "TM"
msgstr "TM"
#: sith/settings.py:404
#: sith/settings.py:428
msgid "OM"
msgstr "OM"
#: sith/settings.py:405
#: sith/settings.py:429
msgid "QC"
msgstr "QC"
#: sith/settings.py:406
#: sith/settings.py:430
msgid "EC"
msgstr "EC"
#: sith/settings.py:407
#: sith/settings.py:431
msgid "RN"
msgstr "RN"
#: sith/settings.py:408
#: sith/settings.py:432
msgid "ST"
msgstr "ST"
#: sith/settings.py:409
#: sith/settings.py:433
msgid "EXT"
msgstr "EXT"
#: sith/settings.py:414
#: sith/settings.py:438
msgid "Autumn"
msgstr "Automne"
#: sith/settings.py:415
#: sith/settings.py:439
msgid "Spring"
msgstr "Printemps"
#: sith/settings.py:416
#: sith/settings.py:440
msgid "Autumn and spring"
msgstr "Automne et printemps"
#: sith/settings.py:422
#: sith/settings.py:446
msgid "German"
msgstr "Allemant"
#: sith/settings.py:423
#: sith/settings.py:447
msgid "Spanich"
msgstr "Espagnol"
#: sith/settings.py:427
#: sith/settings.py:451
msgid "A"
msgstr "A"
#: sith/settings.py:428
#: sith/settings.py:452
msgid "B"
msgstr "B"
#: sith/settings.py:429
#: sith/settings.py:453
msgid "C"
msgstr "C"
#: sith/settings.py:430
#: sith/settings.py:454
msgid "D"
msgstr "D"
#: sith/settings.py:432
#: sith/settings.py:456
msgid "FX"
msgstr "FX"
#: sith/settings.py:433
#: sith/settings.py:457
msgid "F"
msgstr "F"
#: sith/settings.py:434
#: sith/settings.py:458
msgid "Abs"
msgstr "Abs"
#: sith/settings.py:438
#: sith/settings.py:462
msgid "Selling deletion"
msgstr "Suppression de vente"
#: sith/settings.py:439
#: sith/settings.py:463
msgid "Refilling deletion"
msgstr "Suppression de rechargement"
#: sith/settings.py:476
#: sith/settings.py:500
msgid "One semester"
msgstr "Un semestre, 20 €"
#: sith/settings.py:477
#: sith/settings.py:501
msgid "Two semesters"
msgstr "Deux semestres, 35 €"
#: sith/settings.py:479
#: sith/settings.py:503
msgid "Common core cursus"
msgstr "Cursus tronc commun, 60 €"
#: sith/settings.py:483
#: sith/settings.py:507
msgid "Branch cursus"
msgstr "Cursus branche, 60 €"
#: sith/settings.py:484
#: sith/settings.py:508
msgid "Alternating cursus"
msgstr "Cursus alternant, 30 €"
#: sith/settings.py:485
#: sith/settings.py:509
msgid "Honorary member"
msgstr "Membre honoraire, 0 €"
#: sith/settings.py:486
#: sith/settings.py:510
msgid "Assidu member"
msgstr "Membre d'Assidu, 0 €"
#: sith/settings.py:487
#: sith/settings.py:511
msgid "Amicale/DOCEO member"
msgstr "Membre de l'Amicale/DOCEO, 0 €"
#: sith/settings.py:488
#: sith/settings.py:512
msgid "UT network member"
msgstr "Cotisant du réseau UT, 0 €"
#: sith/settings.py:489
#: sith/settings.py:513
msgid "CROUS member"
msgstr "Membres du CROUS, 0 €"
#: sith/settings.py:490
#: sith/settings.py:514
msgid "Sbarro/ESTA member"
msgstr "Membre de Sbarro ou de l'ESTA, 20 €"
#: sith/settings.py:492
#: sith/settings.py:516
msgid "One semester Welcome Week"
msgstr "Un semestre Welcome Week"
#: sith/settings.py:496
#: sith/settings.py:520
msgid "One month for free"
msgstr "Un mois gratuit"
#: sith/settings.py:497
#: sith/settings.py:521
msgid "Two months for free"
msgstr "Deux mois gratuits"
#: sith/settings.py:498
#: sith/settings.py:522
msgid "Eurok's volunteer"
msgstr "Bénévole Eurockéennes"
#: sith/settings.py:500
#: sith/settings.py:524
msgid "Six weeks for free"
msgstr "6 semaines gratuites"
#: sith/settings.py:504
#: sith/settings.py:528
msgid "One day"
msgstr "Un jour"
#: sith/settings.py:505
#: sith/settings.py:529
msgid "GA staff member"
msgstr "Membre staff GA (2 semaines), 1 €"
#: sith/settings.py:508
#: sith/settings.py:532
msgid "One semester (-20%)"
msgstr "Un semestre (-20%), 12 €"
#: sith/settings.py:513
#: sith/settings.py:537
msgid "Two semesters (-20%)"
msgstr "Deux semestres (-20%), 22 €"
#: sith/settings.py:518
#: sith/settings.py:542
msgid "Common core cursus (-20%)"
msgstr "Cursus tronc commun (-20%), 36 €"
#: sith/settings.py:523
#: sith/settings.py:547
msgid "Branch cursus (-20%)"
msgstr "Cursus branche (-20%), 36 €"
#: sith/settings.py:528
#: sith/settings.py:552
msgid "Alternating cursus (-20%)"
msgstr "Cursus alternant (-20%), 24 €"
#: sith/settings.py:534
#: sith/settings.py:558
msgid "One year for free(CA offer)"
msgstr "Une année offerte (Offre CA)"
#: sith/settings.py:556
#: sith/settings.py:580
msgid "President"
msgstr "Président⸱e"
#: sith/settings.py:557
#: sith/settings.py:581
msgid "Vice-President"
msgstr "Vice-Président⸱e"
#: sith/settings.py:558
#: sith/settings.py:582
msgid "Treasurer"
msgstr "Trésorier⸱e"
#: sith/settings.py:559
#: sith/settings.py:583
msgid "Communication supervisor"
msgstr "Responsable communication"
#: sith/settings.py:560
#: sith/settings.py:584
msgid "Secretary"
msgstr "Secrétaire"
#: sith/settings.py:561
#: sith/settings.py:585
msgid "IT supervisor"
msgstr "Responsable info"
#: sith/settings.py:562
#: sith/settings.py:586
msgid "Board member"
msgstr "Membre du bureau"
#: sith/settings.py:563
#: sith/settings.py:587
msgid "Active member"
msgstr "Membre actif⸱ve"
#: sith/settings.py:564
#: sith/settings.py:588
msgid "Curious"
msgstr "Curieux⸱euse"
#: sith/settings.py:600
#: sith/settings.py:624
msgid "A new poster needs to be moderated"
msgstr "Une nouvelle affiche a besoin d'être modérée"
#: sith/settings.py:601
#: sith/settings.py:625
msgid "A new mailing list needs to be moderated"
msgstr "Une nouvelle mailing list a besoin d'être modérée"
#: sith/settings.py:604
#: sith/settings.py:628
msgid "A new pedagogy comment has been signaled for moderation"
msgstr ""
"Un nouveau commentaire de la pédagogie a été signalé pour la modération"
#: sith/settings.py:606
#: sith/settings.py:630
#, python-format
msgid "There are %s fresh news to be moderated"
msgstr "Il y a %s nouvelles toutes fraîches à modérer"
#: sith/settings.py:607
#: sith/settings.py:631
msgid "New files to be moderated"
msgstr "Nouveaux fichiers à modérer"
#: sith/settings.py:608
#: sith/settings.py:632
#, python-format
msgid "There are %s pictures to be moderated in the SAS"
msgstr "Il y a %s photos à modérer dans le SAS"
#: sith/settings.py:609
#: sith/settings.py:633
msgid "You've been identified on some pictures"
msgstr "Vous avez été identifié sur des photos"
#: sith/settings.py:610
#: sith/settings.py:634
#, python-format
msgid "You just refilled of %s"
msgstr "Vous avez rechargé votre compte de %s"
#: sith/settings.py:611
#: sith/settings.py:635
#, python-format
msgid "You just bought %s"
msgstr "Vous avez acheté %s"
#: sith/settings.py:612
#: sith/settings.py:636
msgid "You have a notification"
msgstr "Vous avez une notification"
@ -5732,26 +5780,27 @@ msgid "You do not have any unread notification"
msgstr "Vous n'avez aucune notification non lue"
#: sith/settings.py:624
#: sith/settings.py:648
msgid "Success!"
msgstr "Succès !"
#: sith/settings.py:625
#: sith/settings.py:649
msgid "Fail!"
msgstr "Échec !"
#: sith/settings.py:626
#: sith/settings.py:650
msgid "You successfully posted an article in the Weekmail"
msgstr "Article posté avec succès dans le Weekmail"
#: sith/settings.py:627
#: sith/settings.py:651
msgid "You successfully edited an article in the Weekmail"
msgstr "Article édité avec succès dans le Weekmail"
#: sith/settings.py:628
#: sith/settings.py:652
msgid "You successfully sent the Weekmail"
msgstr "Weekmail envoyé avec succès"
#: sith/settings.py:636
#: sith/settings.py:660
msgid "AE tee-shirt"
msgstr "Tee-shirt AE"
@ -6302,3 +6351,11 @@ msgstr "Vous ne pouvez plus écrire de commentaires, la date est passée."
#, python-format
msgid "Maximum characters: %(max_length)s"
msgstr "Nombre de caractères max: %(max_length)s"
#: eboutic/templates/eboutic/eboutic_main.jinja:127
msgid "Partnership Eurockéennes 2023"
msgstr "Partenariat Eurockéennes 2023"
#: eboutic/templates/eboutic/eboutic_main.jinja:137
msgid "You must be a subscriber to access the Eurockéennes ticketing service."
msgstr "Vous devez être cotisant pour pouvoir accéder à la billetterie des Eurockéennes."

View File

@ -21,7 +21,212 @@
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
from datetime import date, timedelta
from django.core.management import call_command
from django.test import TestCase
from django.urls import reverse
# Create your tests here.
from club.models import Club
from core.models import User, RealGroup
from counter.models import Customer, Product, Selling, Counter, Refilling
from subscription.models import Subscription
class MergeUserTest(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
call_command("populate")
cls.ae = Club.objects.get(unix_name="ae")
cls.eboutic = Counter.objects.get(name="Eboutic")
cls.barbar = Product.objects.get(code="BARB")
cls.barbar.selling_price = 2
cls.barbar.save()
cls.root = User.objects.get(username="root")
def setUp(self) -> None:
super().setUp()
self.to_keep = User(username="to_keep", password="plop", email="u.1@utbm.fr")
self.to_delete = User(username="to_del", password="plop", email="u.2@utbm.fr")
self.to_keep.save()
self.to_delete.save()
self.client.login(username="root", password="plop")
def test_simple(self):
self.to_delete.first_name = "Biggus"
self.to_keep.last_name = "Dickus"
self.to_keep.nick_name = "B'ian"
self.to_keep.address = "Jerusalem"
self.to_delete.parent_address = "Rome"
self.to_delete.address = "Rome"
subscribers = RealGroup.objects.get(name="Subscribers")
mde_admin = RealGroup.objects.get(name="MDE admin")
sas_admin = RealGroup.objects.get(name="SAS admin")
self.to_keep.groups.add(subscribers.id)
self.to_delete.groups.add(mde_admin.id)
self.to_keep.groups.add(sas_admin.id)
self.to_delete.groups.add(sas_admin.id)
self.to_delete.save()
self.to_keep.save()
data = {"user1": self.to_keep.id, "user2": self.to_delete.id}
res = self.client.post(reverse("rootplace:merge"), data)
self.assertRedirects(res, self.to_keep.get_absolute_url())
self.assertFalse(User.objects.filter(pk=self.to_delete.pk).exists())
self.to_keep = User.objects.get(pk=self.to_keep.pk)
# fields of to_delete should be assigned to to_keep
# if they were not set beforehand
self.assertEqual("Biggus", self.to_keep.first_name)
self.assertEqual("Dickus", self.to_keep.last_name)
self.assertEqual("B'ian", self.to_keep.nick_name)
self.assertEqual("Jerusalem", self.to_keep.address)
self.assertEqual("Rome", self.to_keep.parent_address)
self.assertEqual(3, self.to_keep.groups.count())
groups = list(self.to_keep.groups.all())
expected = [subscribers, mde_admin, sas_admin]
self.assertCountEqual(groups, expected)
def test_both_subscribers_and_with_account(self):
Customer(user=self.to_keep, account_id="11000l", amount=0).save()
Customer(user=self.to_delete, account_id="12000m", amount=0).save()
Refilling(
amount=10,
operator=self.root,
customer=self.to_keep.customer,
counter=self.eboutic,
).save()
Refilling(
amount=20,
operator=self.root,
customer=self.to_delete.customer,
counter=self.eboutic,
).save()
Selling(
label="barbar",
counter=self.eboutic,
club=self.ae,
product=self.barbar,
customer=self.to_keep.customer,
seller=self.root,
unit_price=2,
quantity=2,
payment_method="SITH_ACCOUNT",
).save()
Selling(
label="barbar",
counter=self.eboutic,
club=self.ae,
product=self.barbar,
customer=self.to_delete.customer,
seller=self.root,
unit_price=2,
quantity=4,
payment_method="SITH_ACCOUNT",
).save()
today = date.today()
# both subscriptions began last month and shall end in 5 months
Subscription(
member=self.to_keep,
subscription_type="un-semestre",
payment_method="EBOUTIC",
subscription_start=today - timedelta(30),
subscription_end=today + timedelta(5 * 30),
).save()
Subscription(
member=self.to_delete,
subscription_type="un-semestre",
payment_method="EBOUTIC",
subscription_start=today - timedelta(30),
subscription_end=today + timedelta(5 * 30),
).save()
data = {"user1": self.to_keep.id, "user2": self.to_delete.id}
res = self.client.post(reverse("rootplace:merge"), data)
self.to_keep = User.objects.get(pk=self.to_keep.id)
self.assertRedirects(res, self.to_keep.get_absolute_url())
# to_keep had 10€ at first and bought 2 barbar worth 2€ each
# to_delete had 20€ and bought 4 barbar
# total should be 10 - 4 + 20 - 8 = 18
self.assertAlmostEqual(18, self.to_keep.customer.amount, delta=0.0001)
self.assertEqual(2, self.to_keep.customer.buyings.count())
self.assertEqual(2, self.to_keep.customer.refillings.count())
self.assertTrue(self.to_keep.is_subscribed)
# to_keep had 5 months of subscription remaining and received
# 5 more months from to_delete, so he should be subscribed for 10 months
self.assertEqual(
today + timedelta(10 * 30),
self.to_keep.subscriptions.order_by("subscription_end")
.last()
.subscription_end,
)
def test_godfathers(self):
users = list(User.objects.all()[:4])
self.to_keep.godfathers.add(users[0])
self.to_keep.godchildren.add(users[1])
self.to_delete.godfathers.add(users[2])
self.to_delete.godfathers.add(self.to_keep)
self.to_delete.godchildren.add(users[3])
data = {"user1": self.to_keep.id, "user2": self.to_delete.id}
res = self.client.post(reverse("rootplace:merge"), data)
self.assertRedirects(res, self.to_keep.get_absolute_url())
self.to_keep = User.objects.get(pk=self.to_keep.id)
self.assertCountEqual(list(self.to_keep.godfathers.all()), [users[0], users[2]])
self.assertCountEqual(
list(self.to_keep.godchildren.all()), [users[1], users[3]]
)
def test_keep_has_no_account(self):
Customer(user=self.to_delete, account_id="12000m", amount=0).save()
Refilling(
amount=20,
operator=self.root,
customer=self.to_delete.customer,
counter=self.eboutic,
).save()
Selling(
label="barbar",
counter=self.eboutic,
club=self.ae,
product=self.barbar,
customer=self.to_delete.customer,
seller=self.root,
unit_price=2,
quantity=4,
payment_method="SITH_ACCOUNT",
).save()
data = {"user1": self.to_keep.id, "user2": self.to_delete.id}
res = self.client.post(reverse("rootplace:merge"), data)
self.to_keep = User.objects.get(pk=self.to_keep.id)
self.assertRedirects(res, self.to_keep.get_absolute_url())
# to_delete had 20€ and bought 4 barbar worth 2€ each
# total should be 20 - 8 = 12
self.assertTrue(hasattr(self.to_keep, "customer"))
self.assertAlmostEqual(12, self.to_keep.customer.amount, delta=0.0001)
def test_delete_has_no_account(self):
Customer(user=self.to_keep, account_id="12000m", amount=0).save()
Refilling(
amount=20,
operator=self.root,
customer=self.to_keep.customer,
counter=self.eboutic,
).save()
Selling(
label="barbar",
counter=self.eboutic,
club=self.ae,
product=self.barbar,
customer=self.to_keep.customer,
seller=self.root,
unit_price=2,
quantity=4,
payment_method="SITH_ACCOUNT",
).save()
data = {"user1": self.to_keep.id, "user2": self.to_delete.id}
res = self.client.post(reverse("rootplace:merge"), data)
self.to_keep = User.objects.get(pk=self.to_keep.id)
self.assertRedirects(res, self.to_keep.get_absolute_url())
# to_keep had 20€ and bought 4 barbar worth 2€ each
# total should be 20 - 8 = 12
self.assertTrue(hasattr(self.to_keep, "customer"))
self.assertAlmostEqual(12, self.to_keep.customer.amount, delta=0.0001)

View File

@ -23,72 +23,114 @@
#
#
from django.utils.translation import gettext as _
from django.views.generic.edit import FormView
from django.views.generic import ListView
from django.urls import reverse
from ajax_select.fields import AutoCompleteSelectField
from django import forms
from django.core.exceptions import PermissionDenied
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext as _
from django.views.generic import ListView
from django.views.generic.edit import FormView
from ajax_select.fields import AutoCompleteSelectField
from core.models import User, OperationLog, SithFile
from core.views import CanEditPropMixin
from core.models import User, OperationLog
from counter.models import Customer
from forum.models import ForumMessageMeta
def merge_users(u1, u2):
u1.nick_name = u1.nick_name or u2.nick_name
u1.date_of_birth = u1.date_of_birth or u2.date_of_birth
u1.home = u1.home or u2.home
u1.sex = u1.sex or u2.sex
u1.pronouns = u1.pronouns or u2.pronouns
u1.tshirt_size = u1.tshirt_size or u2.tshirt_size
u1.role = u1.role or u2.role
u1.department = u1.department or u2.department
u1.dpt_option = u1.dpt_option or u2.dpt_option
u1.semester = u1.semester or u2.semester
u1.quote = u1.quote or u2.quote
u1.school = u1.school or u2.school
u1.promo = u1.promo or u2.promo
u1.forum_signature = u1.forum_signature or u2.forum_signature
u1.second_email = u1.second_email or u2.second_email
u1.phone = u1.phone or u2.phone
u1.parent_phone = u1.parent_phone or u2.parent_phone
u1.address = u1.address or u2.address
u1.parent_address = u1.parent_address or u2.parent_address
def __merge_subscriptions(u1: User, u2: User):
"""
Give all the subscriptions of the second user to first one
If some subscriptions are still active, update their end date
to increase the overall subscription time of the first user.
Some examples :
- if u1 is not subscribed, his subscription end date become the one of u2
- if u1 is subscribed but not u2, nothing happen
- if u1 is subscribed for, let's say, 2 remaining months and u2 is subscribed for 3 remaining months,
he shall then be subscribed for 5 months
"""
last_subscription = (
u1.subscriptions.filter(
subscription_start__lte=timezone.now(), subscription_end__gte=timezone.now()
)
.order_by("subscription_end")
.last()
)
if last_subscription is not None:
subscription_end = last_subscription.subscription_end
for subscription in u2.subscriptions.filter(
subscription_end__gte=timezone.now()
):
subscription.subscription_start = subscription_end
if subscription.subscription_start > timezone.now().date():
remaining = subscription.subscription_end - timezone.now().date()
else:
remaining = (
subscription.subscription_end - subscription.subscription_start
)
subscription_end += remaining
subscription.subscription_end = subscription_end
subscription.save()
u2.subscriptions.all().update(member=u1)
def __merge_pictures(u1: User, u2: User) -> None:
SithFile.objects.filter(owner=u2).update(owner=u1)
if u1.profile_pict is None and u2.profile_pict is not None:
u1.profile_pict, u2.profile_pict = u2.profile_pict, None
if u1.scrub_pict is None and u2.scrub_pict is not None:
u1.scrub_pict, u2.scrub_pict = u2.scrub_pict, None
if u1.avatar_pict is None and u2.avatar_pict is not None:
u1.avatar_pict, u2.avatar_pict = u2.avatar_pict, None
u2.save()
u1.save()
for u in u2.godfathers.all():
u1.godfathers.add(u)
def merge_users(u1: User, u2: User) -> User:
"""
Merge u2 into u1
This means that u1 shall receive everything that belonged to u2 :
- pictures
- refills of the sith account
- purchases of any item bought on the eboutic or the counters
- subscriptions
- godfathers
- godchildren
If u1 had no account id, he shall receive the one of u2.
If u1 and u2 were both in the middle of a subscription, the remaining
durations stack
If u1 had no profile picture, he shall receive the one of u2
"""
for field in u1._meta.fields:
if not field.is_relation and not u1.__dict__[field.name]:
u1.__dict__[field.name] = u2.__dict__[field.name]
for group in u2.groups.all():
u1.groups.add(group.id)
for godfather in u2.godfathers.exclude(id=u1.id):
u1.godfathers.add(godfather)
for godchild in u2.godchildren.exclude(id=u1.id):
u1.godchildren.add(godchild)
__merge_subscriptions(u1, u2)
__merge_pictures(u1, u2)
u2.invoices.all().update(user=u1)
c_src = Customer.objects.filter(user=u2).first()
if c_src is not None:
c_dest, created = Customer.get_or_create(u1)
c_src.refillings.update(customer=c_dest)
c_src.buyings.update(customer=c_dest)
c_dest.recompute_amount()
if created:
# swap the account numbers, so that the user keep
# the id he is accustomed to
tmp_id = c_src.account_id
# delete beforehand in order not to have a unique constraint violation
c_src.delete()
c_dest.account_id = tmp_id
u1.save()
for i in u2.invoices.all():
for f in i._meta.local_fields: # I have sadly not found anything better :/
if f.name == "date":
f.auto_now = False
u1.invoices.add(i)
u1.save()
s1 = User.objects.filter(id=u1.id).first()
s2 = User.objects.filter(id=u2.id).first()
for s in s2.subscriptions.all():
s1.subscriptions.add(s)
s1.save()
c1 = Customer.objects.filter(user__id=u1.id).first()
c2 = Customer.objects.filter(user__id=u2.id).first()
if c1 and c2:
for r in c2.refillings.all():
c1.refillings.add(r)
c1.save()
for s in c2.buyings.all():
c1.buyings.add(s)
c1.save()
elif c2 and not c1:
c2.user = u1
c1 = c2
c1.save()
c1.recompute_amount()
u2.delete()
u2.delete() # everything remaining in u2 gets deleted thanks to on_delete=CASCADE
return u1
@ -128,9 +170,8 @@ class MergeUsersView(FormView):
form_class = MergeForm
def dispatch(self, request, *arg, **kwargs):
res = super(MergeUsersView, self).dispatch(request, *arg, **kwargs)
if request.user.is_root:
return res
return super().dispatch(request, *arg, **kwargs)
raise PermissionDenied
def form_valid(self, form):
@ -140,7 +181,7 @@ class MergeUsersView(FormView):
return super(MergeUsersView, self).form_valid(form)
def get_success_url(self):
return reverse("core:user_profile", kwargs={"user_id": self.final_user.id})
return self.final_user.get_absolute_url()
class DeleteAllForumUserMessagesView(FormView):

View File

@ -291,6 +291,10 @@ SITH_URL = "my.url.git.an"
SITH_NAME = "Sith website"
SITH_TWITTER = "@ae_utbm"
# Enable experimental features
# Enable/Disable the galaxy button on user profile (urls stay activated)
SITH_ENABLE_GALAXY = False
# AE configuration
# TODO: keep only that first setting, with the ID, and do the same for the other clubs
SITH_MAIN_CLUB_ID = 1
@ -713,6 +717,7 @@ SITH_FRONT_DEP_VERSIONS = {
"https://github.com/viralpatel/jquery.shorten/": "",
"https://github.com/getsentry/sentry-javascript/": "4.0.6",
"https://github.com/jhuckaby/webcamjs/": "1.0.0",
"https://github.com/vuejs/vue-next": "3.2.18",
"https://github.com/alpinejs/alpine": "3.10.5",
"https://github.com/mrdoob/three.js/": "r148",
"https://github.com/vasturiano/three-spritetext": "1.6.5",

View File

@ -165,7 +165,4 @@ class Subscription(models.Model):
return user.is_in_group(settings.SITH_MAIN_BOARD_GROUP) or user.is_root
def is_valid_now(self):
return (
self.subscription_start <= date.today()
and date.today() <= self.subscription_end
)
return self.subscription_start <= date.today() <= self.subscription_end