diff --git a/core/templates/core/404.jinja b/core/templates/core/404.jinja index 71894b2d..a777cea6 100644 --- a/core/templates/core/404.jinja +++ b/core/templates/core/404.jinja @@ -2,7 +2,9 @@ {% block content %} -

{% trans %}404, Not Found{% endtrans %}

+
+

{% trans %}404, Not Found{% endtrans %}

+
{% endblock %} diff --git a/core/views/__init__.py b/core/views/__init__.py index df075b9c..5d4d2ea5 100644 --- a/core/views/__init__.py +++ b/core/views/__init__.py @@ -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): diff --git a/core/views/user.py b/core/views/user.py index d4ccb135..bcff5504 100644 --- a/core/views/user.py +++ b/core/views/user.py @@ -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}), diff --git a/counter/models.py b/counter/models.py index cdc28896..ceec55ab 100644 --- a/counter/models.py +++ b/counter/models.py @@ -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,12 +149,16 @@ 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 - self.save() + 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): return reverse("core:user_account", kwargs={"user_id": self.user.pk}) diff --git a/counter/static/counter/js/counter_click.js b/counter/static/counter/js/counter_click.js index 46f22e93..eee8c686 100644 --- a/counter/static/counter/js/counter_click.js +++ b/counter/static/counter/js/counter_click.js @@ -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 ); })); }, }); diff --git a/eboutic/templates/eboutic/eboutic_main.jinja b/eboutic/templates/eboutic/eboutic_main.jinja index 529fd067..dd0df358 100644 --- a/eboutic/templates/eboutic/eboutic_main.jinja +++ b/eboutic/templates/eboutic/eboutic_main.jinja @@ -123,6 +123,19 @@ {% else %}

{% trans %}There are no items available for sale{% endtrans %}

{% endfor %} + +

{% trans %}Partnership Eurockéennes 2023{% endtrans %}

+ {% if user.is_subscribed %} + Billetterie Weezevent + + {% else %} +
{% trans %}You must be a contributor to access the Eurockéennes ticketing service.{% endtrans %}
+ {% endif %} {% endblock %} diff --git a/galaxy/migrations/0001_initial.py b/galaxy/migrations/0001_initial.py index 8d35cccf..e155d1cb 100644 --- a/galaxy/migrations/0001_initial.py +++ b/galaxy/migrations/0001_initial.py @@ -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", ), ), ], diff --git a/galaxy/models.py b/galaxy/models.py index e5e2cf41..cc4a3e72 100644 --- a/galaxy/models.py +++ b/galaxy/models.py @@ -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}") diff --git a/galaxy/tests.py b/galaxy/tests.py index 06995640..d5957a16 100644 --- a/galaxy/tests.py +++ b/galaxy/tests.py @@ -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, + 'Locate', + 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) diff --git a/galaxy/views.py b/galaxy/views.py index 1aa65bff..47228505 100644 --- a/galaxy/views.py +++ b/galaxy/views.py @@ -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") diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 0c031208..978a8964 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -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 \n" "Language-Team: AE info \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." diff --git a/rootplace/tests.py b/rootplace/tests.py index b8fbbe1e..19ee86e4 100644 --- a/rootplace/tests.py +++ b/rootplace/tests.py @@ -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) diff --git a/rootplace/views.py b/rootplace/views.py index 802262fc..fbb04e79 100644 --- a/rootplace/views.py +++ b/rootplace/views.py @@ -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): diff --git a/sith/settings.py b/sith/settings.py index 28b4e730..80ade64e 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -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", diff --git a/subscription/models.py b/subscription/models.py index 31f6a2de..8461ac6e 100644 --- a/subscription/models.py +++ b/subscription/models.py @@ -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