mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-11-04 02:53:06 +00:00 
			
		
		
		
	Add more Ruff rules (#891)
* ruff: apply rule F * ruff: apply rule E * ruff: apply rule SIM * ruff: apply rule TCH * ruff: apply rule ERA * ruff: apply rule PLW * ruff: apply rule FLY * ruff: apply rule PERF * ruff: apply rules FURB & RUF
This commit is contained in:
		@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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:
 | 
			
		||||
 
 | 
			
		||||
@@ -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):
 | 
			
		||||
 
 | 
			
		||||
@@ -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"]] == [
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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")
 | 
			
		||||
 
 | 
			
		||||
@@ -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]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -255,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
 | 
			
		||||
@@ -266,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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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,7 +15,40 @@
 | 
			
		||||
 | 
			
		||||
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"),
 | 
			
		||||
 
 | 
			
		||||
@@ -91,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 (
 | 
			
		||||
@@ -181,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(
 | 
			
		||||
@@ -219,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")
 | 
			
		||||
@@ -294,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
 | 
			
		||||
@@ -318,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(
 | 
			
		||||
@@ -329,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
 | 
			
		||||
@@ -386,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
 | 
			
		||||
 
 | 
			
		||||
@@ -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]:
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
            )
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
 
 | 
			
		||||
@@ -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"))
 | 
			
		||||
 
 | 
			
		||||
@@ -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):
 | 
			
		||||
 
 | 
			
		||||
@@ -101,18 +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
 | 
			
		||||
	"T100", # breakpoint()
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
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,6 +15,7 @@
 | 
			
		||||
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import contextlib
 | 
			
		||||
from io import BytesIO
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from typing import ClassVar, Self
 | 
			
		||||
@@ -108,10 +109,8 @@ class Picture(SasFile):
 | 
			
		||||
 | 
			
		||||
    def generate_thumbnails(self, *, overwrite=False):
 | 
			
		||||
        im = Image.open(BytesIO(self.file.read()))
 | 
			
		||||
        try:
 | 
			
		||||
        with contextlib.suppress(Exception):
 | 
			
		||||
            im = exif_auto_rotate(im)
 | 
			
		||||
        except:
 | 
			
		||||
            pass
 | 
			
		||||
        # convert the compressed image and the thumbnail into webp
 | 
			
		||||
        # The original image keeps its original type, because it's not
 | 
			
		||||
        # meant to be shown on the website, but rather to keep the real image
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										29
									
								
								sas/urls.py
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								sas/urls.py
									
									
									
									
									
								
							@@ -15,24 +15,33 @@
 | 
			
		||||
 | 
			
		||||
from django.urls import path
 | 
			
		||||
 | 
			
		||||
from sas.views import *
 | 
			
		||||
