mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-21 13:43:20 +00:00
Merge pull request #898 from ae-utbm/taiste
Complete webpack migration, introduction of tom select, better SAS moderation workflow, more ruff and bugfixes
This commit is contained in:
commit
e6f25fb707
@ -15,7 +15,16 @@
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from accounting.models import *
|
||||
from accounting.models import (
|
||||
AccountingType,
|
||||
BankAccount,
|
||||
ClubAccount,
|
||||
Company,
|
||||
GeneralJournal,
|
||||
Label,
|
||||
Operation,
|
||||
SimplifiedAccountingType,
|
||||
)
|
||||
|
||||
admin.site.register(BankAccount)
|
||||
admin.site.register(ClubAccount)
|
||||
|
@ -82,9 +82,7 @@ class Company(models.Model):
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""Check if that object can be edited by the given user."""
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
return True
|
||||
return False
|
||||
return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
|
||||
|
||||
def can_be_edited_by(self, user):
|
||||
"""Check if that object can be edited by the given user."""
|
||||
@ -127,9 +125,7 @@ class BankAccount(models.Model):
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
return True
|
||||
m = self.club.get_membership_for(user)
|
||||
if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
||||
return True
|
||||
return False
|
||||
return m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]
|
||||
|
||||
|
||||
class ClubAccount(models.Model):
|
||||
@ -161,29 +157,20 @@ class ClubAccount(models.Model):
|
||||
"""Check if that object can be edited by the given user."""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
return True
|
||||
return False
|
||||
return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
|
||||
|
||||
def can_be_edited_by(self, user):
|
||||
"""Check if that object can be edited by the given user."""
|
||||
m = self.club.get_membership_for(user)
|
||||
if m and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
||||
return True
|
||||
return False
|
||||
return m and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]
|
||||
|
||||
def can_be_viewed_by(self, user):
|
||||
"""Check if that object can be viewed by the given user."""
|
||||
m = self.club.get_membership_for(user)
|
||||
if m and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
||||
return True
|
||||
return False
|
||||
return m and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]
|
||||
|
||||
def has_open_journal(self):
|
||||
for j in self.journals.all():
|
||||
if not j.closed:
|
||||
return True
|
||||
return False
|
||||
return self.journals.filter(closed=False).exists()
|
||||
|
||||
def get_open_journal(self):
|
||||
return self.journals.filter(closed=False).first()
|
||||
@ -228,17 +215,13 @@ class GeneralJournal(models.Model):
|
||||
return False
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
return True
|
||||
if self.club_account.can_be_edited_by(user):
|
||||
return True
|
||||
return False
|
||||
return self.club_account.can_be_edited_by(user)
|
||||
|
||||
def can_be_edited_by(self, user):
|
||||
"""Check if that object can be edited by the given user."""
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
return True
|
||||
if self.club_account.can_be_edited_by(user):
|
||||
return True
|
||||
return False
|
||||
return self.club_account.can_be_edited_by(user)
|
||||
|
||||
def can_be_viewed_by(self, user):
|
||||
return self.club_account.can_be_viewed_by(user)
|
||||
@ -416,9 +399,7 @@ class Operation(models.Model):
|
||||
if self.journal.closed:
|
||||
return False
|
||||
m = self.journal.club_account.club.get_membership_for(user)
|
||||
if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
||||
return True
|
||||
return False
|
||||
return m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]
|
||||
|
||||
def can_be_edited_by(self, user):
|
||||
"""Check if that object can be edited by the given user."""
|
||||
@ -427,9 +408,7 @@ class Operation(models.Model):
|
||||
if self.journal.closed:
|
||||
return False
|
||||
m = self.journal.club_account.club.get_membership_for(user)
|
||||
if m is not None and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
||||
return True
|
||||
return False
|
||||
return m is not None and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]
|
||||
|
||||
|
||||
class AccountingType(models.Model):
|
||||
@ -472,9 +451,7 @@ class AccountingType(models.Model):
|
||||
"""Check if that object can be edited by the given user."""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
return True
|
||||
return False
|
||||
return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
|
||||
|
||||
|
||||
class SimplifiedAccountingType(models.Model):
|
||||
|
@ -102,7 +102,7 @@ class TestOperation(TestCase):
|
||||
code="443", label="Ce code n'existe pas", movement_type="CREDIT"
|
||||
)
|
||||
at.save()
|
||||
l = Label.objects.create(club_account=self.journal.club_account, name="bob")
|
||||
label = Label.objects.create(club_account=self.journal.club_account, name="bob")
|
||||
self.client.force_login(User.objects.get(username="comptable"))
|
||||
self.op1 = Operation(
|
||||
journal=self.journal,
|
||||
@ -111,7 +111,7 @@ class TestOperation(TestCase):
|
||||
remark="Test bilan",
|
||||
mode="CASH",
|
||||
done=True,
|
||||
label=l,
|
||||
label=label,
|
||||
accounting_type=at,
|
||||
target_type="USER",
|
||||
target_id=self.skia.id,
|
||||
@ -124,7 +124,7 @@ class TestOperation(TestCase):
|
||||
remark="Test bilan",
|
||||
mode="CASH",
|
||||
done=True,
|
||||
label=l,
|
||||
label=label,
|
||||
accounting_type=at,
|
||||
target_type="USER",
|
||||
target_id=self.skia.id,
|
||||
|
@ -15,7 +15,41 @@
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from accounting.views import *
|
||||
from accounting.views import (
|
||||
AccountingTypeCreateView,
|
||||
AccountingTypeEditView,
|
||||
AccountingTypeListView,
|
||||
BankAccountCreateView,
|
||||
BankAccountDeleteView,
|
||||
BankAccountDetailView,
|
||||
BankAccountEditView,
|
||||
BankAccountListView,
|
||||
ClubAccountCreateView,
|
||||
ClubAccountDeleteView,
|
||||
ClubAccountDetailView,
|
||||
ClubAccountEditView,
|
||||
CompanyCreateView,
|
||||
CompanyEditView,
|
||||
CompanyListView,
|
||||
JournalAccountingStatementView,
|
||||
JournalCreateView,
|
||||
JournalDeleteView,
|
||||
JournalDetailView,
|
||||
JournalEditView,
|
||||
JournalNatureStatementView,
|
||||
JournalPersonStatementView,
|
||||
LabelCreateView,
|
||||
LabelDeleteView,
|
||||
LabelEditView,
|
||||
LabelListView,
|
||||
OperationCreateView,
|
||||
OperationEditView,
|
||||
OperationPDFView,
|
||||
RefoundAccountView,
|
||||
SimplifiedAccountingTypeCreateView,
|
||||
SimplifiedAccountingTypeEditView,
|
||||
SimplifiedAccountingTypeListView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
# Accounting types
|
||||
|
@ -182,7 +182,7 @@ class ClubAccountCreateView(CanCreateMixin, CreateView):
|
||||
|
||||
def get_initial(self):
|
||||
ret = super().get_initial()
|
||||
if "parent" in self.request.GET.keys():
|
||||
if "parent" in self.request.GET:
|
||||
obj = BankAccount.objects.filter(id=int(self.request.GET["parent"])).first()
|
||||
if obj is not None:
|
||||
ret["bank_account"] = obj.id
|
||||
@ -264,7 +264,7 @@ class JournalCreateView(CanCreateMixin, CreateView):
|
||||
|
||||
def get_initial(self):
|
||||
ret = super().get_initial()
|
||||
if "parent" in self.request.GET.keys():
|
||||
if "parent" in self.request.GET:
|
||||
obj = ClubAccount.objects.filter(id=int(self.request.GET["parent"])).first()
|
||||
if obj is not None:
|
||||
ret["club_account"] = obj.id
|
||||
@ -362,7 +362,7 @@ class OperationForm(forms.ModelForm):
|
||||
|
||||
def clean(self):
|
||||
self.cleaned_data = super().clean()
|
||||
if "target_type" in self.cleaned_data.keys():
|
||||
if "target_type" in self.cleaned_data:
|
||||
if (
|
||||
self.cleaned_data.get("user") is None
|
||||
and self.cleaned_data.get("club") is None
|
||||
@ -633,19 +633,17 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
||||
ret = collections.OrderedDict()
|
||||
statement = collections.OrderedDict()
|
||||
total_sum = 0
|
||||
for sat in [None] + list(
|
||||
SimplifiedAccountingType.objects.order_by("label").all()
|
||||
):
|
||||
for sat in [
|
||||
None,
|
||||
*list(SimplifiedAccountingType.objects.order_by("label")),
|
||||
]:
|
||||
amount = queryset.filter(
|
||||
accounting_type__movement_type=movement_type, simpleaccounting_type=sat
|
||||
).aggregate(amount_sum=Sum("amount"))["amount_sum"]
|
||||
if sat:
|
||||
sat = sat.label
|
||||
else:
|
||||
sat = ""
|
||||
label = sat.label if sat is not None else ""
|
||||
if amount:
|
||||
total_sum += amount
|
||||
statement[sat] = amount
|
||||
statement[label] = amount
|
||||
ret[movement_type] = statement
|
||||
ret[movement_type + "_sum"] = total_sum
|
||||
return ret
|
||||
@ -668,15 +666,12 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
||||
self.statement(self.object.operations.filter(label=None).all(), "DEBIT")
|
||||
)
|
||||
statement[_("No label operations")] = no_label_statement
|
||||
for l in labels:
|
||||
for label in labels:
|
||||
l_stmt = collections.OrderedDict()
|
||||
l_stmt.update(
|
||||
self.statement(self.object.operations.filter(label=l).all(), "CREDIT")
|
||||
)
|
||||
l_stmt.update(
|
||||
self.statement(self.object.operations.filter(label=l).all(), "DEBIT")
|
||||
)
|
||||
statement[l] = l_stmt
|
||||
journals = self.object.operations.filter(label=label).all()
|
||||
l_stmt.update(self.statement(journals, "CREDIT"))
|
||||
l_stmt.update(self.statement(journals, "DEBIT"))
|
||||
statement[label] = l_stmt
|
||||
return statement
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@ -798,7 +793,7 @@ class LabelCreateView(
|
||||
|
||||
def get_initial(self):
|
||||
ret = super().get_initial()
|
||||
if "parent" in self.request.GET.keys():
|
||||
if "parent" in self.request.GET:
|
||||
obj = ClubAccount.objects.filter(id=int(self.request.GET["parent"])).first()
|
||||
if obj is not None:
|
||||
ret["club_account"] = obj.id
|
||||
|
@ -7,7 +7,7 @@
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"ignore": ["core/static/vendored", "*.min.*", "staticfiles/generated"]
|
||||
"ignore": ["*.min.*", "staticfiles/generated"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
|
@ -111,8 +111,8 @@ class MailingForm(forms.Form):
|
||||
"""Convert given users into real users and check their validity."""
|
||||
cleaned_data = super().clean()
|
||||
users = []
|
||||
for user in cleaned_data["subscription_users"]:
|
||||
user = User.objects.filter(id=user).first()
|
||||
for user_id in cleaned_data["subscription_users"]:
|
||||
user = User.objects.filter(id=user_id).first()
|
||||
if not user:
|
||||
raise forms.ValidationError(
|
||||
_("One of the selected users doesn't exist"), code="invalid"
|
||||
@ -128,7 +128,7 @@ class MailingForm(forms.Form):
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
if not "action" in cleaned_data:
|
||||
if "action" not in cleaned_data:
|
||||
# If there is no action provided, we can stop here
|
||||
raise forms.ValidationError(_("An action is required"), code="invalid")
|
||||
|
||||
|
@ -389,9 +389,7 @@ class Membership(models.Model):
|
||||
if user.is_root or user.is_board_member:
|
||||
return True
|
||||
membership = self.club.get_membership_for(user)
|
||||
if membership is not None and membership.role >= self.role:
|
||||
return True
|
||||
return False
|
||||
return membership is not None and membership.role >= self.role
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
|
51
club/urls.py
51
club/urls.py
@ -24,7 +24,32 @@
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from club.views import *
|
||||
from club.views import (
|
||||
ClubCreateView,
|
||||
ClubEditPropView,
|
||||
ClubEditView,
|
||||
ClubListView,
|
||||
ClubMailingView,
|
||||
ClubMembersView,
|
||||
ClubOldMembersView,
|
||||
ClubPageEditView,
|
||||
ClubPageHistView,
|
||||
ClubRevView,
|
||||
ClubSellingCSVView,
|
||||
ClubSellingView,
|
||||
ClubStatView,
|
||||
ClubToolsView,
|
||||
ClubView,
|
||||
MailingAutoGenerationView,
|
||||
MailingDeleteView,
|
||||
MailingSubscriptionDeleteView,
|
||||
MembershipDeleteView,
|
||||
MembershipSetOldView,
|
||||
PosterCreateView,
|
||||
PosterDeleteView,
|
||||
PosterEditView,
|
||||
PosterListView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("", ClubListView.as_view(), name="club_list"),
|
||||
@ -32,32 +57,20 @@ urlpatterns = [
|
||||
path("stats/", ClubStatView.as_view(), name="club_stats"),
|
||||
path("<int:club_id>/", ClubView.as_view(), name="club_view"),
|
||||
path(
|
||||
"<int:club_id>/rev/<int:rev_id>/",
|
||||
ClubRevView.as_view(),
|
||||
name="club_view_rev",
|
||||
"<int:club_id>/rev/<int:rev_id>/", ClubRevView.as_view(), name="club_view_rev"
|
||||
),
|
||||
path("<int:club_id>/hist/", ClubPageHistView.as_view(), name="club_hist"),
|
||||
path("<int:club_id>/edit/", ClubEditView.as_view(), name="club_edit"),
|
||||
path(
|
||||
"<int:club_id>/edit/page/",
|
||||
ClubPageEditView.as_view(),
|
||||
name="club_edit_page",
|
||||
),
|
||||
path("<int:club_id>/edit/page/", ClubPageEditView.as_view(), name="club_edit_page"),
|
||||
path("<int:club_id>/members/", ClubMembersView.as_view(), name="club_members"),
|
||||
path(
|
||||
"<int:club_id>/elderlies/",
|
||||
ClubOldMembersView.as_view(),
|
||||
name="club_old_members",
|
||||
),
|
||||
path("<int:club_id>/sellings/", ClubSellingView.as_view(), name="club_sellings"),
|
||||
path(
|
||||
"<int:club_id>/sellings/",
|
||||
ClubSellingView.as_view(),
|
||||
name="club_sellings",
|
||||
),
|
||||
path(
|
||||
"<int:club_id>/sellings/csv/",
|
||||
ClubSellingCSVView.as_view(),
|
||||
name="sellings_csv",
|
||||
"<int:club_id>/sellings/csv/", ClubSellingCSVView.as_view(), name="sellings_csv"
|
||||
),
|
||||
path("<int:club_id>/prop/", ClubEditPropView.as_view(), name="club_prop"),
|
||||
path("<int:club_id>/tools/", ClubToolsView.as_view(), name="tools"),
|
||||
@ -89,9 +102,7 @@ urlpatterns = [
|
||||
),
|
||||
path("<int:club_id>/poster/", PosterListView.as_view(), name="poster_list"),
|
||||
path(
|
||||
"<int:club_id>/poster/create/",
|
||||
PosterCreateView.as_view(),
|
||||
name="poster_create",
|
||||
"<int:club_id>/poster/create/", PosterCreateView.as_view(), name="poster_create"
|
||||
),
|
||||
path(
|
||||
"<int:club_id>/poster/<int:poster_id>/edit/",
|
||||
|
@ -397,7 +397,8 @@ class ClubSellingCSVView(ClubSellingView):
|
||||
row.append(selling.customer.user.get_display_name())
|
||||
else:
|
||||
row.append("")
|
||||
row = row + [
|
||||
row = [
|
||||
*row,
|
||||
selling.label,
|
||||
selling.quantity,
|
||||
selling.quantity * selling.unit_price,
|
||||
@ -408,7 +409,7 @@ class ClubSellingCSVView(ClubSellingView):
|
||||
row.append(selling.product.purchase_price)
|
||||
row.append(selling.product.selling_price - selling.product.purchase_price)
|
||||
else:
|
||||
row = row + ["", "", ""]
|
||||
row = [*row, "", "", ""]
|
||||
return row
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
@ -622,9 +623,7 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
||||
def remove_subscription(self, cleaned_data):
|
||||
"""Remove specified users from a mailing list."""
|
||||
fields = [
|
||||
cleaned_data[key]
|
||||
for key in cleaned_data.keys()
|
||||
if key.startswith("removal_")
|
||||
val for key, val in cleaned_data.items() if key.startswith("removal_")
|
||||
]
|
||||
for field in fields:
|
||||
for sub in field:
|
||||
|
@ -15,7 +15,7 @@
|
||||
from django.contrib import admin
|
||||
from haystack.admin import SearchModelAdmin
|
||||
|
||||
from com.models import *
|
||||
from com.models import News, Poster, Screen, Sith, Weekmail
|
||||
|
||||
|
||||
@admin.register(News)
|
||||
|
53
com/urls.py
53
com/urls.py
@ -16,7 +16,36 @@
|
||||
from django.urls import path
|
||||
|
||||
from club.views import MailingDeleteView
|
||||
from com.views import *
|
||||
from com.views import (
|
||||
AlertMsgEditView,
|
||||
InfoMsgEditView,
|
||||
MailingListAdminView,
|
||||
MailingModerateView,
|
||||
NewsAdminListView,
|
||||
NewsCreateView,
|
||||
NewsDeleteView,
|
||||
NewsDetailView,
|
||||
NewsEditView,
|
||||
NewsListView,
|
||||
NewsModerateView,
|
||||
PosterCreateView,
|
||||
PosterDeleteView,
|
||||
PosterEditView,
|
||||
PosterListView,
|
||||
PosterModerateListView,
|
||||
PosterModerateView,
|
||||
ScreenCreateView,
|
||||
ScreenDeleteView,
|
||||
ScreenEditView,
|
||||
ScreenListView,
|
||||
ScreenSlideshowView,
|
||||
WeekmailArticleCreateView,
|
||||
WeekmailArticleDeleteView,
|
||||
WeekmailArticleEditView,
|
||||
WeekmailDestinationEditView,
|
||||
WeekmailEditView,
|
||||
WeekmailPreviewView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("sith/edit/alert/", AlertMsgEditView.as_view(), name="alert_edit"),
|
||||
@ -46,15 +75,9 @@ urlpatterns = [
|
||||
path("news/", NewsListView.as_view(), name="news_list"),
|
||||
path("news/admin/", NewsAdminListView.as_view(), name="news_admin_list"),
|
||||
path("news/create/", NewsCreateView.as_view(), name="news_new"),
|
||||
path("news/<int:news_id>/delete/", NewsDeleteView.as_view(), name="news_delete"),
|
||||
path(
|
||||
"news/<int:news_id>/delete/",
|
||||
NewsDeleteView.as_view(),
|
||||
name="news_delete",
|
||||
),
|
||||
path(
|
||||
"news/<int:news_id>/moderate/",
|
||||
NewsModerateView.as_view(),
|
||||
name="news_moderate",
|
||||
"news/<int:news_id>/moderate/", NewsModerateView.as_view(), name="news_moderate"
|
||||
),
|
||||
path("news/<int:news_id>/edit/", NewsEditView.as_view(), name="news_edit"),
|
||||
path("news/<int:news_id>/", NewsDetailView.as_view(), name="news_detail"),
|
||||
@ -71,11 +94,7 @@ urlpatterns = [
|
||||
),
|
||||
path("poster/", PosterListView.as_view(), name="poster_list"),
|
||||
path("poster/create/", PosterCreateView.as_view(), name="poster_create"),
|
||||
path(
|
||||
"poster/<int:poster_id>/edit/",
|
||||
PosterEditView.as_view(),
|
||||
name="poster_edit",
|
||||
),
|
||||
path("poster/<int:poster_id>/edit/", PosterEditView.as_view(), name="poster_edit"),
|
||||
path(
|
||||
"poster/<int:poster_id>/delete/",
|
||||
PosterDeleteView.as_view(),
|
||||
@ -98,11 +117,7 @@ urlpatterns = [
|
||||
ScreenSlideshowView.as_view(),
|
||||
name="screen_slideshow",
|
||||
),
|
||||
path(
|
||||
"screen/<int:screen_id>/edit/",
|
||||
ScreenEditView.as_view(),
|
||||
name="screen_edit",
|
||||
),
|
||||
path("screen/<int:screen_id>/edit/", ScreenEditView.as_view(), name="screen_edit"),
|
||||
path(
|
||||
"screen/<int:screen_id>/delete/",
|
||||
ScreenDeleteView.as_view(),
|
||||
|
47
com/views.py
47
com/views.py
@ -86,12 +86,11 @@ class PosterForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.user = kwargs.pop("user", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.user:
|
||||
if not self.user.is_com_admin:
|
||||
self.fields["club"].queryset = Club.objects.filter(
|
||||
id__in=self.user.clubs_with_rights
|
||||
)
|
||||
self.fields.pop("display_time")
|
||||
if self.user and not self.user.is_com_admin:
|
||||
self.fields["club"].queryset = Club.objects.filter(
|
||||
id__in=self.user.clubs_with_rights
|
||||
)
|
||||
self.fields.pop("display_time")
|
||||
|
||||
|
||||
class ComTabsMixin(TabedViewMixin):
|
||||
@ -312,7 +311,7 @@ class NewsCreateView(CanCreateMixin, CreateView):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.get_form()
|
||||
if form.is_valid() and "preview" not in request.POST.keys():
|
||||
if form.is_valid() and "preview" not in request.POST:
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
self.object = form.instance
|
||||
@ -354,13 +353,13 @@ class NewsModerateView(CanEditMixin, SingleObjectMixin):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
if "remove" in request.GET.keys():
|
||||
if "remove" in request.GET:
|
||||
self.object.is_moderated = False
|
||||
else:
|
||||
self.object.is_moderated = True
|
||||
self.object.moderator = request.user
|
||||
self.object.save()
|
||||
if "next" in self.request.GET.keys():
|
||||
if "next" in self.request.GET:
|
||||
return redirect(self.request.GET["next"])
|
||||
return redirect("com:news_admin_list")
|
||||
|
||||
@ -424,7 +423,7 @@ class WeekmailPreviewView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, Detai
|
||||
try:
|
||||
self.object.send() # This should fail
|
||||
except SMTPRecipientsRefused as e:
|
||||
users = User.objects.filter(email__in=e.recipients.keys())
|
||||
users = User.objects.filter(email__in=e.recipients)
|
||||
for u in users:
|
||||
u.preferences.receive_weekmail = False
|
||||
u.preferences.save()
|
||||
@ -471,7 +470,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
if "up_article" in request.GET.keys():
|
||||
if "up_article" in request.GET:
|
||||
art = get_object_or_404(
|
||||
WeekmailArticle, id=request.GET["up_article"], weekmail=self.object
|
||||
)
|
||||
@ -483,7 +482,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
|
||||
art.save()
|
||||
prev_art.save()
|
||||
self.quick_notif_list += ["qn_success"]
|
||||
if "down_article" in request.GET.keys():
|
||||
if "down_article" in request.GET:
|
||||
art = get_object_or_404(
|
||||
WeekmailArticle, id=request.GET["down_article"], weekmail=self.object
|
||||
)
|
||||
@ -495,7 +494,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
|
||||
art.save()
|
||||
next_art.save()
|
||||
self.quick_notif_list += ["qn_success"]
|
||||
if "add_article" in request.GET.keys():
|
||||
if "add_article" in request.GET:
|
||||
art = get_object_or_404(
|
||||
WeekmailArticle, id=request.GET["add_article"], weekmail=None
|
||||
)
|
||||
@ -504,7 +503,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
|
||||
art.rank += 1
|
||||
art.save()
|
||||
self.quick_notif_list += ["qn_success"]
|
||||
if "del_article" in request.GET.keys():
|
||||
if "del_article" in request.GET:
|
||||
art = get_object_or_404(
|
||||
WeekmailArticle, id=request.GET["del_article"], weekmail=self.object
|
||||
)
|
||||
@ -571,7 +570,7 @@ class WeekmailArticleCreateView(QuickNotifMixin, CreateView):
|
||||
)
|
||||
),
|
||||
)
|
||||
if form.is_valid() and not "preview" in request.POST.keys():
|
||||
if form.is_valid() and "preview" not in request.POST:
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
@ -689,19 +688,13 @@ class PosterEditBaseView(UpdateView):
|
||||
template_name = "com/poster_edit.jinja"
|
||||
|
||||
def get_initial(self):
|
||||
init = {}
|
||||
try:
|
||||
init["date_begin"] = self.object.date_begin.strftime("%Y-%m-%d %H:%M:%S")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
init["date_end"] = self.object.date_end.strftime("%Y-%m-%d %H:%M:%S")
|
||||
except Exception:
|
||||
pass
|
||||
return init
|
||||
return {
|
||||
"date_begin": self.object.date_begin.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"date_end": self.object.date_end.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
}
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if "club_id" in kwargs and kwargs["club_id"]:
|
||||
if kwargs.get("club_id"):
|
||||
try:
|
||||
self.club = Club.objects.get(pk=kwargs["club_id"])
|
||||
except Club.DoesNotExist as e:
|
||||
@ -737,7 +730,7 @@ class PosterDeleteBaseView(DeleteView):
|
||||
template_name = "core/delete_confirm.jinja"
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if "club_id" in kwargs and kwargs["club_id"]:
|
||||
if kwargs.get("club_id"):
|
||||
try:
|
||||
self.club = Club.objects.get(pk=kwargs["club_id"])
|
||||
except Club.DoesNotExist as e:
|
||||
|
@ -1,117 +0,0 @@
|
||||
import re
|
||||
from subprocess import PIPE, Popen, TimeoutExpired
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
# see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
|
||||
# added "v?"
|
||||
# Please note that this does not match the version of the three.js library.
|
||||
# Hence, you shall have to check this one by yourself
|
||||
semver_regex = re.compile(
|
||||
r"^v?"
|
||||
r"(?P<major>\d+)"
|
||||
r"\.(?P<minor>\d+)"
|
||||
r"\.(?P<patch>\d+)"
|
||||
r"(?:-(?P<prerelease>(?:\d+|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:\d+|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?"
|
||||
r"(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
|
||||
)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Checks the front dependencies are up to date."
|
||||
|
||||
def handle(self, *args, **options):
|
||||
deps = settings.SITH_FRONT_DEP_VERSIONS
|
||||
|
||||
processes = {
|
||||
url: create_process(url)
|
||||
for url in deps.keys()
|
||||
if parse_semver(deps[url]) is not None
|
||||
}
|
||||
|
||||
for url, process in processes.items():
|
||||
try:
|
||||
stdout, stderr = process.communicate(timeout=15)
|
||||
except TimeoutExpired:
|
||||
process.kill()
|
||||
self.stderr.write(self.style.WARNING("{}: timeout".format(url)))
|
||||
continue
|
||||
# error, notice, warning
|
||||
|
||||
stdout = stdout.decode("utf-8")
|
||||
stderr = stderr.decode("utf-8")
|
||||
|
||||
if stderr != "":
|
||||
self.stderr.write(self.style.WARNING(stderr.strip()))
|
||||
continue
|
||||
|
||||
# get all tags, parse them as semvers and find the biggest
|
||||
tags = list_tags(stdout)
|
||||
tags = map(parse_semver, tags)
|
||||
tags = filter(lambda tag: tag is not None, tags)
|
||||
latest_version = max(tags)
|
||||
|
||||
# cannot fail as those which fail are filtered in the processes dict creation
|
||||
current_version = parse_semver(deps[url])
|
||||
assert current_version is not None
|
||||
|
||||
if latest_version == current_version:
|
||||
msg = "{}: {}".format(url, semver_to_s(current_version))
|
||||
self.stdout.write(self.style.SUCCESS(msg))
|
||||
else:
|
||||
msg = "{}: {} < {}".format(
|
||||
url, semver_to_s(current_version), semver_to_s(latest_version)
|
||||
)
|
||||
self.stdout.write(self.style.ERROR(msg))
|
||||
|
||||
|
||||
def create_process(url):
|
||||
"""Spawn a "git ls-remote --tags" child process."""
|
||||
return Popen(["git", "ls-remote", "--tags", url], stdout=PIPE, stderr=PIPE)
|
||||
|
||||
|
||||
def list_tags(s):
|
||||
"""Parses "git ls-remote --tags" output. Takes a string."""
|
||||
tag_prefix = "refs/tags/"
|
||||
|
||||
for line in s.strip().split("\n"):
|
||||
# an example line could be:
|
||||
# "1f41e2293f9c3c1962d2d97afa666207b98a222a\trefs/tags/foo"
|
||||
parts = line.split("\t")
|
||||
|
||||
# check we have a commit ID (SHA-1 hash) and a tag name
|
||||
assert len(parts) == 2
|
||||
assert len(parts[0]) == 40
|
||||
assert parts[1].startswith(tag_prefix)
|
||||
|
||||
# avoid duplicates (a peeled tag will appear twice: as "name" and as "name^{}")
|
||||
if not parts[1].endswith("^{}"):
|
||||
yield parts[1][len(tag_prefix) :]
|
||||
|
||||
|
||||
def parse_semver(s) -> tuple[int, int, int] | None:
|
||||
"""Parse a semver string.
|
||||
|
||||
See https://semver.org
|
||||
|
||||
Returns:
|
||||
A tuple, if the parsing was successful, else None.
|
||||
In the latter case, it must probably be a prerelease
|
||||
or include build metadata.
|
||||
"""
|
||||
m = semver_regex.match(s)
|
||||
|
||||
if (
|
||||
m is None
|
||||
or m.group("prerelease") is not None
|
||||
or m.group("buildmetadata") is not None
|
||||
):
|
||||
return None
|
||||
|
||||
return int(m.group("major")), int(m.group("minor")), int(m.group("patch"))
|
||||
|
||||
|
||||
def semver_to_s(t):
|
||||
"""Expects a 3-tuple with ints and turns it into a string of type "1.2.3"."""
|
||||
return "{}.{}.{}".format(t[0], t[1], t[2])
|
@ -67,5 +67,6 @@ class Command(BaseCommand):
|
||||
subprocess.run(
|
||||
[str(Path(__file__).parent / "install_xapian.sh"), desired],
|
||||
env=dict(os.environ),
|
||||
).check_returncode()
|
||||
check=True,
|
||||
)
|
||||
self.stdout.write("Installation success")
|
||||
|
@ -934,7 +934,7 @@ Welcome to the wiki page!
|
||||
# Adding subscription for sli
|
||||
s = Subscription(
|
||||
member=User.objects.filter(pk=sli.pk).first(),
|
||||
subscription_type=list(settings.SITH_SUBSCRIPTIONS.keys())[0],
|
||||
subscription_type=next(iter(settings.SITH_SUBSCRIPTIONS.keys())),
|
||||
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],
|
||||
)
|
||||
s.subscription_start = s.compute_start()
|
||||
@ -947,7 +947,7 @@ Welcome to the wiki page!
|
||||
# Adding subscription for Krophil
|
||||
s = Subscription(
|
||||
member=User.objects.filter(pk=krophil.pk).first(),
|
||||
subscription_type=list(settings.SITH_SUBSCRIPTIONS.keys())[0],
|
||||
subscription_type=next(iter(settings.SITH_SUBSCRIPTIONS.keys())),
|
||||
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],
|
||||
)
|
||||
s.subscription_start = s.compute_start()
|
||||
|
@ -217,9 +217,9 @@ class Command(BaseCommand):
|
||||
UV.objects.bulk_create(uvs, ignore_conflicts=True)
|
||||
|
||||
def create_products(self):
|
||||
categories = []
|
||||
for _ in range(10):
|
||||
categories.append(ProductType(name=self.faker.text(max_nb_chars=30)))
|
||||
categories = [
|
||||
ProductType(name=self.faker.text(max_nb_chars=30)) for _ in range(10)
|
||||
]
|
||||
ProductType.objects.bulk_create(categories)
|
||||
categories = list(
|
||||
ProductType.objects.filter(name__in=[c.name for c in categories])
|
||||
@ -254,16 +254,16 @@ class Command(BaseCommand):
|
||||
archived=bool(random.random() > 0.7),
|
||||
)
|
||||
products.append(product)
|
||||
for group in random.sample(groups, k=random.randint(0, 3)):
|
||||
# there will be products without buying groups
|
||||
# but there are also such products in the real database
|
||||
buying_groups.append(
|
||||
Product.buying_groups.through(product=product, group=group)
|
||||
)
|
||||
for counter in random.sample(counters, random.randint(0, 4)):
|
||||
selling_places.append(
|
||||
Counter.products.through(counter=counter, product=product)
|
||||
)
|
||||
# there will be products without buying groups
|
||||
# but there are also such products in the real database
|
||||
buying_groups.extend(
|
||||
Product.buying_groups.through(product=product, group=group)
|
||||
for group in random.sample(groups, k=random.randint(0, 3))
|
||||
)
|
||||
selling_places.extend(
|
||||
Counter.products.through(counter=counter, product=product)
|
||||
for counter in random.sample(counters, random.randint(0, 4))
|
||||
)
|
||||
Product.objects.bulk_create(products)
|
||||
Product.buying_groups.through.objects.bulk_create(buying_groups)
|
||||
Counter.products.through.objects.bulk_create(selling_places)
|
||||
|
@ -174,7 +174,7 @@ def validate_promo(value: int) -> None:
|
||||
)
|
||||
|
||||
|
||||
def get_group(*, pk: int = None, name: str = None) -> Group | None:
|
||||
def get_group(*, pk: int | None = None, name: str | None = None) -> Group | None:
|
||||
"""Search for a group by its primary key or its name.
|
||||
Either one of the two must be set.
|
||||
|
||||
@ -445,7 +445,7 @@ class User(AbstractBaseUser):
|
||||
else:
|
||||
return 0
|
||||
|
||||
def is_in_group(self, *, pk: int = None, name: str = None) -> bool:
|
||||
def is_in_group(self, *, pk: int | None = None, name: str | None = None) -> bool:
|
||||
"""Check if this user is in the given group.
|
||||
Either a group id or a group name must be provided.
|
||||
If both are passed, only the id will be considered.
|
||||
@ -649,7 +649,7 @@ class User(AbstractBaseUser):
|
||||
continue
|
||||
links = list(User.godfathers.through.objects.filter(**{key: self.id}))
|
||||
res.extend(links)
|
||||
for _ in range(1, depth):
|
||||
for _ in range(1, depth): # noqa: F402 we don't care about gettext here
|
||||
ids = [getattr(c, reverse_key) for c in links]
|
||||
links = list(
|
||||
User.godfathers.through.objects.filter(
|
||||
@ -703,9 +703,7 @@ class User(AbstractBaseUser):
|
||||
return True
|
||||
if hasattr(obj, "owner_group") and self.is_in_group(pk=obj.owner_group.id):
|
||||
return True
|
||||
if self.is_root:
|
||||
return True
|
||||
return False
|
||||
return self.is_root
|
||||
|
||||
def can_edit(self, obj):
|
||||
"""Determine if the object can be edited by the user."""
|
||||
@ -717,9 +715,7 @@ class User(AbstractBaseUser):
|
||||
return True
|
||||
if isinstance(obj, User) and obj == self:
|
||||
return True
|
||||
if self.is_owner(obj):
|
||||
return True
|
||||
return False
|
||||
return self.is_owner(obj)
|
||||
|
||||
def can_view(self, obj):
|
||||
"""Determine if the object can be viewed by the user."""
|
||||
@ -729,9 +725,7 @@ class User(AbstractBaseUser):
|
||||
for pk in obj.view_groups.values_list("pk", flat=True):
|
||||
if self.is_in_group(pk=pk):
|
||||
return True
|
||||
if self.can_edit(obj):
|
||||
return True
|
||||
return False
|
||||
return self.can_edit(obj)
|
||||
|
||||
def can_be_edited_by(self, user):
|
||||
return user.is_root or user.is_board_member
|
||||
@ -759,23 +753,17 @@ class User(AbstractBaseUser):
|
||||
|
||||
@cached_property
|
||||
def preferences(self):
|
||||
try:
|
||||
if hasattr(self, "_preferences"):
|
||||
return self._preferences
|
||||
except:
|
||||
prefs = Preferences(user=self)
|
||||
prefs.save()
|
||||
return prefs
|
||||
return Preferences.objects.create(user=self)
|
||||
|
||||
@cached_property
|
||||
def forum_infos(self):
|
||||
try:
|
||||
if hasattr(self, "_forum_infos"):
|
||||
return self._forum_infos
|
||||
except:
|
||||
from forum.models import ForumUserInfo
|
||||
from forum.models import ForumUserInfo
|
||||
|
||||
infos = ForumUserInfo(user=self)
|
||||
infos.save()
|
||||
return infos
|
||||
return ForumUserInfo.objects.create(user=self)
|
||||
|
||||
@cached_property
|
||||
def clubs_with_rights(self) -> list[Club]:
|
||||
@ -840,7 +828,7 @@ class AnonymousUser(AuthAnonymousUser):
|
||||
def favorite_topics(self):
|
||||
raise PermissionDenied
|
||||
|
||||
def is_in_group(self, *, pk: int = None, name: str = None) -> bool:
|
||||
def is_in_group(self, *, pk: int | None = None, name: str | None = None) -> bool:
|
||||
"""The anonymous user is only in the public group."""
|
||||
allowed_id = settings.SITH_GROUP_PUBLIC_ID
|
||||
if pk is not None:
|
||||
@ -867,9 +855,7 @@ class AnonymousUser(AuthAnonymousUser):
|
||||
and obj.view_groups.filter(id=settings.SITH_GROUP_PUBLIC_ID).exists()
|
||||
):
|
||||
return True
|
||||
if hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self):
|
||||
return True
|
||||
return False
|
||||
return hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self)
|
||||
|
||||
def get_display_name(self):
|
||||
return _("Visitor")
|
||||
@ -1070,7 +1056,7 @@ class SithFile(models.Model):
|
||||
]:
|
||||
self.file.delete()
|
||||
self.file = None
|
||||
except:
|
||||
except: # noqa E722 I don't know the exception that can be raised
|
||||
self.file = None
|
||||
self.mime_type = "inode/directory"
|
||||
if self.is_file and (self.file is None or self.file == ""):
|
||||
@ -1196,12 +1182,12 @@ class SithFile(models.Model):
|
||||
return Album.objects.filter(id=self.id).first()
|
||||
|
||||
def get_parent_list(self):
|
||||
l = []
|
||||
p = self.parent
|
||||
while p is not None:
|
||||
l.append(p)
|
||||
p = p.parent
|
||||
return l
|
||||
parents = []
|
||||
current = self.parent
|
||||
while current is not None:
|
||||
parents.append(current)
|
||||
current = current.parent
|
||||
return parents
|
||||
|
||||
def get_parent_path(self):
|
||||
return "/" + "/".join([p.name for p in self.get_parent_list()[::-1]])
|
||||
@ -1359,22 +1345,18 @@ class Page(models.Model):
|
||||
if hasattr(self, "club") and self.club.can_be_edited_by(user):
|
||||
# Override normal behavior for clubs
|
||||
return True
|
||||
if self.name == settings.SITH_CLUB_ROOT_PAGE and user.is_board_member:
|
||||
return True
|
||||
return False
|
||||
return self.name == settings.SITH_CLUB_ROOT_PAGE and user.is_board_member
|
||||
|
||||
def can_be_viewed_by(self, user):
|
||||
if self.is_club_page:
|
||||
return True
|
||||
return False
|
||||
return self.is_club_page
|
||||
|
||||
def get_parent_list(self):
|
||||
l = []
|
||||
p = self.parent
|
||||
while p is not None:
|
||||
l.append(p)
|
||||
p = p.parent
|
||||
return l
|
||||
parents = []
|
||||
current = self.parent
|
||||
while current is not None:
|
||||
parents.append(current)
|
||||
current = current.parent
|
||||
return parents
|
||||
|
||||
def is_locked(self):
|
||||
"""Is True if the page is locked, False otherwise.
|
||||
@ -1386,7 +1368,6 @@ class Page(models.Model):
|
||||
if self.lock_timeout and (
|
||||
timezone.now() - self.lock_timeout > timedelta(minutes=5)
|
||||
):
|
||||
# print("Lock timed out")
|
||||
self.unset_lock()
|
||||
return (
|
||||
self.lock_user
|
||||
@ -1401,7 +1382,6 @@ class Page(models.Model):
|
||||
self.lock_user = user
|
||||
self.lock_timeout = timezone.now()
|
||||
super().save()
|
||||
# print("Locking page")
|
||||
|
||||
def set_lock_recursive(self, user):
|
||||
"""Locks recursively all the child pages for editing properties."""
|
||||
@ -1420,7 +1400,6 @@ class Page(models.Model):
|
||||
self.lock_user = None
|
||||
self.lock_timeout = None
|
||||
super().save()
|
||||
# print("Unlocking page")
|
||||
|
||||
def get_lock(self):
|
||||
"""Returns the page's mutex containing the time and the user in a dict."""
|
||||
@ -1435,13 +1414,11 @@ class Page(models.Model):
|
||||
"""
|
||||
if self.parent is None:
|
||||
return self.name
|
||||
return "/".join([self.parent.get_full_name(), self.name])
|
||||
return f"{self.parent.get_full_name()}/{self.name}"
|
||||
|
||||
def get_display_name(self):
|
||||
try:
|
||||
return self.revisions.last().title
|
||||
except:
|
||||
return self.name
|
||||
rev = self.revisions.last()
|
||||
return rev.title if rev is not None else self.name
|
||||
|
||||
@cached_property
|
||||
def is_club_page(self):
|
||||
|
@ -1,265 +0,0 @@
|
||||
/**
|
||||
* Builders to use Select2 in our templates.
|
||||
*
|
||||
* This comes with two flavours : local data or remote data.
|
||||
*
|
||||
* # Local data source
|
||||
*
|
||||
* To use local data source, you must define an array
|
||||
* in your JS code, having the fields `id` and `text`.
|
||||
*
|
||||
* ```js
|
||||
* const data = [
|
||||
* {id: 1, text: "foo"},
|
||||
* {id: 2, text: "bar"},
|
||||
* ];
|
||||
* document.addEventListener("DOMContentLoaded", () => sithSelect2({
|
||||
* element: document.getElementById("select2-input"),
|
||||
* dataSource: localDataSource(data)
|
||||
* }));
|
||||
* ```
|
||||
*
|
||||
* You can also define a callback that return ids to exclude :
|
||||
*
|
||||
* ```js
|
||||
* const data = [
|
||||
* {id: 1, text: "foo"},
|
||||
* {id: 2, text: "bar"},
|
||||
* {id: 3, text: "to exclude"},
|
||||
* ];
|
||||
* document.addEventListener("DOMContentLoaded", () => sithSelect2({
|
||||
* element: document.getElementById("select2-input"),
|
||||
* dataSource: localDataSource(data, {
|
||||
* excluded: () => data.filter((i) => i.text === "to exclude").map((i) => parseInt(i))
|
||||
* })
|
||||
* }));
|
||||
* ```
|
||||
*
|
||||
* # Remote data source
|
||||
*
|
||||
* Select2 with remote data sources are similar to those with local
|
||||
* data, but with some more parameters, like `resultConverter`,
|
||||
* which takes a callback that must return a `Select2Object`.
|
||||
*
|
||||
* ```js
|
||||
* document.addEventListener("DOMContentLoaded", () => sithSelect2({
|
||||
* element: document.getElementById("select2-input"),
|
||||
* dataSource: remoteDataSource("/api/user/search", {
|
||||
* excluded: () => [1, 2], // exclude users 1 and 2 from the search
|
||||
* resultConverter: (user) => Object({id: user.id, text: user.firstName})
|
||||
* })
|
||||
* }));
|
||||
* ```
|
||||
*
|
||||
* # Overrides
|
||||
*
|
||||
* Dealing with a select2 may be complex.
|
||||
* That's why, when defining a select,
|
||||
* you may add an override parameter,
|
||||
* in which you can declare any parameter defined in the
|
||||
* Select2 documentation.
|
||||
*
|
||||
* ```js
|
||||
* document.addEventListener("DOMContentLoaded", () => sithSelect2({
|
||||
* element: document.getElementById("select2-input"),
|
||||
* dataSource: remoteDataSource("/api/user/search", {
|
||||
* resultConverter: (user) => Object({id: user.id, text: user.firstName}),
|
||||
* overrides: {
|
||||
* delay: 500
|
||||
* }
|
||||
* })
|
||||
* }));
|
||||
* ```
|
||||
*
|
||||
* # Caveats with exclude
|
||||
*
|
||||
* With local data source, select2 evaluates the data only once.
|
||||
* Thus, modify the exclude after the initialisation is a no-op.
|
||||
*
|
||||
* With remote data source, the exclude list will be evaluated
|
||||
* after each api response.
|
||||
* It makes it possible to bind the data returned by the callback
|
||||
* to some reactive data, thus making the exclude list dynamic.
|
||||
*
|
||||
* # Images
|
||||
*
|
||||
* Sometimes, you would like to display an image besides
|
||||
* the text on the select items.
|
||||
* In this case, fill the `pictureGetter` option :
|
||||
*
|
||||
* ```js
|
||||
* document.addEventListener("DOMContentLoaded", () => sithSelect2({
|
||||
* element: document.getElementById("select2-input"),
|
||||
* dataSource: remoteDataSource("/api/user/search", {
|
||||
* resultConverter: (user) => Object({id: user.id, text: user.firstName})
|
||||
* })
|
||||
* pictureGetter: (user) => user.profilePict,
|
||||
* }));
|
||||
* ```
|
||||
*
|
||||
* # Binding with alpine
|
||||
*
|
||||
* You can declare your select2 component in an Alpine data.
|
||||
*
|
||||
* ```html
|
||||
* <body>
|
||||
* <div x-data="select2_test">
|
||||
* <select x-ref="search" x-ref="select"></select>
|
||||
* <p x-text="currentSelection.id"></p>
|
||||
* <p x-text="currentSelection.text"></p>
|
||||
* </div>
|
||||
* </body>
|
||||
*
|
||||
* <script>
|
||||
* document.addEventListener("alpine:init", () => {
|
||||
* Alpine.data("select2_test", () => ({
|
||||
* selector: undefined,
|
||||
* currentSelect: {id: "", text: ""},
|
||||
*
|
||||
* init() {
|
||||
* this.selector = sithSelect2({
|
||||
* element: $(this.$refs.select),
|
||||
* dataSource: localDataSource(
|
||||
* [{id: 1, text: "foo"}, {id: 2, text: "bar"}]
|
||||
* ),
|
||||
* });
|
||||
* this.selector.on("select2:select", (event) => {
|
||||
* // select2 => Alpine signals here
|
||||
* this.currentSelect = this.selector.select2("data")
|
||||
* });
|
||||
* this.$watch("currentSelected" (value) => {
|
||||
* // Alpine => select2 signals here
|
||||
* });
|
||||
* },
|
||||
* }));
|
||||
* })
|
||||
* </script>
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef Select2Object
|
||||
* @property {number} id
|
||||
* @property {string} text
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef Select2Options
|
||||
* @property {Element} element
|
||||
* @property {Object} dataSource
|
||||
* the data source, built with `localDataSource` or `remoteDataSource`
|
||||
* @property {number[]} excluded A list of ids to exclude from search
|
||||
* @property {undefined | function(Object): string} pictureGetter
|
||||
* A callback to get the picture field from the API response
|
||||
* @property {Object | undefined} overrides
|
||||
* Any other select2 parameter to apply on the config
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Select2Options} options
|
||||
*/
|
||||
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
|
||||
function sithSelect2(options) {
|
||||
const elem = $(options.element);
|
||||
return elem.select2({
|
||||
theme: elem[0].multiple ? "classic" : "default",
|
||||
minimumInputLength: 2,
|
||||
templateResult: selectItemBuilder(options.pictureGetter),
|
||||
...options.dataSource,
|
||||
...(options.overrides || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef LocalSourceOptions
|
||||
* @property {undefined | function(): number[]} excluded
|
||||
* A callback to the ids to exclude from the search
|
||||
*/
|
||||
|
||||
/**
|
||||
* Build a data source for a Select2 from a local array
|
||||
* @param {Select2Object[]} source The array containing the data
|
||||
* @param {RemoteSourceOptions} options
|
||||
*/
|
||||
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
|
||||
function localDataSource(source, options) {
|
||||
if (options.excluded) {
|
||||
const ids = options.excluded();
|
||||
return { data: source.filter((i) => !ids.includes(i.id)) };
|
||||
}
|
||||
return { data: source };
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef RemoteSourceOptions
|
||||
* @property {undefined | function(): number[]} excluded
|
||||
* A callback to the ids to exclude from the search
|
||||
* @property {undefined | function(): Select2Object} resultConverter
|
||||
* A converter for a value coming from the remote api
|
||||
* @property {undefined | Object} overrides
|
||||
* Any other select2 parameter to apply on the config
|
||||
*/
|
||||
|
||||
/**
|
||||
* Build a data source for a Select2 from a remote url
|
||||
* @param {string} source The url of the endpoint
|
||||
* @param {RemoteSourceOptions} options
|
||||
*/
|
||||
|
||||
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
|
||||
function remoteDataSource(source, options) {
|
||||
jQuery.ajaxSettings.traditional = true;
|
||||
const params = {
|
||||
url: source,
|
||||
dataType: "json",
|
||||
cache: true,
|
||||
delay: 250,
|
||||
data: function (params) {
|
||||
return {
|
||||
search: params.term,
|
||||
exclude: [
|
||||
...(this.val() || []).map((i) => Number.parseInt(i)),
|
||||
...(options.excluded ? options.excluded() : []),
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
if (options.resultConverter) {
|
||||
params.processResults = (data) => ({
|
||||
results: data.results.map(options.resultConverter),
|
||||
});
|
||||
}
|
||||
if (options.overrides) {
|
||||
Object.assign(params, options.overrides);
|
||||
}
|
||||
return { ajax: params };
|
||||
}
|
||||
|
||||
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
|
||||
function itemFormatter(user) {
|
||||
if (user.loading) {
|
||||
return user.text;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a function to display the results
|
||||
* @param {null | function(Object):string} pictureGetter
|
||||
* @return {function(string): jQuery|HTMLElement}
|
||||
*/
|
||||
function selectItemBuilder(pictureGetter) {
|
||||
return (item) => {
|
||||
const picture = typeof pictureGetter === "function" ? pictureGetter(item) : null;
|
||||
const imgHtml = picture
|
||||
? `<img
|
||||
src="${pictureGetter(item)}"
|
||||
alt="${item.text}"
|
||||
onerror="this.src = '/static/core/img/unknown.jpg'"
|
||||
/>`
|
||||
: "";
|
||||
|
||||
return $(`<div class="select-item">
|
||||
${imgHtml}
|
||||
<span class="select-item-text">${item.text}</span>
|
||||
</div>`);
|
||||
};
|
||||
}
|
@ -28,6 +28,7 @@ input[type="file"] {
|
||||
font-size: 1.2em;
|
||||
border-radius: 5px;
|
||||
color: black;
|
||||
|
||||
&:hover {
|
||||
background: hsl(0, 0%, 83%);
|
||||
}
|
||||
@ -63,6 +64,7 @@ textarea[type="text"],
|
||||
border-radius: 5px;
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
textarea {
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
@ -72,6 +74,7 @@ textarea {
|
||||
border-radius: 5px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
select {
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
@ -85,9 +88,11 @@ select {
|
||||
a:not(.button) {
|
||||
text-decoration: none;
|
||||
color: $primary-dark-color;
|
||||
|
||||
&:hover {
|
||||
color: $primary-light-color;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: $primary-color;
|
||||
}
|
||||
@ -116,7 +121,9 @@ a:not(.button) {
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
100% { transform: rotate(360deg); }
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.ib {
|
||||
@ -143,11 +150,13 @@ a:not(.button) {
|
||||
|
||||
.collapse-header-icon {
|
||||
transition: all ease-in-out 150ms;
|
||||
|
||||
&.reverse {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-body {
|
||||
padding: 10px;
|
||||
}
|
||||
@ -202,9 +211,11 @@ a:not(.button) {
|
||||
font-size: 0.9em;
|
||||
margin: 0.2em;
|
||||
border-radius: 0.6em;
|
||||
|
||||
.markdown {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
&:before {
|
||||
font-family: FontAwesome;
|
||||
font-size: 4em;
|
||||
@ -212,15 +223,19 @@ a:not(.button) {
|
||||
margin: 0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
#info_box {
|
||||
background: $primary-neutral-light-color;
|
||||
|
||||
&:before {
|
||||
content: "\f05a";
|
||||
color: hsl(210, 100%, 56%);
|
||||
}
|
||||
}
|
||||
|
||||
#alert_box {
|
||||
background: $second-color;
|
||||
|
||||
&:before {
|
||||
content: "\f06a";
|
||||
color: $white-color;
|
||||
@ -240,7 +255,8 @@ a:not(.button) {
|
||||
#page {
|
||||
width: 90%;
|
||||
margin: 20px auto 0;
|
||||
/*---------------------------------NAV---------------------------------*/
|
||||
|
||||
/*---------------------------------NAV---------------------------------*/
|
||||
.btn {
|
||||
font-size: 15px;
|
||||
font-weight: normal;
|
||||
@ -252,9 +268,11 @@ a:not(.button) {
|
||||
|
||||
&.btn-blue {
|
||||
background-color: $deepblue;
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background-color: darken($deepblue, 10%);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: rgba(70, 90, 126, 0.4);
|
||||
}
|
||||
@ -262,9 +280,11 @@ a:not(.button) {
|
||||
|
||||
&.btn-grey {
|
||||
background-color: grey;
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background-color: darken(gray, 15%);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: lighten(gray, 15%);
|
||||
}
|
||||
@ -273,9 +293,11 @@ a:not(.button) {
|
||||
&.btn-red {
|
||||
background-color: #fc8181;
|
||||
color: black;
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background-color: darken(#fc8181, 15%);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: lighten(#fc8181, 15%);
|
||||
color: grey;
|
||||
@ -287,12 +309,13 @@ a:not(.button) {
|
||||
}
|
||||
}
|
||||
|
||||
/*--------------------------------CONTENT------------------------------*/
|
||||
/*--------------------------------CONTENT------------------------------*/
|
||||
#quick_notif {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
list-style-type: none;
|
||||
background: $second-color;
|
||||
|
||||
li {
|
||||
padding: 10px;
|
||||
}
|
||||
@ -333,14 +356,24 @@ a:not(.button) {
|
||||
border: #fc8181 1px solid;
|
||||
}
|
||||
|
||||
.alert-title {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.alert-main {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.alert-aside {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.tool_bar {
|
||||
overflow: auto;
|
||||
padding: 4px;
|
||||
|
||||
.tools {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@ -349,6 +382,7 @@ a:not(.button) {
|
||||
padding: 5px;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
padding: 7px;
|
||||
display: inline-block;
|
||||
@ -358,11 +392,13 @@ a:not(.button) {
|
||||
flex: 1;
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
|
||||
&.selected_tab {
|
||||
background: $primary-color;
|
||||
color: $white-color;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $primary-color;
|
||||
color: $white-color;
|
||||
@ -372,7 +408,7 @@ a:not(.button) {
|
||||
}
|
||||
}
|
||||
|
||||
/*---------------------------------NEWS--------------------------------*/
|
||||
/*---------------------------------NEWS--------------------------------*/
|
||||
#news {
|
||||
display: flex;
|
||||
|
||||
@ -385,17 +421,21 @@ a:not(.button) {
|
||||
margin: 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#news_admin {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
#right_column {
|
||||
flex: 20%;
|
||||
float: right;
|
||||
margin: 0.2em;
|
||||
}
|
||||
|
||||
#left_column {
|
||||
flex: 79%;
|
||||
margin: 0.2em;
|
||||
|
||||
h3 {
|
||||
background: $second-color;
|
||||
box-shadow: $shadow-color 1px 1px 1px;
|
||||
@ -403,19 +443,22 @@ a:not(.button) {
|
||||
margin: 0 0 0.5em 0;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.1em;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin: 2em 0 1em 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-devices) {
|
||||
|
||||
#left_column,
|
||||
#right_column {
|
||||
flex: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* AGENDA/BIRTHDAYS */
|
||||
/* AGENDA/BIRTHDAYS */
|
||||
#agenda,
|
||||
#birthdays {
|
||||
display: block;
|
||||
@ -423,6 +466,7 @@ a:not(.button) {
|
||||
background: white;
|
||||
font-size: 70%;
|
||||
margin-bottom: 1em;
|
||||
|
||||
#agenda_title,
|
||||
#birthdays_title {
|
||||
margin: 0;
|
||||
@ -435,39 +479,48 @@ a:not(.button) {
|
||||
text-transform: uppercase;
|
||||
background: $second-color;
|
||||
}
|
||||
|
||||
#agenda_content {
|
||||
overflow: auto;
|
||||
box-shadow: $shadow-color 1px 1px 1px;
|
||||
height: 20em;
|
||||
}
|
||||
|
||||
#agenda_content,
|
||||
#birthdays_content {
|
||||
.agenda_item {
|
||||
padding: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
&:nth-of-type(even) {
|
||||
background: $secondary-neutral-light-color;
|
||||
}
|
||||
|
||||
.agenda_time {
|
||||
font-size: 90%;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.agenda_item_content {
|
||||
p {
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul.birthdays_year {
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
font-weight: bold;
|
||||
> li {
|
||||
|
||||
>li {
|
||||
padding: 0.5em;
|
||||
|
||||
&:nth-child(even) {
|
||||
background: $secondary-neutral-light-color;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
margin-left: 1em;
|
||||
@ -478,13 +531,15 @@ a:not(.button) {
|
||||
}
|
||||
}
|
||||
}
|
||||
/* END AGENDA/BIRTHDAYS */
|
||||
|
||||
/* EVENTS TODAY AND NEXT FEW DAYS */
|
||||
/* END AGENDA/BIRTHDAYS */
|
||||
|
||||
/* EVENTS TODAY AND NEXT FEW DAYS */
|
||||
.news_events_group {
|
||||
box-shadow: $shadow-color 1px 1px 1px;
|
||||
margin-left: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
.news_events_group_date {
|
||||
display: table-cell;
|
||||
padding: 0.6em;
|
||||
@ -500,33 +555,42 @@ a:not(.button) {
|
||||
|
||||
div {
|
||||
margin: 0 auto;
|
||||
|
||||
.day {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.news_events_group_items {
|
||||
display: table-cell;
|
||||
width: 100%;
|
||||
|
||||
.news_event:nth-of-type(odd) {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.news_event:nth-of-type(even) {
|
||||
background: $primary-neutral-light-color;
|
||||
}
|
||||
|
||||
.news_event {
|
||||
display: block;
|
||||
padding: 0.4em;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid grey;
|
||||
}
|
||||
|
||||
div {
|
||||
margin: 0.2em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-top: 1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.club_logo {
|
||||
float: left;
|
||||
min-width: 7em;
|
||||
@ -534,6 +598,7 @@ a:not(.button) {
|
||||
margin: 0;
|
||||
margin-right: 1em;
|
||||
margin-top: 0.8em;
|
||||
|
||||
img {
|
||||
max-height: 6em;
|
||||
max-width: 8em;
|
||||
@ -541,16 +606,21 @@ a:not(.button) {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.news_date {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
.news_content {
|
||||
clear: left;
|
||||
|
||||
.button_bar {
|
||||
text-align: right;
|
||||
|
||||
.fb {
|
||||
color: $faceblue;
|
||||
}
|
||||
|
||||
.twitter {
|
||||
color: $twitblue;
|
||||
}
|
||||
@ -559,26 +629,30 @@ a:not(.button) {
|
||||
}
|
||||
}
|
||||
}
|
||||
/* END EVENTS TODAY AND NEXT FEW DAYS */
|
||||
|
||||
/* COMING SOON */
|
||||
/* END EVENTS TODAY AND NEXT FEW DAYS */
|
||||
|
||||
/* COMING SOON */
|
||||
.news_coming_soon {
|
||||
display: list-item;
|
||||
list-style-type: square;
|
||||
list-style-position: inside;
|
||||
margin-left: 1em;
|
||||
padding-left: 0;
|
||||
|
||||
a {
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.news_date {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
/* END COMING SOON */
|
||||
|
||||
/* NOTICES */
|
||||
/* END COMING SOON */
|
||||
|
||||
/* NOTICES */
|
||||
.news_notice {
|
||||
margin: 0 0 1em 1em;
|
||||
padding: 0.4em;
|
||||
@ -586,16 +660,19 @@ a:not(.button) {
|
||||
background: $secondary-neutral-light-color;
|
||||
box-shadow: $shadow-color 0 0 2px;
|
||||
border-radius: 18px 5px 18px 5px;
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.news_content {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
/* END NOTICES */
|
||||
|
||||
/* CALLS */
|
||||
/* END NOTICES */
|
||||
|
||||
/* CALLS */
|
||||
.news_call {
|
||||
margin: 0 0 1em 1em;
|
||||
padding: 0.4em;
|
||||
@ -603,21 +680,26 @@ a:not(.button) {
|
||||
background: $secondary-neutral-light-color;
|
||||
border: 1px solid grey;
|
||||
box-shadow: $shadow-color 1px 1px 1px;
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.news_date {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.news_content {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
/* END CALLS */
|
||||
|
||||
/* END CALLS */
|
||||
|
||||
.news_empty {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.news_date {
|
||||
color: grey;
|
||||
}
|
||||
@ -631,8 +713,8 @@ a:not(.button) {
|
||||
}
|
||||
|
||||
|
||||
.select2 {
|
||||
margin: 10px 0!important;
|
||||
.tomselected {
|
||||
margin: 10px 0 !important;
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
|
||||
@ -648,7 +730,9 @@ a:not(.button) {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
.select2-results {
|
||||
|
||||
.ts-dropdown {
|
||||
|
||||
.select-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -664,16 +748,39 @@ a:not(.button) {
|
||||
}
|
||||
}
|
||||
|
||||
.ts-control {
|
||||
|
||||
.item {
|
||||
.fa-times {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
cursor: pointer;
|
||||
background-color: #e4e4e4;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#news_details {
|
||||
display: inline-block;
|
||||
margin-top: 20px;
|
||||
padding: 0.4em;
|
||||
width: 80%;
|
||||
background: $white-color;
|
||||
|
||||
h4 {
|
||||
margin-top: 1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.club_logo {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
@ -681,6 +788,7 @@ a:not(.button) {
|
||||
float: left;
|
||||
min-width: 15em;
|
||||
margin: 0;
|
||||
|
||||
img {
|
||||
max-height: 15em;
|
||||
max-width: 12em;
|
||||
@ -689,6 +797,7 @@ a:not(.button) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.share_button {
|
||||
border: none;
|
||||
color: white;
|
||||
@ -700,6 +809,7 @@ a:not(.button) {
|
||||
float: right;
|
||||
display: block;
|
||||
margin-left: 0.3em;
|
||||
|
||||
&:hover {
|
||||
color: lightgrey;
|
||||
}
|
||||
@ -731,26 +841,32 @@ a:not(.button) {
|
||||
#poster_edit,
|
||||
#screen_edit {
|
||||
position: relative;
|
||||
|
||||
#title {
|
||||
position: relative;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
border-bottom: 2px solid black;
|
||||
|
||||
h3 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#links {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
bottom: 5px;
|
||||
|
||||
&.left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&.right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.link {
|
||||
padding: 5px;
|
||||
padding-left: 20px;
|
||||
@ -759,27 +875,32 @@ a:not(.button) {
|
||||
border-radius: 20px;
|
||||
background-color: hsl(40, 100%, 50%);
|
||||
color: black;
|
||||
|
||||
&:hover {
|
||||
color: black;
|
||||
background-color: hsl(40, 58%, 50%);
|
||||
}
|
||||
|
||||
&.delete {
|
||||
background-color: hsl(0, 100%, 40%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#posters,
|
||||
#screens {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
#no-posters,
|
||||
#no-screens {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.poster,
|
||||
.screen {
|
||||
min-width: 10%;
|
||||
@ -791,26 +912,31 @@ a:not(.button) {
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
background-color: lightgrey;
|
||||
|
||||
* {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 5px;
|
||||
border-bottom: 1px solid whitesmoke;
|
||||
}
|
||||
|
||||
.image {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 5px;
|
||||
border-bottom: 1px solid whitesmoke;
|
||||
|
||||
img {
|
||||
max-height: 20vw;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
position: absolute;
|
||||
@ -829,10 +955,12 @@ a:not(.button) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dates {
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 5px;
|
||||
border-bottom: 1px solid whitesmoke;
|
||||
|
||||
* {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@ -841,15 +969,18 @@ a:not(.button) {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.begin,
|
||||
.end {
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.begin {
|
||||
border-right: 1px solid whitesmoke;
|
||||
padding-right: 2%;
|
||||
}
|
||||
}
|
||||
|
||||
.edit,
|
||||
.moderate,
|
||||
.slideshow {
|
||||
@ -857,15 +988,18 @@ a:not(.button) {
|
||||
border-radius: 20px;
|
||||
background-color: hsl(40, 100%, 50%);
|
||||
color: black;
|
||||
|
||||
&:hover {
|
||||
color: black;
|
||||
background-color: hsl(40, 58%, 50%);
|
||||
}
|
||||
|
||||
&:nth-child(2n) {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
visibility: hidden;
|
||||
width: 120px;
|
||||
@ -876,23 +1010,28 @@ a:not(.button) {
|
||||
border-radius: 6px;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
|
||||
ul {
|
||||
margin-left: 0;
|
||||
display: inline-block;
|
||||
|
||||
li {
|
||||
display: list-item;
|
||||
list-style-type: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.not_moderated {
|
||||
border: 1px solid red;
|
||||
}
|
||||
|
||||
&:hover .tooltip {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#view {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
@ -906,9 +1045,11 @@ a:not(.button) {
|
||||
visibility: hidden;
|
||||
background-color: rgba(10, 10, 10, 0.9);
|
||||
overflow: hidden;
|
||||
|
||||
&.active {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#placeholder {
|
||||
width: 80vw;
|
||||
height: 80vh;
|
||||
@ -917,6 +1058,7 @@ a:not(.button) {
|
||||
align-items: center;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
@ -931,14 +1073,17 @@ a:not(.button) {
|
||||
tbody {
|
||||
.neg-amount {
|
||||
color: red;
|
||||
|
||||
&:before {
|
||||
font-family: FontAwesome;
|
||||
font-size: 1em;
|
||||
content: "\f063";
|
||||
}
|
||||
}
|
||||
|
||||
.pos-amount {
|
||||
color: green;
|
||||
|
||||
&:before {
|
||||
font-family: FontAwesome;
|
||||
font-size: 1em;
|
||||
@ -1005,6 +1150,7 @@ dt {
|
||||
.edit-bar {
|
||||
display: block;
|
||||
margin: 4px;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
margin: 4px;
|
||||
@ -1044,7 +1190,8 @@ th {
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
padding: 5px 10px;
|
||||
> ul {
|
||||
|
||||
>ul {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
@ -1055,7 +1202,8 @@ td {
|
||||
vertical-align: top;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
> ul {
|
||||
|
||||
>ul {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
@ -1071,15 +1219,17 @@ thead {
|
||||
color: white;
|
||||
}
|
||||
|
||||
tbody > tr {
|
||||
tbody>tr {
|
||||
&:nth-child(even):not(.highlight) {
|
||||
background: $primary-neutral-light-color;
|
||||
}
|
||||
|
||||
&.clickable:hover {
|
||||
cursor: pointer;
|
||||
background: $secondary-neutral-light-color;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.highlight {
|
||||
color: $primary-dark-color;
|
||||
font-style: italic;
|
||||
@ -1139,9 +1289,11 @@ u,
|
||||
margin: 0.2em;
|
||||
height: 100%;
|
||||
background: $secondary-neutral-light-color;
|
||||
|
||||
img {
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
input {
|
||||
background: white;
|
||||
}
|
||||
@ -1153,10 +1305,12 @@ u,
|
||||
.user_mini_profile {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.user_mini_profile_infos {
|
||||
padding: 0.2em;
|
||||
height: 20%;
|
||||
@ -1164,16 +1318,20 @@ u,
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-around;
|
||||
font-size: 0.9em;
|
||||
|
||||
div {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.user_mini_profile_infos_text {
|
||||
text-align: center;
|
||||
|
||||
.user_mini_profile_nick {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user_mini_profile_picture {
|
||||
height: 80%;
|
||||
display: flex;
|
||||
@ -1185,14 +1343,17 @@ u,
|
||||
.mini_profile_link {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
em {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 40px;
|
||||
max-height: 60px;
|
||||
@ -1214,6 +1375,7 @@ u,
|
||||
border: solid 1px red;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 500px;
|
||||
}
|
||||
@ -1223,6 +1385,7 @@ u,
|
||||
.matmat_results {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.matmat_user {
|
||||
flex-basis: 14em;
|
||||
align-self: flex-start;
|
||||
@ -1231,10 +1394,12 @@ u,
|
||||
overflow: hidden;
|
||||
border: 1px solid black;
|
||||
box-shadow: $shadow-color 1px 1px 1px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 1px 1px 5px $second-color;
|
||||
}
|
||||
}
|
||||
|
||||
.matmat_user a {
|
||||
color: $primary-neutral-dark-color;
|
||||
height: 100%;
|
||||
@ -1274,6 +1439,7 @@ footer {
|
||||
font-size: 90%;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
|
||||
div {
|
||||
margin: 0.6em 0;
|
||||
color: $white-color;
|
||||
@ -1283,18 +1449,20 @@ footer {
|
||||
align-items: center;
|
||||
background-color: $primary-neutral-dark-color;
|
||||
box-shadow: $shadow-color 0 0 15px;
|
||||
|
||||
a {
|
||||
padding: 0.8em;
|
||||
flex: 1;
|
||||
font-weight: bold;
|
||||
color: $white-color !important;
|
||||
|
||||
&:hover {
|
||||
color: $primary-dark-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .version {
|
||||
>.version {
|
||||
margin-top: 3px;
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
@ -1326,6 +1494,7 @@ label {
|
||||
* {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100px;
|
||||
}
|
||||
@ -1342,19 +1511,23 @@ label {
|
||||
padding: 2px;
|
||||
display: inline-block;
|
||||
font-size: 0.8em;
|
||||
|
||||
span {
|
||||
width: 70px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 50px;
|
||||
max-height: 50px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
button {
|
||||
vertical-align: middle;
|
||||
}
|
||||
@ -1371,6 +1544,7 @@ a.ui-button:active,
|
||||
background: $primary-color;
|
||||
border-color: $primary-color;
|
||||
}
|
||||
|
||||
.ui-corner-all,
|
||||
.ui-corner-bottom,
|
||||
.ui-corner-right,
|
||||
@ -1382,10 +1556,11 @@ a.ui-button:active,
|
||||
#club_detail {
|
||||
.club_logo {
|
||||
float: right;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
max-height: 10em;
|
||||
max-width: 10em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
16
core/static/vendored/chart/Chart.bundle.min.js
vendored
16
core/static/vendored/chart/Chart.bundle.min.js
vendored
File diff suppressed because one or more lines are too long
1
core/static/vendored/select2/select2.min.css
vendored
1
core/static/vendored/select2/select2.min.css
vendored
File diff suppressed because one or more lines are too long
2
core/static/vendored/select2/select2.min.js
vendored
2
core/static/vendored/select2/select2.min.js
vendored
File diff suppressed because one or more lines are too long
1
core/static/vendored/sentry/bundle.min.js
vendored
1
core/static/vendored/sentry/bundle.min.js
vendored
@ -1 +0,0 @@
|
||||
!function(n,e,r,t,i,o,a,c,s){for(var u=s,f=0;f<document.scripts.length;f++)if(document.scripts[f].src.indexOf(o)>-1){u&&"no"===document.scripts[f].getAttribute("data-lazy")&&(u=!1);break}var p=[];function l(n){return"e"in n}function d(n){return"p"in n}function _(n){return"f"in n}var v=[];function y(n){u&&(l(n)||d(n)||_(n)&&n.f.indexOf("capture")>-1||_(n)&&n.f.indexOf("showReportDialog")>-1)&&m(),v.push(n)}function g(){y({e:[].slice.call(arguments)})}function h(n){y({p:n})}function E(){try{n.SENTRY_SDK_SOURCE="loader";var e=n[i],o=e.init;e.init=function(i){n.removeEventListener(r,g),n.removeEventListener(t,h);var a=c;for(var s in i)Object.prototype.hasOwnProperty.call(i,s)&&(a[s]=i[s]);!function(n,e){var r=n.integrations||[];if(!Array.isArray(r))return;var t=r.map((function(n){return n.name}));n.tracesSampleRate&&-1===t.indexOf("BrowserTracing")&&(e.browserTracingIntegration?r.push(e.browserTracingIntegration({enableInp:!0})):e.BrowserTracing&&r.push(new e.BrowserTracing));(n.replaysSessionSampleRate||n.replaysOnErrorSampleRate)&&-1===t.indexOf("Replay")&&(e.replayIntegration?r.push(e.replayIntegration()):e.Replay&&r.push(new e.Replay));n.integrations=r}(a,e),o(a)},setTimeout((function(){return function(e){try{"function"==typeof n.sentryOnLoad&&(n.sentryOnLoad(),n.sentryOnLoad=void 0);for(var r=0;r<p.length;r++)"function"==typeof p[r]&&p[r]();p.splice(0);for(r=0;r<v.length;r++){_(o=v[r])&&"init"===o.f&&e.init.apply(e,o.a)}L()||e.init();var t=n.onerror,i=n.onunhandledrejection;for(r=0;r<v.length;r++){var o;if(_(o=v[r])){if("init"===o.f)continue;e[o.f].apply(e,o.a)}else l(o)&&t?t.apply(n,o.e):d(o)&&i&&i.apply(n,[o.p])}}catch(n){console.error(n)}}(e)}))}catch(n){console.error(n)}}var O=!1;function m(){if(!O){O=!0;var n=e.scripts[0],r=e.createElement("script");r.src=a,r.crossOrigin="anonymous",r.addEventListener("load",E,{once:!0,passive:!0}),n.parentNode.insertBefore(r,n)}}function L(){var e=n.__SENTRY__,r=void 0!==e&&e.version;return r?!!e[r]:!(void 0===e||!e.hub||!e.hub.getClient())}n[i]=n[i]||{},n[i].onLoad=function(n){L()?n():p.push(n)},n[i].forceLoad=function(){setTimeout((function(){m()}))},["init","addBreadcrumb","captureMessage","captureException","captureEvent","configureScope","withScope","showReportDialog"].forEach((function(e){n[i][e]=function(){y({f:e,a:arguments})}})),n.addEventListener(r,g),n.addEventListener(t,h),u||setTimeout((function(){m()}))}(window,document,"error","unhandledrejection","Sentry",'ab63c6820882cab2883218a4b9deba4d','https://browser.sentry-cdn.com/8.26.0/bundle.min.js',{"dsn":"https://ab63c6820882cab2883218a4b9deba4d@o4505360748642304.ingest.us.sentry.io/4507633486266368"},true);
|
93
core/static/webpack/ajax-select-index.ts
Normal file
93
core/static/webpack/ajax-select-index.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import "tom-select/dist/css/tom-select.css";
|
||||
import { inheritHtmlElement, registerComponent } from "#core:utils/web-components";
|
||||
import TomSelect from "tom-select";
|
||||
import type { TomItem, TomLoadCallback, TomOption } from "tom-select/dist/types/types";
|
||||
import type { escape_html } from "tom-select/dist/types/utils";
|
||||
import { type UserProfileSchema, userSearchUsers } from "#openapi";
|
||||
|
||||
@registerComponent("ajax-select")
|
||||
export class AjaxSelect extends inheritHtmlElement("select") {
|
||||
public widget: TomSelect;
|
||||
public filter?: <T>(items: T[]) => T[];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
this.loadTomSelect();
|
||||
});
|
||||
}
|
||||
|
||||
loadTomSelect() {
|
||||
const minCharNumberForSearch = 2;
|
||||
let maxItems = 1;
|
||||
|
||||
if (this.node.multiple) {
|
||||
maxItems = Number.parseInt(this.node.dataset.max) ?? null;
|
||||
}
|
||||
|
||||
this.widget = new TomSelect(this.node, {
|
||||
hideSelected: true,
|
||||
diacritics: true,
|
||||
duplicates: false,
|
||||
maxItems: maxItems,
|
||||
loadThrottle: Number.parseInt(this.node.dataset.delay) ?? null,
|
||||
valueField: "id",
|
||||
labelField: "display_name",
|
||||
searchField: ["display_name", "nick_name", "first_name", "last_name"],
|
||||
placeholder: this.node.dataset.placeholder ?? "",
|
||||
shouldLoad: (query: string) => {
|
||||
return query.length >= minCharNumberForSearch; // Avoid launching search with less than 2 characters
|
||||
},
|
||||
load: (query: string, callback: TomLoadCallback) => {
|
||||
userSearchUsers({
|
||||
query: {
|
||||
search: query,
|
||||
},
|
||||
}).then((response) => {
|
||||
if (response.data) {
|
||||
if (this.filter) {
|
||||
callback(this.filter(response.data.results), []);
|
||||
} else {
|
||||
callback(response.data.results, []);
|
||||
}
|
||||
return;
|
||||
}
|
||||
callback([], []);
|
||||
});
|
||||
},
|
||||
render: {
|
||||
option: (item: UserProfileSchema, sanitize: typeof escape_html) => {
|
||||
return `<div class="select-item">
|
||||
<img
|
||||
src="${sanitize(item.profile_pict)}"
|
||||
alt="${sanitize(item.display_name)}"
|
||||
onerror="this.src = '/static/core/img/unknown.jpg'"
|
||||
/>
|
||||
<span class="select-item-text">${sanitize(item.display_name)}</span>
|
||||
</div>`;
|
||||
},
|
||||
item: (item: UserProfileSchema, sanitize: typeof escape_html) => {
|
||||
return `<span><i class="fa fa-times"></i>${sanitize(item.display_name)}</span>`;
|
||||
},
|
||||
// biome-ignore lint/style/useNamingConvention: that's how it's defined
|
||||
not_loading: (data: TomOption, _sanitize: typeof escape_html) => {
|
||||
return `<div class="no-results">${interpolate(gettext("You need to type %(number)s more characters"), { number: minCharNumberForSearch - data.input.length }, true)}</div>`;
|
||||
},
|
||||
// biome-ignore lint/style/useNamingConvention: that's how it's defined
|
||||
no_results: (_data: TomOption, _sanitize: typeof escape_html) => {
|
||||
return `<div class="no-results">${gettext("No results found")}</div>`;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Allow removing selected items by clicking on them
|
||||
this.widget.on("item_select", (item: TomItem) => {
|
||||
this.widget.removeItem(item);
|
||||
});
|
||||
// Remove typed text once an item has been selected
|
||||
this.widget.on("item_add", () => {
|
||||
this.widget.setTextboxValue("");
|
||||
});
|
||||
}
|
||||
}
|
@ -1,53 +1,57 @@
|
||||
// biome-ignore lint/correctness/noUndeclaredDependencies: shipped by easymde
|
||||
import "codemirror/lib/codemirror.css";
|
||||
import "easymde/src/css/easymde.css";
|
||||
import easyMde from "easymde";
|
||||
import { inheritHtmlElement, registerComponent } from "#core:utils/web-components";
|
||||
// biome-ignore lint/correctness/noUndeclaredDependencies: Imported by EasyMDE
|
||||
import type CodeMirror from "codemirror";
|
||||
// biome-ignore lint/style/useNamingConvention: This is how they called their namespace
|
||||
import EasyMDE from "easymde";
|
||||
import { markdownRenderMarkdown } from "#openapi";
|
||||
|
||||
/**
|
||||
* Create a new easymde based textarea
|
||||
* @param {HTMLTextAreaElement} textarea to use
|
||||
**/
|
||||
window.easymdeFactory = (textarea) => {
|
||||
const easymde = new easyMde({
|
||||
const loadEasyMde = (textarea: HTMLTextAreaElement) => {
|
||||
new EasyMDE({
|
||||
element: textarea,
|
||||
spellChecker: false,
|
||||
autoDownloadFontAwesome: false,
|
||||
previewRender: Alpine.debounce(async (plainText, preview) => {
|
||||
preview.innerHTML = (
|
||||
await markdownRenderMarkdown({ body: { text: plainText } })
|
||||
).data;
|
||||
previewRender: Alpine.debounce((plainText: string, preview: MarkdownInput) => {
|
||||
const func = async (plainText: string, preview: MarkdownInput): Promise<null> => {
|
||||
preview.innerHTML = (
|
||||
await markdownRenderMarkdown({ body: { text: plainText } })
|
||||
).data as string;
|
||||
return null;
|
||||
};
|
||||
func(plainText, preview);
|
||||
return null;
|
||||
}, 300),
|
||||
forceSync: true, // Avoid validation error on generic create view
|
||||
toolbar: [
|
||||
{
|
||||
name: "heading-smaller",
|
||||
action: easyMde.toggleHeadingSmaller,
|
||||
action: EasyMDE.toggleHeadingSmaller,
|
||||
className: "fa fa-header",
|
||||
title: gettext("Heading"),
|
||||
},
|
||||
{
|
||||
name: "italic",
|
||||
action: easyMde.toggleItalic,
|
||||
action: EasyMDE.toggleItalic,
|
||||
className: "fa fa-italic",
|
||||
title: gettext("Italic"),
|
||||
},
|
||||
{
|
||||
name: "bold",
|
||||
action: easyMde.toggleBold,
|
||||
action: EasyMDE.toggleBold,
|
||||
className: "fa fa-bold",
|
||||
title: gettext("Bold"),
|
||||
},
|
||||
{
|
||||
name: "strikethrough",
|
||||
action: easyMde.toggleStrikethrough,
|
||||
action: EasyMDE.toggleStrikethrough,
|
||||
className: "fa fa-strikethrough",
|
||||
title: gettext("Strikethrough"),
|
||||
},
|
||||
{
|
||||
name: "underline",
|
||||
action: function customFunction(editor) {
|
||||
action: function customFunction(editor: { codemirror: CodeMirror.Editor }) {
|
||||
const cm = editor.codemirror;
|
||||
cm.replaceSelection(`__${cm.getSelection()}__`);
|
||||
},
|
||||
@ -56,7 +60,7 @@ window.easymdeFactory = (textarea) => {
|
||||
},
|
||||
{
|
||||
name: "superscript",
|
||||
action: function customFunction(editor) {
|
||||
action: function customFunction(editor: { codemirror: CodeMirror.Editor }) {
|
||||
const cm = editor.codemirror;
|
||||
cm.replaceSelection(`^${cm.getSelection()}^`);
|
||||
},
|
||||
@ -65,7 +69,7 @@ window.easymdeFactory = (textarea) => {
|
||||
},
|
||||
{
|
||||
name: "subscript",
|
||||
action: function customFunction(editor) {
|
||||
action: function customFunction(editor: { codemirror: CodeMirror.Editor }) {
|
||||
const cm = editor.codemirror;
|
||||
cm.replaceSelection(`~${cm.getSelection()}~`);
|
||||
},
|
||||
@ -74,71 +78,71 @@ window.easymdeFactory = (textarea) => {
|
||||
},
|
||||
{
|
||||
name: "code",
|
||||
action: easyMde.toggleCodeBlock,
|
||||
action: EasyMDE.toggleCodeBlock,
|
||||
className: "fa fa-code",
|
||||
title: gettext("Code"),
|
||||
},
|
||||
"|",
|
||||
{
|
||||
name: "quote",
|
||||
action: easyMde.toggleBlockquote,
|
||||
action: EasyMDE.toggleBlockquote,
|
||||
className: "fa fa-quote-left",
|
||||
title: gettext("Quote"),
|
||||
},
|
||||
{
|
||||
name: "unordered-list",
|
||||
action: easyMde.toggleUnorderedList,
|
||||
action: EasyMDE.toggleUnorderedList,
|
||||
className: "fa fa-list-ul",
|
||||
title: gettext("Unordered list"),
|
||||
},
|
||||
{
|
||||
name: "ordered-list",
|
||||
action: easyMde.toggleOrderedList,
|
||||
action: EasyMDE.toggleOrderedList,
|
||||
className: "fa fa-list-ol",
|
||||
title: gettext("Ordered list"),
|
||||
},
|
||||
"|",
|
||||
{
|
||||
name: "link",
|
||||
action: easyMde.drawLink,
|
||||
action: EasyMDE.drawLink,
|
||||
className: "fa fa-link",
|
||||
title: gettext("Insert link"),
|
||||
},
|
||||
{
|
||||
name: "image",
|
||||
action: easyMde.drawImage,
|
||||
action: EasyMDE.drawImage,
|
||||
className: "fa-regular fa-image",
|
||||
title: gettext("Insert image"),
|
||||
},
|
||||
{
|
||||
name: "table",
|
||||
action: easyMde.drawTable,
|
||||
action: EasyMDE.drawTable,
|
||||
className: "fa fa-table",
|
||||
title: gettext("Insert table"),
|
||||
},
|
||||
"|",
|
||||
{
|
||||
name: "clean-block",
|
||||
action: easyMde.cleanBlock,
|
||||
action: EasyMDE.cleanBlock,
|
||||
className: "fa fa-eraser fa-clean-block",
|
||||
title: gettext("Clean block"),
|
||||
},
|
||||
"|",
|
||||
{
|
||||
name: "preview",
|
||||
action: easyMde.togglePreview,
|
||||
action: EasyMDE.togglePreview,
|
||||
className: "fa fa-eye no-disable",
|
||||
title: gettext("Toggle preview"),
|
||||
},
|
||||
{
|
||||
name: "side-by-side",
|
||||
action: easyMde.toggleSideBySide,
|
||||
action: EasyMDE.toggleSideBySide,
|
||||
className: "fa fa-columns no-disable no-mobile",
|
||||
title: gettext("Toggle side by side"),
|
||||
},
|
||||
{
|
||||
name: "fullscreen",
|
||||
action: easyMde.toggleFullScreen,
|
||||
action: EasyMDE.toggleFullScreen,
|
||||
className: "fa fa-expand no-mobile",
|
||||
title: gettext("Toggle fullscreen"),
|
||||
},
|
||||
@ -152,27 +156,25 @@ window.easymdeFactory = (textarea) => {
|
||||
],
|
||||
});
|
||||
|
||||
const submits = textarea.closest("form").querySelectorAll('input[type="submit"]');
|
||||
const parentDiv = textarea.parentElement;
|
||||
let submitPressed = false;
|
||||
const submits: HTMLInputElement[] = Array.from(
|
||||
textarea.closest("form").querySelectorAll('input[type="submit"]'),
|
||||
);
|
||||
const parentDiv = textarea.parentElement.parentElement;
|
||||
|
||||
function checkMarkdownInput() {
|
||||
function checkMarkdownInput(event: Event) {
|
||||
// an attribute is null if it does not exist, else a string
|
||||
const required = textarea.getAttribute("required") != null;
|
||||
const length = textarea.value.trim().length;
|
||||
|
||||
if (required && length === 0) {
|
||||
parentDiv.style.boxShadow = "red 0px 0px 1.5px 1px";
|
||||
event.preventDefault();
|
||||
} else {
|
||||
parentDiv.style.boxShadow = "";
|
||||
}
|
||||
}
|
||||
|
||||
function onSubmitClick(e) {
|
||||
if (!submitPressed) {
|
||||
easymde.codemirror.on("change", checkMarkdownInput);
|
||||
}
|
||||
submitPressed = true;
|
||||
function onSubmitClick(e: Event) {
|
||||
checkMarkdownInput(e);
|
||||
}
|
||||
|
||||
@ -180,3 +182,11 @@ window.easymdeFactory = (textarea) => {
|
||||
submit.addEventListener("click", onSubmitClick);
|
||||
}
|
||||
};
|
||||
|
||||
@registerComponent("markdown-input")
|
||||
class MarkdownInput extends inheritHtmlElement("textarea") {
|
||||
constructor() {
|
||||
super();
|
||||
window.addEventListener("DOMContentLoaded", () => loadEasyMde(this.node));
|
||||
}
|
||||
}
|
24
core/static/webpack/sentry-popup-index.ts
Normal file
24
core/static/webpack/sentry-popup-index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { exportToHtml } from "#core:utils/globals";
|
||||
// biome-ignore lint/style/noNamespaceImport: this is the recommended way from the documentation
|
||||
import * as Sentry from "@sentry/browser";
|
||||
|
||||
interface LoggedUser {
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface SentryOptions {
|
||||
dsn: string;
|
||||
eventId: string;
|
||||
user?: LoggedUser;
|
||||
}
|
||||
|
||||
exportToHtml("loadSentryPopup", (options: SentryOptions) => {
|
||||
Sentry.init({
|
||||
dsn: options.dsn,
|
||||
});
|
||||
Sentry.showReportDialog({
|
||||
eventId: options.eventId,
|
||||
...(options.user ?? {}),
|
||||
});
|
||||
});
|
21
core/static/webpack/utils/globals.ts
Normal file
21
core/static/webpack/utils/globals.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import type { Alpine as AlpineType } from "alpinejs";
|
||||
|
||||
declare global {
|
||||
const Alpine: AlpineType;
|
||||
const gettext: (text: string) => string;
|
||||
const interpolate: <T>(fmt: string, args: string[] | T, isNamed?: boolean) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to export typescript functions to regular html and jinja files
|
||||
* Without it, you either have to use the any keyword and suppress warnings or do a
|
||||
* very painful type conversion workaround which is only here to please the linter
|
||||
*
|
||||
* This is only useful if you're using typescript, this is equivalent to doing
|
||||
* window.yourFunction = yourFunction
|
||||
**/
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Avoid strange tricks to export functions
|
||||
export function exportToHtml(name: string, func: any) {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Avoid strange tricks to export functions
|
||||
(window as any)[name] = func;
|
||||
}
|
50
core/static/webpack/utils/web-components.ts
Normal file
50
core/static/webpack/utils/web-components.ts
Normal file
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Class decorator to register components easily
|
||||
* It's a wrapper around window.customElements.define
|
||||
* What's nice about it is that you don't separate the component registration
|
||||
* and the class definition
|
||||
**/
|
||||
export function registerComponent(name: string, options?: ElementDefinitionOptions) {
|
||||
return (component: CustomElementConstructor) => {
|
||||
window.customElements.define(name, component, options);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Safari doesn't support inheriting from HTML tags on web components
|
||||
* The technique is to:
|
||||
* create a new web component
|
||||
* create the desired type inside
|
||||
* pass all attributes to the child component
|
||||
* store is at as `node` inside the parent
|
||||
*
|
||||
* Since we can't use the generic type to instantiate the node, we create a generator function
|
||||
*
|
||||
* ```js
|
||||
* class MyClass extends inheritHtmlElement("select") {
|
||||
* // do whatever
|
||||
* }
|
||||
* ```
|
||||
**/
|
||||
export function inheritHtmlElement<K extends keyof HTMLElementTagNameMap>(tagName: K) {
|
||||
return class Inherited extends HTMLElement {
|
||||
protected node: HTMLElementTagNameMap[K];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.node = document.createElement(tagName);
|
||||
const attributes: Attr[] = []; // We need to make a copy to delete while iterating
|
||||
for (const attr of this.attributes) {
|
||||
if (attr.name in this.node) {
|
||||
attributes.push(attr);
|
||||
}
|
||||
}
|
||||
|
||||
for (const attr of attributes) {
|
||||
this.removeAttributeNode(attr);
|
||||
this.node.setAttributeNode(attr);
|
||||
}
|
||||
this.appendChild(this.node);
|
||||
}
|
||||
};
|
||||
}
|
@ -1,26 +1,26 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<script
|
||||
src="{{ static('vendored/sentry/bundle.min.js') }}"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
{% endblock head %}
|
||||
{% block additional_js %}
|
||||
{% if settings.SENTRY_DSN %}
|
||||
<script src="{{ static('webpack/sentry-popup-index.ts') }}" defer ></script>
|
||||
{% endif %}
|
||||
{% endblock additional_js %}
|
||||
|
||||
{% block content %}
|
||||
<h3>{% trans %}500, Server Error{% endtrans %}</h3>
|
||||
{% if settings.SENTRY_DSN %}
|
||||
<script>
|
||||
Sentry.init({ dsn: '{{ settings.SENTRY_DSN }}' });
|
||||
Sentry.showReportDialog({
|
||||
eventId: '{{ request.sentry_last_event_id() }}',
|
||||
{% if user.is_authenticated %}
|
||||
user: {
|
||||
'name': '{{user.first_name}} {{user.last_name}}',
|
||||
'email': '{{user.email}}'
|
||||
}
|
||||
{% endif %}
|
||||
})
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
loadSentryPopup({
|
||||
dsn: "{{ settings.SENTRY_DSN }}",
|
||||
eventId: "{{ request.sentry_last_event_id() }}",
|
||||
{% if user.is_authenticated %}
|
||||
user: {
|
||||
name: '{{user.first_name}} {{user.last_name}}',
|
||||
email: '{{user.email}}'
|
||||
}
|
||||
{% endif %}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
|
@ -5,6 +5,7 @@
|
||||
<title>{% block title %}{% trans %}Welcome!{% endtrans %}{% endblock %} - Association des Étudiants UTBM</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
|
||||
<link rel="stylesheet" href="{{ static('user/user_stats.scss') }}">
|
||||
<link rel="stylesheet" href="{{ static('core/base.css') }}">
|
||||
<link rel="stylesheet" href="{{ static('ajax_select/css/ajax_select.css') }}">
|
||||
<link rel="stylesheet" href="{{ static('core/style.scss') }}">
|
||||
@ -12,7 +13,6 @@
|
||||
<link rel="stylesheet" href="{{ static('core/header.scss') }}">
|
||||
<link rel="stylesheet" href="{{ static('core/navbar.scss') }}">
|
||||
<link rel="stylesheet" href="{{ static('core/pagination.scss') }}">
|
||||
<link rel="stylesheet" href="{{ static('vendored/select2/select2.min.css') }}">
|
||||
|
||||
{% block jquery_css %}
|
||||
{# Thile file is quite heavy (around 250kb), so declaring it in a block allows easy removal #}
|
||||
@ -26,8 +26,6 @@
|
||||
<script src="{{ static('webpack/jquery-index.js') }}"></script>
|
||||
<!-- Put here to always have access to those functions on django widgets -->
|
||||
<script src="{{ static('core/js/script.js') }}"></script>
|
||||
<script defer src="{{ static('vendored/select2/select2.min.js') }}"></script>
|
||||
<script defer src="{{ static('core/js/sith-select2.js') }}"></script>
|
||||
|
||||
|
||||
|
||||
|
@ -1,13 +1,7 @@
|
||||
<div>
|
||||
<textarea name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>{% if widget.value %}{{ widget.value }}{% endif %}</textarea>
|
||||
<markdown-input name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>{% if widget.value %}{{ widget.value }}{% endif %}</markdown-input>
|
||||
|
||||
{# The easymde script can be included twice, it's safe in the code #}
|
||||
<script src="{{ statics.js }}" defer> </script>
|
||||
<link rel="stylesheet" type="text/css" href="{{ statics.css }}" defer>
|
||||
<script type="text/javascript">
|
||||
addEventListener("DOMContentLoaded", (event) => {
|
||||
easymdeFactory(
|
||||
document.getElementById("{{ widget.attrs.id }}"));
|
||||
})
|
||||
</script>
|
||||
</div>
|
||||
|
@ -50,7 +50,7 @@ def phonenumber(
|
||||
try:
|
||||
parsed = phonenumbers.parse(value, country)
|
||||
return phonenumbers.format_number(parsed, number_format)
|
||||
except phonenumbers.NumberParseException as e:
|
||||
except phonenumbers.NumberParseException:
|
||||
return value
|
||||
|
||||
|
||||
|
@ -343,7 +343,7 @@ class TestUserTools:
|
||||
response = client.get(reverse("core:user_tools"))
|
||||
assertRedirects(
|
||||
response,
|
||||
expected_url=f"/login?next=%2Fuser%2Ftools%2F",
|
||||
expected_url="/login?next=%2Fuser%2Ftools%2F",
|
||||
target_status_code=301,
|
||||
)
|
||||
|
||||
|
@ -73,7 +73,7 @@ class TestFetchFamilyApi(TestCase):
|
||||
self.client.force_login(self.main_user)
|
||||
response = self.client.get(
|
||||
reverse("api:family_graph", args=[self.main_user.id])
|
||||
+ f"?godfathers_depth=0&godchildren_depth=0"
|
||||
+ "?godfathers_depth=0&godchildren_depth=0"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert [u["id"] for u in response.json()["users"]] == [self.main_user.id]
|
||||
@ -91,7 +91,7 @@ class TestFetchFamilyApi(TestCase):
|
||||
self.client.force_login(self.main_user)
|
||||
response = self.client.get(
|
||||
reverse("api:family_graph", args=[self.main_user.id])
|
||||
+ f"?godfathers_depth=10&godchildren_depth=10"
|
||||
+ "?godfathers_depth=10&godchildren_depth=10"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert [u["id"] for u in response.json()["users"]] == [
|
||||
@ -126,7 +126,7 @@ class TestFetchFamilyApi(TestCase):
|
||||
self.client.force_login(self.main_user)
|
||||
response = self.client.get(
|
||||
reverse("api:family_graph", args=[self.main_user.id])
|
||||
+ f"?godfathers_depth=1&godchildren_depth=1"
|
||||
+ "?godfathers_depth=1&godchildren_depth=1"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert [u["id"] for u in response.json()["users"]] == [
|
||||
@ -150,7 +150,7 @@ class TestFetchFamilyApi(TestCase):
|
||||
self.client.force_login(self.main_user)
|
||||
response = self.client.get(
|
||||
reverse("api:family_graph", args=[self.main_user.id])
|
||||
+ f"?godfathers_depth=10&godchildren_depth=0"
|
||||
+ "?godfathers_depth=10&godchildren_depth=0"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert [u["id"] for u in response.json()["users"]] == [
|
||||
|
@ -158,10 +158,11 @@ def test_user_invoice_with_multiple_items():
|
||||
item_recipe = Recipe(InvoiceItem, invoice=foreign_key(Recipe(Invoice, user=user)))
|
||||
item_recipe.make(_quantity=3, quantity=1, product_unit_price=5)
|
||||
item_recipe.make(_quantity=1, quantity=1, product_unit_price=5)
|
||||
item_recipe.make(_quantity=2, quantity=1, product_unit_price=iter([5, 8]))
|
||||
res = list(
|
||||
Invoice.objects.filter(user=user)
|
||||
.annotate_total()
|
||||
.order_by("-total")
|
||||
.values_list("total", flat=True)
|
||||
)
|
||||
assert res == [15, 5]
|
||||
assert res == [15, 13, 5]
|
||||
|
136
core/urls.py
136
core/urls.py
@ -29,13 +29,67 @@ from core.converters import (
|
||||
FourDigitYearConverter,
|
||||
TwoDigitMonthConverter,
|
||||
)
|
||||
from core.views import *
|
||||
from core.views import (
|
||||
FileDeleteView,
|
||||
FileEditPropView,
|
||||
FileEditView,
|
||||
FileListView,
|
||||
FileModerateView,
|
||||
FileModerationView,
|
||||
FileView,
|
||||
GiftCreateView,
|
||||
GiftDeleteView,
|
||||
GroupCreateView,
|
||||
GroupDeleteView,
|
||||
GroupEditView,
|
||||
GroupListView,
|
||||
GroupTemplateView,
|
||||
NotificationList,
|
||||
PageCreateView,
|
||||
PageDeleteView,
|
||||
PageEditView,
|
||||
PageHistView,
|
||||
PageListView,
|
||||
PagePropView,
|
||||
PageRevView,
|
||||
PageView,
|
||||
SithLoginView,
|
||||
SithPasswordChangeDoneView,
|
||||
SithPasswordChangeView,
|
||||
SithPasswordResetCompleteView,
|
||||
SithPasswordResetConfirmView,
|
||||
SithPasswordResetDoneView,
|
||||
SithPasswordResetView,
|
||||
UserAccountDetailView,
|
||||
UserAccountView,
|
||||
UserClubView,
|
||||
UserCreationView,
|
||||
UserGodfathersTreeView,
|
||||
UserGodfathersView,
|
||||
UserListView,
|
||||
UserMiniView,
|
||||
UserPicturesView,
|
||||
UserPreferencesView,
|
||||
UserStatsView,
|
||||
UserToolsView,
|
||||
UserUpdateGroupView,
|
||||
UserUpdateProfileView,
|
||||
UserView,
|
||||
delete_user_godfather,
|
||||
index,
|
||||
logout,
|
||||
notification,
|
||||
password_root_change,
|
||||
search_json,
|
||||
search_user_json,
|
||||
search_view,
|
||||
send_file,
|
||||
)
|
||||
|
||||
register_converter(FourDigitYearConverter, "yyyy")
|
||||
register_converter(TwoDigitMonthConverter, "mm")
|
||||
register_converter(BooleanStringConverter, "bool")
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("", index, name="index"),
|
||||
path("notifications/", NotificationList.as_view(), name="notification_list"),
|
||||
@ -80,27 +134,17 @@ urlpatterns = [
|
||||
path("group/new/", GroupCreateView.as_view(), name="group_new"),
|
||||
path("group/<int:group_id>/", GroupEditView.as_view(), name="group_edit"),
|
||||
path(
|
||||
"group/<int:group_id>/delete/",
|
||||
GroupDeleteView.as_view(),
|
||||
name="group_delete",
|
||||
"group/<int:group_id>/delete/", GroupDeleteView.as_view(), name="group_delete"
|
||||
),
|
||||
path(
|
||||
"group/<int:group_id>/detail/",
|
||||
GroupTemplateView.as_view(),
|
||||
name="group_detail",
|
||||
"group/<int:group_id>/detail/", GroupTemplateView.as_view(), name="group_detail"
|
||||
),
|
||||
# User views
|
||||
path("user/", UserListView.as_view(), name="user_list"),
|
||||
path(
|
||||
"user/<int:user_id>/mini/",
|
||||
UserMiniView.as_view(),
|
||||
name="user_profile_mini",
|
||||
),
|
||||
path("user/<int:user_id>/mini/", UserMiniView.as_view(), name="user_profile_mini"),
|
||||
path("user/<int:user_id>/", UserView.as_view(), name="user_profile"),
|
||||
path(
|
||||
"user/<int:user_id>/pictures/",
|
||||
UserPicturesView.as_view(),
|
||||
name="user_pictures",
|
||||
"user/<int:user_id>/pictures/", UserPicturesView.as_view(), name="user_pictures"
|
||||
),
|
||||
path(
|
||||
"user/<int:user_id>/godfathers/",
|
||||
@ -117,28 +161,14 @@ urlpatterns = [
|
||||
delete_user_godfather,
|
||||
name="user_godfathers_delete",
|
||||
),
|
||||
path(
|
||||
"user/<int:user_id>/edit/",
|
||||
UserUpdateProfileView.as_view(),
|
||||
name="user_edit",
|
||||
),
|
||||
path("user/<int:user_id>/edit/", UserUpdateProfileView.as_view(), name="user_edit"),
|
||||
path("user/<int:user_id>/clubs/", UserClubView.as_view(), name="user_clubs"),
|
||||
path("user/<int:user_id>/prefs/", UserPreferencesView.as_view(), name="user_prefs"),
|
||||
path(
|
||||
"user/<int:user_id>/prefs/",
|
||||
UserPreferencesView.as_view(),
|
||||
name="user_prefs",
|
||||
),
|
||||
path(
|
||||
"user/<int:user_id>/groups/",
|
||||
UserUpdateGroupView.as_view(),
|
||||
name="user_groups",
|
||||
"user/<int:user_id>/groups/", UserUpdateGroupView.as_view(), name="user_groups"
|
||||
),
|
||||
path("user/tools/", UserToolsView.as_view(), name="user_tools"),
|
||||
path(
|
||||
"user/<int:user_id>/account/",
|
||||
UserAccountView.as_view(),
|
||||
name="user_account",
|
||||
),
|
||||
path("user/<int:user_id>/account/", UserAccountView.as_view(), name="user_account"),
|
||||
path(
|
||||
"user/<int:user_id>/account/<yyyy:year>/<mm:month>/",
|
||||
UserAccountDetailView.as_view(),
|
||||
@ -179,42 +209,18 @@ urlpatterns = [
|
||||
),
|
||||
path("file/moderation/", FileModerationView.as_view(), name="file_moderation"),
|
||||
path(
|
||||
"file/<int:file_id>/moderate/",
|
||||
FileModerateView.as_view(),
|
||||
name="file_moderate",
|
||||
"file/<int:file_id>/moderate/", FileModerateView.as_view(), name="file_moderate"
|
||||
),
|
||||
path("file/<int:file_id>/download/", send_file, name="download"),
|
||||
# Page views
|
||||
path("page/", PageListView.as_view(), name="page_list"),
|
||||
path("page/create/", PageCreateView.as_view(), name="page_new"),
|
||||
path("page/<int:page_id>/delete/", PageDeleteView.as_view(), name="page_delete"),
|
||||
path("page/<path:page_name>/edit/", PageEditView.as_view(), name="page_edit"),
|
||||
path("page/<path:page_name>/prop/", PagePropView.as_view(), name="page_prop"),
|
||||
path("page/<path:page_name>/hist/", PageHistView.as_view(), name="page_hist"),
|
||||
path(
|
||||
"page/<int:page_id>/delete/",
|
||||
PageDeleteView.as_view(),
|
||||
name="page_delete",
|
||||
),
|
||||
path(
|
||||
"page/<path:page_name>/edit/",
|
||||
PageEditView.as_view(),
|
||||
name="page_edit",
|
||||
),
|
||||
path(
|
||||
"page/<path:page_name>/prop/",
|
||||
PagePropView.as_view(),
|
||||
name="page_prop",
|
||||
),
|
||||
path(
|
||||
"page/<path:page_name>/hist/",
|
||||
PageHistView.as_view(),
|
||||
name="page_hist",
|
||||
),
|
||||
path(
|
||||
"page/<path:page_name>/rev/<int:rev>/",
|
||||
PageRevView.as_view(),
|
||||
name="page_rev",
|
||||
),
|
||||
path(
|
||||
"page/<path:page_name>/",
|
||||
PageView.as_view(),
|
||||
name="page",
|
||||
"page/<path:page_name>/rev/<int:rev>/", PageRevView.as_view(), name="page_rev"
|
||||
),
|
||||
path("page/<path:page_name>/", PageView.as_view(), name="page"),
|
||||
]
|
||||
|
@ -127,7 +127,7 @@ def resize_image_explicit(
|
||||
|
||||
|
||||
def exif_auto_rotate(image):
|
||||
for orientation in ExifTags.TAGS.keys():
|
||||
for orientation in ExifTags.TAGS:
|
||||
if ExifTags.TAGS[orientation] == "Orientation":
|
||||
break
|
||||
exif = dict(image._getexif().items())
|
||||
|
@ -25,6 +25,7 @@
|
||||
import types
|
||||
from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import AccessMixin
|
||||
from django.core.exceptions import (
|
||||
ImproperlyConfigured,
|
||||
@ -35,6 +36,7 @@ from django.http import (
|
||||
HttpResponseNotFound,
|
||||
HttpResponseServerError,
|
||||
)
|
||||
from django.shortcuts import render
|
||||
from django.utils.functional import cached_property
|
||||
from django.views.generic.base import View
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
@ -79,9 +81,7 @@ def can_edit_prop(obj: Any, user: User) -> bool:
|
||||
raise PermissionDenied
|
||||
```
|
||||
"""
|
||||
if obj is None or user.is_owner(obj):
|
||||
return True
|
||||
return False
|
||||
return obj is None or user.is_owner(obj)
|
||||
|
||||
|
||||
def can_edit(obj: Any, user: User) -> bool:
|
||||
@ -232,7 +232,9 @@ class UserIsRootMixin(GenericContentPermissionMixinBuilder):
|
||||
PermissionDenied: if the user isn't root
|
||||
"""
|
||||
|
||||
permission_function = lambda obj, user: user.is_root
|
||||
@staticmethod
|
||||
def permission_function(obj: Any, user: User):
|
||||
return user.is_root
|
||||
|
||||
|
||||
class FormerSubscriberMixin(AccessMixin):
|
||||
@ -304,10 +306,10 @@ class QuickNotifMixin:
|
||||
kwargs["quick_notifs"] = []
|
||||
for n in self.quick_notif_list:
|
||||
kwargs["quick_notifs"].append(settings.SITH_QUICK_NOTIF[n])
|
||||
for k, v in settings.SITH_QUICK_NOTIF.items():
|
||||
for gk in self.request.GET.keys():
|
||||
if k == gk:
|
||||
kwargs["quick_notifs"].append(v)
|
||||
for key, val in settings.SITH_QUICK_NOTIF.items():
|
||||
for gk in self.request.GET:
|
||||
if key == gk:
|
||||
kwargs["quick_notifs"].append(val)
|
||||
return kwargs
|
||||
|
||||
|
||||
@ -324,8 +326,10 @@ class DetailFormView(SingleObjectMixin, FormView):
|
||||
return super().get_object()
|
||||
|
||||
|
||||
from .files import *
|
||||
from .group import *
|
||||
from .page import *
|
||||
from .site import *
|
||||
from .user import *
|
||||
# F403: those star-imports would be hellish to refactor
|
||||
# E402: putting those import at the top of the file would also be difficult
|
||||
from .files import * # noqa: F403 E402
|
||||
from .group import * # noqa: F403 E402
|
||||
from .page import * # noqa: F403 E402
|
||||
from .site import * # noqa: F403 E402
|
||||
from .user import * # noqa: F403 E402
|
||||
|
@ -193,7 +193,7 @@ class FileEditView(CanEditMixin, UpdateView):
|
||||
def get_form_class(self):
|
||||
fields = ["name", "is_moderated"]
|
||||
if self.object.is_file:
|
||||
fields = ["file"] + fields
|
||||
fields = ["file", *fields]
|
||||
return modelform_factory(SithFile, fields=fields)
|
||||
|
||||
def get_success_url(self):
|
||||
@ -283,38 +283,38 @@ class FileView(CanViewMixin, DetailView, FormMixin):
|
||||
`obj` is the SithFile object you want to put in the clipboard, or
|
||||
where you want to paste the clipboard
|
||||
"""
|
||||
if "delete" in request.POST.keys():
|
||||
if "delete" in request.POST:
|
||||
for f_id in request.POST.getlist("file_list"):
|
||||
sf = SithFile.objects.filter(id=f_id).first()
|
||||
if sf:
|
||||
sf.delete()
|
||||
if "clear" in request.POST.keys():
|
||||
file = SithFile.objects.filter(id=f_id).first()
|
||||
if file:
|
||||
file.delete()
|
||||
if "clear" in request.POST:
|
||||
request.session["clipboard"] = []
|
||||
if "cut" in request.POST.keys():
|
||||
for f_id in request.POST.getlist("file_list"):
|
||||
f_id = int(f_id)
|
||||
if "cut" in request.POST:
|
||||
for f_id_str in request.POST.getlist("file_list"):
|
||||
f_id = int(f_id_str)
|
||||
if (
|
||||
f_id in [c.id for c in obj.children.all()]
|
||||
and f_id not in request.session["clipboard"]
|
||||
):
|
||||
request.session["clipboard"].append(f_id)
|
||||
if "paste" in request.POST.keys():
|
||||
if "paste" in request.POST:
|
||||
for f_id in request.session["clipboard"]:
|
||||
sf = SithFile.objects.filter(id=f_id).first()
|
||||
if sf:
|
||||
sf.move_to(obj)
|
||||
file = SithFile.objects.filter(id=f_id).first()
|
||||
if file:
|
||||
file.move_to(obj)
|
||||
request.session["clipboard"] = []
|
||||
request.session.modified = True
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.form = self.get_form()
|
||||
if "clipboard" not in request.session.keys():
|
||||
if "clipboard" not in request.session:
|
||||
request.session["clipboard"] = []
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
if "clipboard" not in request.session.keys():
|
||||
if "clipboard" not in request.session:
|
||||
request.session["clipboard"] = []
|
||||
if request.user.can_edit(self.object):
|
||||
# XXX this call can fail!
|
||||
@ -398,6 +398,6 @@ class FileModerateView(CanEditPropMixin, SingleObjectMixin):
|
||||
self.object.is_moderated = True
|
||||
self.object.moderator = request.user
|
||||
self.object.save()
|
||||
if "next" in self.request.GET.keys():
|
||||
if "next" in self.request.GET:
|
||||
return redirect(self.request.GET["next"])
|
||||
return redirect("core:file_moderation")
|
||||
|
@ -29,6 +29,7 @@ from captcha.fields import CaptchaField
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.forms import (
|
||||
@ -38,7 +39,6 @@ from django.forms import (
|
||||
Textarea,
|
||||
TextInput,
|
||||
)
|
||||
from django.templatetags.static import static
|
||||
from django.utils.translation import gettext
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||
@ -72,8 +72,8 @@ class MarkdownInput(Textarea):
|
||||
context = super().get_context(name, value, attrs)
|
||||
|
||||
context["statics"] = {
|
||||
"js": static("webpack/easymde-index.js"),
|
||||
"css": static("webpack/easymde-index.css"),
|
||||
"js": staticfiles_storage.url("webpack/easymde-index.ts"),
|
||||
"css": staticfiles_storage.url("webpack/easymde-index.css"),
|
||||
}
|
||||
return context
|
||||
|
||||
@ -140,7 +140,7 @@ class SelectUser(TextInput):
|
||||
|
||||
class LoginForm(AuthenticationForm):
|
||||
def __init__(self, *arg, **kwargs):
|
||||
if "data" in kwargs.keys():
|
||||
if "data" in kwargs:
|
||||
from counter.models import Customer
|
||||
|
||||
data = kwargs["data"].copy()
|
||||
@ -157,7 +157,7 @@ class LoginForm(AuthenticationForm):
|
||||
else:
|
||||
user = User.objects.filter(username=data["username"]).first()
|
||||
data["username"] = user.username
|
||||
except:
|
||||
except: # noqa E722 I don't know what error is supposed to be raised here
|
||||
pass
|
||||
kwargs["data"] = data
|
||||
super().__init__(*arg, **kwargs)
|
||||
|
@ -55,7 +55,7 @@ class PageView(CanViewMixin, DetailView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
if "page" not in context.keys():
|
||||
if "page" not in context:
|
||||
context["new_page"] = self.kwargs["page_name"]
|
||||
return context
|
||||
|
||||
@ -92,22 +92,16 @@ class PageRevView(CanViewMixin, DetailView):
|
||||
)
|
||||
return res
|
||||
|
||||
def get_object(self):
|
||||
def get_object(self, *args, **kwargs):
|
||||
self.page = Page.get_page_by_full_name(self.kwargs["page_name"])
|
||||
return self.page
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
if self.page is not None:
|
||||
context["page"] = self.page
|
||||
try:
|
||||
rev = self.page.revisions.get(id=self.kwargs["rev"])
|
||||
context["rev"] = rev
|
||||
except:
|
||||
# By passing, the template will just display the normal page without taking revision into account
|
||||
pass
|
||||
else:
|
||||
context["new_page"] = self.kwargs["page_name"]
|
||||
if not self.page:
|
||||
return context | {"new_page": self.kwargs["page_name"]}
|
||||
context["page"] = self.page
|
||||
context["rev"] = self.page.revisions.filter(id=self.kwargs["rev"]).first()
|
||||
return context
|
||||
|
||||
|
||||
@ -118,7 +112,7 @@ class PageCreateView(CanCreateMixin, CreateView):
|
||||
|
||||
def get_initial(self):
|
||||
init = {}
|
||||
if "page" in self.request.GET.keys():
|
||||
if "page" in self.request.GET:
|
||||
page_name = self.request.GET["page"]
|
||||
parent_name = "/".join(page_name.split("/")[:-1])
|
||||
parent = Page.get_page_by_full_name(parent_name)
|
||||
@ -145,18 +139,8 @@ class PagePropView(CanEditPagePropMixin, UpdateView):
|
||||
slug_field = "_full_name"
|
||||
slug_url_kwarg = "page_name"
|
||||
|
||||
def get_object(self):
|
||||
o = super().get_object()
|
||||
# Create the page if it does not exists
|
||||
# if p == None:
|
||||
# parent_name = '/'.join(page_name.split('/')[:-1])
|
||||
# name = page_name.split('/')[-1]
|
||||
# if parent_name == "":
|
||||
# p = Page(name=name)
|
||||
# else:
|
||||
# parent = Page.get_page_by_full_name(parent_name)
|
||||
# p = Page(name=name, parent=parent)
|
||||
self.page = o
|
||||
def get_object(self, queryset=None):
|
||||
self.page = super().get_object()
|
||||
try:
|
||||
self.page.set_lock_recursive(self.request.user)
|
||||
except LockError as e:
|
||||
|
@ -53,11 +53,8 @@ class NotificationList(ListView):
|
||||
if self.request.user.is_anonymous:
|
||||
return Notification.objects.none()
|
||||
# TODO: Bulk update in django 2.2
|
||||
if "see_all" in self.request.GET.keys():
|
||||
for n in self.request.user.notifications.filter(viewed=False):
|
||||
n.viewed = True
|
||||
n.save()
|
||||
|
||||
if "see_all" in self.request.GET:
|
||||
self.request.user.notifications.filter(viewed=False).update(viewed=True)
|
||||
return self.request.user.notifications.order_by("-date")[:20]
|
||||
|
||||
|
||||
|
@ -21,9 +21,11 @@
|
||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#
|
||||
#
|
||||
import itertools
|
||||
|
||||
# This file contains all the views that concern the user model
|
||||
from datetime import date, timedelta
|
||||
from operator import itemgetter
|
||||
from smtplib import SMTPException
|
||||
|
||||
from django.conf import settings
|
||||
@ -253,8 +255,10 @@ class UserTabsMixin(TabedViewMixin):
|
||||
"name": _("Groups"),
|
||||
}
|
||||
)
|
||||
try:
|
||||
if user.customer and (
|
||||
if (
|
||||
hasattr(user, "customer")
|
||||
and user.customer
|
||||
and (
|
||||
user == self.request.user
|
||||
or self.request.user.is_in_group(
|
||||
pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID
|
||||
@ -264,25 +268,22 @@ class UserTabsMixin(TabedViewMixin):
|
||||
+ settings.SITH_BOARD_SUFFIX
|
||||
)
|
||||
or self.request.user.is_root
|
||||
):
|
||||
tab_list.append(
|
||||
{
|
||||
"url": reverse("core:user_stats", kwargs={"user_id": user.id}),
|
||||
"slug": "stats",
|
||||
"name": _("Stats"),
|
||||
}
|
||||
)
|
||||
tab_list.append(
|
||||
{
|
||||
"url": reverse(
|
||||
"core:user_account", kwargs={"user_id": user.id}
|
||||
),
|
||||
"slug": "account",
|
||||
"name": _("Account") + " (%s €)" % user.customer.amount,
|
||||
}
|
||||
)
|
||||
except:
|
||||
pass
|
||||
)
|
||||
):
|
||||
tab_list.append(
|
||||
{
|
||||
"url": reverse("core:user_stats", kwargs={"user_id": user.id}),
|
||||
"slug": "stats",
|
||||
"name": _("Stats"),
|
||||
}
|
||||
)
|
||||
tab_list.append(
|
||||
{
|
||||
"url": reverse("core:user_account", kwargs={"user_id": user.id}),
|
||||
"slug": "account",
|
||||
"name": _("Account") + " (%s €)" % user.customer.amount,
|
||||
}
|
||||
)
|
||||
return tab_list
|
||||
|
||||
|
||||
@ -665,9 +666,15 @@ class UserAccountView(UserAccountBase):
|
||||
kwargs["refilling_month"] = self.expense_by_month(
|
||||
Refilling.objects.filter(customer=self.object.customer)
|
||||
)
|
||||
kwargs["invoices_month"] = self.expense_by_month(
|
||||
Invoice.objects.filter(user=self.object)
|
||||
)
|
||||
kwargs["invoices_month"] = [
|
||||
# the django ORM removes the `group by` clause in this query,
|
||||
# so a little of post-processing is needed
|
||||
{"grouped_date": key, "total": sum(i["total"] for i in group)}
|
||||
for key, group in itertools.groupby(
|
||||
self.expense_by_month(Invoice.objects.filter(user=self.object)),
|
||||
key=itemgetter("grouped_date"),
|
||||
)
|
||||
]
|
||||
kwargs["etickets"] = self.object.customer.buyings.exclude(product__eticket=None)
|
||||
return kwargs
|
||||
|
||||
|
@ -15,7 +15,19 @@
|
||||
from django.contrib import admin
|
||||
from haystack.admin import SearchModelAdmin
|
||||
|
||||
from counter.models import *
|
||||
from counter.models import (
|
||||
AccountDump,
|
||||
BillingInfo,
|
||||
CashRegisterSummary,
|
||||
Counter,
|
||||
Customer,
|
||||
Eticket,
|
||||
Permanency,
|
||||
Product,
|
||||
ProductType,
|
||||
Refilling,
|
||||
Selling,
|
||||
)
|
||||
|
||||
|
||||
@admin.register(Product)
|
||||
|
@ -154,7 +154,7 @@ class Customer(models.Model):
|
||||
self.save()
|
||||
|
||||
def get_full_url(self):
|
||||
return "".join(["https://", settings.SITH_URL, self.get_absolute_url()])
|
||||
return f"https://{settings.SITH_URL}{self.get_absolute_url()}"
|
||||
|
||||
|
||||
class BillingInfo(models.Model):
|
||||
@ -287,9 +287,7 @@ class ProductType(models.Model):
|
||||
"""Method to see if that object can be edited by the given user."""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
return True
|
||||
return False
|
||||
return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
|
||||
|
||||
|
||||
class Product(models.Model):
|
||||
@ -346,21 +344,19 @@ class Product(models.Model):
|
||||
|
||||
@property
|
||||
def is_record_product(self):
|
||||
return settings.SITH_ECOCUP_CONS == self.id
|
||||
return self.id == settings.SITH_ECOCUP_CONS
|
||||
|
||||
@property
|
||||
def is_unrecord_product(self):
|
||||
return settings.SITH_ECOCUP_DECO == self.id
|
||||
return self.id == settings.SITH_ECOCUP_DECO
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""Method to see if that object can be edited by the given user."""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
if user.is_in_group(
|
||||
return user.is_in_group(
|
||||
pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID
|
||||
) or user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID):
|
||||
return True
|
||||
return False
|
||||
) or user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)
|
||||
|
||||
def can_be_sold_to(self, user: User) -> bool:
|
||||
"""Check if whether the user given in parameter has the right to buy
|
||||
@ -392,10 +388,7 @@ class Product(models.Model):
|
||||
buying_groups = list(self.buying_groups.all())
|
||||
if not buying_groups:
|
||||
return True
|
||||
for group in buying_groups:
|
||||
if user.is_in_group(pk=group.id):
|
||||
return True
|
||||
return False
|
||||
return any(user.is_in_group(pk=group.id) for group in buying_groups)
|
||||
|
||||
@property
|
||||
def profit(self):
|
||||
@ -887,27 +880,19 @@ class Selling(models.Model):
|
||||
"You bought an eticket for the event %(event)s.\nYou can download it directly from this link %(eticket)s.\nYou can also retrieve all your e-tickets on your account page %(url)s."
|
||||
) % {
|
||||
"event": event,
|
||||
"url": "".join(
|
||||
(
|
||||
'<a href="',
|
||||
self.customer.get_full_url(),
|
||||
'">',
|
||||
self.customer.get_full_url(),
|
||||
"</a>",
|
||||
)
|
||||
"url": (
|
||||
f'<a href="{self.customer.get_full_url()}">'
|
||||
f"{self.customer.get_full_url()}</a>"
|
||||
),
|
||||
"eticket": "".join(
|
||||
(
|
||||
'<a href="',
|
||||
self.get_eticket_full_url(),
|
||||
'">',
|
||||
self.get_eticket_full_url(),
|
||||
"</a>",
|
||||
)
|
||||
"eticket": (
|
||||
f'<a href="{self.get_eticket_full_url()}">'
|
||||
f"{self.get_eticket_full_url()}</a>"
|
||||
),
|
||||
}
|
||||
message_txt = _(
|
||||
"You bought an eticket for the event %(event)s.\nYou can download it directly from this link %(eticket)s.\nYou can also retrieve all your e-tickets on your account page %(url)s."
|
||||
"You bought an eticket for the event %(event)s.\n"
|
||||
"You can download it directly from this link %(eticket)s.\n"
|
||||
"You can also retrieve all your e-tickets on your account page %(url)s."
|
||||
) % {
|
||||
"event": event,
|
||||
"url": self.customer.get_full_url(),
|
||||
@ -919,7 +904,7 @@ class Selling(models.Model):
|
||||
|
||||
def get_eticket_full_url(self):
|
||||
eticket_url = reverse("counter:eticket_pdf", kwargs={"selling_id": self.id})
|
||||
return "".join(["https://", settings.SITH_URL, eticket_url])
|
||||
return f"https://{settings.SITH_URL}{eticket_url}"
|
||||
|
||||
|
||||
class Permanency(models.Model):
|
||||
@ -1019,15 +1004,15 @@ class CashRegisterSummary(models.Model):
|
||||
elif name == "hundred_euros":
|
||||
return self.items.filter(value=100, is_check=False).first()
|
||||
elif name == "check_1":
|
||||
return checks[0] if 0 < len(checks) else None
|
||||
return checks[0] if len(checks) > 0 else None
|
||||
elif name == "check_2":
|
||||
return checks[1] if 1 < len(checks) else None
|
||||
return checks[1] if len(checks) > 1 else None
|
||||
elif name == "check_3":
|
||||
return checks[2] if 2 < len(checks) else None
|
||||
return checks[2] if len(checks) > 2 else None
|
||||
elif name == "check_4":
|
||||
return checks[3] if 3 < len(checks) else None
|
||||
return checks[3] if len(checks) > 3 else None
|
||||
elif name == "check_5":
|
||||
return checks[4] if 4 < len(checks) else None
|
||||
return checks[4] if len(checks) > 4 else None
|
||||
else:
|
||||
return object.__getattribute__(self, name)
|
||||
|
||||
@ -1035,9 +1020,7 @@ class CashRegisterSummary(models.Model):
|
||||
"""Method to see if that object can be edited by the given user."""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID):
|
||||
return True
|
||||
return False
|
||||
return user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)
|
||||
|
||||
def get_total(self):
|
||||
t = 0
|
||||
|
@ -51,7 +51,7 @@ def write_log(instance, operation_type):
|
||||
# Return None by default
|
||||
return None
|
||||
|
||||
log = OperationLog(
|
||||
OperationLog(
|
||||
label=str(instance),
|
||||
operator=get_user(),
|
||||
operation_type=operation_type,
|
||||
|
@ -8,25 +8,17 @@
|
||||
{% if current_tab == "products" %}
|
||||
<p><a href="{{ url('counter:new_product') }}">{% trans %}New product{% endtrans %}</a></p>
|
||||
{% endif %}
|
||||
{% if product_list %}
|
||||
<h3>{% trans %}Product list{% endtrans %}</h3>
|
||||
{% for t in ProductType.objects.all().order_by('name') %}
|
||||
<h4>{{ t }}</h4>
|
||||
<ul>
|
||||
{% for p in product_list.filter(product_type=t).all().order_by('name') %}
|
||||
<li><a href="{{ url('counter:product_edit', product_id=p.id) }}">{{ p }} ({{ p.code }})</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
<h4>{% trans %}Uncategorized{% endtrans %}</h4>
|
||||
<h3>{% trans %}Product list{% endtrans %}</h3>
|
||||
{%- for product_type, products in object_list -%}
|
||||
<h4>{{ product_type or _("Uncategorized") }}</h4>
|
||||
<ul>
|
||||
{% for p in product_list.filter(product_type=None).all().order_by('name') %}
|
||||
<li><a href="{{ url('counter:product_edit', product_id=p.id) }}">{{ p }} ({{ p.code }})</a></li>
|
||||
{% endfor %}
|
||||
{%- for product in products -%}
|
||||
<li><a href="{{ url('counter:product_edit', product_id=product.id) }}">{{ product }} ({{ product.code }})</a></li>
|
||||
{%- endfor -%}
|
||||
</ul>
|
||||
{% else %}
|
||||
{%- else -%}
|
||||
{% trans %}There is no products in this website.{% endtrans %}
|
||||
{% endif %}
|
||||
{%- endfor -%}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
@ -503,7 +503,7 @@ class TestBarmanConnection(TestCase):
|
||||
)
|
||||
response = self.client.get(reverse("counter:activity", args=[self.counter.id]))
|
||||
|
||||
assert not '<li><a href="/user/1/">S' Kia</a></li>' in str(response.content)
|
||||
assert '<li><a href="/user/1/">S' Kia</a></li>' not in str(response.content)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@ -853,7 +853,7 @@ class TestCustomerAccountId(TestCase):
|
||||
number = account_id[:-1]
|
||||
assert created is True
|
||||
assert number == "12346"
|
||||
assert 6 == len(account_id)
|
||||
assert len(account_id) == 6
|
||||
assert account_id[-1] in string.ascii_lowercase
|
||||
assert customer.amount == 0
|
||||
|
||||
|
@ -15,15 +15,44 @@
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from counter.views import *
|
||||
from counter.views import (
|
||||
ActiveProductListView,
|
||||
ArchivedProductListView,
|
||||
CashSummaryEditView,
|
||||
CashSummaryListView,
|
||||
CounterActivityView,
|
||||
CounterCashSummaryView,
|
||||
CounterClick,
|
||||
CounterCreateView,
|
||||
CounterDeleteView,
|
||||
CounterEditPropView,
|
||||
CounterEditView,
|
||||
CounterLastOperationsView,
|
||||
CounterListView,
|
||||
CounterMain,
|
||||
CounterRefillingListView,
|
||||
CounterStatView,
|
||||
EticketCreateView,
|
||||
EticketEditView,
|
||||
EticketListView,
|
||||
EticketPDFView,
|
||||
InvoiceCallView,
|
||||
ProductCreateView,
|
||||
ProductEditView,
|
||||
ProductTypeCreateView,
|
||||
ProductTypeEditView,
|
||||
ProductTypeListView,
|
||||
RefillingDeleteView,
|
||||
SellingDeleteView,
|
||||
StudentCardDeleteView,
|
||||
StudentCardFormView,
|
||||
counter_login,
|
||||
counter_logout,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("<int:counter_id>/", CounterMain.as_view(), name="details"),
|
||||
path(
|
||||
"<int:counter_id>/click/<int:user_id>/",
|
||||
CounterClick.as_view(),
|
||||
name="click",
|
||||
),
|
||||
path("<int:counter_id>/click/<int:user_id>/", CounterClick.as_view(), name="click"),
|
||||
path(
|
||||
"<int:counter_id>/last_ops/",
|
||||
CounterLastOperationsView.as_view(),
|
||||
@ -34,19 +63,11 @@ urlpatterns = [
|
||||
CounterCashSummaryView.as_view(),
|
||||
name="cash_summary",
|
||||
),
|
||||
path(
|
||||
"<int:counter_id>/activity/",
|
||||
CounterActivityView.as_view(),
|
||||
name="activity",
|
||||
),
|
||||
path("<int:counter_id>/activity/", CounterActivityView.as_view(), name="activity"),
|
||||
path("<int:counter_id>/stats/", CounterStatView.as_view(), name="stats"),
|
||||
path("<int:counter_id>/login/", counter_login, name="login"),
|
||||
path("<int:counter_id>/logout/", counter_logout, name="logout"),
|
||||
path(
|
||||
"eticket/<int:selling_id>/pdf/",
|
||||
EticketPDFView.as_view(),
|
||||
name="eticket_pdf",
|
||||
),
|
||||
path("eticket/<int:selling_id>/pdf/", EticketPDFView.as_view(), name="eticket_pdf"),
|
||||
path(
|
||||
"customer/<int:customer_id>/card/add/",
|
||||
StudentCardFormView.as_view(),
|
||||
@ -59,17 +80,11 @@ urlpatterns = [
|
||||
),
|
||||
path("admin/<int:counter_id>/", CounterEditView.as_view(), name="admin"),
|
||||
path(
|
||||
"admin/<int:counter_id>/prop/",
|
||||
CounterEditPropView.as_view(),
|
||||
name="prop_admin",
|
||||
"admin/<int:counter_id>/prop/", CounterEditPropView.as_view(), name="prop_admin"
|
||||
),
|
||||
path("admin/", CounterListView.as_view(), name="admin_list"),
|
||||
path("admin/new/", CounterCreateView.as_view(), name="new"),
|
||||
path(
|
||||
"admin/delete/<int:counter_id>/",
|
||||
CounterDeleteView.as_view(),
|
||||
name="delete",
|
||||
),
|
||||
path("admin/delete/<int:counter_id>/", CounterDeleteView.as_view(), name="delete"),
|
||||
path("admin/invoices_call/", InvoiceCallView.as_view(), name="invoices_call"),
|
||||
path(
|
||||
"admin/cash_summary/list/",
|
||||
@ -81,10 +96,10 @@ urlpatterns = [
|
||||
CashSummaryEditView.as_view(),
|
||||
name="cash_summary_edit",
|
||||
),
|
||||
path("admin/product/list/", ProductListView.as_view(), name="product_list"),
|
||||
path("admin/product/list/", ActiveProductListView.as_view(), name="product_list"),
|
||||
path(
|
||||
"admin/product/list_archived/",
|
||||
ProductArchivedListView.as_view(),
|
||||
ArchivedProductListView.as_view(),
|
||||
name="product_list_archived",
|
||||
),
|
||||
path("admin/product/create/", ProductCreateView.as_view(), name="new_product"),
|
||||
|
@ -12,10 +12,12 @@
|
||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||
#
|
||||
#
|
||||
import itertools
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import timezone as tz
|
||||
from http import HTTPStatus
|
||||
from operator import attrgetter
|
||||
from typing import TYPE_CHECKING
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
@ -89,16 +91,10 @@ class CounterAdminMixin(View):
|
||||
edit_club = []
|
||||
|
||||
def _test_group(self, user):
|
||||
for grp_id in self.edit_group:
|
||||
if user.is_in_group(pk=grp_id):
|
||||
return True
|
||||
return False
|
||||
return any(user.is_in_group(pk=grp_id) for grp_id in self.edit_group)
|
||||
|
||||
def _test_club(self, user):
|
||||
for c in self.edit_club:
|
||||
if c.can_be_edited_by(user):
|
||||
return True
|
||||
return False
|
||||
return any(c.can_be_edited_by(user) for c in self.edit_club)
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not (
|
||||
@ -179,7 +175,7 @@ class CounterMain(
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
if self.object.type == "BAR" and not (
|
||||
"counter_token" in self.request.session.keys()
|
||||
"counter_token" in self.request.session
|
||||
and self.request.session["counter_token"] == self.object.token
|
||||
): # Check the token to avoid the bar to be stolen
|
||||
return HttpResponseRedirect(
|
||||
@ -217,7 +213,7 @@ class CounterMain(
|
||||
kwargs["barmen"] = self.object.barmen_list
|
||||
elif self.request.user.is_authenticated:
|
||||
kwargs["barmen"] = [self.request.user]
|
||||
if "last_basket" in self.request.session.keys():
|
||||
if "last_basket" in self.request.session:
|
||||
kwargs["last_basket"] = self.request.session.pop("last_basket")
|
||||
kwargs["last_customer"] = self.request.session.pop("last_customer")
|
||||
kwargs["last_total"] = self.request.session.pop("last_total")
|
||||
@ -292,7 +288,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Simple get view."""
|
||||
if "basket" not in request.session.keys(): # Init the basket session entry
|
||||
if "basket" not in request.session: # Init the basket session entry
|
||||
request.session["basket"] = {}
|
||||
request.session["basket_total"] = 0
|
||||
request.session["not_enough"] = False # Reset every variable
|
||||
@ -316,7 +312,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
): # Check that at least one barman is logged in
|
||||
return self.cancel(request)
|
||||
if self.object.type == "BAR" and not (
|
||||
"counter_token" in self.request.session.keys()
|
||||
"counter_token" in self.request.session
|
||||
and self.request.session["counter_token"] == self.object.token
|
||||
): # Also check the token to avoid the bar to be stolen
|
||||
return HttpResponseRedirect(
|
||||
@ -327,7 +323,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
)
|
||||
+ "?bad_location"
|
||||
)
|
||||
if "basket" not in request.session.keys():
|
||||
if "basket" not in request.session:
|
||||
request.session["basket"] = {}
|
||||
request.session["basket_total"] = 0
|
||||
request.session["not_enough"] = False # Reset every variable
|
||||
@ -384,13 +380,12 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
|
||||
def get_total_quantity_for_pid(self, request, pid):
|
||||
pid = str(pid)
|
||||
try:
|
||||
return (
|
||||
request.session["basket"][pid]["qty"]
|
||||
+ request.session["basket"][pid]["bonus_qty"]
|
||||
)
|
||||
except:
|
||||
if pid not in request.session["basket"]:
|
||||
return 0
|
||||
return (
|
||||
request.session["basket"][pid]["qty"]
|
||||
+ request.session["basket"][pid]["bonus_qty"]
|
||||
)
|
||||
|
||||
def compute_record_product(self, request, product=None):
|
||||
recorded = 0
|
||||
@ -804,25 +799,41 @@ class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
||||
current_tab = "products"
|
||||
|
||||
|
||||
class ProductArchivedListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
||||
class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
||||
model = Product
|
||||
queryset = Product.objects.annotate(type_name=F("product_type__name"))
|
||||
template_name = "counter/product_list.jinja"
|
||||
ordering = [
|
||||
F("product_type__priority").desc(nulls_last=True),
|
||||
"product_type",
|
||||
"name",
|
||||
]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
res = super().get_context_data(**kwargs)
|
||||
res["object_list"] = itertools.groupby(
|
||||
res["object_list"], key=attrgetter("type_name")
|
||||
)
|
||||
return res
|
||||
|
||||
|
||||
class ArchivedProductListView(ProductListView):
|
||||
"""A list view for the admins."""
|
||||
|
||||
model = Product
|
||||
template_name = "counter/product_list.jinja"
|
||||
queryset = Product.objects.filter(archived=True)
|
||||
ordering = ["name"]
|
||||
current_tab = "archive"
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(archived=True)
|
||||
|
||||
class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
||||
|
||||
class ActiveProductListView(ProductListView):
|
||||
"""A list view for the admins."""
|
||||
|
||||
model = Product
|
||||
template_name = "counter/product_list.jinja"
|
||||
queryset = Product.objects.filter(archived=False)
|
||||
ordering = ["name"]
|
||||
current_tab = "products"
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(archived=False)
|
||||
|
||||
|
||||
class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
|
||||
"""A create view for the admins."""
|
||||
|
@ -24,6 +24,12 @@ Si le mot apparaît dans le template Jinja :
|
||||
{% trans %}Hello{% endtrans %}
|
||||
```
|
||||
|
||||
Si on est dans un fichier javascript ou typescript :
|
||||
|
||||
```js
|
||||
gettext("Hello");
|
||||
```
|
||||
|
||||
## Générer le fichier django.po
|
||||
|
||||
La traduction se fait en trois étapes.
|
||||
@ -32,7 +38,7 @@ l'éditer et enfin le compiler au format binaire pour qu'il soit lu par le serve
|
||||
|
||||
```bash
|
||||
./manage.py makemessages --locale=fr -e py,jinja --ignore=node_modules # Pour le backend
|
||||
./manage.py makemessages --locale=fr -d djangojs --ignore=node_modules # Pour le frontend
|
||||
./manage.py makemessages --locale=fr -d djangojs -e js,ts --ignore=node_modules # Pour le frontend
|
||||
```
|
||||
|
||||
## Éditer le fichier django.po
|
||||
|
@ -190,6 +190,10 @@ que sont VsCode et Sublime Text.
|
||||
"[javascript]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -246,15 +246,3 @@ pytest core/tests/tests_core.py::TestUserRegistration
|
||||
tous les tests avant de push un commit.
|
||||
|
||||
|
||||
|
||||
## Vérifier les dépendances Javascript
|
||||
|
||||
Une commande a été écrite pour vérifier les éventuelles mises
|
||||
à jour à faire sur les librairies Javascript utilisées.
|
||||
N'oubliez pas de mettre à jour à la fois le fichier
|
||||
de la librairie, mais également sa version dans `sith/settings.py`.
|
||||
|
||||
```bash
|
||||
# Vérifier les mises à jour
|
||||
python manage.py check_front
|
||||
```
|
||||
|
@ -13,8 +13,9 @@
|
||||
#
|
||||
#
|
||||
from django.contrib import admin
|
||||
from django.db.models import F, Sum
|
||||
|
||||
from eboutic.models import *
|
||||
from eboutic.models import Basket, BasketItem, Invoice, InvoiceItem
|
||||
|
||||
|
||||
@admin.register(Basket)
|
||||
|
@ -117,9 +117,7 @@ class BasketForm:
|
||||
"""
|
||||
if not self.error_messages and not self.correct_items:
|
||||
self.clean()
|
||||
if self.error_messages:
|
||||
return False
|
||||
return True
|
||||
return not self.error_messages
|
||||
|
||||
@cached_property
|
||||
def errors(self) -> list[str]:
|
||||
|
@ -173,9 +173,8 @@ class InvoiceQueryset(models.QuerySet):
|
||||
return self.annotate(
|
||||
total=Subquery(
|
||||
InvoiceItem.objects.filter(invoice_id=OuterRef("pk"))
|
||||
.annotate(item_amount=F("product_unit_price") * F("quantity"))
|
||||
.values("item_amount")
|
||||
.annotate(total=Sum("item_amount"))
|
||||
.values("invoice_id")
|
||||
.annotate(total=Sum(F("product_unit_price") * F("quantity")))
|
||||
.values("total")
|
||||
)
|
||||
)
|
||||
|
@ -2,8 +2,6 @@ from typing import Annotated
|
||||
|
||||
from ninja import ModelSchema, Schema
|
||||
from pydantic import Field, NonNegativeInt, PositiveInt, TypeAdapter
|
||||
|
||||
# from phonenumber_field.phonenumber import PhoneNumber
|
||||
from pydantic_extra_types.phone_numbers import PhoneNumber, PhoneNumberValidator
|
||||
|
||||
from counter.models import BillingInfo
|
||||
|
@ -7,11 +7,11 @@
|
||||
|
||||
import base64
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey
|
||||
from cryptography.hazmat.primitives.hashes import SHA1
|
||||
from cryptography.hazmat.primitives.serialization import (
|
||||
load_pem_private_key,
|
||||
@ -19,6 +19,12 @@ from cryptography.hazmat.primitives.serialization import (
|
||||
)
|
||||
from django.conf import settings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import (
|
||||
RSAPrivateKey,
|
||||
RSAPublicKey,
|
||||
)
|
||||
|
||||
|
||||
def test_signature_valid():
|
||||
"""Test that data sent to the bank is correctly signed."""
|
||||
|
@ -24,9 +24,9 @@
|
||||
import base64
|
||||
import json
|
||||
import urllib
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
||||
from cryptography.hazmat.primitives.hashes import SHA1
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||
from django.conf import settings
|
||||
@ -38,6 +38,9 @@ from core.models import User
|
||||
from counter.models import Counter, Customer, Product, Selling
|
||||
from eboutic.models import Basket, BasketItem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
||||
|
||||
|
||||
class TestEboutic(TestCase):
|
||||
@classmethod
|
||||
|
@ -25,7 +25,14 @@
|
||||
from django.urls import path, register_converter
|
||||
|
||||
from eboutic.converters import PaymentResultConverter
|
||||
from eboutic.views import *
|
||||
from eboutic.views import (
|
||||
EbouticCommand,
|
||||
EtransactionAutoAnswer,
|
||||
e_transaction_data,
|
||||
eboutic_main,
|
||||
pay_with_sith,
|
||||
payment_result,
|
||||
)
|
||||
|
||||
register_converter(PaymentResultConverter, "res")
|
||||
|
||||
|
@ -17,11 +17,11 @@ import base64
|
||||
import json
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import sentry_sdk
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
|
||||
from cryptography.hazmat.primitives.hashes import SHA1
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_public_key
|
||||
from django.conf import settings
|
||||
@ -47,6 +47,9 @@ from eboutic.models import (
|
||||
)
|
||||
from eboutic.schemas import PurchaseItemList, PurchaseItemSchema
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
|
||||
|
||||
|
||||
@login_required
|
||||
@require_GET
|
||||
@ -221,7 +224,7 @@ class EtransactionAutoAnswer(View):
|
||||
# Payment authorized:
|
||||
# * 'Error' is '00000'
|
||||
# * 'Auto' is in the request
|
||||
if request.GET["Error"] == "00000" and "Auto" in request.GET.keys():
|
||||
if request.GET["Error"] == "00000" and "Auto" in request.GET:
|
||||
try:
|
||||
with transaction.atomic():
|
||||
b = (
|
||||
|
@ -1,6 +1,22 @@
|
||||
from django.urls import path
|
||||
|
||||
from election.views import *
|
||||
from election.views import (
|
||||
CandidatureCreateView,
|
||||
CandidatureDeleteView,
|
||||
CandidatureUpdateView,
|
||||
ElectionCreateView,
|
||||
ElectionDeleteView,
|
||||
ElectionDetailView,
|
||||
ElectionListArchivedView,
|
||||
ElectionListCreateView,
|
||||
ElectionListDeleteView,
|
||||
ElectionsListView,
|
||||
ElectionUpdateView,
|
||||
RoleCreateView,
|
||||
RoleDeleteView,
|
||||
RoleUpdateView,
|
||||
VoteFormView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("", ElectionsListView.as_view(), name="list"),
|
||||
@ -19,16 +35,10 @@ urlpatterns = [
|
||||
name="delete_list",
|
||||
),
|
||||
path(
|
||||
"<int:election_id>/role/create/",
|
||||
RoleCreateView.as_view(),
|
||||
name="create_role",
|
||||
"<int:election_id>/role/create/", RoleCreateView.as_view(), name="create_role"
|
||||
),
|
||||
path("<int:role_id>/role/edit/", RoleUpdateView.as_view(), name="update_role"),
|
||||
path(
|
||||
"<int:role_id>/role/delete/",
|
||||
RoleDeleteView.as_view(),
|
||||
name="delete_role",
|
||||
),
|
||||
path("<int:role_id>/role/delete/", RoleDeleteView.as_view(), name="delete_role"),
|
||||
path(
|
||||
"<int:election_id>/candidate/add/",
|
||||
CandidatureCreateView.as_view(),
|
||||
|
@ -1,3 +1,5 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ajax_select import make_ajax_field
|
||||
from ajax_select.fields import AutoCompleteSelectField
|
||||
from django import forms
|
||||
@ -10,11 +12,14 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import DetailView, ListView
|
||||
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
|
||||
|
||||
from core.models import User
|
||||
from core.views import CanCreateMixin, CanEditMixin, CanViewMixin
|
||||
from core.views.forms import MarkdownInput, SelectDateTime
|
||||
from election.models import Candidature, Election, ElectionList, Role, Vote
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.models import User
|
||||
|
||||
|
||||
# Custom form field
|
||||
|
||||
|
||||
@ -23,7 +28,6 @@ class LimitedCheckboxField(forms.ModelMultipleChoiceField):
|
||||
|
||||
def __init__(self, queryset, max_choice, **kwargs):
|
||||
self.max_choice = max_choice
|
||||
widget = forms.CheckboxSelectMultiple()
|
||||
super().__init__(queryset, **kwargs)
|
||||
|
||||
def clean(self, value):
|
||||
@ -251,7 +255,7 @@ class VoteFormView(CanCreateMixin, FormView):
|
||||
|
||||
def vote(self, election_data):
|
||||
with transaction.atomic():
|
||||
for role_title in election_data.keys():
|
||||
for role_title in election_data:
|
||||
# If we have a multiple choice field
|
||||
if isinstance(election_data[role_title], QuerySet):
|
||||
if election_data[role_title].count() > 0:
|
||||
@ -444,28 +448,16 @@ class ElectionUpdateView(CanEditMixin, UpdateView):
|
||||
pk_url_kwarg = "election_id"
|
||||
|
||||
def get_initial(self):
|
||||
init = {}
|
||||
try:
|
||||
init["start_date"] = self.object.start_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
init["end_date"] = self.object.end_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
init["start_candidature"] = self.object.start_candidature.strftime(
|
||||
return {
|
||||
"start_date": self.object.start_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"end_date": self.object.end_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"start_candidature": self.object.start_candidature.strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
init["end_candidature"] = self.object.end_candidature.strftime(
|
||||
),
|
||||
"end_candidature": self.object.end_candidature.strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return init
|
||||
),
|
||||
}
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
return reverse_lazy("election:detail", kwargs={"election_id": self.object.id})
|
||||
|
@ -16,7 +16,7 @@
|
||||
from django.contrib import admin
|
||||
from haystack.admin import SearchModelAdmin
|
||||
|
||||
from forum.models import *
|
||||
from forum.models import Forum, ForumMessage, ForumTopic
|
||||
|
||||
|
||||
@admin.register(Forum)
|
||||
|
@ -25,6 +25,7 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from datetime import timezone as tz
|
||||
from itertools import chain
|
||||
from typing import Self
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
@ -207,12 +208,12 @@ class Forum(models.Model):
|
||||
return self.get_parent_list()
|
||||
|
||||
def get_parent_list(self):
|
||||
l = []
|
||||
p = self.parent
|
||||
while p is not None:
|
||||
l.append(p)
|
||||
p = p.parent
|
||||
return l
|
||||
parents = []
|
||||
current = self.parent
|
||||
while current is not None:
|
||||
parents.append(current)
|
||||
current = current.parent
|
||||
return parents
|
||||
|
||||
@property
|
||||
def topic_number(self):
|
||||
@ -228,12 +229,12 @@ class Forum(models.Model):
|
||||
def last_message(self):
|
||||
return self._last_message
|
||||
|
||||
def get_children_list(self):
|
||||
l = [self.id]
|
||||
def get_children_list(self) -> list[Self]:
|
||||
children = [self.id]
|
||||
for c in self.children.all():
|
||||
l.append(c.id)
|
||||
l += c.get_children_list()
|
||||
return l
|
||||
children.append(c.id)
|
||||
children.extend(c.get_children_list())
|
||||
return children
|
||||
|
||||
|
||||
class ForumTopic(models.Model):
|
||||
|
@ -23,7 +23,26 @@
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from forum.views import *
|
||||
from forum.views import (
|
||||
ForumCreateView,
|
||||
ForumDeleteView,
|
||||
ForumDetailView,
|
||||
ForumEditView,
|
||||
ForumFavoriteTopics,
|
||||
ForumLastUnread,
|
||||
ForumMainView,
|
||||
ForumMarkAllAsRead,
|
||||
ForumMessageCreateView,
|
||||
ForumMessageDeleteView,
|
||||
ForumMessageEditView,
|
||||
ForumMessageUndeleteView,
|
||||
ForumMessageView,
|
||||
ForumSearchView,
|
||||
ForumTopicCreateView,
|
||||
ForumTopicDetailView,
|
||||
ForumTopicEditView,
|
||||
ForumTopicSubscribeView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("", ForumMainView.as_view(), name="main"),
|
||||
@ -35,21 +54,9 @@ urlpatterns = [
|
||||
path("<int:forum_id>/", ForumDetailView.as_view(), name="view_forum"),
|
||||
path("<int:forum_id>/edit/", ForumEditView.as_view(), name="edit_forum"),
|
||||
path("<int:forum_id>/delete/", ForumDeleteView.as_view(), name="delete_forum"),
|
||||
path(
|
||||
"<int:forum_id>/new_topic/",
|
||||
ForumTopicCreateView.as_view(),
|
||||
name="new_topic",
|
||||
),
|
||||
path(
|
||||
"topic/<int:topic_id>/",
|
||||
ForumTopicDetailView.as_view(),
|
||||
name="view_topic",
|
||||
),
|
||||
path(
|
||||
"topic/<int:topic_id>/edit/",
|
||||
ForumTopicEditView.as_view(),
|
||||
name="edit_topic",
|
||||
),
|
||||
path("<int:forum_id>/new_topic/", ForumTopicCreateView.as_view(), name="new_topic"),
|
||||
path("topic/<int:topic_id>/", ForumTopicDetailView.as_view(), name="view_topic"),
|
||||
path("topic/<int:topic_id>/edit/", ForumTopicEditView.as_view(), name="edit_topic"),
|
||||
path(
|
||||
"topic/<int:topic_id>/new_message/",
|
||||
ForumMessageCreateView.as_view(),
|
||||
@ -60,11 +67,7 @@ urlpatterns = [
|
||||
ForumTopicSubscribeView.as_view(),
|
||||
name="toggle_subscribe_topic",
|
||||
),
|
||||
path(
|
||||
"message/<int:message_id>/",
|
||||
ForumMessageView.as_view(),
|
||||
name="view_message",
|
||||
),
|
||||
path("message/<int:message_id>/", ForumMessageView.as_view(), name="view_message"),
|
||||
path(
|
||||
"message/<int:message_id>/edit/",
|
||||
ForumMessageEditView.as_view(),
|
||||
|
@ -71,7 +71,7 @@ class Command(BaseCommand):
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.logger = logging.getLogger("main")
|
||||
if options["verbosity"] < 0 or 2 < options["verbosity"]:
|
||||
if not 0 <= options["verbosity"] <= 2:
|
||||
warnings.warn(
|
||||
"verbosity level should be between 0 and 2 included", stacklevel=2
|
||||
)
|
||||
|
@ -40,7 +40,7 @@ class Command(BaseCommand):
|
||||
|
||||
def handle(self, *args, **options):
|
||||
logger = logging.getLogger("main")
|
||||
if options["verbosity"] < 0 or 2 < options["verbosity"]:
|
||||
if not 0 <= options["verbosity"] <= 2:
|
||||
warnings.warn(
|
||||
"verbosity level should be between 0 and 2 included", stacklevel=2
|
||||
)
|
||||
|
File diff suppressed because one or more lines are too long
2
galaxy/static/galaxy/js/d3-force-3d.min.js
vendored
2
galaxy/static/galaxy/js/d3-force-3d.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
6
galaxy/static/galaxy/js/three.min.js
vendored
6
galaxy/static/galaxy/js/three.min.js
vendored
File diff suppressed because one or more lines are too long
138
galaxy/static/webpack/galaxy/galaxy-index.js
Normal file
138
galaxy/static/webpack/galaxy/galaxy-index.js
Normal file
@ -0,0 +1,138 @@
|
||||
import { default as ForceGraph3D } from "3d-force-graph";
|
||||
import { forceX, forceY, forceZ } from "d3-force-3d";
|
||||
// biome-ignore lint/style/noNamespaceImport: This is how it should be imported
|
||||
import * as Three from "three";
|
||||
import SpriteText from "three-spritetext";
|
||||
|
||||
/**
|
||||
* @typedef GalaxyConfig
|
||||
* @property {number} nodeId id of the current user node
|
||||
* @property {string} dataUrl url to fetch the galaxy data from
|
||||
**/
|
||||
|
||||
/**
|
||||
* Load the galaxy of an user
|
||||
* @param {GalaxyConfig} config
|
||||
**/
|
||||
window.loadGalaxy = (config) => {
|
||||
window.getNodeFromId = (id) => {
|
||||
return Graph.graphData().nodes.find((n) => n.id === id);
|
||||
};
|
||||
|
||||
window.getLinksFromNodeId = (id) => {
|
||||
return Graph.graphData().links.filter(
|
||||
(l) => l.source.id === id || l.target.id === id,
|
||||
);
|
||||
};
|
||||
|
||||
window.focusNode = (node) => {
|
||||
highlightNodes.clear();
|
||||
highlightLinks.clear();
|
||||
|
||||
hoverNode = node || null;
|
||||
if (node) {
|
||||
// collect neighbors and links for highlighting
|
||||
for (const link of window.getLinksFromNodeId(node.id)) {
|
||||
highlightLinks.add(link);
|
||||
highlightNodes.add(link.source);
|
||||
highlightNodes.add(link.target);
|
||||
}
|
||||
}
|
||||
|
||||
// refresh node and link display
|
||||
Graph.nodeThreeObject(Graph.nodeThreeObject())
|
||||
.linkWidth(Graph.linkWidth())
|
||||
.linkDirectionalParticles(Graph.linkDirectionalParticles());
|
||||
|
||||
// Aim at node from outside it
|
||||
const distance = 42;
|
||||
const distRatio = 1 + distance / Math.hypot(node.x, node.y, node.z);
|
||||
|
||||
const newPos =
|
||||
node.x || node.y || node.z
|
||||
? { x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio }
|
||||
: { x: 0, y: 0, z: distance }; // special case if node is in (0,0,0)
|
||||
|
||||
Graph.cameraPosition(
|
||||
newPos, // new position
|
||||
node, // lookAt ({ x, y, z })
|
||||
3000, // ms transition duration
|
||||
);
|
||||
};
|
||||
|
||||
const highlightNodes = new Set();
|
||||
const highlightLinks = new Set();
|
||||
let hoverNode = null;
|
||||
|
||||
const grpahDiv = document.getElementById("3d-graph");
|
||||
const Graph = ForceGraph3D();
|
||||
Graph(grpahDiv);
|
||||
Graph.jsonUrl(config.dataUrl)
|
||||
.width(
|
||||
grpahDiv.parentElement.clientWidth > 1200
|
||||
? 1200
|
||||
: grpahDiv.parentElement.clientWidth,
|
||||
) // Not perfect at all. JS-fu master from the future, please fix this :-)
|
||||
.height(1000)
|
||||
.enableNodeDrag(false) // allow easier navigation
|
||||
.onNodeClick((node) => {
|
||||
const camera = Graph.cameraPosition();
|
||||
const distance = Math.sqrt(
|
||||
(node.x - camera.x) ** 2 + (node.y - camera.y) ** 2 + (node.z - camera.z) ** 2,
|
||||
);
|
||||
if (distance < 120 || highlightNodes.has(node)) {
|
||||
window.focusNode(node);
|
||||
}
|
||||
})
|
||||
.linkWidth((link) => (highlightLinks.has(link) ? 0.4 : 0.0))
|
||||
.linkColor((link) =>
|
||||
highlightLinks.has(link) ? "rgba(255,160,0,1)" : "rgba(128,255,255,0.6)",
|
||||
)
|
||||
.linkVisibility((link) => highlightLinks.has(link))
|
||||
.nodeVisibility((node) => highlightNodes.has(node) || node.mass > 4)
|
||||
// .linkDirectionalParticles(link => highlightLinks.has(link) ? 3 : 1) // kinda buggy for now, and slows this a bit, but would be great to help visualize lanes
|
||||
.linkDirectionalParticleWidth(0.2)
|
||||
.linkDirectionalParticleSpeed(-0.006)
|
||||
.nodeThreeObject((node) => {
|
||||
const sprite = new SpriteText(node.name);
|
||||
sprite.material.depthWrite = false; // make sprite background transparent
|
||||
sprite.color = highlightNodes.has(node)
|
||||
? node === hoverNode
|
||||
? "rgba(200,0,0,1)"
|
||||
: "rgba(255,160,0,0.8)"
|
||||
: "rgba(0,255,255,0.2)";
|
||||
sprite.textHeight = 2;
|
||||
sprite.center = new Three.Vector2(1.2, 0.5);
|
||||
return sprite;
|
||||
})
|
||||
.onEngineStop(() => {
|
||||
window.focusNode(window.getNodeFromId(config.nodeId));
|
||||
Graph.onEngineStop(() => {
|
||||
/* nope */
|
||||
}); // don't call ourselves in a loop while moving the focus
|
||||
});
|
||||
|
||||
// Set distance between stars
|
||||
Graph.d3Force("link").distance((link) => link.value);
|
||||
|
||||
// Set high masses nearer the center of the galaxy
|
||||
// TODO: quick and dirty strength computation, this will need tuning.
|
||||
Graph.d3Force(
|
||||
"positionX",
|
||||
forceX().strength((node) => {
|
||||
return 1 - 1 / node.mass;
|
||||
}),
|
||||
);
|
||||
Graph.d3Force(
|
||||
"positionY",
|
||||
forceY().strength((node) => {
|
||||
return 1 - 1 / node.mass;
|
||||
}),
|
||||
);
|
||||
Graph.d3Force(
|
||||
"positionZ",
|
||||
forceZ().strength((node) => {
|
||||
return 1 - 1 / node.mass;
|
||||
}),
|
||||
);
|
||||
};
|
@ -4,13 +4,18 @@
|
||||
{% trans user_name=object.get_display_name() %}{{ user_name }}'s Galaxy{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block additional_js %}
|
||||
<script src="{{ static('webpack/galaxy/galaxy-index.js') }}" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% if object.current_star %}
|
||||
<div style="display: flex; flex-wrap: wrap;">
|
||||
<div id="3d-graph"></div>
|
||||
|
||||
<div style="margin: 1em;">
|
||||
<p><a onclick="focus_node(get_node_from_id({{ object.id }}))">Reset on {{ object.get_display_name() }}</a></p>
|
||||
<p><a onclick="window.focusNode(window.getNodeFromId({{ object.id }}))">Reset on {{ object.get_display_name() }}</a></p>
|
||||
<p>Self score: {{ object.current_star.mass }}</p>
|
||||
<table style="width: initial;">
|
||||
<tr>
|
||||
@ -24,7 +29,7 @@
|
||||
</tr>
|
||||
{% for lane in lanes %}
|
||||
<tr>
|
||||
<td><a onclick="focus_node(get_node_from_id({{ lane.other_star_id }}))">Locate</a></td>
|
||||
<td><a onclick="window.focusNode(window.getNodeFromId({{ lane.other_star_id }}))">Locate</a></td>
|
||||
<td><a href="{{ url("galaxy:user", user_id=lane.other_star_id) }}">{{ lane.other_star_name }}</a></td>
|
||||
<td>{{ lane.other_star_mass }}</td>
|
||||
<td>{{ lane.distance }}</td>
|
||||
@ -45,106 +50,13 @@
|
||||
|
||||
{% block script %}
|
||||
{{ super() }}
|
||||
|
||||
<script src="{{ static('galaxy/js/three.min.js') }}" defer></script>
|
||||
<script src="{{ static('galaxy/js/three-spritetext.min.js') }}" defer></script>
|
||||
<script src="{{ static('galaxy/js/3d-force-graph.min.js') }}" defer></script>
|
||||
<script src="{{ static('galaxy/js/d3-force-3d.min.js') }}" defer></script>
|
||||
|
||||
<script>
|
||||
var Graph;
|
||||
|
||||
function get_node_from_id(id) {
|
||||
return Graph.graphData().nodes.find(n => n.id === id);
|
||||
}
|
||||
|
||||
function get_links_from_node_id(id) {
|
||||
return Graph.graphData().links.filter(l => l.source.id === id || l.target.id === id);
|
||||
}
|
||||
|
||||
function focus_node(node) {
|
||||
highlightNodes.clear();
|
||||
highlightLinks.clear();
|
||||
|
||||
hoverNode = node || null;
|
||||
if (node) { // collect neighbors and links for highlighting
|
||||
get_links_from_node_id(node.id).forEach(link => {
|
||||
highlightLinks.add(link);
|
||||
highlightNodes.add(link.source);
|
||||
highlightNodes.add(link.target);
|
||||
});
|
||||
}
|
||||
|
||||
// refresh node and link display
|
||||
Graph
|
||||
.nodeThreeObject(Graph.nodeThreeObject())
|
||||
.linkWidth(Graph.linkWidth())
|
||||
.linkDirectionalParticles(Graph.linkDirectionalParticles());
|
||||
|
||||
// Aim at node from outside it
|
||||
const distance = 42;
|
||||
const distRatio = 1 + distance/Math.hypot(node.x, node.y, node.z);
|
||||
|
||||
const newPos = node.x || node.y || node.z
|
||||
? { x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio }
|
||||
: { x: 0, y: 0, z: distance }; // special case if node is in (0,0,0)
|
||||
|
||||
Graph.cameraPosition(
|
||||
newPos, // new position
|
||||
node, // lookAt ({ x, y, z })
|
||||
3000 // ms transition duration
|
||||
);
|
||||
}
|
||||
|
||||
const highlightNodes = new Set();
|
||||
const highlightLinks = new Set();
|
||||
let hoverNode = null;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
var graph_div = document.getElementById('3d-graph');
|
||||
Graph = ForceGraph3D();
|
||||
Graph(graph_div);
|
||||
Graph
|
||||
.jsonUrl('{{ url("galaxy:data") }}')
|
||||
.width(graph_div.parentElement.clientWidth > 1200 ? 1200 : graph_div.parentElement.clientWidth) // Not perfect at all. JS-fu master from the future, please fix this :-)
|
||||
.height(1000)
|
||||
.enableNodeDrag(false) // allow easier navigation
|
||||
.onNodeClick(node => {
|
||||
camera = Graph.cameraPosition();
|
||||
var distance = Math.sqrt(Math.pow(node.x - camera.x, 2) + Math.pow(node.y - camera.y, 2) + Math.pow(node.z - camera.z, 2))
|
||||
if (distance < 120 || highlightNodes.has(node)) {
|
||||
focus_node(node);
|
||||
}
|
||||
})
|
||||
.linkWidth(link => highlightLinks.has(link) ? 0.4 : 0.0)
|
||||
.linkColor(link => highlightLinks.has(link) ? 'rgba(255,160,0,1)' : 'rgba(128,255,255,0.6)')
|
||||
.linkVisibility(link => highlightLinks.has(link))
|
||||
.nodeVisibility(node => highlightNodes.has(node) || node.mass > 4)
|
||||
// .linkDirectionalParticles(link => highlightLinks.has(link) ? 3 : 1) // kinda buggy for now, and slows this a bit, but would be great to help visualize lanes
|
||||
.linkDirectionalParticleWidth(0.2)
|
||||
.linkDirectionalParticleSpeed(-0.006)
|
||||
.nodeThreeObject(node => {
|
||||
const sprite = new SpriteText(node.name);
|
||||
sprite.material.depthWrite = false; // make sprite background transparent
|
||||
sprite.color = highlightNodes.has(node) ? node === hoverNode ? 'rgba(200,0,0,1)' : 'rgba(255,160,0,0.8)' : 'rgba(0,255,255,0.2)';
|
||||
sprite.textHeight = 2;
|
||||
sprite.center = new THREE.Vector2(1.2, 0.5);
|
||||
return sprite;
|
||||
})
|
||||
.onEngineStop( () => {
|
||||
focus_node(get_node_from_id({{ object.id }}));
|
||||
Graph.onEngineStop(() => {}); // don't call ourselves in a loop while moving the focus
|
||||
});
|
||||
|
||||
// Set distance between stars
|
||||
Graph.d3Force('link').distance(link => link.value);
|
||||
|
||||
// Set high masses nearer the center of the galaxy
|
||||
// TODO: quick and dirty strength computation, this will need tuning.
|
||||
Graph.d3Force('positionX', d3.forceX().strength(node => { return 1 - (1 / node.mass); }));
|
||||
Graph.d3Force('positionY', d3.forceY().strength(node => { return 1 - (1 / node.mass); }));
|
||||
Graph.d3Force('positionZ', d3.forceZ().strength(node => { return 1 - (1 / node.mass); }));
|
||||
})
|
||||
window.loadGalaxy({
|
||||
nodeId: {{ object.id }},
|
||||
dataUrl: '{{ url("galaxy:data") }}',
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -160,7 +160,7 @@ class TestGalaxyView(TestCase):
|
||||
response = self.client.get(reverse("galaxy:user", args=[user.id]))
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<a onclick="focus_node(get_node_from_id({user.id}))">Reset on {user}</a>',
|
||||
f'<a onclick="window.focusNode(window.getNodeFromId({user.id}))">Reset on {user}</a>',
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
|
@ -23,17 +23,9 @@
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from galaxy.views import *
|
||||
from galaxy.views import GalaxyDataView, GalaxyUserView
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"<int:user_id>/",
|
||||
GalaxyUserView.as_view(),
|
||||
name="user",
|
||||
),
|
||||
path(
|
||||
"data.json",
|
||||
GalaxyDataView.as_view(),
|
||||
name="data",
|
||||
),
|
||||
path("<int:user_id>/", GalaxyUserView.as_view(), name="user"),
|
||||
path("data.json", GalaxyDataView.as_view(), name="data"),
|
||||
]
|
||||
|
@ -14,7 +14,7 @@
|
||||
#
|
||||
from django.contrib import admin
|
||||
|
||||
from launderette.models import *
|
||||
from launderette.models import Launderette, Machine, Slot, Token
|
||||
|
||||
|
||||
@admin.register(Launderette)
|
||||
|
@ -51,18 +51,14 @@ class Launderette(models.Model):
|
||||
unix_name=settings.SITH_LAUNDERETTE_MANAGER["unix_name"]
|
||||
).first()
|
||||
m = launderette_club.get_membership_for(user)
|
||||
if m and m.role >= 9:
|
||||
return True
|
||||
return False
|
||||
return bool(m and m.role >= 9)
|
||||
|
||||
def can_be_edited_by(self, user):
|
||||
launderette_club = Club.objects.filter(
|
||||
unix_name=settings.SITH_LAUNDERETTE_MANAGER["unix_name"]
|
||||
).first()
|
||||
m = launderette_club.get_membership_for(user)
|
||||
if m and m.role >= 2:
|
||||
return True
|
||||
return False
|
||||
return bool(m and m.role >= 2)
|
||||
|
||||
def can_be_viewed_by(self, user):
|
||||
return user.is_subscribed
|
||||
@ -113,9 +109,7 @@ class Machine(models.Model):
|
||||
unix_name=settings.SITH_LAUNDERETTE_MANAGER["unix_name"]
|
||||
).first()
|
||||
m = launderette_club.get_membership_for(user)
|
||||
if m and m.role >= 9:
|
||||
return True
|
||||
return False
|
||||
return bool(m and m.role >= 9)
|
||||
|
||||
|
||||
class Token(models.Model):
|
||||
@ -164,15 +158,7 @@ class Token(models.Model):
|
||||
unix_name=settings.SITH_LAUNDERETTE_MANAGER["unix_name"]
|
||||
).first()
|
||||
m = launderette_club.get_membership_for(user)
|
||||
if m and m.role >= 9:
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_avaliable(self):
|
||||
if not self.borrow_date and not self.user:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return bool(m and m.role >= 9)
|
||||
|
||||
|
||||
class Slot(models.Model):
|
||||
|
@ -15,22 +15,28 @@
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from launderette.views import *
|
||||
from launderette.views import (
|
||||
LaunderetteAdminView,
|
||||
LaunderetteBookMainView,
|
||||
LaunderetteBookView,
|
||||
LaunderetteClickView,
|
||||
LaunderetteCreateView,
|
||||
LaunderetteEditView,
|
||||
LaunderetteListView,
|
||||
LaunderetteMainClickView,
|
||||
LaunderetteMainView,
|
||||
MachineCreateView,
|
||||
MachineDeleteView,
|
||||
MachineEditView,
|
||||
SlotDeleteView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
# views
|
||||
path("", LaunderetteMainView.as_view(), name="launderette_main"),
|
||||
path(
|
||||
"slot/<int:slot_id>/delete/",
|
||||
SlotDeleteView.as_view(),
|
||||
name="delete_slot",
|
||||
),
|
||||
path("slot/<int:slot_id>/delete/", SlotDeleteView.as_view(), name="delete_slot"),
|
||||
path("book/", LaunderetteBookMainView.as_view(), name="book_main"),
|
||||
path(
|
||||
"book/<int:launderette_id>/",
|
||||
LaunderetteBookView.as_view(),
|
||||
name="book_slot",
|
||||
),
|
||||
path("book/<int:launderette_id>/", LaunderetteBookView.as_view(), name="book_slot"),
|
||||
path(
|
||||
"<int:launderette_id>/click/",
|
||||
LaunderetteMainClickView.as_view(),
|
||||
|
@ -19,7 +19,7 @@ from datetime import timezone as tz
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.db import DataError, transaction
|
||||
from django.db import transaction
|
||||
from django.template import defaultfilters
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils import dateparse, timezone
|
||||
@ -73,15 +73,15 @@ class LaunderetteBookView(CanViewMixin, DetailView):
|
||||
self.machines = {}
|
||||
with transaction.atomic():
|
||||
self.object = self.get_object()
|
||||
if "slot_type" in request.POST.keys():
|
||||
if "slot_type" in request.POST:
|
||||
self.slot_type = request.POST["slot_type"]
|
||||
if "slot" in request.POST.keys() and request.user.is_authenticated:
|
||||
if "slot" in request.POST and request.user.is_authenticated:
|
||||
self.subscriber = request.user
|
||||
if self.subscriber.is_subscribed:
|
||||
self.date = dateparse.parse_datetime(request.POST["slot"]).replace(
|
||||
tzinfo=tz.utc
|
||||
)
|
||||
if self.slot_type == "WASHING":
|
||||
if self.slot_type in ["WASHING", "DRYING"]:
|
||||
if self.check_slot(self.slot_type):
|
||||
Slot(
|
||||
user=self.subscriber,
|
||||
@ -89,30 +89,21 @@ class LaunderetteBookView(CanViewMixin, DetailView):
|
||||
machine=self.machines[self.slot_type],
|
||||
type=self.slot_type,
|
||||
).save()
|
||||
elif self.slot_type == "DRYING":
|
||||
if self.check_slot(self.slot_type):
|
||||
Slot(
|
||||
user=self.subscriber,
|
||||
start_date=self.date,
|
||||
machine=self.machines[self.slot_type],
|
||||
type=self.slot_type,
|
||||
).save()
|
||||
else:
|
||||
if self.check_slot("WASHING") and self.check_slot(
|
||||
"DRYING", self.date + timedelta(hours=1)
|
||||
):
|
||||
Slot(
|
||||
user=self.subscriber,
|
||||
start_date=self.date,
|
||||
machine=self.machines["WASHING"],
|
||||
type="WASHING",
|
||||
).save()
|
||||
Slot(
|
||||
user=self.subscriber,
|
||||
start_date=self.date + timedelta(hours=1),
|
||||
machine=self.machines["DRYING"],
|
||||
type="DRYING",
|
||||
).save()
|
||||
elif self.check_slot("WASHING") and self.check_slot(
|
||||
"DRYING", self.date + timedelta(hours=1)
|
||||
):
|
||||
Slot(
|
||||
user=self.subscriber,
|
||||
start_date=self.date,
|
||||
machine=self.machines["WASHING"],
|
||||
type="WASHING",
|
||||
).save()
|
||||
Slot(
|
||||
user=self.subscriber,
|
||||
start_date=self.date + timedelta(hours=1),
|
||||
machine=self.machines["DRYING"],
|
||||
type="DRYING",
|
||||
).save()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def check_slot(self, machine_type, date=None):
|
||||
@ -149,15 +140,17 @@ class LaunderetteBookView(CanViewMixin, DetailView):
|
||||
):
|
||||
free = False
|
||||
if (
|
||||
self.slot_type == "BOTH"
|
||||
(
|
||||
self.slot_type == "BOTH"
|
||||
and self.check_slot("WASHING", h)
|
||||
and self.check_slot("DRYING", h + timedelta(hours=1))
|
||||
)
|
||||
or self.slot_type == "WASHING"
|
||||
and self.check_slot("WASHING", h)
|
||||
and self.check_slot("DRYING", h + timedelta(hours=1))
|
||||
or self.slot_type == "DRYING"
|
||||
and self.check_slot("DRYING", h)
|
||||
):
|
||||
free = True
|
||||
elif self.slot_type == "WASHING" and self.check_slot("WASHING", h):
|
||||
free = True
|
||||
elif self.slot_type == "DRYING" and self.check_slot("DRYING", h):
|
||||
free = True
|
||||
if free and datetime.now().replace(tzinfo=tz.utc) < h:
|
||||
kwargs["planning"][date].append(h)
|
||||
else:
|
||||
@ -236,42 +229,39 @@ class ManageTokenForm(forms.Form):
|
||||
token_list = cleaned_data["tokens"].strip(" \n\r").split(" ")
|
||||
token_type = cleaned_data["token_type"]
|
||||
self.data = {}
|
||||
|
||||
if cleaned_data["action"] not in ["BACK", "ADD", "DEL"]:
|
||||
return
|
||||
|
||||
tokens = list(
|
||||
Token.objects.filter(
|
||||
launderette=launderette, type=token_type, name__in=token_list
|
||||
)
|
||||
)
|
||||
existing_names = {t.name for t in tokens}
|
||||
if cleaned_data["action"] in ["BACK", "DEL"]:
|
||||
for t in set(token_list) - existing_names:
|
||||
self.add_error(
|
||||
None,
|
||||
_("Token %(token_name)s does not exists") % {"token_name": t},
|
||||
)
|
||||
if cleaned_data["action"] == "BACK":
|
||||
for t in token_list:
|
||||
try:
|
||||
tok = Token.objects.filter(
|
||||
launderette=launderette, type=token_type, name=t
|
||||
).first()
|
||||
tok.borrow_date = None
|
||||
tok.user = None
|
||||
tok.save()
|
||||
except:
|
||||
self.add_error(
|
||||
None,
|
||||
_("Token %(token_name)s does not exists") % {"token_name": t},
|
||||
)
|
||||
elif cleaned_data["action"] == "ADD":
|
||||
for t in token_list:
|
||||
try:
|
||||
Token(launderette=launderette, type=token_type, name=t).save()
|
||||
except DataError as e:
|
||||
self.add_error(None, e)
|
||||
except:
|
||||
self.add_error(
|
||||
None,
|
||||
_("Token %(token_name)s already exists") % {"token_name": t},
|
||||
)
|
||||
Token.objects.filter(id__in=[t.id for t in tokens]).update(
|
||||
borrow_date=None, user=None
|
||||
)
|
||||
elif cleaned_data["action"] == "DEL":
|
||||
Token.objects.filter(id__in=[t.id for t in tokens]).delete()
|
||||
elif cleaned_data["action"] == "ADD":
|
||||
for name in existing_names:
|
||||
self.add_error(
|
||||
None,
|
||||
_("Token %(token_name)s already exists") % {"token_name": name},
|
||||
)
|
||||
for t in token_list:
|
||||
try:
|
||||
Token.objects.filter(
|
||||
launderette=launderette, type=token_type, name=t
|
||||
).delete()
|
||||
except:
|
||||
self.add_error(
|
||||
None,
|
||||
_("Token %(token_name)s does not exists") % {"token_name": t},
|
||||
)
|
||||
if t == "":
|
||||
self.add_error(None, _("Token name can not be blank"))
|
||||
else:
|
||||
Token(launderette=launderette, type=token_type, name=t).save()
|
||||
|
||||
|
||||
class LaunderetteAdminView(CanEditPropMixin, BaseFormView, DetailView):
|
||||
@ -288,13 +278,7 @@ class LaunderetteAdminView(CanEditPropMixin, BaseFormView, DetailView):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
form = self.get_form()
|
||||
return super().post(request, *args, **kwargs)
|
||||
form.launderette = self.object
|
||||
if form.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def form_valid(self, form):
|
||||
"""We handle here the redirection, passing the user id of the asked customer."""
|
||||
@ -353,7 +337,7 @@ class LaunderetteMainClickView(CanEditMixin, BaseFormView, DetailView):
|
||||
kwargs["counter"] = self.object.counter
|
||||
kwargs["form"] = self.get_form()
|
||||
kwargs["barmen"] = [self.request.user]
|
||||
if "last_basket" in self.request.session.keys():
|
||||
if "last_basket" in self.request.session:
|
||||
kwargs["last_basket"] = self.request.session.pop("last_basket", None)
|
||||
kwargs["last_customer"] = self.request.session.pop("last_customer", None)
|
||||
kwargs["last_total"] = self.request.session.pop("last_total", None)
|
||||
@ -479,7 +463,7 @@ class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView):
|
||||
def get_context_data(self, **kwargs):
|
||||
"""We handle here the login form for the barman."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
if "form" not in kwargs.keys():
|
||||
if "form" not in kwargs:
|
||||
kwargs["form"] = self.get_form()
|
||||
kwargs["counter"] = self.object.counter
|
||||
kwargs["customer"] = self.customer
|
||||
@ -519,7 +503,7 @@ class MachineCreateView(CanCreateMixin, CreateView):
|
||||
|
||||
def get_initial(self):
|
||||
ret = super().get_initial()
|
||||
if "launderette" in self.request.GET.keys():
|
||||
if "launderette" in self.request.GET:
|
||||
obj = Launderette.objects.filter(
|
||||
id=int(self.request.GET["launderette"])
|
||||
).first()
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,7 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-09 11:50+0200\n"
|
||||
"POT-Creation-Date: 2024-10-16 02:19+0200\n"
|
||||
"PO-Revision-Date: 2024-09-17 11:54+0200\n"
|
||||
"Last-Translator: Sli <antoine@bartuccio.fr>\n"
|
||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
||||
@ -22,87 +22,95 @@ msgstr ""
|
||||
msgid "captured.%s"
|
||||
msgstr "capture.%s"
|
||||
|
||||
#: core/static/webpack/easymde-index.js:32
|
||||
#: core/static/webpack/ajax-select-index.ts:73
|
||||
msgid "You need to type %(number)s more characters"
|
||||
msgstr "Vous devez taper %(number)s caractères de plus"
|
||||
|
||||
#: core/static/webpack/ajax-select-index.ts:76
|
||||
msgid "No results found"
|
||||
msgstr "Aucun résultat trouvé"
|
||||
|
||||
#: core/static/webpack/easymde-index.ts:31
|
||||
msgid "Heading"
|
||||
msgstr "Titre"
|
||||
|
||||
#: core/static/webpack/easymde-index.js:38
|
||||
#: core/static/webpack/easymde-index.ts:37
|
||||
msgid "Italic"
|
||||
msgstr "Italique"
|
||||
|
||||
#: core/static/webpack/easymde-index.js:44
|
||||
#: core/static/webpack/easymde-index.ts:43
|
||||
msgid "Bold"
|
||||
msgstr "Gras"
|
||||
|
||||
#: core/static/webpack/easymde-index.js:50
|
||||
#: core/static/webpack/easymde-index.ts:49
|
||||
msgid "Strikethrough"
|
||||
msgstr "Barré"
|
||||
|
||||
#: core/static/webpack/easymde-index.js:59
|
||||
#: core/static/webpack/easymde-index.ts:58
|
||||
msgid "Underline"
|
||||
msgstr "Souligné"
|
||||
|
||||
#: core/static/webpack/easymde-index.js:68
|
||||
#: core/static/webpack/easymde-index.ts:67
|
||||
msgid "Superscript"
|
||||
msgstr "Exposant"
|
||||
|
||||
#: core/static/webpack/easymde-index.js:77
|
||||
#: core/static/webpack/easymde-index.ts:76
|
||||
msgid "Subscript"
|
||||
msgstr "Indice"
|
||||
|
||||
#: core/static/webpack/easymde-index.js:83
|
||||
#: core/static/webpack/easymde-index.ts:82
|
||||
msgid "Code"
|
||||
msgstr "Code"
|
||||
|
||||
#: core/static/webpack/easymde-index.js:90
|
||||
#: core/static/webpack/easymde-index.ts:89
|
||||
msgid "Quote"
|
||||
msgstr "Citation"
|
||||
|
||||
#: core/static/webpack/easymde-index.js:96
|
||||
#: core/static/webpack/easymde-index.ts:95
|
||||
msgid "Unordered list"
|
||||
msgstr "Liste non ordonnée"
|
||||
|
||||
#: core/static/webpack/easymde-index.js:102
|
||||
#: core/static/webpack/easymde-index.ts:101
|
||||
msgid "Ordered list"
|
||||
msgstr "Liste ordonnée"
|
||||
|
||||
#: core/static/webpack/easymde-index.js:109
|
||||
#: core/static/webpack/easymde-index.ts:108
|
||||
msgid "Insert link"
|
||||
msgstr "Insérer lien"
|
||||
|
||||
#: core/static/webpack/easymde-index.js:115
|
||||
#: core/static/webpack/easymde-index.ts:114
|
||||
msgid "Insert image"
|
||||
msgstr "Insérer image"
|
||||
|
||||
#: core/static/webpack/easymde-index.js:121
|
||||
#: core/static/webpack/easymde-index.ts:120
|
||||
msgid "Insert table"
|
||||
msgstr "Insérer tableau"
|
||||
|
||||
#: core/static/webpack/easymde-index.js:128
|
||||
#: core/static/webpack/easymde-index.ts:127
|
||||
msgid "Clean block"
|
||||
msgstr "Nettoyer bloc"
|
||||
|
||||
#: core/static/webpack/easymde-index.js:135
|
||||
#: core/static/webpack/easymde-index.ts:134
|
||||
msgid "Toggle preview"
|
||||
msgstr "Activer la prévisualisation"
|
||||
|
||||
#: core/static/webpack/easymde-index.js:141
|
||||
#: core/static/webpack/easymde-index.ts:140
|
||||
msgid "Toggle side by side"
|
||||
msgstr "Activer la vue côte à côte"
|
||||
|
||||
#: core/static/webpack/easymde-index.js:147
|
||||
#: core/static/webpack/easymde-index.ts:146
|
||||
msgid "Toggle fullscreen"
|
||||
msgstr "Activer le plein écran"
|
||||
|
||||
#: core/static/webpack/easymde-index.js:154
|
||||
#: core/static/webpack/easymde-index.ts:153
|
||||
msgid "Markdown guide"
|
||||
msgstr "Guide markdown"
|
||||
|
||||
#: core/static/webpack/user/family-graph-index.js:222
|
||||
#: core/static/webpack/user/family-graph-index.js:233
|
||||
msgid "family_tree.%(extension)s"
|
||||
msgstr "arbre_genealogique.%(extension)s"
|
||||
|
||||
#: core/static/webpack/user/pictures-index.js:67
|
||||
#: core/static/webpack/user/pictures-index.js:76
|
||||
msgid "pictures.%(extension)s"
|
||||
msgstr "photos.%(extension)s"
|
||||
|
||||
@ -110,10 +118,10 @@ msgstr "photos.%(extension)s"
|
||||
msgid "Incorrect value"
|
||||
msgstr "Valeur incorrecte"
|
||||
|
||||
#: sas/static/sas/js/viewer.js:205
|
||||
#: sas/static/webpack/sas/viewer-index.ts:271
|
||||
msgid "Couldn't moderate picture"
|
||||
msgstr "Il n'a pas été possible de modérer l'image"
|
||||
|
||||
#: sas/static/sas/js/viewer.js:217
|
||||
#: sas/static/webpack/sas/viewer-index.ts:284
|
||||
msgid "Couldn't delete picture"
|
||||
msgstr "Il n'a pas été possible de supprimer l'image"
|
||||
|
@ -23,7 +23,12 @@
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from matmat.views import *
|
||||
from matmat.views import (
|
||||
SearchClearFormView,
|
||||
SearchNormalFormView,
|
||||
SearchQuickFormView,
|
||||
SearchReverseFormView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("", SearchNormalFormView.as_view(), name="search"),
|
||||
|
@ -71,15 +71,15 @@ class SearchForm(forms.ModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for key in self.fields.keys():
|
||||
for key in self.fields:
|
||||
self.fields[key].required = False
|
||||
|
||||
@property
|
||||
def cleaned_data_json(self):
|
||||
data = self.cleaned_data
|
||||
for key in data.keys():
|
||||
if key in ("date_of_birth", "phone") and data[key] is not None:
|
||||
data[key] = str(data[key])
|
||||
for key, val in data.items():
|
||||
if key in ("date_of_birth", "phone") and val is not None:
|
||||
data[key] = str(val)
|
||||
return data
|
||||
|
||||
|
||||
@ -98,10 +98,7 @@ class SearchFormListView(FormerSubscriberMixin, SingleObjectMixin, ListView):
|
||||
self.session = request.session
|
||||
self.last_search = self.session.get("matmat_search_result", str([]))
|
||||
self.last_search = literal_eval(self.last_search)
|
||||
if "valid_form" in kwargs.keys():
|
||||
self.valid_form = kwargs["valid_form"]
|
||||
else:
|
||||
self.valid_form = None
|
||||
self.valid_form = kwargs.get("valid_form")
|
||||
|
||||
self.init_query = self.model.objects
|
||||
self.can_see_hidden = True
|
||||
@ -202,8 +199,8 @@ class SearchClearFormView(FormerSubscriberMixin, View):
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
super().dispatch(request, *args, **kwargs)
|
||||
if "matmat_search_form" in request.session.keys():
|
||||
if "matmat_search_form" in request.session:
|
||||
request.session.pop("matmat_search_form")
|
||||
if "matmat_search_result" in request.session.keys():
|
||||
if "matmat_search_result" in request.session:
|
||||
request.session.pop("matmat_search_result")
|
||||
return HttpResponseRedirect(reverse("matmat:search"))
|
||||
|
656
package-lock.json
generated
656
package-lock.json
generated
@ -11,23 +11,32 @@
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"@hey-api/client-fetch": "^0.4.0",
|
||||
"@sentry/browser": "^8.34.0",
|
||||
"@zip.js/zip.js": "^2.7.52",
|
||||
"3d-force-graph": "^1.73.4",
|
||||
"alpinejs": "^3.14.1",
|
||||
"chart.js": "^4.4.4",
|
||||
"cytoscape": "^3.30.2",
|
||||
"cytoscape-cxtmenu": "^3.5.0",
|
||||
"cytoscape-klay": "^3.1.4",
|
||||
"d3-force-3d": "^3.0.5",
|
||||
"easymde": "^2.18.0",
|
||||
"glob": "^11.0.0",
|
||||
"jquery": "^3.7.1",
|
||||
"jquery-ui": "^1.14.0",
|
||||
"jquery.shorten": "^1.0.0",
|
||||
"native-file-system-adapter": "^3.0.1"
|
||||
"native-file-system-adapter": "^3.0.1",
|
||||
"three": "^0.169.0",
|
||||
"three-spritetext": "^1.9.0",
|
||||
"tom-select": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@biomejs/biome": "1.9.3",
|
||||
"@hey-api/openapi-ts": "^0.53.8",
|
||||
"@types/alpinejs": "^3.13.10",
|
||||
"@types/jquery": "^3.5.31",
|
||||
"babel-loader": "^9.2.1",
|
||||
"css-loader": "^7.1.2",
|
||||
"css-minimizer-webpack-plugin": "^7.0.0",
|
||||
@ -38,6 +47,7 @@
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.94.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"webpack-cli": "^5.1.4"
|
||||
}
|
||||
},
|
||||
@ -1790,7 +1800,6 @@
|
||||
"version": "7.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz",
|
||||
"integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
@ -2160,6 +2169,24 @@
|
||||
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@kurkle/color": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
|
||||
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
|
||||
},
|
||||
"node_modules/@orchidjs/sifter": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@orchidjs/sifter/-/sifter-1.0.3.tgz",
|
||||
"integrity": "sha512-zCZbwKegHytfsPm8Amcfh7v/4vHqTAaOu6xFswBYcn8nznBOuseu6COB2ON7ez0tFV0mKL0nRNnCiZZA+lU9/g==",
|
||||
"dependencies": {
|
||||
"@orchidjs/unicode-variants": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@orchidjs/unicode-variants": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@orchidjs/unicode-variants/-/unicode-variants-1.0.4.tgz",
|
||||
"integrity": "sha512-NvVBRnZNE+dugiXERFsET1JlKZfM5lJDEpSMilKW4bToYJ7pxf0Zne78xyXB2ny2c2aHfJ6WLnz1AaTNHAmQeQ=="
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
@ -2169,6 +2196,114 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@polka/url": {
|
||||
"version": "1.0.0-next.28",
|
||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz",
|
||||
"integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@sentry-internal/browser-utils": {
|
||||
"version": "8.34.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.34.0.tgz",
|
||||
"integrity": "sha512-4AcYOzPzD1tL5eSRQ/GpKv5enquZf4dMVUez99/Bh3va8qiJrNP55AcM7UzZ7WZLTqKygIYruJTU5Zu2SpEAPQ==",
|
||||
"dependencies": {
|
||||
"@sentry/core": "8.34.0",
|
||||
"@sentry/types": "8.34.0",
|
||||
"@sentry/utils": "8.34.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/feedback": {
|
||||
"version": "8.34.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.34.0.tgz",
|
||||
"integrity": "sha512-aYSM2KPUs0FLPxxbJCFSwCYG70VMzlT04xepD1Y/tTlPPOja/02tSv2tyOdZbv8Uw7xslZs3/8Lhj74oYcTBxw==",
|
||||
"dependencies": {
|
||||
"@sentry/core": "8.34.0",
|
||||
"@sentry/types": "8.34.0",
|
||||
"@sentry/utils": "8.34.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay": {
|
||||
"version": "8.34.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.34.0.tgz",
|
||||
"integrity": "sha512-EoMh9NYljNewZK1quY23YILgtNdGgrkzJ9TPsj6jXUG0LZ0Q7N7eFWd0xOEDBvFxrmI3cSXF1i4d1sBb+eyKRw==",
|
||||
"dependencies": {
|
||||
"@sentry-internal/browser-utils": "8.34.0",
|
||||
"@sentry/core": "8.34.0",
|
||||
"@sentry/types": "8.34.0",
|
||||
"@sentry/utils": "8.34.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay-canvas": {
|
||||
"version": "8.34.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.34.0.tgz",
|
||||
"integrity": "sha512-x8KhZcCDpbKHqFOykYXiamX6x0LRxv6N1OJHoH+XCrMtiDBZr4Yo30d/MaS6rjmKGMtSRij30v+Uq+YWIgxUrg==",
|
||||
"dependencies": {
|
||||
"@sentry-internal/replay": "8.34.0",
|
||||
"@sentry/core": "8.34.0",
|
||||
"@sentry/types": "8.34.0",
|
||||
"@sentry/utils": "8.34.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/browser": {
|
||||
"version": "8.34.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.34.0.tgz",
|
||||
"integrity": "sha512-3HHG2NXxzHq1lVmDy2uRjYjGNf9NsJsTPlOC70vbQdOb+S49EdH/XMPy+J3ruIoyv6Cu0LwvA6bMOM6rHZOgNQ==",
|
||||
"dependencies": {
|
||||
"@sentry-internal/browser-utils": "8.34.0",
|
||||
"@sentry-internal/feedback": "8.34.0",
|
||||
"@sentry-internal/replay": "8.34.0",
|
||||
"@sentry-internal/replay-canvas": "8.34.0",
|
||||
"@sentry/core": "8.34.0",
|
||||
"@sentry/types": "8.34.0",
|
||||
"@sentry/utils": "8.34.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/core": {
|
||||
"version": "8.34.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.34.0.tgz",
|
||||
"integrity": "sha512-adrXCTK/zsg5pJ67lgtZqdqHvyx6etMjQW3P82NgWdj83c8fb+zH+K79Z47pD4zQjX0ou2Ws5nwwi4wJbz4bfA==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "8.34.0",
|
||||
"@sentry/utils": "8.34.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/types": {
|
||||
"version": "8.34.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.34.0.tgz",
|
||||
"integrity": "sha512-zLRc60CzohGCo6zNsNeQ9JF3SiEeRE4aDCP9fDDdIVCOKovS+mn1rtSip0qd0Vp2fidOu0+2yY0ALCz1A3PJSQ==",
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/utils": {
|
||||
"version": "8.34.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.34.0.tgz",
|
||||
"integrity": "sha512-W1KoRlFUjprlh3t86DZPFxLfM6mzjRzshVfMY7vRlJFymBelJsnJ3A1lPeBZM9nCraOSiw6GtOWu6k5BAkiGIg==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "8.34.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sinclair/typebox": {
|
||||
"version": "0.27.8",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
||||
@ -2184,6 +2319,17 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tweenjs/tween.js": {
|
||||
"version": "25.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz",
|
||||
"integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A=="
|
||||
},
|
||||
"node_modules/@types/alpinejs": {
|
||||
"version": "3.13.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/alpinejs/-/alpinejs-3.13.10.tgz",
|
||||
"integrity": "sha512-ah53tF6mWuuwerpDE7EHwbZErNDJQlsLISPqJhYj2RZ9nuTYbRknSkqebUd3igkhLIZKkPa7IiXjSn9qsU9O2w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/codemirror": {
|
||||
"version": "5.60.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.15.tgz",
|
||||
@ -2221,6 +2367,15 @@
|
||||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jquery": {
|
||||
"version": "3.5.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.31.tgz",
|
||||
"integrity": "sha512-rf/iB+cPJ/YZfMwr+FVuQbm7IaWC4y3FVYfVDxRGqmUCFjjPII0HWaP0vTPJGp6m4o13AXySCcMbWfrWtBFAKw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/sizzle": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
@ -2241,6 +2396,12 @@
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/sizzle": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz",
|
||||
"integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/tern": {
|
||||
"version": "0.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
|
||||
@ -2489,6 +2650,29 @@
|
||||
"node": ">=16.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/3d-force-graph": {
|
||||
"version": "1.73.4",
|
||||
"resolved": "https://registry.npmjs.org/3d-force-graph/-/3d-force-graph-1.73.4.tgz",
|
||||
"integrity": "sha512-eMHZ1LVzh9APLv+An0AXz2dVPwasJlqAnJ61ABlb1qaO6DYuqIUTTErh0DN/24nIWJu1jCim2WiVujzz7slnWQ==",
|
||||
"dependencies": {
|
||||
"accessor-fn": "1",
|
||||
"kapsule": "1",
|
||||
"three": ">=0.118 <1",
|
||||
"three-forcegraph": "1",
|
||||
"three-render-objects": "^1.29"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/accessor-fn": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.1.tgz",
|
||||
"integrity": "sha512-zZpFYBqIL1Aqg+f2qmYHJ8+yIZF7/tP6PUGx2/QM0uGPSO5UegpinmkNwDohxWtOj586BpMPVRUjce2HI6xB3A==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.12.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||
@ -2510,6 +2694,18 @@
|
||||
"acorn": "^8"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
@ -2900,6 +3096,17 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "4.4.4",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.4.tgz",
|
||||
"integrity": "sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==",
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"pnpm": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
|
||||
@ -3419,6 +3626,159 @@
|
||||
"cytoscape": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-binarytree": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz",
|
||||
"integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw=="
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-dispatch": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
||||
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-force-3d": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.5.tgz",
|
||||
"integrity": "sha512-tdwhAhoTYZY/a6eo9nR7HP3xSW/C6XvJTbeRpR92nlPzH6OiE+4MliN9feuSFd0tPtEUo+191qOhCTWx3NYifg==",
|
||||
"dependencies": {
|
||||
"d3-binarytree": "1",
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-octree": "1",
|
||||
"d3-quadtree": "1 - 3",
|
||||
"d3-timer": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-format": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
||||
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-octree": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.0.2.tgz",
|
||||
"integrity": "sha512-Qxg4oirJrNXauiuC94uKMbgxwnhdda9xRLl9ihq45srlJ4Ga3CSgqGcAL8iW7N5CIv4Oz8x3E734ulxyvHPvwA=="
|
||||
},
|
||||
"node_modules/d3-quadtree": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
|
||||
"integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||
"dependencies": {
|
||||
"d3-array": "2.10.0 - 3",
|
||||
"d3-format": "1 - 3",
|
||||
"d3-interpolate": "1.2.0 - 3",
|
||||
"d3-time": "2.1.1 - 3",
|
||||
"d3-time-format": "2 - 4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale-chromatic": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
||||
"integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3",
|
||||
"d3-interpolate": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||
"dependencies": {
|
||||
"d3-array": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time-format": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||
"dependencies": {
|
||||
"d3-time": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/data-joint": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/data-joint/-/data-joint-1.3.1.tgz",
|
||||
"integrity": "sha512-tMK0m4OVGqiA3zkn8JmO6YAqD8UwJqIAx4AAwFl1SKTtKAqcXePuT+n2aayiX9uITtlN3DFtKKTOxJRUc2+HvQ==",
|
||||
"dependencies": {
|
||||
"index-array-by": "^1.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/debounce": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
|
||||
"integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
@ -3515,6 +3875,12 @@
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/duplexer": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
|
||||
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
@ -4026,6 +4392,21 @@
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/gzip-size": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
|
||||
"integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"duplexer": "^0.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/handlebars": {
|
||||
"version": "4.7.8",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
|
||||
@ -4068,6 +4449,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
|
||||
@ -4120,6 +4507,22 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/index-array-by": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz",
|
||||
"integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/interpret": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz",
|
||||
@ -4328,6 +4731,17 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/kapsule": {
|
||||
"version": "1.14.6",
|
||||
"resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.14.6.tgz",
|
||||
"integrity": "sha512-wSi6tHNOfXrIK2Pvv6BhZ9ukzhbp+XZlOOPWSVGUbqfFsnnli4Eq8FN6TaWJv2e17sY5+fKYVxa4DP2oPGlKhg==",
|
||||
"dependencies": {
|
||||
"lodash-es": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/kind-of": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
@ -4375,6 +4789,11 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||
@ -4629,6 +5048,15 @@
|
||||
"ufo": "^1.5.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mrmime": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz",
|
||||
"integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@ -4680,6 +5108,39 @@
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ngraph.events": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.2.2.tgz",
|
||||
"integrity": "sha512-JsUbEOzANskax+WSYiAPETemLWYXmixuPAlmZmhIbIj6FH/WDgEGCGnRwUQBK0GjOnVm8Ui+e5IJ+5VZ4e32eQ=="
|
||||
},
|
||||
"node_modules/ngraph.forcelayout": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/ngraph.forcelayout/-/ngraph.forcelayout-3.3.1.tgz",
|
||||
"integrity": "sha512-MKBuEh1wujyQHFTW57y5vd/uuEOK0XfXYxm3lC7kktjJLRdt/KEKEknyOlc6tjXflqBKEuYBBcu7Ax5VY+S6aw==",
|
||||
"dependencies": {
|
||||
"ngraph.events": "^1.0.0",
|
||||
"ngraph.merge": "^1.0.0",
|
||||
"ngraph.random": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ngraph.graph": {
|
||||
"version": "20.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ngraph.graph/-/ngraph.graph-20.0.1.tgz",
|
||||
"integrity": "sha512-VFsQ+EMkT+7lcJO1QP8Ik3w64WbHJl27Q53EO9hiFU9CRyxJ8HfcXtfWz/U8okuoYKDctbciL6pX3vG5dt1rYA==",
|
||||
"dependencies": {
|
||||
"ngraph.events": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ngraph.merge": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ngraph.merge/-/ngraph.merge-1.0.0.tgz",
|
||||
"integrity": "sha512-5J8YjGITUJeapsomtTALYsw7rFveYkM+lBj3QiYZ79EymQcuri65Nw3knQtFxQBU1r5iOaVRXrSwMENUPK62Vg=="
|
||||
},
|
||||
"node_modules/ngraph.random": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ngraph.random/-/ngraph.random-1.1.0.tgz",
|
||||
"integrity": "sha512-h25UdUN/g8U7y29TzQtRm/GvGr70lK37yQPvPKXXuVfs7gCm82WipYFZcksQfeKumtOemAzBIcT7lzzyK/edLw=="
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
@ -4791,6 +5252,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/opener": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
|
||||
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"opener": "bin/opener-bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
@ -4923,6 +5393,17 @@
|
||||
"pathe": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/polished": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz",
|
||||
"integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.17.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.47",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
||||
@ -5514,8 +5995,7 @@
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
|
||||
},
|
||||
"node_modules/regenerator-transform": {
|
||||
"version": "0.15.2",
|
||||
@ -5718,6 +6198,20 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/sirv": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
|
||||
"integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@polka/url": "^1.0.0-next.24",
|
||||
"mrmime": "^2.0.0",
|
||||
"totalist": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@ -6036,6 +6530,67 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.169.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.169.0.tgz",
|
||||
"integrity": "sha512-Ed906MA3dR4TS5riErd4QBsRGPcx+HBDX2O5yYE5GqJeFQTPU+M56Va/f/Oph9X7uZo3W3o4l2ZhBZ6f6qUv0w=="
|
||||
},
|
||||
"node_modules/three-forcegraph": {
|
||||
"version": "1.41.15",
|
||||
"resolved": "https://registry.npmjs.org/three-forcegraph/-/three-forcegraph-1.41.15.tgz",
|
||||
"integrity": "sha512-E1j6bKt7lWg9t/ERdEiuxYfPbAioTCd9RG2bgqyC0yM3rwkBqn5VZN3fvb7umaOuTB1Tqpq6m07iVfJSfzTnCQ==",
|
||||
"dependencies": {
|
||||
"accessor-fn": "1",
|
||||
"d3-array": "1 - 3",
|
||||
"d3-force-3d": "2 - 3",
|
||||
"d3-scale": "1 - 4",
|
||||
"d3-scale-chromatic": "1 - 3",
|
||||
"data-joint": "1",
|
||||
"kapsule": "1",
|
||||
"ngraph.forcelayout": "3",
|
||||
"ngraph.graph": "20",
|
||||
"tinycolor2": "1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.118.3"
|
||||
}
|
||||
},
|
||||
"node_modules/three-render-objects": {
|
||||
"version": "1.29.5",
|
||||
"resolved": "https://registry.npmjs.org/three-render-objects/-/three-render-objects-1.29.5.tgz",
|
||||
"integrity": "sha512-OLtETrjF184NuaaI/vpRlIP9FxVNAgBBCgWYXhGFUDnPdl/2iX8rialUPGA1gEXvOTiKyepArVgm1LUkJw15rQ==",
|
||||
"dependencies": {
|
||||
"@tweenjs/tween.js": "18 - 25",
|
||||
"accessor-fn": "1",
|
||||
"kapsule": "1",
|
||||
"polished": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/three-spritetext": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/three-spritetext/-/three-spritetext-1.9.0.tgz",
|
||||
"integrity": "sha512-+dMrxBsxTu5OviykIg5jTMry5TQ8u5yuS9zKH0mWElyldoFGdegEkIm71kDk34bxBp/NQhRLW+iom1b/GMTioA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.86.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinycolor2": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
|
||||
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="
|
||||
},
|
||||
"node_modules/to-fast-properties": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
||||
@ -6057,6 +6612,31 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tom-select": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tom-select/-/tom-select-2.3.1.tgz",
|
||||
"integrity": "sha512-QS4vnOcB6StNGqX4sGboGXL2fkhBF2gIBB+8Hwv30FZXYPn0CyYO8kkdATRvwfCTThxiR4WcXwKJZ3cOmtI9eg==",
|
||||
"dependencies": {
|
||||
"@orchidjs/sifter": "^1.0.3",
|
||||
"@orchidjs/unicode-variants": "^1.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/tom-select"
|
||||
}
|
||||
},
|
||||
"node_modules/totalist": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-loader": {
|
||||
"version": "9.5.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz",
|
||||
@ -6282,6 +6862,53 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-bundle-analyzer": {
|
||||
"version": "4.10.2",
|
||||
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz",
|
||||
"integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@discoveryjs/json-ext": "0.5.7",
|
||||
"acorn": "^8.0.4",
|
||||
"acorn-walk": "^8.0.0",
|
||||
"commander": "^7.2.0",
|
||||
"debounce": "^1.2.1",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"gzip-size": "^6.0.0",
|
||||
"html-escaper": "^2.0.2",
|
||||
"opener": "^1.5.2",
|
||||
"picocolors": "^1.0.0",
|
||||
"sirv": "^2.0.3",
|
||||
"ws": "^7.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"webpack-bundle-analyzer": "lib/bin/analyzer.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-bundle-analyzer/node_modules/commander": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-bundle-analyzer/node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-cli": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz",
|
||||
@ -6469,6 +7096,27 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "7.5.10",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
14
package.json
14
package.json
@ -7,6 +7,8 @@
|
||||
"compile": "webpack --mode production",
|
||||
"compile-dev": "webpack --mode development",
|
||||
"serve": "webpack --mode development --watch",
|
||||
"analyse-dev": "webpack --config webpack.analyze.config.js --mode development",
|
||||
"analyse-prod": "webpack --config webpack.analyze.config.js --mode production",
|
||||
"check": "biome check --write"
|
||||
},
|
||||
"keywords": [],
|
||||
@ -23,6 +25,8 @@
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@biomejs/biome": "1.9.3",
|
||||
"@hey-api/openapi-ts": "^0.53.8",
|
||||
"@types/alpinejs": "^3.13.10",
|
||||
"@types/jquery": "^3.5.31",
|
||||
"babel-loader": "^9.2.1",
|
||||
"css-loader": "^7.1.2",
|
||||
"css-minimizer-webpack-plugin": "^7.0.0",
|
||||
@ -33,21 +37,29 @@
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.94.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"@hey-api/client-fetch": "^0.4.0",
|
||||
"@sentry/browser": "^8.34.0",
|
||||
"@zip.js/zip.js": "^2.7.52",
|
||||
"3d-force-graph": "^1.73.4",
|
||||
"alpinejs": "^3.14.1",
|
||||
"chart.js": "^4.4.4",
|
||||
"cytoscape": "^3.30.2",
|
||||
"cytoscape-cxtmenu": "^3.5.0",
|
||||
"cytoscape-klay": "^3.1.4",
|
||||
"d3-force-3d": "^3.0.5",
|
||||
"easymde": "^2.18.0",
|
||||
"glob": "^11.0.0",
|
||||
"jquery": "^3.7.1",
|
||||
"jquery-ui": "^1.14.0",
|
||||
"jquery.shorten": "^1.0.0",
|
||||
"native-file-system-adapter": "^3.0.1"
|
||||
"native-file-system-adapter": "^3.0.1",
|
||||
"three": "^0.169.0",
|
||||
"three-spritetext": "^1.9.0",
|
||||
"tom-select": "^2.3.1"
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,10 @@ class Migration(migrations.Migration):
|
||||
unique=True,
|
||||
validators=[
|
||||
django.core.validators.RegexValidator(
|
||||
message="The code of an UV must only contains uppercase characters without accent and numbers",
|
||||
message=(
|
||||
"The code of an UV must only contains "
|
||||
"uppercase characters without accent and numbers"
|
||||
),
|
||||
regex="([A-Z0-9]+)",
|
||||
)
|
||||
],
|
||||
|
@ -45,7 +45,8 @@ class UV(models.Model):
|
||||
validators.RegexValidator(
|
||||
regex="([A-Z0-9]+)",
|
||||
message=_(
|
||||
"The code of an UV must only contains uppercase characters without accent and numbers"
|
||||
"The code of an UV must only contains "
|
||||
"uppercase characters without accent and numbers"
|
||||
),
|
||||
)
|
||||
],
|
||||
|
@ -27,7 +27,10 @@ class TestUVSearch(TestCase):
|
||||
semester="AUTUMN",
|
||||
department="GI",
|
||||
manager="francky",
|
||||
title="Programmation Orientée Objet: Concepts fondamentaux et mise en pratique avec le langage C++",
|
||||
title=(
|
||||
"Programmation Orientée Objet: "
|
||||
"Concepts fondamentaux et mise en pratique avec le langage C++"
|
||||
),
|
||||
),
|
||||
uv_recipe.prepare(
|
||||
code="MT01",
|
||||
@ -118,7 +121,7 @@ class TestUVSearch(TestCase):
|
||||
("M", {"MT01", "MT10"}),
|
||||
("mt", {"MT01", "MT10"}),
|
||||
("MT", {"MT01", "MT10"}),
|
||||
("algèbre", {"MT01"}), # Title search case insensitive
|
||||
("algèbre", {"MT01"}), # Title search case insensitive
|
||||
# Manager search
|
||||
("moss", {"TNEV"}),
|
||||
("francky", {"DA50", "AP4A"}),
|
||||
|
@ -381,7 +381,9 @@ class TestUVCommentCreationAndDisplay(TestCase):
|
||||
self.assertContains(
|
||||
response,
|
||||
_(
|
||||
"You already posted a comment on this UV. If you want to comment again, please modify or delete your previous comment."
|
||||
"You already posted a comment on this UV. "
|
||||
"If you want to comment again, "
|
||||
"please modify or delete your previous comment."
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -23,7 +23,17 @@
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from pedagogy.views import *
|
||||
from pedagogy.views import (
|
||||
UVCommentDeleteView,
|
||||
UVCommentReportCreateView,
|
||||
UVCommentUpdateView,
|
||||
UVCreateView,
|
||||
UVDeleteView,
|
||||
UVDetailFormView,
|
||||
UVGuideView,
|
||||
UVModerationFormView,
|
||||
UVUpdateView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
# Urls displaying the actual application for visitors
|
||||
|
@ -23,7 +23,7 @@
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.views.generic import (
|
||||
@ -193,18 +193,12 @@ class UVModerationFormView(FormView):
|
||||
|
||||
def form_valid(self, form):
|
||||
form_clean = form.clean()
|
||||
for report in form_clean.get("accepted_reports", []):
|
||||
try:
|
||||
report.comment.delete() # Delete the related comment
|
||||
except ObjectDoesNotExist:
|
||||
# To avoid errors when two reports points the same comment
|
||||
pass
|
||||
for report in form_clean.get("denied_reports", []):
|
||||
try:
|
||||
report.delete() # Delete the report itself
|
||||
except ObjectDoesNotExist:
|
||||
# To avoid errors when two reports points the same comment
|
||||
pass
|
||||
accepted = form_clean.get("accepted_reports", [])
|
||||
if len(accepted) > 0: # delete the reported comments
|
||||
UVComment.objects.filter(reports__in=accepted).delete()
|
||||
denied = form_clean.get("denied_reports", [])
|
||||
if len(denied) > 0: # delete the comments themselves
|
||||
UVCommentReport.objects.filter(id__in={d.id for d in denied}).delete()
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
|
284
poetry.lock
generated
284
poetry.lock
generated
@ -343,73 +343,73 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.6.2"
|
||||
version = "7.6.3"
|
||||
description = "Code coverage measurement for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "coverage-7.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9df1950fb92d49970cce38100d7e7293c84ed3606eaa16ea0b6bc27175bb667"},
|
||||
{file = "coverage-7.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:24500f4b0e03aab60ce575c85365beab64b44d4db837021e08339f61d1fbfe52"},
|
||||
{file = "coverage-7.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a663b180b6669c400b4630a24cc776f23a992d38ce7ae72ede2a397ce6b0f170"},
|
||||
{file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfde025e2793a22efe8c21f807d276bd1d6a4bcc5ba6f19dbdfc4e7a12160909"},
|
||||
{file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:087932079c065d7b8ebadd3a0160656c55954144af6439886c8bcf78bbbcde7f"},
|
||||
{file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9c6b0c1cafd96213a0327cf680acb39f70e452caf8e9a25aeb05316db9c07f89"},
|
||||
{file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6e85830eed5b5263ffa0c62428e43cb844296f3b4461f09e4bdb0d44ec190bc2"},
|
||||
{file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62ab4231c01e156ece1b3a187c87173f31cbeee83a5e1f6dff17f288dca93345"},
|
||||
{file = "coverage-7.6.2-cp310-cp310-win32.whl", hash = "sha256:7b80fbb0da3aebde102a37ef0138aeedff45997e22f8962e5f16ae1742852676"},
|
||||
{file = "coverage-7.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:d20c3d1f31f14d6962a4e2f549c21d31e670b90f777ef4171be540fb7fb70f02"},
|
||||
{file = "coverage-7.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bb21bac7783c1bf6f4bbe68b1e0ff0d20e7e7732cfb7995bc8d96e23aa90fc7b"},
|
||||
{file = "coverage-7.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a7b2e437fbd8fae5bc7716b9c7ff97aecc95f0b4d56e4ca08b3c8d8adcaadb84"},
|
||||
{file = "coverage-7.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:536f77f2bf5797983652d1d55f1a7272a29afcc89e3ae51caa99b2db4e89d658"},
|
||||
{file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f361296ca7054f0936b02525646b2731b32c8074ba6defab524b79b2b7eeac72"},
|
||||
{file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7926d8d034e06b479797c199747dd774d5e86179f2ce44294423327a88d66ca7"},
|
||||
{file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0bbae11c138585c89fb4e991faefb174a80112e1a7557d507aaa07675c62e66b"},
|
||||
{file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fcad7d5d2bbfeae1026b395036a8aa5abf67e8038ae7e6a25c7d0f88b10a8e6a"},
|
||||
{file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f01e53575f27097d75d42de33b1b289c74b16891ce576d767ad8c48d17aeb5e0"},
|
||||
{file = "coverage-7.6.2-cp311-cp311-win32.whl", hash = "sha256:7781f4f70c9b0b39e1b129b10c7d43a4e0c91f90c60435e6da8288efc2b73438"},
|
||||
{file = "coverage-7.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:9bcd51eeca35a80e76dc5794a9dd7cb04b97f0e8af620d54711793bfc1fbba4b"},
|
||||
{file = "coverage-7.6.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ebc94fadbd4a3f4215993326a6a00e47d79889391f5659bf310f55fe5d9f581c"},
|
||||
{file = "coverage-7.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9681516288e3dcf0aa7c26231178cc0be6cac9705cac06709f2353c5b406cfea"},
|
||||
{file = "coverage-7.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d9c5d13927d77af4fbe453953810db766f75401e764727e73a6ee4f82527b3e"},
|
||||
{file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92f9ca04b3e719d69b02dc4a69debb795af84cb7afd09c5eb5d54b4a1ae2191"},
|
||||
{file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ff2ef83d6d0b527b5c9dad73819b24a2f76fdddcfd6c4e7a4d7e73ecb0656b4"},
|
||||
{file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:47ccb6e99a3031ffbbd6e7cc041e70770b4fe405370c66a54dbf26a500ded80b"},
|
||||
{file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a867d26f06bcd047ef716175b2696b315cb7571ccb951006d61ca80bbc356e9e"},
|
||||
{file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cdfcf2e914e2ba653101157458afd0ad92a16731eeba9a611b5cbb3e7124e74b"},
|
||||
{file = "coverage-7.6.2-cp312-cp312-win32.whl", hash = "sha256:f9035695dadfb397bee9eeaf1dc7fbeda483bf7664a7397a629846800ce6e276"},
|
||||
{file = "coverage-7.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:5ed69befa9a9fc796fe015a7040c9398722d6b97df73a6b608e9e275fa0932b0"},
|
||||
{file = "coverage-7.6.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eea60c79d36a8f39475b1af887663bc3ae4f31289cd216f514ce18d5938df40"},
|
||||
{file = "coverage-7.6.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa68a6cdbe1bc6793a9dbfc38302c11599bbe1837392ae9b1d238b9ef3dafcf1"},
|
||||
{file = "coverage-7.6.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ec528ae69f0a139690fad6deac8a7d33629fa61ccce693fdd07ddf7e9931fba"},
|
||||
{file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed5ac02126f74d190fa2cc14a9eb2a5d9837d5863920fa472b02eb1595cdc925"},
|
||||
{file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21c0ea0d4db8a36b275cb6fb2437a3715697a4ba3cb7b918d3525cc75f726304"},
|
||||
{file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35a51598f29b2a19e26d0908bd196f771a9b1c5d9a07bf20be0adf28f1ad4f77"},
|
||||
{file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c9192925acc33e146864b8cf037e2ed32a91fdf7644ae875f5d46cd2ef086a5f"},
|
||||
{file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf4eeecc9e10f5403ec06138978235af79c9a79af494eb6b1d60a50b49ed2869"},
|
||||
{file = "coverage-7.6.2-cp313-cp313-win32.whl", hash = "sha256:e4ee15b267d2dad3e8759ca441ad450c334f3733304c55210c2a44516e8d5530"},
|
||||
{file = "coverage-7.6.2-cp313-cp313-win_amd64.whl", hash = "sha256:c71965d1ced48bf97aab79fad56df82c566b4c498ffc09c2094605727c4b7e36"},
|
||||
{file = "coverage-7.6.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7571e8bbecc6ac066256f9de40365ff833553e2e0c0c004f4482facb131820ef"},
|
||||
{file = "coverage-7.6.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:078a87519057dacb5d77e333f740708ec2a8f768655f1db07f8dfd28d7a005f0"},
|
||||
{file = "coverage-7.6.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5e92e3e84a8718d2de36cd8387459cba9a4508337b8c5f450ce42b87a9e760"},
|
||||
{file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebabdf1c76593a09ee18c1a06cd3022919861365219ea3aca0247ededf6facd6"},
|
||||
{file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12179eb0575b8900912711688e45474f04ab3934aaa7b624dea7b3c511ecc90f"},
|
||||
{file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:39d3b964abfe1519b9d313ab28abf1d02faea26cd14b27f5283849bf59479ff5"},
|
||||
{file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:84c4315577f7cd511d6250ffd0f695c825efe729f4205c0340f7004eda51191f"},
|
||||
{file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ff797320dcbff57caa6b2301c3913784a010e13b1f6cf4ab3f563f3c5e7919db"},
|
||||
{file = "coverage-7.6.2-cp313-cp313t-win32.whl", hash = "sha256:2b636a301e53964550e2f3094484fa5a96e699db318d65398cfba438c5c92171"},
|
||||
{file = "coverage-7.6.2-cp313-cp313t-win_amd64.whl", hash = "sha256:d03a060ac1a08e10589c27d509bbdb35b65f2d7f3f8d81cf2fa199877c7bc58a"},
|
||||
{file = "coverage-7.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c37faddc8acd826cfc5e2392531aba734b229741d3daec7f4c777a8f0d4993e5"},
|
||||
{file = "coverage-7.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab31fdd643f162c467cfe6a86e9cb5f1965b632e5e65c072d90854ff486d02cf"},
|
||||
{file = "coverage-7.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97df87e1a20deb75ac7d920c812e9326096aa00a9a4b6d07679b4f1f14b06c90"},
|
||||
{file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:343056c5e0737487a5291f5691f4dfeb25b3e3c8699b4d36b92bb0e586219d14"},
|
||||
{file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4ef1c56b47b6b9024b939d503ab487231df1f722065a48f4fc61832130b90e"},
|
||||
{file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fca4a92c8a7a73dee6946471bce6d1443d94155694b893b79e19ca2a540d86e"},
|
||||
{file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69f251804e052fc46d29d0e7348cdc5fcbfc4861dc4a1ebedef7e78d241ad39e"},
|
||||
{file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e8ea055b3ea046c0f66217af65bc193bbbeca1c8661dc5fd42698db5795d2627"},
|
||||
{file = "coverage-7.6.2-cp39-cp39-win32.whl", hash = "sha256:6c2ba1e0c24d8fae8f2cf0aeb2fc0a2a7f69b6d20bd8d3749fd6b36ecef5edf0"},
|
||||
{file = "coverage-7.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:2186369a654a15628e9c1c9921409a6b3eda833e4b91f3ca2a7d9f77abb4987c"},
|
||||
{file = "coverage-7.6.2-pp39.pp310-none-any.whl", hash = "sha256:667952739daafe9616db19fbedbdb87917eee253ac4f31d70c7587f7ab531b4e"},
|
||||
{file = "coverage-7.6.2.tar.gz", hash = "sha256:a5f81e68aa62bc0cfca04f7b19eaa8f9c826b53fc82ab9e2121976dc74f131f3"},
|
||||
{file = "coverage-7.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6da42bbcec130b188169107ecb6ee7bd7b4c849d24c9370a0c884cf728d8e976"},
|
||||
{file = "coverage-7.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c222958f59b0ae091f4535851cbb24eb57fc0baea07ba675af718fb5302dddb2"},
|
||||
{file = "coverage-7.6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab84a8b698ad5a6c365b08061920138e7a7dd9a04b6feb09ba1bfae68346ce6d"},
|
||||
{file = "coverage-7.6.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70a6756ce66cd6fe8486c775b30889f0dc4cb20c157aa8c35b45fd7868255c5c"},
|
||||
{file = "coverage-7.6.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c2e6fa98032fec8282f6b27e3f3986c6e05702828380618776ad794e938f53a"},
|
||||
{file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:921fbe13492caf6a69528f09d5d7c7d518c8d0e7b9f6701b7719715f29a71e6e"},
|
||||
{file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6d99198203f0b9cb0b5d1c0393859555bc26b548223a769baf7e321a627ed4fc"},
|
||||
{file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:87cd2e29067ea397a47e352efb13f976eb1b03e18c999270bb50589323294c6e"},
|
||||
{file = "coverage-7.6.3-cp310-cp310-win32.whl", hash = "sha256:a3328c3e64ea4ab12b85999eb0779e6139295bbf5485f69d42cf794309e3d007"},
|
||||
{file = "coverage-7.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:bca4c8abc50d38f9773c1ec80d43f3768df2e8576807d1656016b9d3eeaa96fd"},
|
||||
{file = "coverage-7.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c51ef82302386d686feea1c44dbeef744585da16fcf97deea2a8d6c1556f519b"},
|
||||
{file = "coverage-7.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0ca37993206402c6c35dc717f90d4c8f53568a8b80f0bf1a1b2b334f4d488fba"},
|
||||
{file = "coverage-7.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c77326300b839c44c3e5a8fe26c15b7e87b2f32dfd2fc9fee1d13604347c9b38"},
|
||||
{file = "coverage-7.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e484e479860e00da1f005cd19d1c5d4a813324e5951319ac3f3eefb497cc549"},
|
||||
{file = "coverage-7.6.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c6c0f4d53ef603397fc894a895b960ecd7d44c727df42a8d500031716d4e8d2"},
|
||||
{file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:37be7b5ea3ff5b7c4a9db16074dc94523b5f10dd1f3b362a827af66a55198175"},
|
||||
{file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:43b32a06c47539fe275106b376658638b418c7cfdfff0e0259fbf877e845f14b"},
|
||||
{file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee77c7bef0724165e795b6b7bf9c4c22a9b8468a6bdb9c6b4281293c6b22a90f"},
|
||||
{file = "coverage-7.6.3-cp311-cp311-win32.whl", hash = "sha256:43517e1f6b19f610a93d8227e47790722c8bf7422e46b365e0469fc3d3563d97"},
|
||||
{file = "coverage-7.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:04f2189716e85ec9192df307f7c255f90e78b6e9863a03223c3b998d24a3c6c6"},
|
||||
{file = "coverage-7.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27bd5f18d8f2879e45724b0ce74f61811639a846ff0e5c0395b7818fae87aec6"},
|
||||
{file = "coverage-7.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d546cfa78844b8b9c1c0533de1851569a13f87449897bbc95d698d1d3cb2a30f"},
|
||||
{file = "coverage-7.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9975442f2e7a5cfcf87299c26b5a45266ab0696348420049b9b94b2ad3d40234"},
|
||||
{file = "coverage-7.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:583049c63106c0555e3ae3931edab5669668bbef84c15861421b94e121878d3f"},
|
||||
{file = "coverage-7.6.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2341a78ae3a5ed454d524206a3fcb3cec408c2a0c7c2752cd78b606a2ff15af4"},
|
||||
{file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a4fb91d5f72b7e06a14ff4ae5be625a81cd7e5f869d7a54578fc271d08d58ae3"},
|
||||
{file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e279f3db904e3b55f520f11f983cc8dc8a4ce9b65f11692d4718ed021ec58b83"},
|
||||
{file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa23ce39661a3e90eea5f99ec59b763b7d655c2cada10729ed920a38bfc2b167"},
|
||||
{file = "coverage-7.6.3-cp312-cp312-win32.whl", hash = "sha256:52ac29cc72ee7e25ace7807249638f94c9b6a862c56b1df015d2b2e388e51dbd"},
|
||||
{file = "coverage-7.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:40e8b1983080439d4802d80b951f4a93d991ef3261f69e81095a66f86cf3c3c6"},
|
||||
{file = "coverage-7.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9134032f5aa445ae591c2ba6991d10136a1f533b1d2fa8f8c21126468c5025c6"},
|
||||
{file = "coverage-7.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99670790f21a96665a35849990b1df447993880bb6463a0a1d757897f30da929"},
|
||||
{file = "coverage-7.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc7d6b380ca76f5e817ac9eef0c3686e7834c8346bef30b041a4ad286449990"},
|
||||
{file = "coverage-7.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7b26757b22faf88fcf232f5f0e62f6e0fd9e22a8a5d0d5016888cdfe1f6c1c4"},
|
||||
{file = "coverage-7.6.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c59d6a4a4633fad297f943c03d0d2569867bd5372eb5684befdff8df8522e39"},
|
||||
{file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f263b18692f8ed52c8de7f40a0751e79015983dbd77b16906e5b310a39d3ca21"},
|
||||
{file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:79644f68a6ff23b251cae1c82b01a0b51bc40c8468ca9585c6c4b1aeee570e0b"},
|
||||
{file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:71967c35828c9ff94e8c7d405469a1fb68257f686bca7c1ed85ed34e7c2529c4"},
|
||||
{file = "coverage-7.6.3-cp313-cp313-win32.whl", hash = "sha256:e266af4da2c1a4cbc6135a570c64577fd3e6eb204607eaff99d8e9b710003c6f"},
|
||||
{file = "coverage-7.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:ea52bd218d4ba260399a8ae4bb6b577d82adfc4518b93566ce1fddd4a49d1dce"},
|
||||
{file = "coverage-7.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8d4c6ea0f498c7c79111033a290d060c517853a7bcb2f46516f591dab628ddd3"},
|
||||
{file = "coverage-7.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:331b200ad03dbaa44151d74daeb7da2cf382db424ab923574f6ecca7d3b30de3"},
|
||||
{file = "coverage-7.6.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54356a76b67cf8a3085818026bb556545ebb8353951923b88292556dfa9f812d"},
|
||||
{file = "coverage-7.6.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebec65f5068e7df2d49466aab9128510c4867e532e07cb6960075b27658dca38"},
|
||||
{file = "coverage-7.6.3-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d33a785ea8354c480515e781554d3be582a86297e41ccbea627a5c632647f2cd"},
|
||||
{file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f7ddb920106bbbbcaf2a274d56f46956bf56ecbde210d88061824a95bdd94e92"},
|
||||
{file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:70d24936ca6c15a3bbc91ee9c7fc661132c6f4c9d42a23b31b6686c05073bde5"},
|
||||
{file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c30e42ea11badb147f0d2e387115b15e2bd8205a5ad70d6ad79cf37f6ac08c91"},
|
||||
{file = "coverage-7.6.3-cp313-cp313t-win32.whl", hash = "sha256:365defc257c687ce3e7d275f39738dcd230777424117a6c76043459db131dd43"},
|
||||
{file = "coverage-7.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:23bb63ae3f4c645d2d82fa22697364b0046fbafb6261b258a58587441c5f7bd0"},
|
||||
{file = "coverage-7.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:da29ceabe3025a1e5a5aeeb331c5b1af686daab4ff0fb4f83df18b1180ea83e2"},
|
||||
{file = "coverage-7.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df8c05a0f574d480947cba11b947dc41b1265d721c3777881da2fb8d3a1ddfba"},
|
||||
{file = "coverage-7.6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec1e3b40b82236d100d259854840555469fad4db64f669ab817279eb95cd535c"},
|
||||
{file = "coverage-7.6.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4adeb878a374126f1e5cf03b87f66279f479e01af0e9a654cf6d1509af46c40"},
|
||||
{file = "coverage-7.6.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43d6a66e33b1455b98fc7312b124296dad97a2e191c80320587234a77b1b736e"},
|
||||
{file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1990b1f4e2c402beb317840030bb9f1b6a363f86e14e21b4212e618acdfce7f6"},
|
||||
{file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:12f9515d875859faedb4144fd38694a761cd2a61ef9603bf887b13956d0bbfbb"},
|
||||
{file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99ded130555c021d99729fabd4ddb91a6f4cc0707df4b1daf912c7850c373b13"},
|
||||
{file = "coverage-7.6.3-cp39-cp39-win32.whl", hash = "sha256:c3a79f56dee9136084cf84a6c7c4341427ef36e05ae6415bf7d787c96ff5eaa3"},
|
||||
{file = "coverage-7.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:aac7501ae73d4a02f4b7ac8fcb9dc55342ca98ffb9ed9f2dfb8a25d53eda0e4d"},
|
||||
{file = "coverage-7.6.3-pp39.pp310-none-any.whl", hash = "sha256:b9853509b4bf57ba7b1f99b9d866c422c9c5248799ab20e652bbb8a184a38181"},
|
||||
{file = "coverage-7.6.3.tar.gz", hash = "sha256:bb7d5fe92bd0dc235f63ebe9f8c6e0884f7360f88f3411bfed1350c872ef2054"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@ -491,13 +491,13 @@ tests = ["noseofyeti[black] (==2.4.9)", "pytest (==8.3.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "distlib"
|
||||
version = "0.3.8"
|
||||
version = "0.3.9"
|
||||
description = "Distribution utilities"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
|
||||
{file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
|
||||
{file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"},
|
||||
{file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -814,13 +814,13 @@ dev = ["flake8", "markdown", "twine", "wheel"]
|
||||
|
||||
[[package]]
|
||||
name = "griffe"
|
||||
version = "1.3.2"
|
||||
version = "1.4.1"
|
||||
description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "griffe-1.3.2-py3-none-any.whl", hash = "sha256:2e34b5e46507d615915c8e6288bb1a2234bd35dee44d01e40a2bc2f25bd4d10c"},
|
||||
{file = "griffe-1.3.2.tar.gz", hash = "sha256:1ec50335aa507ed2445f2dd45a15c9fa3a45f52c9527e880571dfc61912fd60c"},
|
||||
{file = "griffe-1.4.1-py3-none-any.whl", hash = "sha256:84295ee0b27743bd880aea75632830ef02ded65d16124025e4c263bb826ab645"},
|
||||
{file = "griffe-1.4.1.tar.gz", hash = "sha256:911a201b01dc92e08c0e84c38a301e9da5ec067f00e7d9f2e39bc24dbfa3c176"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -1283,13 +1283,13 @@ cache = ["platformdirs"]
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-material"
|
||||
version = "9.5.39"
|
||||
version = "9.5.40"
|
||||
description = "Documentation that simply works"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "mkdocs_material-9.5.39-py3-none-any.whl", hash = "sha256:0f2f68c8db89523cb4a59705cd01b4acd62b2f71218ccb67e1e004e560410d2b"},
|
||||
{file = "mkdocs_material-9.5.39.tar.gz", hash = "sha256:25faa06142afa38549d2b781d475a86fb61de93189f532b88e69bf11e5e5c3be"},
|
||||
{file = "mkdocs_material-9.5.40-py3-none-any.whl", hash = "sha256:8e7a16ada34e79a7b6459ff2602584222f522c738b6a023d1bea853d5049da6f"},
|
||||
{file = "mkdocs_material-9.5.40.tar.gz", hash = "sha256:b69d70e667ec51fc41f65e006a3184dd00d95b2439d982cb1586e4c018943156"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -1323,13 +1323,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "mkdocstrings"
|
||||
version = "0.26.1"
|
||||
version = "0.26.2"
|
||||
description = "Automatic documentation from sources, for MkDocs."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "mkdocstrings-0.26.1-py3-none-any.whl", hash = "sha256:29738bfb72b4608e8e55cc50fb8a54f325dc7ebd2014e4e3881a49892d5983cf"},
|
||||
{file = "mkdocstrings-0.26.1.tar.gz", hash = "sha256:bb8b8854d6713d5348ad05b069a09f3b79edbc6a0f33a34c6821141adb03fe33"},
|
||||
{file = "mkdocstrings-0.26.2-py3-none-any.whl", hash = "sha256:1248f3228464f3b8d1a15bd91249ce1701fe3104ac517a5f167a0e01ca850ba5"},
|
||||
{file = "mkdocstrings-0.26.2.tar.gz", hash = "sha256:34a8b50f1e6cfd29546c6c09fbe02154adfb0b361bb758834bf56aa284ba876e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -1349,13 +1349,13 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "mkdocstrings-python"
|
||||
version = "1.11.1"
|
||||
version = "1.12.1"
|
||||
description = "A Python handler for mkdocstrings."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "mkdocstrings_python-1.11.1-py3-none-any.whl", hash = "sha256:a21a1c05acef129a618517bb5aae3e33114f569b11588b1e7af3e9d4061a71af"},
|
||||
{file = "mkdocstrings_python-1.11.1.tar.gz", hash = "sha256:8824b115c5359304ab0b5378a91f6202324a849e1da907a3485b59208b797322"},
|
||||
{file = "mkdocstrings_python-1.12.1-py3-none-any.whl", hash = "sha256:205244488199c9aa2a39787ad6a0c862d39b74078ea9aa2be817bc972399563f"},
|
||||
{file = "mkdocstrings_python-1.12.1.tar.gz", hash = "sha256:60d6a5ca912c9af4ad431db6d0111ce9f79c6c48d33377dde6a05a8f5f48d792"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -1365,13 +1365,13 @@ mkdocstrings = ">=0.26"
|
||||
|
||||
[[package]]
|
||||
name = "model-bakery"
|
||||
version = "1.19.5"
|
||||
version = "1.20.0"
|
||||
description = "Smart object creation facility for Django."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "model_bakery-1.19.5-py3-none-any.whl", hash = "sha256:09ecbbf124d32614339581b642c82ac4a73147442f598c7bad23eece24187e5c"},
|
||||
{file = "model_bakery-1.19.5.tar.gz", hash = "sha256:37cece544a33f8899ed8f0488cd6a9d2b0b6925e7b478a4ff2786dece8c63745"},
|
||||
{file = "model_bakery-1.20.0-py3-none-any.whl", hash = "sha256:875326466f5982ee8f0281abdfa774d78893d5473562575dfd5a9304ac7c5b8c"},
|
||||
{file = "model_bakery-1.20.0.tar.gz", hash = "sha256:ec9dc846b9a00b20f92df38fac310263323ab61b59b6eeebf77a4aefb0412724"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -1599,13 +1599,13 @@ testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "3.8.0"
|
||||
version = "4.0.1"
|
||||
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"},
|
||||
{file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"},
|
||||
{file = "pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878"},
|
||||
{file = "pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -2192,58 +2192,70 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||
|
||||
[[package]]
|
||||
name = "rjsmin"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
description = "Javascript Minifier"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "rjsmin-1.2.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:4420107304ba7a00b5b9b56cdcd166b9876b34e626829fc4552c85d8fdc3737a"},
|
||||
{file = "rjsmin-1.2.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:155a2f3312c1f8c6cec7b5080581cafc761dc0e41d64bfb5d46a772c5230ded8"},
|
||||
{file = "rjsmin-1.2.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:88fcb58d65f88cbfa752d51c1ebe5845553f9706def6d9671e98283411575e3e"},
|
||||
{file = "rjsmin-1.2.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:6eae13608b88f4ce32e0557c8fdef58e69bb4d293182202a03e800f0d33b5268"},
|
||||
{file = "rjsmin-1.2.2-cp310-cp310-manylinux1_i686.whl", hash = "sha256:81f92fb855fb613ebd04a6d6d46483e71fe3c4f22042dc30dcc938fbd748e59c"},
|
||||
{file = "rjsmin-1.2.2-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:897db9bf25538047e9388951d532dc291a629b5d041180a8a1a8c102e9d44b90"},
|
||||
{file = "rjsmin-1.2.2-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:5938af8c46734f92f74fdc4d0b6324137c0e09f0a8c3825c83e4cfca1b532e40"},
|
||||
{file = "rjsmin-1.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0424a7b9096fa2b0ab577c4dc7acd683e6cfb5c718ad39a9fb293cb6cbaba95b"},
|
||||
{file = "rjsmin-1.2.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1714ed93c2bd40c5f970905d2eeda4a6844e09087ae11277d4d43b3e68c32a47"},
|
||||
{file = "rjsmin-1.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:35596fa6d2d44a5471715c464657123995da78aa6f79bccfbb4b8d6ff7d0a4b4"},
|
||||
{file = "rjsmin-1.2.2-cp311-cp311-manylinux1_i686.whl", hash = "sha256:3968667158948355b9a62e9641497aac7ac069c076a595e93199d0fe3a40217a"},
|
||||
{file = "rjsmin-1.2.2-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:d07d14354694f6a47f572f2aa2a1ad74b76723e62a0d2b6df796138b71888247"},
|
||||
{file = "rjsmin-1.2.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:a78dfa6009235b902454ac53264252b7b94f1e43e3a9e97c4cadae88e409b882"},
|
||||
{file = "rjsmin-1.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9b7a45001e58243a455d11d2de925cadb8c2a0dc737001de646a0f4d90cf0034"},
|
||||
{file = "rjsmin-1.2.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:86c5e657b74b6c9482bb96f18a79d61750f4e8204759cce179f7eb17d395c683"},
|
||||
{file = "rjsmin-1.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8c2c30b86c7232443a4a726e1bbee34f800556e581e95fc07194ecbf8e02d1d2"},
|
||||
{file = "rjsmin-1.2.2-cp312-cp312-manylinux1_i686.whl", hash = "sha256:8982c3ef27fac26dd6b7d0c55ae98fa550fee72da2db010b87211e4b5dd78a67"},
|
||||
{file = "rjsmin-1.2.2-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:3fc27ae4ece99e2c994cd79df2f0d3f7ac650249f632d19aa8ce85118e33bf0f"},
|
||||
{file = "rjsmin-1.2.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:41113d8d6cae7f7406b30143cc49cc045bbb3fadc2f28df398cea30e1daa60b1"},
|
||||
{file = "rjsmin-1.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3aa09a89b2b7aa2b9251329fe0c3e36c2dc2f10f78b8811e5be92a072596348b"},
|
||||
{file = "rjsmin-1.2.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:5abb8d1241f4ea97950b872fa97a422ba8413fe02358f64128ff0cf745017f07"},
|
||||
{file = "rjsmin-1.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5abc686a9ef7eaf208f9ad1fb5fb949556ecb7cc1fee27290eb7f194e01d97bd"},
|
||||
{file = "rjsmin-1.2.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:076adcf04c34f712c9427fd9ba6a75bbf7aab975650dfc78cbdd0fbdbe49ca63"},
|
||||
{file = "rjsmin-1.2.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8cb8947ddd250fce58261b0357846cd5d55419419c0f7dfb131dc4b733579a26"},
|
||||
{file = "rjsmin-1.2.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9069c48b6508b9c5b05435e2c6042c2a0e2f97b35d7b9c27ceaea5fd377ffdc5"},
|
||||
{file = "rjsmin-1.2.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:02b61cf9b6bc518fdac667f3ca3dab051cb8bd1bf4cba28b6d29153ec27990ad"},
|
||||
{file = "rjsmin-1.2.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:09eca8581797244587916e5e07e36c4c86d54a4b7e5c7697484a95b75803515d"},
|
||||
{file = "rjsmin-1.2.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:c52b9dd45c837f1c5c2e8d40776f9e63257f8dbd5f79b85f648cc70da6c1e4e9"},
|
||||
{file = "rjsmin-1.2.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4fe4ce990412c053a6bcd47d55133927e22fd3d100233d73355f60f9053054c5"},
|
||||
{file = "rjsmin-1.2.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:aa883b9363b5134239066060879d5eb422a0d4ccf24ccf871f65a5b34c64926f"},
|
||||
{file = "rjsmin-1.2.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:6f4e95c5ac95b4cbb519917b3aa1d3d92fc6939c371637674c4a42b67b2b3f44"},
|
||||
{file = "rjsmin-1.2.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ae3cd64e18e62aa330b24dd6f7b9809ce0a694afd1f01fe99c21f9acd1cb0ea6"},
|
||||
{file = "rjsmin-1.2.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7999d797fcf805844d2d91598651785497249f592f31674da0964e794b3be019"},
|
||||
{file = "rjsmin-1.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e733fea039a7b5ad7c06cc8bf215ee7afac81d462e273b3ab55c1ccc906cf127"},
|
||||
{file = "rjsmin-1.2.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ccca74461bd53a99ff3304fcf299ea861df89846be3207329cb82d717ce47ea6"},
|
||||
{file = "rjsmin-1.2.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:88f59ad24f91bf9c25d5c2ca3c84a72eed0028f57a98e3b85a915ece5c25be1e"},
|
||||
{file = "rjsmin-1.2.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7a8b56fbd64adcc4402637f0e07b90b441e9981d720a10eb6265118018b42682"},
|
||||
{file = "rjsmin-1.2.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2c24686cfdf86e55692183f7867e72c9e982add479c244eda7b8390f96db2c6c"},
|
||||
{file = "rjsmin-1.2.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6c0d9f9ea8d9cd48cbcdc74a1c2e85d4d588af12bb8f0b672070ae7c9b6e6306"},
|
||||
{file = "rjsmin-1.2.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:27abd32c9f5b6e0c0a3bcad43e8e24108c6d6c13a4e6c50c97497ea2b4614bb4"},
|
||||
{file = "rjsmin-1.2.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:e0e009f6f8460901f5144b34ac2948f94af2f9b8c9b5425da705dbc8152c36c2"},
|
||||
{file = "rjsmin-1.2.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:41e6013cb37a5b3563c19aa35f8e659fa536aa4197a0e3b6a57a381638294a15"},
|
||||
{file = "rjsmin-1.2.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:62cbd38c9f5090f0a6378a45c415b4f96ae871216cedab0dfa21965620c0be4c"},
|
||||
{file = "rjsmin-1.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2fd5254d36f10a17564b63e8bf9ac579c7b5f211364e11e9753ff5b562843c67"},
|
||||
{file = "rjsmin-1.2.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6cf0309d001a0d45d731dbaab1afd0c23d135c9e029fe56c935c1798094686fc"},
|
||||
{file = "rjsmin-1.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bfbe333dab8d23f0a71da90e2d8e8b762a739cbd55a6f948b2dfda089b6d5853"},
|
||||
{file = "rjsmin-1.2.2.tar.gz", hash = "sha256:8c1bcd821143fecf23242012b55e13610840a839cd467b358f16359010d62dae"},
|
||||
{file = "rjsmin-1.2.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:86e4257875d89b0f7968af9e7c0292e72454f6c75031d1818997782b2e8425a8"},
|
||||
{file = "rjsmin-1.2.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f2ab72093591127e627b13c1243d4fef40c10593c733517999682f7f2ebf47ee"},
|
||||
{file = "rjsmin-1.2.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:448b9eb9fd7b6a70beb5c728a41bc23561dd011f0b8fcf7ed9855b6be198c9a2"},
|
||||
{file = "rjsmin-1.2.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ebd3948f9bc912525bab93f61c694b11410296b5fd0806e988d42378ef302b8e"},
|
||||
{file = "rjsmin-1.2.3-cp310-cp310-manylinux1_i686.whl", hash = "sha256:823f856b40681328157e5dffc0a588dddefb4b6ce49f79de994dfca6084617be"},
|
||||
{file = "rjsmin-1.2.3-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:15e3019f0823a003741ddb93e0c70c5d22567acd0757a7edacc40face1517029"},
|
||||
{file = "rjsmin-1.2.3-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:dece04e309e29879c12dca8af166ea5d77c497ec932cf82e4a1eb24d1489c398"},
|
||||
{file = "rjsmin-1.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dd4a1e527568c3a9711ff1d5251763645c14df02d52a45aec089836600b664ea"},
|
||||
{file = "rjsmin-1.2.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:78aaa3b79a244a4e21164ce355ce22a5a0d7f2d7841a10343009406a3d34d9bb"},
|
||||
{file = "rjsmin-1.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ea4617618cbf78d98756878a292309f6f54fb4ea1b1ea406f79e88eda4d5d50"},
|
||||
{file = "rjsmin-1.2.3-cp311-cp311-manylinux1_i686.whl", hash = "sha256:85957171184ef2dee1957cef5e4adb93a7e2702c12c30bd74420ebace1756e89"},
|
||||
{file = "rjsmin-1.2.3-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:b6485014e9cbec9a41fb4a7b96ce511ab45a5db8c54ca57ad610f53747e7bab1"},
|
||||
{file = "rjsmin-1.2.3-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:64ac6ef8753c56179a53e237ea4d2b3ccdef88b8b51141618311d48e31013207"},
|
||||
{file = "rjsmin-1.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dbd5f653b5ebcd4920793009ffa210ad5523c523e39e45ee1a0770e4323126dc"},
|
||||
{file = "rjsmin-1.2.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b0174d7786dcebab808485d1c27f049c74b97590cddcd62f6ed54796a2c6503b"},
|
||||
{file = "rjsmin-1.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6cf24720ea214cbffa0ed96ba0dc24a5cf3ff3cbf59d44a1018292424b48452a"},
|
||||
{file = "rjsmin-1.2.3-cp312-cp312-manylinux1_i686.whl", hash = "sha256:ac911d1a12a6d7879ba52e08c56b0ad1a74377bae52610ea74f0f9d936d41785"},
|
||||
{file = "rjsmin-1.2.3-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:57a0b2f13402623e4ec44eb7ad8846387b2d5605aa8732a05ebefb2289c24b96"},
|
||||
{file = "rjsmin-1.2.3-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:e28610cca3ab03e43113eadad4f7dd9ea235ddc29a8dc5462bb161a80e5d251f"},
|
||||
{file = "rjsmin-1.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d4afb4fc3624dc44a7fbae4e41c0b5dc5d861a7f5de865ad463041ec1b5d835c"},
|
||||
{file = "rjsmin-1.2.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ca26b80c7e63cf0788b41571a4bd08d175df7719364e0dd9a3cf7b6cb1ab834c"},
|
||||
{file = "rjsmin-1.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fcc22001588b84d34bbf2c77afa519894244150c4b0754a6e573298ffac24666"},
|
||||
{file = "rjsmin-1.2.3-cp313-cp313-manylinux1_i686.whl", hash = "sha256:624d1a0a35122f3f8955d160a39305cf6f786a5b346ee34c516b391cb153a106"},
|
||||
{file = "rjsmin-1.2.3-cp313-cp313-manylinux1_x86_64.whl", hash = "sha256:72bd04b7db6190339d8214a5fd289ca31fc1ed30a240f8b0ca13acb9ce3a88af"},
|
||||
{file = "rjsmin-1.2.3-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:7559f59f4749519b92d72bb68e33b68463f479a82a2a739f1b28a853067aa0e7"},
|
||||
{file = "rjsmin-1.2.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:aa8bdecf278f754d1a133ab51119a63a4d38500557912bb0930ae0fd61437ec6"},
|
||||
{file = "rjsmin-1.2.3-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:2078acc2d02a005ef122eb330e941462c8c3102cf798ad49f1c5ec18ac714240"},
|
||||
{file = "rjsmin-1.2.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:fa40584fddb4f1d2236119505f6c2fe2b57a1ebaf6eaee2bb2eaac33d2a4ca73"},
|
||||
{file = "rjsmin-1.2.3-cp313-cp313t-manylinux1_i686.whl", hash = "sha256:bbe5d8340878b38dd4f7b879ed7728f6fc3d7524ad81a5cfbe4eb8ae63951407"},
|
||||
{file = "rjsmin-1.2.3-cp313-cp313t-manylinux1_x86_64.whl", hash = "sha256:c298c93f5633cf894325907cf49fc7fb010c0f75dc9cda90b0fc1684ad19e5a3"},
|
||||
{file = "rjsmin-1.2.3-cp313-cp313t-manylinux2014_aarch64.whl", hash = "sha256:35f18cffe3f1bf6d96bcfd977199378ebfd641d823b08e235d1e0bb0fbaa5532"},
|
||||
{file = "rjsmin-1.2.3-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:9aeadf4dd5f941bebf110fe83960a4bafdac176647537819bb7662f5e9a37aaa"},
|
||||
{file = "rjsmin-1.2.3-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:c3219e6e22897b31c8598cb412ed56bc12a722c1d4f88a71710c16efe8c07d0c"},
|
||||
{file = "rjsmin-1.2.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:bceccb06b118be890fe735fc09ee256851f4993708cb3647f6c71dd0151cce89"},
|
||||
{file = "rjsmin-1.2.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:f3620271f00b8ba3c7c5134ca1d99cde5fd1bf1e84aa96aa65c177ee634122f7"},
|
||||
{file = "rjsmin-1.2.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f3d86f70fcca5f68b65eabbce365d07d80404ecd6aa9c55ba9e9f1042a3514c7"},
|
||||
{file = "rjsmin-1.2.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:1dae9230eae6d7eb2820a511cb640ca6f2e5b91ff78805d71332e8a65a898ea1"},
|
||||
{file = "rjsmin-1.2.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:b788c3ec9d68d8fda2240eb7831bdfb2cc0c88d5fb38c9ed6e0fd090eb5d1490"},
|
||||
{file = "rjsmin-1.2.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:4763efbfad7fbf3240a33f08f64991bf0db07453caf283eea51ade84053e9bb7"},
|
||||
{file = "rjsmin-1.2.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e1379e448da75e2426205c756e79d7b9ba1b7ed616fe97122d72c3fe054e8cac"},
|
||||
{file = "rjsmin-1.2.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:27e134f4d91a5986cba6dced5cb539947a3ec61544ab5ef31b74b384ddc03931"},
|
||||
{file = "rjsmin-1.2.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2674fcad70d0fab4c1c71e4ac1d4d67935f67e6ecc3924de0dd1264c80a9f9a2"},
|
||||
{file = "rjsmin-1.2.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b1f2540bd0ce7eda326df7b3bfa360f6edd526bfcb959b5d136afdbccddf0765"},
|
||||
{file = "rjsmin-1.2.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:23f3b4adde995a0d0b7835558840dd4673adf99d2473b6d40474d30801d6c57b"},
|
||||
{file = "rjsmin-1.2.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c5fb0574eb541d374a2751e9c0ae019fdd86c9e3eb2e7cf893756886e7b3923f"},
|
||||
{file = "rjsmin-1.2.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18d6a3229d1ed511a0b0a9a7271ef58ff3b02ba408b92b426857b33b137e7f15"},
|
||||
{file = "rjsmin-1.2.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7fe1181452fca7713f377cb6c43cd139638a9edc8c8c29c67119626df164b317"},
|
||||
{file = "rjsmin-1.2.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:57708a4e637aac571c578424a7092d3ec64afb1eabbb73e0c71659457eac9ee4"},
|
||||
{file = "rjsmin-1.2.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:4c1b5a888d43063a22e2e2c2b4db4d6139dfa6e0d2903ae9bb050ed63a340f40"},
|
||||
{file = "rjsmin-1.2.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e4ac3f85df88d636a9680432fbbf5d3fe1f171821688106a6710738f06575fc2"},
|
||||
{file = "rjsmin-1.2.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9dff6b14f92ca7a9f6fbf13548358715e47c5e69576aa5dd8b0ad5048fdc967f"},
|
||||
{file = "rjsmin-1.2.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07c4f1efbbbcd16a645ada1f012595f3eb3e5d5933395effb6104d3731de2d96"},
|
||||
{file = "rjsmin-1.2.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:37a73f6ff49dd8c662399575a249a2a028d098c1fa940c6e88aa9082beb44eca"},
|
||||
{file = "rjsmin-1.2.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:73357ec91465cf69173d637ccde7b46ed3a8001161c9650325fa305a486e89a3"},
|
||||
{file = "rjsmin-1.2.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:236c792fbe18c3b18d4e0ad5ff1b1145f1fbe02126aee9f21bca757b00b63b7e"},
|
||||
{file = "rjsmin-1.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a630a3131a4e63e10665a0ea7cfe0784a3e1e1c854edf79a8ac0654e3756648"},
|
||||
{file = "rjsmin-1.2.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a1c98f60ca57adbae023cf989eec91d052f0601df63ddc52a0a48303b21a7f9e"},
|
||||
{file = "rjsmin-1.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:32a0174efac83ac72a681edcb9acf5e1c87c5b6aae65ed3424468b5945a90f9d"},
|
||||
{file = "rjsmin-1.2.3.tar.gz", hash = "sha256:1388b52493a4c04fbc970a2d757c301fa05a3c37640314c2ce9dfc8d8a730cc6"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2679,4 +2691,4 @@ filelock = ">=3.4"
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "730b020b335ea67342069c40591f6959f1e5f01eef40806c95777def2f39eb37"
|
||||
content-hash = "cb47f6409e629d8369a19d82f44a57dbe9414c79e6e72bd88a6bcb34d78f0bc0"
|
||||
|
@ -65,7 +65,7 @@ optional = true
|
||||
# deps used for development purposes, but unneeded in prod
|
||||
django-debug-toolbar = "^4.4.6"
|
||||
ipython = "^8.26.0"
|
||||
pre-commit = "^3.8.0"
|
||||
pre-commit = "^4.0.1"
|
||||
ruff = "^0.6.9" # Version used in pipeline is controlled by pre-commit hooks in .pre-commit.config.yaml
|
||||
djhtml = "^3.0.6"
|
||||
faker = "^30.3.0"
|
||||
@ -77,14 +77,14 @@ freezegun = "^1.5.1" # used to test time-dependent code
|
||||
pytest = "^8.3.2"
|
||||
pytest-cov = "^5.0.0"
|
||||
pytest-django = "^4.9.0"
|
||||
model-bakery = "^1.19.5"
|
||||
model-bakery = "^1.20.0"
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
# deps used to work on the documentation
|
||||
mkdocs = "^1.6.1"
|
||||
mkdocs-material = "^9.5.39"
|
||||
mkdocstrings = "^0.26.1"
|
||||
mkdocstrings-python = "^1.11.1"
|
||||
mkdocs-material = "^9.5.40"
|
||||
mkdocstrings = "^0.26.2"
|
||||
mkdocstrings-python = "^1.12.0"
|
||||
mkdocs-include-markdown-plugin = "^6.2.2"
|
||||
|
||||
[tool.poetry.group.docs]
|
||||
@ -101,17 +101,30 @@ select = [
|
||||
"A", # shadowing of Python builtins
|
||||
"B",
|
||||
"C4", # use comprehensions when possible
|
||||
"I", # isort
|
||||
"DJ", # django-specific rules,
|
||||
"F401", # unused import
|
||||
"E", # pycodestyle (https://docs.astral.sh/ruff/rules/#pycodestyle-e-w)
|
||||
"ERA", # commented code
|
||||
"F", # pyflakes (https://docs.astral.sh/ruff/rules/#pyflakes-f)
|
||||
"FBT", # boolean trap
|
||||
"FLY", # f-string instead of str.join
|
||||
"FURB", # https://docs.astral.sh/ruff/rules/#refurb-furb
|
||||
"I", # isort
|
||||
"INT", # gettext
|
||||
"PERF", # performance
|
||||
"PLW", # pylint warnings (https://docs.astral.sh/ruff/rules/#pylint-pl)
|
||||
"RUF", # Ruff specific rules
|
||||
"SIM", # simplify (https://docs.astral.sh/ruff/rules/#flake8-simplify-sim)
|
||||
"T100", # breakpoint()
|
||||
"T2", # print statements
|
||||
"TCH", # type-checking block
|
||||
"UP008", # Use super() instead of super(__class__, self)
|
||||
"UP009", # utf-8 encoding declaration is unnecessary
|
||||
"T2", # print statements
|
||||
]
|
||||
|
||||
ignore = [
|
||||
"DJ001", # null=True in CharField/TextField. this one would require a migration
|
||||
"E501", # line too long. The rule is too harsh, and the formatter deals with it in most cases
|
||||
"RUF012" # mutable class attributes. This rule doesn't integrate well with django
|
||||
]
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
|
@ -44,8 +44,8 @@ class Command(BaseCommand):
|
||||
exit(1)
|
||||
|
||||
confirm = input(
|
||||
"User selected: %s\nDo you really want to delete all message from this user ? [y/N] "
|
||||
% (user,)
|
||||
"User selected: %s\nDo you really want "
|
||||
"to delete all message from this user ? [y/N] " % (user,)
|
||||
)
|
||||
|
||||
if not confirm.lower().startswith("y"):
|
||||
|
@ -66,11 +66,11 @@ class TestMergeUser(TestCase):
|
||||
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
|
||||
assert "Biggus" == self.to_keep.first_name
|
||||
assert "Dickus" == self.to_keep.last_name
|
||||
assert "B'ian" == self.to_keep.nick_name
|
||||
assert "Jerusalem" == self.to_keep.address
|
||||
assert "Rome" == self.to_keep.parent_address
|
||||
assert self.to_keep.first_name == "Biggus"
|
||||
assert self.to_keep.last_name == "Dickus"
|
||||
assert self.to_keep.nick_name == "B'ian"
|
||||
assert self.to_keep.address == "Jerusalem"
|
||||
assert self.to_keep.parent_address == "Rome"
|
||||
assert self.to_keep.groups.count() == 3
|
||||
groups = sorted(self.to_keep.groups.all(), key=lambda i: i.id)
|
||||
expected = sorted([subscribers, mde_admin, sas_admin], key=lambda i: i.id)
|
||||
|
@ -24,7 +24,11 @@
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from rootplace.views import *
|
||||
from rootplace.views import (
|
||||
DeleteAllForumUserMessagesView,
|
||||
MergeUsersView,
|
||||
OperationLogListView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("merge/", MergeUsersView.as_view(), name="merge"),
|
||||
|
@ -48,7 +48,8 @@ def __merge_subscriptions(u1: User, u2: 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,
|
||||
- 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 = (
|
||||
|
15
sas/admin.py
15
sas/admin.py
@ -15,7 +15,7 @@
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from sas.models import Album, PeoplePictureRelation, Picture
|
||||
from sas.models import Album, PeoplePictureRelation, Picture, PictureModerationRequest
|
||||
|
||||
|
||||
@admin.register(Picture)
|
||||
@ -31,4 +31,15 @@ class PeoplePictureRelationAdmin(admin.ModelAdmin):
|
||||
autocomplete_fields = ("picture", "user")
|
||||
|
||||
|
||||
admin.site.register(Album)
|
||||
@admin.register(Album)
|
||||
class AlbumAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "parent", "date", "owner", "is_moderated")
|
||||
search_fields = ("name",)
|
||||
autocomplete_fields = ("owner", "parent", "edit_groups", "view_groups")
|
||||
|
||||
|
||||
@admin.register(PictureModerationRequest)
|
||||
class PictureModerationRequestAdmin(admin.ModelAdmin):
|
||||
list_display = ("author", "picture", "created_at")
|
||||
search_fields = ("author", "picture")
|
||||
autocomplete_fields = ("author", "picture")
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user