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 %} {% block content %}
<div id="page">
<h3>{% trans %}404, Not Found{% endtrans %}</h3> <h3>{% trans %}404, Not Found{% endtrans %}</h3>
</div>
{% endblock %} {% endblock %}

View File

@ -72,7 +72,9 @@ def forbidden(request, exception):
def not_found(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): def internal_servor_error(request):

View File

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

View File

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

View File

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

View File

@ -123,6 +123,19 @@
{% else %} {% else %}
<p>{% trans %}There are no items available for sale{% endtrans %}</p> <p>{% trans %}There are no items available for sale{% endtrans %}</p>
{% endfor %} {% 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>
</div> </div>
{% endblock %} {% 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.conf import settings
from django.db import migrations, models from django.db import migrations, models
@ -51,7 +51,7 @@ class Migration(migrations.Migration):
on_delete=django.db.models.deletion.CASCADE, on_delete=django.db.models.deletion.CASCADE,
related_name="galaxy_user", related_name="galaxy_user",
to=settings.AUTH_USER_MODEL, 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, on_delete=django.db.models.deletion.CASCADE,
related_name="lanes1", related_name="lanes1",
to="galaxy.galaxystar", 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, on_delete=django.db.models.deletion.CASCADE,
related_name="lanes2", related_name="lanes2",
to="galaxy.galaxystar", to="galaxy.galaxystar",
verbose_name="galaxy lanes 2", verbose_name="galaxy star 2",
), ),
), ),
], ],

View File