from sas.views import (
 | 
			
		||||
    AlbumEditView,
 | 
			
		||||
    AlbumUploadView,
 | 
			
		||||
    AlbumView,
 | 
			
		||||
    ModerationView,
 | 
			
		||||
    PictureAskRemovalView,
 | 
			
		||||
    PictureEditView,
 | 
			
		||||
    PictureView,
 | 
			
		||||
    SASMainView,
 | 
			
		||||
    send_album,
 | 
			
		||||
    send_compressed,
 | 
			
		||||
    send_pict,
 | 
			
		||||
    send_thumb,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path("", SASMainView.as_view(), name="main"),
 | 
			
		||||
    path("moderation/", ModerationView.as_view(), name="moderation"),
 | 
			
		||||
    path("album/<int:album_id>/", AlbumView.as_view(), name="album"),
 | 
			
		||||
    path(
 | 
			
		||||
        "album/<int:album_id>/upload/",
 | 
			
		||||
        AlbumUploadView.as_view(),
 | 
			
		||||
        name="album_upload",
 | 
			
		||||
        "album/<int:album_id>/upload/", AlbumUploadView.as_view(), name="album_upload"
 | 
			
		||||
    ),
 | 
			
		||||
    path("album/<int:album_id>/edit/", AlbumEditView.as_view(), name="album_edit"),
 | 
			
		||||
    path("album/<int:album_id>/preview/", send_album, name="album_preview"),
 | 
			
		||||
    path("picture/<int:picture_id>/", PictureView.as_view(), name="picture"),
 | 
			
		||||
    path(
 | 
			
		||||
        "picture/<int:picture_id>/edit/",
 | 
			
		||||
        PictureEditView.as_view(),
 | 
			
		||||
        name="picture_edit",
 | 
			
		||||
        "picture/<int:picture_id>/edit/", PictureEditView.as_view(), name="picture_edit"
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        "picture/<int:picture_id>/report",
 | 
			
		||||
@@ -45,9 +54,5 @@ urlpatterns = [
 | 
			
		||||
        send_compressed,
 | 
			
		||||
        name="download_compressed",
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        "picture/<int:picture_id>/download/thumb/",
 | 
			
		||||
        send_thumb,
 | 
			
		||||
        name="download_thumb",
 | 
			
		||||
    ),
 | 
			
		||||
    path("picture/<int:picture_id>/download/thumb/", send_thumb, name="download_thumb"),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										27
									
								
								sas/views.py
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								sas/views.py
									
									
									
									
									
								
							@@ -115,19 +115,18 @@ class AlbumUploadView(CanViewMixin, DetailView, FormMixin):
 | 
			
		||||
        self.form = self.get_form()
 | 
			
		||||
        parent = SithFile.objects.filter(id=self.object.id).first()
 | 
			
		||||
        files = request.FILES.getlist("images")
 | 
			
		||||
        if request.user.is_authenticated and request.user.is_subscribed:
 | 
			
		||||
        if request.user.is_subscribed and self.form.is_valid():
 | 
			
		||||
            self.form.process(
 | 
			
		||||
                parent=parent,
 | 
			
		||||
                owner=request.user,
 | 
			
		||||
                files=files,
 | 
			
		||||
                automodere=(
 | 
			
		||||
                    request.user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID)
 | 
			
		||||
                    or request.user.is_root
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
            if self.form.is_valid():
 | 
			
		||||
                self.form.process(
 | 
			
		||||
                    parent=parent,
 | 
			
		||||
                    owner=request.user,
 | 
			
		||||
                    files=files,
 | 
			
		||||
                    automodere=(
 | 
			
		||||
                        request.user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID)
 | 
			
		||||
                        or request.user.is_root
 | 
			
		||||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
                if self.form.is_valid():
 | 
			
		||||
                    return HttpResponse(str(self.form.errors), status=200)
 | 
			
		||||
                return HttpResponse(str(self.form.errors), status=200)
 | 
			
		||||
        return HttpResponse(str(self.form.errors), status=500)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -146,7 +145,7 @@ class AlbumView(CanViewMixin, DetailView, FormMixin):
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
 | 
			
		||||
@@ -155,7 +154,7 @@ class AlbumView(CanViewMixin, DetailView, FormMixin):
 | 
			
		||||
        if not self.object.file:
 | 
			
		||||
            self.object.generate_thumbnail()
 | 
			
		||||
        self.form = self.get_form()
 | 
			
		||||
        if "clipboard" not in request.session.keys():
 | 
			
		||||
        if "clipboard" not in request.session:
 | 
			
		||||
            request.session["clipboard"] = []
 | 
			
		||||
        if request.user.can_edit(self.object):  # Handle the copy-paste functions
 | 
			
		||||
            FileView.handle_clipboard(request, self.object)
 | 
			
		||||
 
 | 
			
		||||
@@ -345,8 +345,8 @@ SITH_LAUNDERETTE_MANAGER = {
 | 
			
		||||
# Main root for club pages
 | 
			
		||||
SITH_CLUB_ROOT_PAGE = "clubs"
 | 
			
		||||
 | 
			
		||||
# Define the date in the year serving as reference for the subscriptions calendar
 | 
			
		||||
# (month, day)
 | 
			
		||||
# Define the date in the year serving as
 | 
			
		||||
# reference for the subscriptions calendar (month, day)
 | 
			
		||||
SITH_SEMESTER_START_AUTUMN = (8, 15)  # 15 August
 | 
			
		||||
SITH_SEMESTER_START_SPRING = (2, 15)  # 15 February
 | 
			
		||||
 | 
			
		||||
@@ -509,10 +509,12 @@ SITH_ACCOUNT_INACTIVITY_DELTA = relativedelta(years=2)
 | 
			
		||||
SITH_ACCOUNT_DUMP_DELTA = timedelta(days=30)
 | 
			
		||||
"""timedelta between the warning mail and the actual account dump"""
 | 
			
		||||
 | 
			
		||||
# Defines which product type is the refilling type, and thus increases the account amount
 | 
			
		||||
# Defines which product type is the refilling type,
 | 
			
		||||
# and thus increases the account amount
 | 
			
		||||
SITH_COUNTER_PRODUCTTYPE_REFILLING = 3
 | 
			
		||||
 | 
			
		||||
# Defines which product is the one year subscription and which one is the six month subscription
 | 
			
		||||
# Defines which product is the one year subscription
 | 
			
		||||
# and which one is the six month subscription
 | 
			
		||||
SITH_PRODUCT_SUBSCRIPTION_ONE_SEMESTER = 1
 | 
			
		||||
SITH_PRODUCT_SUBSCRIPTION_TWO_SEMESTERS = 2
 | 
			
		||||
SITH_PRODUCTTYPE_SUBSCRIPTION = 2
 | 
			
		||||
@@ -700,15 +702,15 @@ TOXIC_DOMAINS_PROVIDERS = [
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
try:
 | 
			
		||||
    from .settings_custom import *
 | 
			
		||||
    from .settings_custom import *  # noqa F403 (this star-import is actually useful)
 | 
			
		||||
 | 
			
		||||
    logging.getLogger("django").info("Custom settings imported")
 | 
			
		||||
except:
 | 
			
		||||
except ImportError:
 | 
			
		||||
    logging.getLogger("django").warning("Custom settings failed")
 | 
			
		||||
 | 
			
		||||
if DEBUG:
 | 
			
		||||
    INSTALLED_APPS += ("debug_toolbar",)
 | 
			
		||||
    MIDDLEWARE = ("debug_toolbar.middleware.DebugToolbarMiddleware",) + MIDDLEWARE
 | 
			
		||||
    MIDDLEWARE = ("debug_toolbar.middleware.DebugToolbarMiddleware", *MIDDLEWARE)
 | 
			
		||||
    DEBUG_TOOLBAR_PANELS = [
 | 
			
		||||
        "debug_toolbar.panels.versions.VersionsPanel",
 | 
			
		||||
        "debug_toolbar.panels.timer.TimerPanel",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								sith/urls.py
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								sith/urls.py
									
									
									
									
									
								
							@@ -71,20 +71,20 @@ if settings.DEBUG:
 | 
			
		||||
 | 
			
		||||
    urlpatterns += [path("__debug__/", include(debug_toolbar.urls))]
 | 
			
		||||
 | 
			
		||||
if settings.SENTRY_ENV == "development":
 | 
			
		||||
if settings.SENTRY_ENV == "development" and settings.SENTRY_DSN:
 | 
			
		||||
    """Sentry debug endpoint
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    This function always crash and allows us to test
 | 
			
		||||
    the sentry configuration and the modal popup 
 | 
			
		||||
    displayed to users on production
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    The error will be displayed on Sentry
 | 
			
		||||
    inside the "development" environment
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    NOTE : you need to specify the SENTRY_DSN setting in settings_custom.py
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def raise_exception(request):
 | 
			
		||||
        division_by_zero = 1 / 0
 | 
			
		||||
        _division_by_zero = 1 / 0
 | 
			
		||||
 | 
			
		||||
    urlpatterns += [path("sentry-debug/", raise_exception)]
 | 
			
		||||
 
 | 
			
		||||
@@ -14,9 +14,12 @@ IGNORE_PATTERNS = [
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# We override the original staticfiles app according to https://docs.djangoproject.com/en/4.2/ref/contrib/staticfiles/#customizing-the-ignored-pattern-list
 | 
			
		||||
# However, this is buggy and requires us to have an exact naming of the class like this to be detected
 | 
			
		||||
# Also, it requires to create all commands in management/commands again or they don't get detected by django
 | 
			
		||||
# We override the original staticfiles app according to
 | 
			
		||||
# https://docs.djangoproject.com/en/4.2/ref/contrib/staticfiles/#customizing-the-ignored-pattern-list
 | 
			
		||||
# However, this is buggy and requires us
 | 
			
		||||
# to have an exact naming of the class like this to be detected
 | 
			
		||||
# Also, it requires to create all commands in management/commands again
 | 
			
		||||
# or they don't get detected by django
 | 
			
		||||
# Workaround originates from https://stackoverflow.com/a/78724835/12640533
 | 
			
		||||
class StaticFilesConfig(StaticFilesConfig):
 | 
			
		||||
    """
 | 
			
		||||
 
 | 
			
		||||
@@ -28,10 +28,10 @@ class Command(CollectStatic):
 | 
			
		||||
    def collect_scss(self) -> list[Scss.CompileArg]:
 | 
			
		||||
        files: list[Scss.CompileArg] = []
 | 
			
		||||
        for finder in get_finders():
 | 
			
		||||
            for path, storage in finder.list(
 | 
			
		||||
            for path_str, storage in finder.list(
 | 
			
		||||
                set(self.ignore_patterns) - set(IGNORE_PATTERNS_SCSS)
 | 
			
		||||
            ):
 | 
			
		||||
                path = Path(path)
 | 
			
		||||
                path = Path(path_str)
 | 
			
		||||
                if path.suffix != ".scss":
 | 
			
		||||
                    continue
 | 
			
		||||
                files.append(
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ from staticfiles.processors import OpenApi, Webpack
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(Runserver):
 | 
			
		||||
    """Light wrapper around the statics runserver that integrates webpack auto bundling"""
 | 
			
		||||
    """Light wrapper around default runserver that integrates webpack auto bundling."""
 | 
			
		||||
 | 
			
		||||
    def run(self, **options):
 | 
			
		||||
        # OpenApi generation needs to be before webpack
 | 
			
		||||
 
 | 
			
		||||
@@ -83,7 +83,7 @@ class OpenApi:
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def compile(cls):
 | 
			
		||||
        """Compile a typescript client for the sith API. Only generates it if it changed"""
 | 
			
		||||
        """Compile a TS client for the sith API. Only generates it if it changed."""
 | 
			
		||||
        logging.getLogger("django").info("Compiling open api typescript client")
 | 
			
		||||
        out = cls.OPENAPI_DIR / "schema.json"
 | 
			
		||||
        cls.OPENAPI_DIR.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
@@ -110,4 +110,4 @@ class OpenApi:
 | 
			
		||||
        with open(out, "w") as f:
 | 
			
		||||
            _ = f.write(schema)
 | 
			
		||||
 | 
			
		||||
        subprocess.run(["npx", "openapi-ts"]).check_returncode()
 | 
			
		||||
        subprocess.run(["npx", "openapi-ts"], check=True)
 | 
			
		||||
 
 | 
			
		||||
@@ -125,7 +125,10 @@ class Migration(migrations.Migration):
 | 
			
		||||
                    "minimal_quantity",
 | 
			
		||||
                    models.IntegerField(
 | 
			
		||||
                        verbose_name="minimal quantity",
 | 
			
		||||
                        help_text="if the effective quantity is less than the minimal, item is added to the shopping list",
 | 
			
		||||
                        help_text=(
 | 
			
		||||
                            "if the effective quantity is less than the minimal, "
 | 
			
		||||
                            "item is added to the shopping list"
 | 
			
		||||
                        ),
 | 
			
		||||
                        default=1,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ from core.utils import get_start_of_semester
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_type(value):
 | 
			
		||||
    if value not in settings.SITH_SUBSCRIPTIONS.keys():
 | 
			
		||||
    if value not in settings.SITH_SUBSCRIPTIONS:
 | 
			
		||||
        raise ValidationError(_("Bad subscription type"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -107,7 +107,9 @@ class Subscription(models.Model):
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def compute_start(d: date = None, duration: int = 1, user: User = None) -> date:
 | 
			
		||||
    def compute_start(
 | 
			
		||||
        d: date | None = None, duration: int = 1, user: User | None = None
 | 
			
		||||
    ) -> date:
 | 
			
		||||
        """Computes the start date of the subscription.
 | 
			
		||||
 | 
			
		||||
        The computation is done with respect to the given date (default is today)
 | 
			
		||||
@@ -129,7 +131,9 @@ class Subscription(models.Model):
 | 
			
		||||
        return get_start_of_semester(d)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def compute_end(duration: int, start: date = None, user: User = None) -> date:
 | 
			
		||||
    def compute_end(
 | 
			
		||||
        duration: int, start: date | None = None, user: User | None = None
 | 
			
		||||
    ) -> date:
 | 
			
		||||
        """Compute the end date of the subscription.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@
 | 
			
		||||
 | 
			
		||||
from django.urls import path
 | 
			
		||||
 | 
			
		||||
from subscription.views import *
 | 
			
		||||
from subscription.views import NewSubscription, SubscriptionsStatsView
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    # Subscription views
 | 
			
		||||
 
 | 
			
		||||
@@ -94,11 +94,13 @@ class SubscriptionForm(forms.ModelForm):
 | 
			
		||||
            self.errors.pop("email", None)
 | 
			
		||||
            self.errors.pop("date_of_birth", None)
 | 
			
		||||
        if cleaned_data.get("member") is None:
 | 
			
		||||
            # This should be handled here, but it is done in the Subscription model's clean method
 | 
			
		||||
            # This should be handled here,
 | 
			
		||||
            # but it is done in the Subscription model's clean method
 | 
			
		||||
            # TODO investigate why!
 | 
			
		||||
            raise ValidationError(
 | 
			
		||||
                _(
 | 
			
		||||
                    "You must either choose an existing user or create a new one properly"
 | 
			
		||||
                    "You must either choose an existing "
 | 
			
		||||
                    "user or create a new one properly"
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        return cleaned_data
 | 
			
		||||
@@ -114,7 +116,7 @@ class NewSubscription(CreateView):
 | 
			
		||||
        raise PermissionDenied
 | 
			
		||||
 | 
			
		||||
    def get_initial(self):
 | 
			
		||||
        if "member" in self.request.GET.keys():
 | 
			
		||||
        if "member" in self.request.GET:
 | 
			
		||||
            return {
 | 
			
		||||
                "member": self.request.GET["member"],
 | 
			
		||||
                "subscription_type": "deux-semestres",
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,12 @@ class Migration(migrations.Migration):
 | 
			
		||||
                    "subscription_deadline",
 | 
			
		||||
                    models.DateField(
 | 
			
		||||
                        default=datetime.date.today,
 | 
			
		||||
                        help_text="Before this date, users are allowed to subscribe to this Trombi. After this date, users subscribed will be allowed to comment on each other.",
 | 
			
		||||
                        help_text=(
 | 
			
		||||
                            "Before this date, users are allowed "
 | 
			
		||||
                            "to subscribe to this Trombi. "
 | 
			
		||||
                            "After this date, users subscribed will "
 | 
			
		||||
                            "be allowed to comment on each other."
 | 
			
		||||
                        ),
 | 
			
		||||
                        verbose_name="subscription deadline",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
@@ -38,7 +43,10 @@ class Migration(migrations.Migration):
 | 
			
		||||
                    "comments_deadline",
 | 
			
		||||
                    models.DateField(
 | 
			
		||||
                        default=datetime.date.today,
 | 
			
		||||
                        help_text="After this date, users won't be able to make comments anymore.",
 | 
			
		||||
                        help_text=(
 | 
			
		||||
                            "After this date, users won't be able "
 | 
			
		||||
                            "to make comments anymore."
 | 
			
		||||
                        ),
 | 
			
		||||
                        verbose_name="comments deadline",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
@@ -92,7 +100,10 @@ class Migration(migrations.Migration):
 | 
			
		||||
                    models.ImageField(
 | 
			
		||||
                        upload_to="trombi",
 | 
			
		||||
                        blank=True,
 | 
			
		||||
                        help_text="The profile picture you want in the trombi (warning: this picture may be published)",
 | 
			
		||||
                        help_text=(
 | 
			
		||||
                            "The profile picture you want in the trombi "
 | 
			
		||||
                            "(warning: this picture may be published)"
 | 
			
		||||
                        ),
 | 
			
		||||
                        verbose_name="profile pict",
 | 
			
		||||
                        null=True,
 | 
			
		||||
                    ),
 | 
			
		||||
@@ -102,7 +113,10 @@ class Migration(migrations.Migration):
 | 
			
		||||
                    models.ImageField(
 | 
			
		||||
                        upload_to="trombi",
 | 
			
		||||
                        blank=True,
 | 
			
		||||
                        help_text="The scrub picture you want in the trombi (warning: this picture may be published)",
 | 
			
		||||
                        help_text=(
 | 
			
		||||
                            "The scrub picture you want in the trombi "
 | 
			
		||||
                            "(warning: this picture may be published)"
 | 
			
		||||
                        ),
 | 
			
		||||
                        verbose_name="scrub pict",
 | 
			
		||||
                        null=True,
 | 
			
		||||
                    ),
 | 
			
		||||
 
 | 
			
		||||
@@ -55,9 +55,9 @@ class Trombi(models.Model):
 | 
			
		||||
        _("subscription deadline"),
 | 
			
		||||
        default=date.today,
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            "Before this date, users are "
 | 
			
		||||
            "allowed to subscribe to this Trombi. "
 | 
			
		||||
            "After this date, users subscribed will be allowed to comment on each other."
 | 
			
		||||
            "Before this date, users are allowed to subscribe to this Trombi. "
 | 
			
		||||
            "After this date, users subscribed will"
 | 
			
		||||
            " be allowed to comment on each other."
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
    comments_deadline = models.DateField(
 | 
			
		||||
@@ -131,7 +131,8 @@ class TrombiUser(models.Model):
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            "The profile picture you want in the trombi (warning: this picture may be published)"
 | 
			
		||||
            "The profile picture you want in the trombi "
 | 
			
		||||
            "(warning: this picture may be published)"
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
    scrub_pict = models.ImageField(
 | 
			
		||||
@@ -140,7 +141,8 @@ class TrombiUser(models.Model):
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            "The scrub picture you want in the trombi (warning: this picture may be published)"
 | 
			
		||||
            "The scrub picture you want in the trombi "
 | 
			
		||||
            "(warning: this picture may be published)"
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@@ -158,10 +160,7 @@ class TrombiUser(models.Model):
 | 
			
		||||
            role = str(settings.SITH_CLUB_ROLES[m.role])
 | 
			
		||||
            if m.description:
 | 
			
		||||
                role += " (%s)" % m.description
 | 
			
		||||
            if m.end_date:
 | 
			
		||||
                end_date = get_semester_code(m.end_date)
 | 
			
		||||
            else:
 | 
			
		||||
                end_date = ""
 | 
			
		||||
            end_date = get_semester_code(m.end_date) if m.end_date else ""
 | 
			
		||||
            TrombiClubMembership(
 | 
			
		||||
                user=self,
 | 
			
		||||
                club=str(m.club),
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,25 @@
 | 
			
		||||
 | 
			
		||||
from django.urls import path
 | 
			
		||||
 | 
			
		||||
from trombi.views import *
 | 
			
		||||
from trombi.views import (
 | 
			
		||||
    TrombiCommentCreateView,
 | 
			
		||||
    TrombiCommentEditView,
 | 
			
		||||
    TrombiCreateView,
 | 
			
		||||
    TrombiDeleteUserView,
 | 
			
		||||
    TrombiDetailView,
 | 
			
		||||
    TrombiEditView,
 | 
			
		||||
    TrombiExportView,
 | 
			
		||||
    TrombiModerateCommentsView,
 | 
			
		||||
    TrombiModerateCommentView,
 | 
			
		||||
    UserTrombiAddMembershipView,
 | 
			
		||||
    UserTrombiDeleteMembershipView,
 | 
			
		||||
    UserTrombiEditMembershipView,
 | 
			
		||||
    UserTrombiEditPicturesView,
 | 
			
		||||
    UserTrombiEditProfileView,
 | 
			
		||||
    UserTrombiProfileView,
 | 
			
		||||
    UserTrombiResetClubMembershipsView,
 | 
			
		||||
    UserTrombiToolsView,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path("<int:club_id>/new/", TrombiCreateView.as_view(), name="create"),
 | 
			
		||||
@@ -41,9 +59,7 @@ urlpatterns = [
 | 
			
		||||
        name="moderate_comment",
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        "user/<int:user_id>/delete/",
 | 
			
		||||
        TrombiDeleteUserView.as_view(),
 | 
			
		||||
        name="delete_user",
 | 
			
		||||
        "user/<int:user_id>/delete/", TrombiDeleteUserView.as_view(), name="delete_user"
 | 
			
		||||
    ),
 | 
			
		||||
    path("<int:trombi_id>/", TrombiDetailView.as_view(), name="detail"),
 | 
			
		||||
    path(
 | 
			
		||||
@@ -52,9 +68,7 @@ urlpatterns = [
 | 
			
		||||
        name="new_comment",
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        "<int:user_id>/profile/",
 | 
			
		||||
        UserTrombiProfileView.as_view(),
 | 
			
		||||
        name="user_profile",
 | 
			
		||||
        "<int:user_id>/profile/", UserTrombiProfileView.as_view(), name="user_profile"
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        "comment/<int:comment_id>/edit/",
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,7 @@ from django import forms
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib.auth.mixins import LoginRequiredMixin
 | 
			
		||||
from django.core.exceptions import PermissionDenied
 | 
			
		||||
from django.db import IntegrityError
 | 
			
		||||
from django.forms.models import modelform_factory
 | 
			
		||||
from django.http import Http404, HttpResponseRedirect
 | 
			
		||||
from django.shortcuts import get_object_or_404, redirect
 | 
			
		||||
@@ -75,7 +76,10 @@ class TrombiTabsMixin(TabedViewMixin):
 | 
			
		||||
                    "name": _("My pictures"),
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
        try:
 | 
			
		||||
        if (
 | 
			
		||||
            hasattr(self.request.user, "trombi_user")
 | 
			
		||||
            and self.request.user.trombi_user.trombi
 | 
			
		||||
        ):
 | 
			
		||||
            trombi = self.request.user.trombi_user.trombi
 | 
			
		||||
            if self.request.user.is_owner(trombi):
 | 
			
		||||
                tab_list.append(
 | 
			
		||||
@@ -87,8 +91,6 @@ class TrombiTabsMixin(TabedViewMixin):
 | 
			
		||||
                        "name": _("Admin tools"),
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
        except:
 | 
			
		||||
            pass
 | 
			
		||||
        return tab_list
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -163,7 +165,7 @@ class TrombiDetailView(CanEditMixin, QuickNotifMixin, TrombiTabsMixin, DetailVie
 | 
			
		||||
            try:
 | 
			
		||||
                TrombiUser(user=form.cleaned_data["user"], trombi=self.object).save()
 | 
			
		||||
                self.quick_notif_list.append("qn_success")
 | 
			
		||||
            except:  # We don't care about duplicate keys
 | 
			
		||||
            except IntegrityError:  # We don't care about duplicate keys
 | 
			
		||||
                self.quick_notif_list.append("qn_fail")
 | 
			
		||||
        return super().get(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
@@ -239,12 +241,12 @@ class TrombiModerateCommentView(DetailView):
 | 
			
		||||
                )
 | 
			
		||||
            elif request.POST["action"] == "reject":
 | 
			
		||||
                return super().get(request, *args, **kwargs)
 | 
			
		||||
            elif request.POST["action"] == "delete" and "reason" in request.POST.keys():
 | 
			
		||||
            elif request.POST["action"] == "delete" and "reason" in request.POST:
 | 
			
		||||
                self.object.author.user.email_user(
 | 
			
		||||
                    subject="[%s] %s" % (settings.SITH_NAME, _("Rejected comment")),
 | 
			
		||||
                    message=_(
 | 
			
		||||
                        'Your comment to %(target)s on the Trombi "%(trombi)s" was rejected for the following '
 | 
			
		||||
                        "reason: %(reason)s\n\n"
 | 
			
		||||
                        'Your comment to %(target)s on the Trombi "%(trombi)s" '
 | 
			
		||||
                        "was rejected for the following reason: %(reason)s\n\n"
 | 
			
		||||
                        "Your comment was:\n\n%(content)s"
 | 
			
		||||
                    )
 | 
			
		||||
                    % {
 | 
			
		||||
@@ -498,7 +500,7 @@ class TrombiCommentFormView(LoginRequiredMixin, View):
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        kwargs = super().get_context_data(**kwargs)
 | 
			
		||||
        if "user_id" in self.kwargs.keys():
 | 
			
		||||
        if "user_id" in self.kwargs:
 | 
			
		||||
            kwargs["target"] = get_object_or_404(TrombiUser, id=self.kwargs["user_id"])
 | 
			
		||||
        else:
 | 
			
		||||
            kwargs["target"] = self.object.target
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user