@ -25,6 +25,7 @@
import math import math
import logging import logging
from typing import Tuple
from django.db import models from django.db import models
from django.db.models import Q, Case, F, Value, When, Count from django.db.models import Q, Case, F, Value, When, Count
from django.db.models.functions import Concat from django.db.models.functions import Concat
@ -47,7 +48,7 @@ class GalaxyStar(models.Model):
owner = models.OneToOneField( owner = models.OneToOneField(
User, User,
verbose_name=_("galaxy user"), verbose_name=_("star owner"),
related_name="galaxy_user", related_name="galaxy_user",
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
@ -69,13 +70,13 @@ class GalaxyLane(models.Model):
star1 = models.ForeignKey( star1 = models.ForeignKey(
GalaxyStar, GalaxyStar,
verbose_name=_("galaxy lanes 1"), verbose_name=_("galaxy star 1"),
related_name="lanes1", related_name="lanes1",
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
star2 = models.ForeignKey( star2 = models.ForeignKey(
GalaxyStar, GalaxyStar,
verbose_name=_("galaxy lanes 2"), verbose_name=_("galaxy star 2"),
related_name="lanes2", related_name="lanes2",
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
@ -120,7 +121,7 @@ class Galaxy(models.Model):
state = models.JSONField("current state") state = models.JSONField("current state")
@staticmethod @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/ 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 @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 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. higher scores towards the center of the galaxy.
@ -202,7 +203,7 @@ class Galaxy(models.Model):
return user_score return user_score
@classmethod @classmethod
def query_user_score(cls, user): def query_user_score(cls, user) -> int:
score_query = ( score_query = (
User.objects.filter(id=user.id) User.objects.filter(id=user.id)
.annotate( .annotate(
@ -229,7 +230,7 @@ class Galaxy(models.Model):
#################### ####################
@classmethod @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) family = cls.compute_users_family_score(user1, user2)
pictures = cls.compute_users_pictures_score(user1, user2) pictures = cls.compute_users_pictures_score(user1, user2)
clubs = cls.compute_users_clubs_score(user1, user2) clubs = cls.compute_users_clubs_score(user1, user2)
@ -237,7 +238,7 @@ class Galaxy(models.Model):
return score, family, pictures, clubs return score, family, pictures, clubs
@classmethod @classmethod
def compute_users_family_score(cls, user1, user2): def compute_users_family_score(cls, user1, user2) -> int:
link_count = User.objects.filter( link_count = User.objects.filter(
Q(id=user1.id, godfathers=user2) | Q(id=user2.id, godfathers=user1) Q(id=user1.id, godfathers=user2) | Q(id=user2.id, godfathers=user1)
).count() ).count()
@ -248,7 +249,7 @@ class Galaxy(models.Model):
return link_count * cls.FAMILY_LINK_POINTS return link_count * cls.FAMILY_LINK_POINTS
@classmethod @classmethod
def compute_users_pictures_score(cls, user1, user2): def compute_users_pictures_score(cls, user1, user2) -> int:
picture_count = ( picture_count = (
Picture.objects.filter(people__user__in=(user1,)) Picture.objects.filter(people__user__in=(user1,))
.filter(people__user__in=(user2,)) .filter(people__user__in=(user2,))
@ -261,7 +262,7 @@ class Galaxy(models.Model):
return picture_count * cls.PICTURE_POINTS return picture_count * cls.PICTURE_POINTS
@classmethod @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( common_clubs = Club.objects.filter(members__in=user1.memberships.all()).filter(
members__in=user2.memberships.all() members__in=user2.memberships.all()
) )
@ -311,7 +312,7 @@ class Galaxy(models.Model):
################### ###################
@classmethod @classmethod
def rule(cls): def rule(cls) -> None:
GalaxyStar.objects.all().delete() 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. # 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() GalaxyLane.objects.all().delete()
@ -357,7 +358,7 @@ class Galaxy(models.Model):
).save() ).save()
@classmethod @classmethod
def scale_distance(cls, value): def scale_distance(cls, value) -> int:
# TODO: this will need adjustements with the real, typical data on Taiste # TODO: this will need adjustements with the real, typical data on Taiste
cls.logger.debug(f"\t\t> Score: {value}") 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.maxDiff = None # Yes, we want to see the diff if any
self.assertDictEqual(expected_scores, computed_scores) 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.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 import Q, Case, F, When, Value
from django.db.models.functions import Concat from django.db.models.functions import Concat
from django.utils.translation import gettext_lazy as _
from core.views import ( from core.views import (
CanViewMixin, CanViewMixin,
@ -42,6 +43,12 @@ class GalaxyUserView(CanViewMixin, UserTabsMixin, DetailView):
template_name = "galaxy/user.jinja" template_name = "galaxy/user.jinja"
current_tab = "galaxy" 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): def get_queryset(self):
return super(GalaxyUserView, self).get_queryset().select_related("galaxy_user") return super(GalaxyUserView, self).get_queryset().select_related("galaxy_user")

View File

@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "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" "PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Skia <skia@libskia.so>\n" "Last-Translator: Skia <skia@libskia.so>\n"
"Language-Team: AE info <ae.info@utbm.fr>\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:61 accounting/models.py:110 accounting/models.py:143
#: accounting/models.py:216 club/models.py:48 com/models.py:279 #: accounting/models.py:216 club/models.py:48 com/models.py:279
#: com/models.py:296 counter/models.py:199 counter/models.py:232 #: com/models.py:296 counter/models.py:196 counter/models.py:229
#: counter/models.py:316 forum/models.py:58 launderette/models.py:38 #: counter/models.py:317 forum/models.py:58 launderette/models.py:38
#: launderette/models.py:93 launderette/models.py:131 stock/models.py:40 #: launderette/models.py:93 launderette/models.py:131 stock/models.py:40
#: stock/models.py:63 stock/models.py:105 stock/models.py:133 #: stock/models.py:63 stock/models.py:105 stock/models.py:133
msgid "name" msgid "name"
@ -66,8 +66,8 @@ msgid "account number"
msgstr "numero de compte" msgstr "numero de compte"
#: accounting/models.py:116 accounting/models.py:147 club/models.py:275 #: 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 #: com/models.py:75 com/models.py:266 com/models.py:302 counter/models.py:247
#: counter/models.py:318 trombi/models.py:217 #: counter/models.py:319 trombi/models.py:217
msgid "club" msgid "club"
msgstr "club" msgstr "club"
@ -88,12 +88,12 @@ msgstr "Compte club"
msgid "%(club_account)s on %(bank_account)s" msgid "%(club_account)s on %(bank_account)s"
msgstr "%(club_account)s sur %(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 #: election/models.py:18 launderette/models.py:194
msgid "start date" msgid "start date"
msgstr "date de début" 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 #: election/models.py:19
msgid "end date" msgid "end date"
msgstr "date de fin" msgstr "date de fin"
@ -107,7 +107,7 @@ msgid "club account"
msgstr "compte club" msgstr "compte club"
#: accounting/models.py:225 accounting/models.py:289 counter/models.py:60 #: accounting/models.py:225 accounting/models.py:289 counter/models.py:60
#: counter/models.py:474 #: counter/models.py:475
msgid "amount" msgid "amount"
msgstr "montant" msgstr "montant"
@ -129,18 +129,18 @@ msgstr "classeur"
#: accounting/models.py:290 core/models.py:862 core/models.py:1400 #: accounting/models.py:290 core/models.py:862 core/models.py:1400
#: core/models.py:1448 core/models.py:1477 core/models.py:1501 #: core/models.py:1448 core/models.py:1477 core/models.py:1501
#: counter/models.py:484 counter/models.py:577 counter/models.py:782 #: counter/models.py:485 counter/models.py:578 counter/models.py:789
#: eboutic/models.py:66 eboutic/models.py:240 forum/models.py:311 #: eboutic/models.py:67 eboutic/models.py:236 forum/models.py:311
#: forum/models.py:408 stock/models.py:104 #: forum/models.py:408 stock/models.py:104
msgid "date" msgid "date"
msgstr "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 #: pedagogy/models.py:219 stock/models.py:107
msgid "comment" msgid "comment"
msgstr "commentaire" 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 #: subscription/models.py:66
msgid "payment method" msgid "payment method"
msgstr "méthode de paiement" msgstr "méthode de paiement"
@ -149,7 +149,7 @@ msgstr "méthode de paiement"
msgid "cheque number" msgid "cheque number"
msgstr "numéro de chèque" msgstr "numéro de chèque"
#: accounting/models.py:303 eboutic/models.py:332 #: accounting/models.py:303 eboutic/models.py:328
msgid "invoice" msgid "invoice"
msgstr "facture" msgstr "facture"
@ -167,7 +167,7 @@ msgstr "type comptable"
#: accounting/models.py:328 accounting/models.py:475 accounting/models.py:510 #: accounting/models.py:328 accounting/models.py:475 accounting/models.py:510
#: accounting/models.py:545 core/models.py:1476 core/models.py:1502 #: accounting/models.py:545 core/models.py:1476 core/models.py:1502
#: counter/models.py:543 #: counter/models.py:544
msgid "label" msgid "label"
msgstr "étiquette" msgstr "étiquette"
@ -211,7 +211,7 @@ msgstr "Utilisateur"
msgid "Club" msgid "Club"
msgstr "Club" msgstr "Club"
#: accounting/models.py:339 core/views/user.py:287 #: accounting/models.py:339 core/views/user.py:279
msgid "Account" msgid "Account"
msgstr "Compte" msgstr "Compte"
@ -219,7 +219,7 @@ msgstr "Compte"
msgid "Company" msgid "Company"
msgstr "Entreprise" 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 #: stock/templates/stock/shopping_list_items.jinja:37
msgid "Other" msgid "Other"
msgstr "Autre" msgstr "Autre"
@ -266,7 +266,7 @@ msgstr ""
"Vous devez fournir soit un type comptable simplifié ou un type comptable " "Vous devez fournir soit un type comptable simplifié ou un type comptable "
"standard" "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" msgid "code"
msgstr "code" msgstr "code"
@ -530,7 +530,7 @@ msgid "Effective amount"
msgstr "Montant effectif" msgstr "Montant effectif"
#: accounting/templates/accounting/club_account_details.jinja:36 #: accounting/templates/accounting/club_account_details.jinja:36
#: sith/settings.py:413 #: sith/settings.py:437
msgid "Closed" msgid "Closed"
msgstr "Fermé" msgstr "Fermé"
@ -670,7 +670,7 @@ msgid "Done"
msgstr "Effectuées" msgstr "Effectuées"
#: accounting/templates/accounting/journal_details.jinja:41 #: 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/moderation.jinja:13
#: pedagogy/templates/pedagogy/uv_detail.jinja:138 #: pedagogy/templates/pedagogy/uv_detail.jinja:138
#: trombi/templates/trombi/comment.jinja:4 #: trombi/templates/trombi/comment.jinja:4
@ -935,15 +935,15 @@ msgstr "Retirer"
msgid "Action" msgid "Action"
msgstr "Action" msgstr "Action"
#: club/forms.py:116 club/tests.py:576 #: club/forms.py:116 club/tests.py:578
msgid "This field is required" msgid "This field is required"
msgstr "Ce champ est obligatoire" 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" msgid "One of the selected users doesn't exist"
msgstr "Un des utilisateurs sélectionné n'existe pas" 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" 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" 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" msgid "An action is required"
msgstr "Une action est requise" 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" 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" 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" msgid "Begin date"
msgstr "Date de début" 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 #: election/views.py:172 subscription/views.py:49
msgid "End date" msgid "End date"
msgstr "Date de fin" msgstr "Date de fin"
@ -967,15 +967,15 @@ msgstr "Date de fin"
#: club/forms.py:166 club/templates/club/club_sellings.jinja:21 #: 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:18
#: core/templates/core/user_account_detail.jinja:51 #: 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" msgid "Counter"
msgstr "Comptoir" msgstr "Comptoir"
#: club/forms.py:174 counter/views.py:762 #: club/forms.py:174 counter/views.py:770
msgid "Products" msgid "Products"
msgstr "Produits" msgstr "Produits"
#: club/forms.py:179 counter/views.py:767 #: club/forms.py:179 counter/views.py:775
msgid "Archived products" msgid "Archived products"
msgstr "Produits archivés" 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" msgid "A club with that unix_name already exists"
msgstr "Un club avec ce nom UNIX existe déjà." msgstr "Un club avec ce nom UNIX existe déjà."
#: club/models.py:267 counter/models.py:743 counter/models.py:773 #: club/models.py:267 counter/models.py:744 counter/models.py:780
#: eboutic/models.py:62 eboutic/models.py:236 election/models.py:192 #: eboutic/models.py:63 eboutic/models.py:232 election/models.py:192
#: launderette/models.py:145 launderette/models.py:213 sas/models.py:244 #: launderette/models.py:145 launderette/models.py:213 sas/models.py:244
#: trombi/models.py:213 #: trombi/models.py:213
msgid "user" msgid "user"
@ -1057,8 +1057,8 @@ msgstr "nom d'utilisateur"
msgid "role" msgid "role"
msgstr "rôle" msgstr "rôle"
#: club/models.py:289 core/models.py:81 counter/models.py:200 #: club/models.py:289 core/models.py:81 counter/models.py:197
#: counter/models.py:233 election/models.py:15 election/models.py:120 #: counter/models.py:230 election/models.py:15 election/models.py:120
#: election/models.py:197 forum/models.py:59 forum/models.py:240 #: election/models.py:197 forum/models.py:59 forum/models.py:240
msgid "description" msgid "description"
msgstr "description" msgstr "description"
@ -1096,7 +1096,7 @@ msgstr "Liste de diffusion"
msgid "At least user or email is required" msgid "At least user or email is required"
msgstr "Au moins un utilisateur ou un email est nécessaire" 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" msgid "This email is already suscribed in this mailing"
msgstr "Cet email est déjà abonné à cette mailing" msgstr "Cet email est déjà abonné à cette mailing"
@ -1362,7 +1362,7 @@ msgstr "Anciens membres"
msgid "History" msgid "History"
msgstr "Historique" 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 #: sas/templates/sas/picture.jinja:95 trombi/views.py:63
msgid "Tools" msgid "Tools"
msgstr "Outils" msgstr "Outils"
@ -1860,7 +1860,7 @@ msgstr "Retour"
#: com/templates/com/weekmail_preview.jinja:13 #: com/templates/com/weekmail_preview.jinja:13
msgid "The following recipients were refused by the SMTP:" 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 #: com/templates/com/weekmail_preview.jinja:24
msgid "Clean subscribers" msgid "Clean subscribers"
@ -2379,7 +2379,7 @@ msgstr "type d'opération"
msgid "403, Forbidden" msgid "403, Forbidden"
msgstr "403, Non autorisé" msgstr "403, Non autorisé"
#: core/templates/core/404.jinja:5 #: core/templates/core/404.jinja:6
msgid "404, Not Found" msgid "404, Not Found"
msgstr "404. Non trouvé" msgstr "404. Non trouvé"
@ -2488,13 +2488,13 @@ msgstr "Forum"
msgid "Gallery" msgid "Gallery"
msgstr "Photos" 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 #: counter/templates/counter/counter_list.jinja:11
#: eboutic/templates/eboutic/eboutic_main.jinja:4 #: eboutic/templates/eboutic/eboutic_main.jinja:4
#: eboutic/templates/eboutic/eboutic_main.jinja:23 #: eboutic/templates/eboutic/eboutic_main.jinja:23
#: eboutic/templates/eboutic/eboutic_makecommand.jinja:17 #: eboutic/templates/eboutic/eboutic_makecommand.jinja:17
#: eboutic/templates/eboutic/eboutic_payment_result.jinja:4 #: 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" msgid "Eboutic"
msgstr "Eboutic" msgstr "Eboutic"
@ -3021,7 +3021,7 @@ msgstr "Résultat de la recherche"
msgid "Users" msgid "Users"
msgstr "Utilisateurs" 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 #: counter/templates/counter/stats.jinja:17
msgid "Clubs" msgid "Clubs"
msgstr "Clubs" msgstr "Clubs"
@ -3268,7 +3268,7 @@ msgstr "Voir l'arbre des ancêtres"
msgid "No godfathers / godmothers" msgid "No godfathers / godmothers"
msgstr "Pas de famille" 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" msgid "Godchildren"
msgstr "Fillots / Fillotes" msgstr "Fillots / Fillotes"
@ -3341,7 +3341,7 @@ msgid "Picture Unavailable"
msgstr "Photo Indisponible" msgstr "Photo Indisponible"
#: core/templates/core/user_preferences.jinja:4 #: 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" msgid "Preferences"
msgstr "Préférences" msgstr "Préférences"
@ -3411,7 +3411,7 @@ msgstr "Outils utilisateurs"
msgid "Sith management" msgid "Sith management"
msgstr "Gestion de Sith" 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" msgid "Groups"
msgstr "Groupes" msgstr "Groupes"
@ -3439,8 +3439,8 @@ msgstr "Cotisations"
msgid "Subscription stats" msgid "Subscription stats"
msgstr "Statistiques de cotisation" msgstr "Statistiques de cotisation"
#: core/templates/core/user_tools.jinja:29 counter/forms.py:131 #: core/templates/core/user_tools.jinja:29 counter/forms.py:139
#: counter/views.py:757 #: counter/views.py:765
msgid "Counters" msgid "Counters"
msgstr "Comptoirs" msgstr "Comptoirs"
@ -3457,16 +3457,16 @@ msgid "Product types management"
msgstr "Gestion des types de produit" msgstr "Gestion des types de produit"
#: core/templates/core/user_tools.jinja:35 #: 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" msgid "Cash register summaries"
msgstr "Relevés de caisse" msgstr "Relevés de caisse"
#: core/templates/core/user_tools.jinja:36 #: 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" msgid "Invoices call"
msgstr "Appels à facture" 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:18
#: counter/templates/counter/counter_list.jinja:34 #: counter/templates/counter/counter_list.jinja:34
#: counter/templates/counter/counter_list.jinja:56 #: counter/templates/counter/counter_list.jinja:56
@ -3691,7 +3691,7 @@ msgstr "Parrain / Marraine"
msgid "Godchild" msgid "Godchild"
msgstr "Fillot / Fillote" 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" msgid "Select user"
msgstr "Choisir un utilisateur" msgstr "Choisir un utilisateur"
@ -3713,16 +3713,20 @@ msgstr "Utilisateurs à retirer du groupe"
msgid "Users to add to group" msgid "Users to add to group"
msgstr "Utilisateurs à ajouter au groupe" 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" msgid "Family"
msgstr "Famille" 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 #: trombi/templates/trombi/user_profile.jinja:11
msgid "Pictures" msgid "Pictures"
msgstr "Photos" 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" msgid "User already has a profile picture"
msgstr "L'utilisateur a déjà une photo de profil" msgstr "L'utilisateur a déjà une photo de profil"
@ -4359,23 +4363,23 @@ msgstr "Nombre de chèque"
msgid "people(s)" msgid "people(s)"
msgstr "personne(s)" msgstr "personne(s)"
#: eboutic/forms.py:102 #: eboutic/forms.py:107
msgid "You have no basket." msgid "You have no basket."
msgstr "Vous n'avez pas de panier." msgstr "Vous n'avez pas de panier."
#: eboutic/forms.py:107 #: eboutic/forms.py:120
msgid "The request was badly formatted." msgid "The request was badly formatted."
msgstr "La requête a été mal formatée." msgstr "La requête a été mal formatée."
#: eboutic/forms.py:112 #: eboutic/forms.py:126
msgid "The basket cookie was badly formatted." msgid "The basket cookie was badly formatted."
msgstr "Le cookie du panier a été mal formaté." msgstr "Le cookie du panier a été mal formaté."
#: eboutic/forms.py:115 #: eboutic/forms.py:130
msgid "Your basket is empty." msgid "Your basket is empty."
msgstr "Votre panier est vide" msgstr "Votre panier est vide"
#: eboutic/forms.py:125 #: eboutic/forms.py:141
#, python-format #, python-format
msgid "%(name)s : this product does not exist." msgid "%(name)s : this product does not exist."
msgstr "%(name)s : ce produit n'existe pas." msgstr "%(name)s : ce produit n'existe pas."
@ -4602,11 +4606,11 @@ msgstr "votes"
#: election/templates/election/election_detail.jinja:146 #: election/templates/election/election_detail.jinja:146
msgid "✏️" msgid "✏️"
msgstr "" msgstr "✏️"
#: election/templates/election/election_detail.jinja:147 #: election/templates/election/election_detail.jinja:147
msgid "" msgid ""
msgstr "" msgstr ""
#: election/templates/election/election_detail.jinja:178 #: election/templates/election/election_detail.jinja:178
msgid "Add a new list" msgid "Add a new list"
@ -4824,6 +4828,50 @@ msgstr "Appliquer les droits et le club propriétaire récursivement"
msgid "%(author)s said" msgid "%(author)s said"
msgstr "Citation de %(author)s" 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 #: launderette/models.py:97 launderette/models.py:135
msgid "launderette" msgid "launderette"
msgstr "laverie" msgstr "laverie"
@ -4877,12 +4925,12 @@ msgid "Washing and drying"
msgstr "Lavage et séchage" msgstr "Lavage et séchage"
#: launderette/templates/launderette/launderette_book.jinja:27 #: launderette/templates/launderette/launderette_book.jinja:27
#: sith/settings.py:596 #: sith/settings.py:620
msgid "Washing" msgid "Washing"
msgstr "Lavage" msgstr "Lavage"
#: launderette/templates/launderette/launderette_book.jinja:31 #: launderette/templates/launderette/launderette_book.jinja:31
#: sith/settings.py:596 #: sith/settings.py:620
msgid "Drying" msgid "Drying"
msgstr "Séchage" msgstr "Séchage"
@ -5374,356 +5422,356 @@ msgstr "Erreur de création de l'album %(album)s : %(msg)s"
msgid "Add user" msgid "Add user"
msgstr "Ajouter une personne" msgstr "Ajouter une personne"
#: sith/settings.py:218 sith/settings.py:421 #: sith/settings.py:242 sith/settings.py:445
msgid "English" msgid "English"
msgstr "Anglais" msgstr "Anglais"
#: sith/settings.py:218 sith/settings.py:420 #: sith/settings.py:242 sith/settings.py:444
msgid "French" msgid "French"
msgstr "Français" msgstr "Français"
#: sith/settings.py:340 #: sith/settings.py:364
msgid "TC" msgid "TC"
msgstr "TC" msgstr "TC"
#: sith/settings.py:341 #: sith/settings.py:365
msgid "IMSI" msgid "IMSI"
msgstr "IMSI" msgstr "IMSI"
#: sith/settings.py:342 #: sith/settings.py:366
msgid "IMAP" msgid "IMAP"
msgstr "IMAP" msgstr "IMAP"
#: sith/settings.py:343 #: sith/settings.py:367
msgid "INFO" msgid "INFO"
msgstr "INFO" msgstr "INFO"
#: sith/settings.py:344 #: sith/settings.py:368
msgid "GI" msgid "GI"
msgstr "GI" msgstr "GI"
#: sith/settings.py:345 sith/settings.py:431 #: sith/settings.py:369 sith/settings.py:455
msgid "E" msgid "E"
msgstr "E" msgstr "E"
#: sith/settings.py:346 #: sith/settings.py:370
msgid "EE" msgid "EE"
msgstr "EE" msgstr "EE"
#: sith/settings.py:347 #: sith/settings.py:371
msgid "GESC" msgid "GESC"
msgstr "GESC" msgstr "GESC"
#: sith/settings.py:348 #: sith/settings.py:372
msgid "GMC" msgid "GMC"
msgstr "GMC" msgstr "GMC"
#: sith/settings.py:349 #: sith/settings.py:373
msgid "MC" msgid "MC"
msgstr "MC" msgstr "MC"
#: sith/settings.py:350 #: sith/settings.py:374
msgid "EDIM" msgid "EDIM"
msgstr "EDIM" msgstr "EDIM"
#: sith/settings.py:351 #: sith/settings.py:375
msgid "Humanities" msgid "Humanities"
msgstr "Humanités" msgstr "Humanités"
#: sith/settings.py:352 #: sith/settings.py:376
msgid "N/A" msgid "N/A"
msgstr "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" msgid "Check"
msgstr "Chèque" 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" msgid "Cash"
msgstr "Espèces" msgstr "Espèces"
#: sith/settings.py:358 #: sith/settings.py:382
msgid "Transfert" msgid "Transfert"
msgstr "Virement" msgstr "Virement"
#: sith/settings.py:371 #: sith/settings.py:395
msgid "Belfort" msgid "Belfort"
msgstr "Belfort" msgstr "Belfort"
#: sith/settings.py:372 #: sith/settings.py:396
msgid "Sevenans" msgid "Sevenans"
msgstr "Sevenans" msgstr "Sevenans"
#: sith/settings.py:373 #: sith/settings.py:397
msgid "Montbéliard" msgid "Montbéliard"
msgstr "Montbéliard" msgstr "Montbéliard"
#: sith/settings.py:401 #: sith/settings.py:425
msgid "Free" msgid "Free"
msgstr "Libre" msgstr "Libre"
#: sith/settings.py:402 #: sith/settings.py:426
msgid "CS" msgid "CS"
msgstr "CS" msgstr "CS"
#: sith/settings.py:403 #: sith/settings.py:427
msgid "TM" msgid "TM"
msgstr "TM" msgstr "TM"
#: sith/settings.py:404 #: sith/settings.py:428
msgid "OM" msgid "OM"
msgstr "OM" msgstr "OM"
#: sith/settings.py:405 #: sith/settings.py:429
msgid "QC" msgid "QC"
msgstr "QC" msgstr "QC"
#: sith/settings.py:406 #: sith/settings.py:430
msgid "EC" msgid "EC"
msgstr "EC" msgstr "EC"
#: sith/settings.py:407 #: sith/settings.py:431
msgid "RN" msgid "RN"
msgstr "RN" msgstr "RN"
#: sith/settings.py:408 #: sith/settings.py:432
msgid "ST" msgid "ST"
msgstr "ST" msgstr "ST"
#: sith/settings.py:409 #: sith/settings.py:433
msgid "EXT" msgid "EXT"
msgstr "EXT" msgstr "EXT"
#: sith/settings.py:414 #: sith/settings.py:438
msgid "Autumn" msgid "Autumn"
msgstr "Automne" msgstr "Automne"
#: sith/settings.py:415 #: sith/settings.py:439
msgid "Spring" msgid "Spring"
msgstr "Printemps" msgstr "Printemps"
#: sith/settings.py:416 #: sith/settings.py:440
msgid "Autumn and spring" msgid "Autumn and spring"
msgstr "Automne et printemps" msgstr "Automne et printemps"
#: sith/settings.py:422 #: sith/settings.py:446
msgid "German" msgid "German"
msgstr "Allemant" msgstr "Allemant"
#: sith/settings.py:423 #: sith/settings.py:447
msgid "Spanich" msgid "Spanich"
msgstr "Espagnol" msgstr "Espagnol"
#: sith/settings.py:427 #: sith/settings.py:451
msgid "A" msgid "A"
msgstr "A" msgstr "A"
#: sith/settings.py:428 #: sith/settings.py:452
msgid "B" msgid "B"
msgstr "B" msgstr "B"
#: sith/settings.py:429 #: sith/settings.py:453
msgid "C" msgid "C"
msgstr "C" msgstr "C"
#: sith/settings.py:430 #: sith/settings.py:454
msgid "D" msgid "D"
msgstr "D" msgstr "D"
#: sith/settings.py:432 #: sith/settings.py:456
msgid "FX" msgid "FX"
msgstr "FX" msgstr "FX"
#: sith/settings.py:433 #: sith/settings.py:457
msgid "F" msgid "F"
msgstr "F" msgstr "F"
#: sith/settings.py:434 #: sith/settings.py:458
msgid "Abs" msgid "Abs"
msgstr "Abs" msgstr "Abs"
#: sith/settings.py:438 #: sith/settings.py:462
msgid "Selling deletion" msgid "Selling deletion"
msgstr "Suppression de vente" msgstr "Suppression de vente"
#: sith/settings.py:439 #: sith/settings.py:463
msgid "Refilling deletion" msgid "Refilling deletion"
msgstr "Suppression de rechargement" msgstr "Suppression de rechargement"
#: sith/settings.py:476 #: sith/settings.py:500
msgid "One semester" msgid "One semester"
msgstr "Un semestre, 20 €" msgstr "Un semestre, 20 €"
#: sith/settings.py:477 #: sith/settings.py:501
msgid "Two semesters" msgid "Two semesters"
msgstr "Deux semestres, 35 €" msgstr "Deux semestres, 35 €"
#: sith/settings.py:479 #: sith/settings.py:503
msgid "Common core cursus" msgid "Common core cursus"
msgstr "Cursus tronc commun, 60 €" msgstr "Cursus tronc commun, 60 €"
#: sith/settings.py:483 #: sith/settings.py:507
msgid "Branch cursus" msgid "Branch cursus"
msgstr "Cursus branche, 60 €" msgstr "Cursus branche, 60 €"
#: sith/settings.py:484 #: sith/settings.py:508
msgid "Alternating cursus" msgid "Alternating cursus"
msgstr "Cursus alternant, 30 €" msgstr "Cursus alternant, 30 €"
#: sith/settings.py:485 #: sith/settings.py:509
msgid "Honorary member" msgid "Honorary member"
msgstr "Membre honoraire, 0 €" msgstr "Membre honoraire, 0 €"
#: sith/settings.py:486 #: sith/settings.py:510
msgid "Assidu member" msgid "Assidu member"
msgstr "Membre d'Assidu, 0 €" msgstr "Membre d'Assidu, 0 €"
#: sith/settings.py:487 #: sith/settings.py:511
msgid "Amicale/DOCEO member" msgid "Amicale/DOCEO member"
msgstr "Membre de l'Amicale/DOCEO, 0 €" msgstr "Membre de l'Amicale/DOCEO, 0 €"
#: sith/settings.py:488 #: sith/settings.py:512
msgid "UT network member" msgid "UT network member"
msgstr "Cotisant du réseau UT, 0 €" msgstr "Cotisant du réseau UT, 0 €"
#: sith/settings.py:489 #: sith/settings.py:513
msgid "CROUS member" msgid "CROUS member"
msgstr "Membres du CROUS, 0 €" msgstr "Membres du CROUS, 0 €"
#: sith/settings.py:490 #: sith/settings.py:514
msgid "Sbarro/ESTA member" msgid "Sbarro/ESTA member"
msgstr "Membre de Sbarro ou de l'ESTA, 20 €" msgstr "Membre de Sbarro ou de l'ESTA, 20 €"
#: sith/settings.py:492 #: sith/settings.py:516
msgid "One semester Welcome Week" msgid "One semester Welcome Week"
msgstr "Un semestre Welcome Week" msgstr "Un semestre Welcome Week"
#: sith/settings.py:496 #: sith/settings.py:520
msgid "One month for free" msgid "One month for free"
msgstr "Un mois gratuit" msgstr "Un mois gratuit"
#: sith/settings.py:497 #: sith/settings.py:521
msgid "Two months for free" msgid "Two months for free"
msgstr "Deux mois gratuits" msgstr "Deux mois gratuits"
#: sith/settings.py:498 #: sith/settings.py:522
msgid "Eurok's volunteer" msgid "Eurok's volunteer"
msgstr "Bénévole Eurockéennes" msgstr "Bénévole Eurockéennes"
#: sith/settings.py:500 #: sith/settings.py:524
msgid "Six weeks for free" msgid "Six weeks for free"
msgstr "6 semaines gratuites" msgstr "6 semaines gratuites"
#: sith/settings.py:504 #: sith/settings.py:528
msgid "One day" msgid "One day"
msgstr "Un jour" msgstr "Un jour"
#: sith/settings.py:505 #: sith/settings.py:529
msgid "GA staff member" msgid "GA staff member"
msgstr "Membre staff GA (2 semaines), 1 €" msgstr "Membre staff GA (2 semaines), 1 €"
#: sith/settings.py:508 #: sith/settings.py:532
msgid "One semester (-20%)" msgid "One semester (-20%)"
msgstr "Un semestre (-20%), 12 €" msgstr "Un semestre (-20%), 12 €"
#: sith/settings.py:513 #: sith/settings.py:537
msgid "Two semesters (-20%)" msgid "Two semesters (-20%)"
msgstr "Deux semestres (-20%), 22 €" msgstr "Deux semestres (-20%), 22 €"
#: sith/settings.py:518 #: sith/settings.py:542
msgid "Common core cursus (-20%)" msgid "Common core cursus (-20%)"
msgstr "Cursus tronc commun (-20%), 36 €" msgstr "Cursus tronc commun (-20%), 36 €"
#: sith/settings.py:523 #: sith/settings.py:547
msgid "Branch cursus (-20%)" msgid "Branch cursus (-20%)"
msgstr "Cursus branche (-20%), 36 €" msgstr "Cursus branche (-20%), 36 €"
#: sith/settings.py:528 #: sith/settings.py:552
msgid "Alternating cursus (-20%)" msgid "Alternating cursus (-20%)"
msgstr "Cursus alternant (-20%), 24 €" msgstr "Cursus alternant (-20%), 24 €"
#: sith/settings.py:534 #: sith/settings.py:558
msgid "One year for free(CA offer)" msgid "One year for free(CA offer)"
msgstr "Une année offerte (Offre CA)" msgstr "Une année offerte (Offre CA)"
#: sith/settings.py:556 #: sith/settings.py:580
msgid "President" msgid "President"
msgstr "Président⸱e" msgstr "Président⸱e"
#: sith/settings.py:557 #: sith/settings.py:581
msgid "Vice-President" msgid "Vice-President"
msgstr "Vice-Président⸱e" msgstr "Vice-Président⸱e"
#: sith/settings.py:558 #: sith/settings.py:582
msgid "Treasurer" msgid "Treasurer"
msgstr "Trésorier⸱e" msgstr "Trésorier⸱e"
#: sith/settings.py:559 #: sith/settings.py:583
msgid "Communication supervisor" msgid "Communication supervisor"
msgstr "Responsable communication" msgstr "Responsable communication"
#: sith/settings.py:560 #: sith/settings.py:584
msgid "Secretary" msgid "Secretary"
msgstr "Secrétaire" msgstr "Secrétaire"
#: sith/settings.py:561 #: sith/settings.py:585
msgid "IT supervisor" msgid "IT supervisor"
msgstr "Responsable info" msgstr "Responsable info"
#: sith/settings.py:562 #: sith/settings.py:586
msgid "Board member" msgid "Board member"
msgstr "Membre du bureau" msgstr "Membre du bureau"
#: sith/settings.py:563 #: sith/settings.py:587
msgid "Active member" msgid "Active member"
msgstr "Membre actif⸱ve" msgstr "Membre actif⸱ve"
#: sith/settings.py:564 #: sith/settings.py:588
msgid "Curious" msgid "Curious"
msgstr "Curieux⸱euse" msgstr "Curieux⸱euse"
#: sith/settings.py:600 #: sith/settings.py:624
msgid "A new poster needs to be moderated" msgid "A new poster needs to be moderated"
msgstr "Une nouvelle affiche a besoin d'être modérée" 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" msgid "A new mailing list needs to be moderated"
msgstr "Une nouvelle mailing list a besoin d'être modérée" 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" msgid "A new pedagogy comment has been signaled for moderation"
msgstr "" msgstr ""
"Un nouveau commentaire de la pédagogie a été signalé pour la modération" "Un nouveau commentaire de la pédagogie a été signalé pour la modération"
#: sith/settings.py:606 #: sith/settings.py:630
#, python-format #, python-format
msgid "There are %s fresh news to be moderated" msgid "There are %s fresh news to be moderated"
msgstr "Il y a %s nouvelles toutes fraîches à modérer" 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" msgid "New files to be moderated"
msgstr "Nouveaux fichiers à modérer" msgstr "Nouveaux fichiers à modérer"
#: sith/settings.py:608 #: sith/settings.py:632
#, python-format #, python-format
msgid "There are %s pictures to be moderated in the SAS" msgid "There are %s pictures to be moderated in the SAS"
msgstr "Il y a %s photos à modérer dans le 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" msgid "You've been identified on some pictures"
msgstr "Vous avez été identifié sur des photos" msgstr "Vous avez été identifié sur des photos"
#: sith/settings.py:610 #: sith/settings.py:634
#, python-format #, python-format
msgid "You just refilled of %s" msgid "You just refilled of %s"
msgstr "Vous avez rechargé votre compte de %s" msgstr "Vous avez rechargé votre compte de %s"
#: sith/settings.py:611 #: sith/settings.py:635
#, python-format #, python-format
msgid "You just bought %s" msgid "You just bought %s"
msgstr "Vous avez acheté %s" msgstr "Vous avez acheté %s"
#: sith/settings.py:612 #: sith/settings.py:636
msgid "You have a notification" msgid "You have a notification"
msgstr "Vous avez une 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" msgstr "Vous n'avez aucune notification non lue"
#: sith/settings.py:624 #: sith/settings.py:624
#: sith/settings.py:648
msgid "Success!" msgid "Success!"
msgstr "Succès !" msgstr "Succès !"
#: sith/settings.py:625 #: sith/settings.py:649
msgid "Fail!" msgid "Fail!"
msgstr "Échec !" msgstr "Échec !"
#: sith/settings.py:626 #: sith/settings.py:650
msgid "You successfully posted an article in the Weekmail" msgid "You successfully posted an article in the Weekmail"
msgstr "Article posté avec succès dans le 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" msgid "You successfully edited an article in the Weekmail"
msgstr "Article édité avec succès dans le Weekmail" msgstr "Article édité avec succès dans le Weekmail"
#: sith/settings.py:628 #: sith/settings.py:652
msgid "You successfully sent the Weekmail" msgid "You successfully sent the Weekmail"
msgstr "Weekmail envoyé avec succès" msgstr "Weekmail envoyé avec succès"
#: sith/settings.py:636 #: sith/settings.py:660
msgid "AE tee-shirt" msgid "AE tee-shirt"
msgstr "Tee-shirt AE" msgstr "Tee-shirt AE"
@ -6302,3 +6351,11 @@ msgstr "Vous ne pouvez plus écrire de commentaires, la date est passée."
#, python-format #, python-format
msgid "Maximum characters: %(max_length)s" msgid "Maximum characters: %(max_length)s"
msgstr "Nombre de caractères max: %(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. # 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.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 ajax_select.fields import AutoCompleteSelectField
from django.views.generic.edit import FormView
from django.views.generic import ListView
from django.urls import reverse
from django import forms from django import forms
from django.core.exceptions import PermissionDenied 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.views import CanEditPropMixin
from core.models import User, OperationLog
from counter.models import Customer from counter.models import Customer
from forum.models import ForumMessageMeta from forum.models import ForumMessageMeta
def merge_users(u1, u2): def __merge_subscriptions(u1: User, u2: User):
u1.nick_name = u1.nick_name or u2.nick_name """
u1.date_of_birth = u1.date_of_birth or u2.date_of_birth Give all the subscriptions of the second user to first one
u1.home = u1.home or u2.home If some subscriptions are still active, update their end date
u1.sex = u1.sex or u2.sex to increase the overall subscription time of the first user.
u1.pronouns = u1.pronouns or u2.pronouns
u1.tshirt_size = u1.tshirt_size or u2.tshirt_size Some examples :
u1.role = u1.role or u2.role - if u1 is not subscribed, his subscription end date become the one of u2
u1.department = u1.department or u2.department - if u1 is subscribed but not u2, nothing happen
u1.dpt_option = u1.dpt_option or u2.dpt_option - if u1 is subscribed for, let's say, 2 remaining months and u2 is subscribed for 3 remaining months,
u1.semester = u1.semester or u2.semester he shall then be subscribed for 5 months
u1.quote = u1.quote or u2.quote """
u1.school = u1.school or u2.school last_subscription = (
u1.promo = u1.promo or u2.promo u1.subscriptions.filter(
u1.forum_signature = u1.forum_signature or u2.forum_signature subscription_start__lte=timezone.now(), subscription_end__gte=timezone.now()
u1.second_email = u1.second_email or u2.second_email )
u1.phone = u1.phone or u2.phone .order_by("subscription_end")
u1.parent_phone = u1.parent_phone or u2.parent_phone .last()
u1.address = u1.address or u2.address )
u1.parent_address = u1.parent_address or u2.parent_address 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() 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() u1.save()
for i in u2.invoices.all(): u2.delete() # everything remaining in u2 gets deleted thanks to on_delete=CASCADE
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()
return u1 return u1
@ -128,9 +170,8 @@ class MergeUsersView(FormView):
form_class = MergeForm form_class = MergeForm
def dispatch(self, request, *arg, **kwargs): def dispatch(self, request, *arg, **kwargs):
res = super(MergeUsersView, self).dispatch(request, *arg, **kwargs)
if request.user.is_root: if request.user.is_root:
return res return super().dispatch(request, *arg, **kwargs)
raise PermissionDenied raise PermissionDenied
def form_valid(self, form): def form_valid(self, form):
@ -140,7 +181,7 @@ class MergeUsersView(FormView):
return super(MergeUsersView, self).form_valid(form) return super(MergeUsersView, self).form_valid(form)
def get_success_url(self): 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): class DeleteAllForumUserMessagesView(FormView):

View File

@ -291,6 +291,10 @@ SITH_URL = "my.url.git.an"
SITH_NAME = "Sith website" SITH_NAME = "Sith website"
SITH_TWITTER = "@ae_utbm" SITH_TWITTER = "@ae_utbm"
# Enable experimental features
# Enable/Disable the galaxy button on user profile (urls stay activated)
SITH_ENABLE_GALAXY = False
# AE configuration # AE configuration
# TODO: keep only that first setting, with the ID, and do the same for the other clubs # TODO: keep only that first setting, with the ID, and do the same for the other clubs
SITH_MAIN_CLUB_ID = 1 SITH_MAIN_CLUB_ID = 1
@ -713,6 +717,7 @@ SITH_FRONT_DEP_VERSIONS = {
"https://github.com/viralpatel/jquery.shorten/": "", "https://github.com/viralpatel/jquery.shorten/": "",
"https://github.com/getsentry/sentry-javascript/": "4.0.6", "https://github.com/getsentry/sentry-javascript/": "4.0.6",
"https://github.com/jhuckaby/webcamjs/": "1.0.0", "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/alpinejs/alpine": "3.10.5",
"https://github.com/mrdoob/three.js/": "r148", "https://github.com/mrdoob/three.js/": "r148",
"https://github.com/vasturiano/three-spritetext": "1.6.5", "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 return user.is_in_group(settings.SITH_MAIN_BOARD_GROUP) or user.is_root
def is_valid_now(self): def is_valid_now(self):
return ( return self.subscription_start <= date.today() <= self.subscription_end
self.subscription_start <= date.today()
and date.today() <= self.subscription_end
)