mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-11-04 02:53:06 +00:00 
			
		
		
		
	use google convention for docstrings
This commit is contained in:
		@@ -29,9 +29,7 @@ from core.models import SithFile, User
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CurrencyField(models.DecimalField):
 | 
					class CurrencyField(models.DecimalField):
 | 
				
			||||||
    """
 | 
					    """Custom database field used for currency."""
 | 
				
			||||||
    This is a custom database field used for currency
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
        kwargs["max_digits"] = 12
 | 
					        kwargs["max_digits"] = 12
 | 
				
			||||||
@@ -71,30 +69,22 @@ class Company(models.Model):
 | 
				
			|||||||
        return self.name
 | 
					        return self.name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_owned_by(self, user):
 | 
					    def is_owned_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Check if that object can be edited by the given user."""
 | 
				
			||||||
        Method to see if that object can be edited by the given user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
 | 
					        if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
 | 
				
			||||||
            return True
 | 
					            return True
 | 
				
			||||||
        return False
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def can_be_edited_by(self, user):
 | 
					    def can_be_edited_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Check if that object can be edited by the given user."""
 | 
				
			||||||
        Method to see if that object can be edited by the given user
 | 
					        return user.memberships.filter(
 | 
				
			||||||
        """
 | 
					            end_date=None, club__role=settings.SITH_CLUB_ROLES_ID["Treasurer"]
 | 
				
			||||||
        for club in user.memberships.filter(end_date=None).all():
 | 
					        ).exists()
 | 
				
			||||||
            if club and club.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]:
 | 
					 | 
				
			||||||
                return True
 | 
					 | 
				
			||||||
        return False
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def can_be_viewed_by(self, user):
 | 
					    def can_be_viewed_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Check if that object can be viewed by the given user."""
 | 
				
			||||||
        Method to see if that object can be viewed by the given user
 | 
					        return user.memberships.filter(
 | 
				
			||||||
        """
 | 
					            end_date=None, club__role_gte=settings.SITH_CLUB_ROLES_ID["Treasurer"]
 | 
				
			||||||
        for club in user.memberships.filter(end_date=None).all():
 | 
					        ).exists()
 | 
				
			||||||
            if club and club.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
 | 
					 | 
				
			||||||
                return True
 | 
					 | 
				
			||||||
        return False
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BankAccount(models.Model):
 | 
					class BankAccount(models.Model):
 | 
				
			||||||
@@ -119,9 +109,7 @@ class BankAccount(models.Model):
 | 
				
			|||||||
        return reverse("accounting:bank_details", kwargs={"b_account_id": self.id})
 | 
					        return reverse("accounting:bank_details", kwargs={"b_account_id": self.id})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_owned_by(self, user):
 | 
					    def is_owned_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Check if that object can be edited by the given user."""
 | 
				
			||||||
        Method to see if that object can be edited by the given user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if user.is_anonymous:
 | 
					        if user.is_anonymous:
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
        if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
 | 
					        if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
 | 
				
			||||||
@@ -158,9 +146,7 @@ class ClubAccount(models.Model):
 | 
				
			|||||||
        return reverse("accounting:club_details", kwargs={"c_account_id": self.id})
 | 
					        return reverse("accounting:club_details", kwargs={"c_account_id": self.id})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_owned_by(self, user):
 | 
					    def is_owned_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Check if that object can be edited by the given user."""
 | 
				
			||||||
        Method to see if that object can be edited by the given user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if user.is_anonymous:
 | 
					        if user.is_anonymous:
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
        if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
 | 
					        if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
 | 
				
			||||||
@@ -168,18 +154,14 @@ class ClubAccount(models.Model):
 | 
				
			|||||||
        return False
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def can_be_edited_by(self, user):
 | 
					    def can_be_edited_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Check if that object can be edited by the given user."""
 | 
				
			||||||
        Method to see if that object can be edited by the given user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        m = self.club.get_membership_for(user)
 | 
					        m = self.club.get_membership_for(user)
 | 
				
			||||||
        if m and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]:
 | 
					        if m and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]:
 | 
				
			||||||
            return True
 | 
					            return True
 | 
				
			||||||
        return False
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def can_be_viewed_by(self, user):
 | 
					    def can_be_viewed_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Check if that object can be viewed by the given user."""
 | 
				
			||||||
        Method to see if that object can be viewed by the given user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        m = self.club.get_membership_for(user)
 | 
					        m = self.club.get_membership_for(user)
 | 
				
			||||||
        if m and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
 | 
					        if m and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
 | 
				
			||||||
            return True
 | 
					            return True
 | 
				
			||||||
@@ -202,9 +184,7 @@ class ClubAccount(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GeneralJournal(models.Model):
 | 
					class GeneralJournal(models.Model):
 | 
				
			||||||
    """
 | 
					    """Class storing all the operations for a period of time."""
 | 
				
			||||||
    Class storing all the operations for a period of time
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    start_date = models.DateField(_("start date"))
 | 
					    start_date = models.DateField(_("start date"))
 | 
				
			||||||
    end_date = models.DateField(_("end date"), null=True, blank=True, default=None)
 | 
					    end_date = models.DateField(_("end date"), null=True, blank=True, default=None)
 | 
				
			||||||
@@ -231,9 +211,7 @@ class GeneralJournal(models.Model):
 | 
				
			|||||||
        return reverse("accounting:journal_details", kwargs={"j_id": self.id})
 | 
					        return reverse("accounting:journal_details", kwargs={"j_id": self.id})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_owned_by(self, user):
 | 
					    def is_owned_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Check if that object can be edited by the given user."""
 | 
				
			||||||
        Method to see if that object can be edited by the given user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if user.is_anonymous:
 | 
					        if user.is_anonymous:
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
        if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
 | 
					        if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
 | 
				
			||||||
@@ -243,9 +221,7 @@ class GeneralJournal(models.Model):
 | 
				
			|||||||
        return False
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def can_be_edited_by(self, user):
 | 
					    def can_be_edited_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Check if that object can be edited by the given user."""
 | 
				
			||||||
        Method to see if that object can be edited by the given user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
 | 
					        if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
 | 
				
			||||||
            return True
 | 
					            return True
 | 
				
			||||||
        if self.club_account.can_be_edited_by(user):
 | 
					        if self.club_account.can_be_edited_by(user):
 | 
				
			||||||
@@ -271,9 +247,7 @@ class GeneralJournal(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Operation(models.Model):
 | 
					class Operation(models.Model):
 | 
				
			||||||
    """
 | 
					    """An operation is a line in the journal, a debit or a credit."""
 | 
				
			||||||
    An operation is a line in the journal, a debit or a credit
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    number = models.IntegerField(_("number"))
 | 
					    number = models.IntegerField(_("number"))
 | 
				
			||||||
    journal = models.ForeignKey(
 | 
					    journal = models.ForeignKey(
 | 
				
			||||||
@@ -422,9 +396,7 @@ class Operation(models.Model):
 | 
				
			|||||||
        return tar
 | 
					        return tar
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_owned_by(self, user):
 | 
					    def is_owned_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Check if that object can be edited by the given user."""
 | 
				
			||||||
        Method to see if that object can be edited by the given user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if user.is_anonymous:
 | 
					        if user.is_anonymous:
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
        if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
 | 
					        if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
 | 
				
			||||||
@@ -437,9 +409,7 @@ class Operation(models.Model):
 | 
				
			|||||||
        return False
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def can_be_edited_by(self, user):
 | 
					    def can_be_edited_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Check if that object can be edited by the given user."""
 | 
				
			||||||
        Method to see if that object can be edited by the given user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
 | 
					        if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
 | 
				
			||||||
            return True
 | 
					            return True
 | 
				
			||||||
        if self.journal.closed:
 | 
					        if self.journal.closed:
 | 
				
			||||||
@@ -451,10 +421,9 @@ class Operation(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AccountingType(models.Model):
 | 
					class AccountingType(models.Model):
 | 
				
			||||||
    """
 | 
					    """Accounting types.
 | 
				
			||||||
    Class describing the accounting types.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Thoses are numbers used in accounting to classify operations
 | 
					    Those are numbers used in accounting to classify operations
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    code = models.CharField(
 | 
					    code = models.CharField(
 | 
				
			||||||
@@ -488,9 +457,7 @@ class AccountingType(models.Model):
 | 
				
			|||||||
        return reverse("accounting:type_list")
 | 
					        return reverse("accounting:type_list")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_owned_by(self, user):
 | 
					    def is_owned_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Check if that object can be edited by the given user."""
 | 
				
			||||||
        Method to see if that object can be edited by the given user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if user.is_anonymous:
 | 
					        if user.is_anonymous:
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
        if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
 | 
					        if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
 | 
				
			||||||
@@ -499,9 +466,7 @@ class AccountingType(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SimplifiedAccountingType(models.Model):
 | 
					class SimplifiedAccountingType(models.Model):
 | 
				
			||||||
    """
 | 
					    """Simplified version of `AccountingType`."""
 | 
				
			||||||
    Class describing the simplified accounting types.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    label = models.CharField(_("label"), max_length=128)
 | 
					    label = models.CharField(_("label"), max_length=128)
 | 
				
			||||||
    accounting_type = models.ForeignKey(
 | 
					    accounting_type = models.ForeignKey(
 | 
				
			||||||
@@ -533,7 +498,7 @@ class SimplifiedAccountingType(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Label(models.Model):
 | 
					class Label(models.Model):
 | 
				
			||||||
    """Label allow a club to sort its operations"""
 | 
					    """Label allow a club to sort its operations."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    name = models.CharField(_("label"), max_length=64)
 | 
					    name = models.CharField(_("label"), max_length=64)
 | 
				
			||||||
    club_account = models.ForeignKey(
 | 
					    club_account = models.ForeignKey(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -53,9 +53,7 @@ from counter.models import Counter, Product, Selling
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BankAccountListView(CanViewMixin, ListView):
 | 
					class BankAccountListView(CanViewMixin, ListView):
 | 
				
			||||||
    """
 | 
					    """A list view for the admins."""
 | 
				
			||||||
    A list view for the admins
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = BankAccount
 | 
					    model = BankAccount
 | 
				
			||||||
    template_name = "accounting/bank_account_list.jinja"
 | 
					    template_name = "accounting/bank_account_list.jinja"
 | 
				
			||||||
@@ -66,18 +64,14 @@ class BankAccountListView(CanViewMixin, ListView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SimplifiedAccountingTypeListView(CanViewMixin, ListView):
 | 
					class SimplifiedAccountingTypeListView(CanViewMixin, ListView):
 | 
				
			||||||
    """
 | 
					    """A list view for the admins."""
 | 
				
			||||||
    A list view for the admins
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = SimplifiedAccountingType
 | 
					    model = SimplifiedAccountingType
 | 
				
			||||||
    template_name = "accounting/simplifiedaccountingtype_list.jinja"
 | 
					    template_name = "accounting/simplifiedaccountingtype_list.jinja"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SimplifiedAccountingTypeEditView(CanViewMixin, UpdateView):
 | 
					class SimplifiedAccountingTypeEditView(CanViewMixin, UpdateView):
 | 
				
			||||||
    """
 | 
					    """An edit view for the admins."""
 | 
				
			||||||
    An edit view for the admins
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = SimplifiedAccountingType
 | 
					    model = SimplifiedAccountingType
 | 
				
			||||||
    pk_url_kwarg = "type_id"
 | 
					    pk_url_kwarg = "type_id"
 | 
				
			||||||
@@ -86,9 +80,7 @@ class SimplifiedAccountingTypeEditView(CanViewMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SimplifiedAccountingTypeCreateView(CanCreateMixin, CreateView):
 | 
					class SimplifiedAccountingTypeCreateView(CanCreateMixin, CreateView):
 | 
				
			||||||
    """
 | 
					    """Create an accounting type (for the admins)."""
 | 
				
			||||||
    Create an accounting type (for the admins)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = SimplifiedAccountingType
 | 
					    model = SimplifiedAccountingType
 | 
				
			||||||
    fields = ["label", "accounting_type"]
 | 
					    fields = ["label", "accounting_type"]
 | 
				
			||||||
@@ -99,18 +91,14 @@ class SimplifiedAccountingTypeCreateView(CanCreateMixin, CreateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AccountingTypeListView(CanViewMixin, ListView):
 | 
					class AccountingTypeListView(CanViewMixin, ListView):
 | 
				
			||||||
    """
 | 
					    """A list view for the admins."""
 | 
				
			||||||
    A list view for the admins
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = AccountingType
 | 
					    model = AccountingType
 | 
				
			||||||
    template_name = "accounting/accountingtype_list.jinja"
 | 
					    template_name = "accounting/accountingtype_list.jinja"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AccountingTypeEditView(CanViewMixin, UpdateView):
 | 
					class AccountingTypeEditView(CanViewMixin, UpdateView):
 | 
				
			||||||
    """
 | 
					    """An edit view for the admins."""
 | 
				
			||||||
    An edit view for the admins
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = AccountingType
 | 
					    model = AccountingType
 | 
				
			||||||
    pk_url_kwarg = "type_id"
 | 
					    pk_url_kwarg = "type_id"
 | 
				
			||||||
@@ -119,9 +107,7 @@ class AccountingTypeEditView(CanViewMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AccountingTypeCreateView(CanCreateMixin, CreateView):
 | 
					class AccountingTypeCreateView(CanCreateMixin, CreateView):
 | 
				
			||||||
    """
 | 
					    """Create an accounting type (for the admins)."""
 | 
				
			||||||
    Create an accounting type (for the admins)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = AccountingType
 | 
					    model = AccountingType
 | 
				
			||||||
    fields = ["code", "label", "movement_type"]
 | 
					    fields = ["code", "label", "movement_type"]
 | 
				
			||||||
@@ -132,9 +118,7 @@ class AccountingTypeCreateView(CanCreateMixin, CreateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BankAccountEditView(CanViewMixin, UpdateView):
 | 
					class BankAccountEditView(CanViewMixin, UpdateView):
 | 
				
			||||||
    """
 | 
					    """An edit view for the admins."""
 | 
				
			||||||
    An edit view for the admins
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = BankAccount
 | 
					    model = BankAccount
 | 
				
			||||||
    pk_url_kwarg = "b_account_id"
 | 
					    pk_url_kwarg = "b_account_id"
 | 
				
			||||||
@@ -143,9 +127,7 @@ class BankAccountEditView(CanViewMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BankAccountDetailView(CanViewMixin, DetailView):
 | 
					class BankAccountDetailView(CanViewMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """A detail view, listing every club account."""
 | 
				
			||||||
    A detail view, listing every club account
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = BankAccount
 | 
					    model = BankAccount
 | 
				
			||||||
    pk_url_kwarg = "b_account_id"
 | 
					    pk_url_kwarg = "b_account_id"
 | 
				
			||||||
@@ -153,9 +135,7 @@ class BankAccountDetailView(CanViewMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BankAccountCreateView(CanCreateMixin, CreateView):
 | 
					class BankAccountCreateView(CanCreateMixin, CreateView):
 | 
				
			||||||
    """
 | 
					    """Create a bank account (for the admins)."""
 | 
				
			||||||
    Create a bank account (for the admins)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = BankAccount
 | 
					    model = BankAccount
 | 
				
			||||||
    fields = ["name", "club", "iban", "number"]
 | 
					    fields = ["name", "club", "iban", "number"]
 | 
				
			||||||
@@ -165,9 +145,7 @@ class BankAccountCreateView(CanCreateMixin, CreateView):
 | 
				
			|||||||
class BankAccountDeleteView(
 | 
					class BankAccountDeleteView(
 | 
				
			||||||
    CanEditPropMixin, DeleteView
 | 
					    CanEditPropMixin, DeleteView
 | 
				
			||||||
):  # TODO change Delete to Close
 | 
					):  # TODO change Delete to Close
 | 
				
			||||||
    """
 | 
					    """Delete a bank account (for the admins)."""
 | 
				
			||||||
    Delete a bank account (for the admins)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = BankAccount
 | 
					    model = BankAccount
 | 
				
			||||||
    pk_url_kwarg = "b_account_id"
 | 
					    pk_url_kwarg = "b_account_id"
 | 
				
			||||||
@@ -179,9 +157,7 @@ class BankAccountDeleteView(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClubAccountEditView(CanViewMixin, UpdateView):
 | 
					class ClubAccountEditView(CanViewMixin, UpdateView):
 | 
				
			||||||
    """
 | 
					    """An edit view for the admins."""
 | 
				
			||||||
    An edit view for the admins
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = ClubAccount
 | 
					    model = ClubAccount
 | 
				
			||||||
    pk_url_kwarg = "c_account_id"
 | 
					    pk_url_kwarg = "c_account_id"
 | 
				
			||||||
@@ -190,9 +166,7 @@ class ClubAccountEditView(CanViewMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClubAccountDetailView(CanViewMixin, DetailView):
 | 
					class ClubAccountDetailView(CanViewMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """A detail view, listing every journal."""
 | 
				
			||||||
    A detail view, listing every journal
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = ClubAccount
 | 
					    model = ClubAccount
 | 
				
			||||||
    pk_url_kwarg = "c_account_id"
 | 
					    pk_url_kwarg = "c_account_id"
 | 
				
			||||||
@@ -200,9 +174,7 @@ class ClubAccountDetailView(CanViewMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClubAccountCreateView(CanCreateMixin, CreateView):
 | 
					class ClubAccountCreateView(CanCreateMixin, CreateView):
 | 
				
			||||||
    """
 | 
					    """Create a club account (for the admins)."""
 | 
				
			||||||
    Create a club account (for the admins)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = ClubAccount
 | 
					    model = ClubAccount
 | 
				
			||||||
    fields = ["name", "club", "bank_account"]
 | 
					    fields = ["name", "club", "bank_account"]
 | 
				
			||||||
@@ -220,9 +192,7 @@ class ClubAccountCreateView(CanCreateMixin, CreateView):
 | 
				
			|||||||
class ClubAccountDeleteView(
 | 
					class ClubAccountDeleteView(
 | 
				
			||||||
    CanEditPropMixin, DeleteView
 | 
					    CanEditPropMixin, DeleteView
 | 
				
			||||||
):  # TODO change Delete to Close
 | 
					):  # TODO change Delete to Close
 | 
				
			||||||
    """
 | 
					    """Delete a club account (for the admins)."""
 | 
				
			||||||
    Delete a club account (for the admins)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = ClubAccount
 | 
					    model = ClubAccount
 | 
				
			||||||
    pk_url_kwarg = "c_account_id"
 | 
					    pk_url_kwarg = "c_account_id"
 | 
				
			||||||
@@ -282,9 +252,7 @@ class JournalTabsMixin(TabedViewMixin):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class JournalCreateView(CanCreateMixin, CreateView):
 | 
					class JournalCreateView(CanCreateMixin, CreateView):
 | 
				
			||||||
    """
 | 
					    """Create a general journal."""
 | 
				
			||||||
    Create a general journal
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = GeneralJournal
 | 
					    model = GeneralJournal
 | 
				
			||||||
    form_class = modelform_factory(
 | 
					    form_class = modelform_factory(
 | 
				
			||||||
@@ -304,9 +272,7 @@ class JournalCreateView(CanCreateMixin, CreateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class JournalDetailView(JournalTabsMixin, CanViewMixin, DetailView):
 | 
					class JournalDetailView(JournalTabsMixin, CanViewMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """A detail view, listing every operation."""
 | 
				
			||||||
    A detail view, listing every operation
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = GeneralJournal
 | 
					    model = GeneralJournal
 | 
				
			||||||
    pk_url_kwarg = "j_id"
 | 
					    pk_url_kwarg = "j_id"
 | 
				
			||||||
@@ -315,9 +281,7 @@ class JournalDetailView(JournalTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class JournalEditView(CanEditMixin, UpdateView):
 | 
					class JournalEditView(CanEditMixin, UpdateView):
 | 
				
			||||||
    """
 | 
					    """Update a general journal."""
 | 
				
			||||||
    Update a general journal
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = GeneralJournal
 | 
					    model = GeneralJournal
 | 
				
			||||||
    pk_url_kwarg = "j_id"
 | 
					    pk_url_kwarg = "j_id"
 | 
				
			||||||
@@ -326,9 +290,7 @@ class JournalEditView(CanEditMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class JournalDeleteView(CanEditPropMixin, DeleteView):
 | 
					class JournalDeleteView(CanEditPropMixin, DeleteView):
 | 
				
			||||||
    """
 | 
					    """Delete a club account (for the admins)."""
 | 
				
			||||||
    Delete a club account (for the admins)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = GeneralJournal
 | 
					    model = GeneralJournal
 | 
				
			||||||
    pk_url_kwarg = "j_id"
 | 
					    pk_url_kwarg = "j_id"
 | 
				
			||||||
@@ -467,9 +429,7 @@ class OperationForm(forms.ModelForm):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class OperationCreateView(CanCreateMixin, CreateView):
 | 
					class OperationCreateView(CanCreateMixin, CreateView):
 | 
				
			||||||
    """
 | 
					    """Create an operation."""
 | 
				
			||||||
    Create an operation
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Operation
 | 
					    model = Operation
 | 
				
			||||||
    form_class = OperationForm
 | 
					    form_class = OperationForm
 | 
				
			||||||
@@ -487,7 +447,7 @@ class OperationCreateView(CanCreateMixin, CreateView):
 | 
				
			|||||||
        return ret
 | 
					        return ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """Add journal to the context"""
 | 
					        """Add journal to the context."""
 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        if self.journal:
 | 
					        if self.journal:
 | 
				
			||||||
            kwargs["object"] = self.journal
 | 
					            kwargs["object"] = self.journal
 | 
				
			||||||
@@ -495,9 +455,7 @@ class OperationCreateView(CanCreateMixin, CreateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class OperationEditView(CanEditMixin, UpdateView):
 | 
					class OperationEditView(CanEditMixin, UpdateView):
 | 
				
			||||||
    """
 | 
					    """An edit view, working as detail for the moment."""
 | 
				
			||||||
    An edit view, working as detail for the moment
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Operation
 | 
					    model = Operation
 | 
				
			||||||
    pk_url_kwarg = "op_id"
 | 
					    pk_url_kwarg = "op_id"
 | 
				
			||||||
@@ -505,16 +463,14 @@ class OperationEditView(CanEditMixin, UpdateView):
 | 
				
			|||||||
    template_name = "accounting/operation_edit.jinja"
 | 
					    template_name = "accounting/operation_edit.jinja"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """Add journal to the context"""
 | 
					        """Add journal to the context."""
 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        kwargs["object"] = self.object.journal
 | 
					        kwargs["object"] = self.object.journal
 | 
				
			||||||
        return kwargs
 | 
					        return kwargs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class OperationPDFView(CanViewMixin, DetailView):
 | 
					class OperationPDFView(CanViewMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Display the PDF of a given operation."""
 | 
				
			||||||
    Display the PDF of a given operation
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Operation
 | 
					    model = Operation
 | 
				
			||||||
    pk_url_kwarg = "op_id"
 | 
					    pk_url_kwarg = "op_id"
 | 
				
			||||||
@@ -666,9 +622,7 @@ class OperationPDFView(CanViewMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
 | 
					class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Display a statement sorted by labels."""
 | 
				
			||||||
    Display a statement sorted by labels
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = GeneralJournal
 | 
					    model = GeneralJournal
 | 
				
			||||||
    pk_url_kwarg = "j_id"
 | 
					    pk_url_kwarg = "j_id"
 | 
				
			||||||
@@ -726,16 +680,14 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
        return statement
 | 
					        return statement
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """Add infos to the context"""
 | 
					        """Add infos to the context."""
 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        kwargs["statement"] = self.big_statement()
 | 
					        kwargs["statement"] = self.big_statement()
 | 
				
			||||||
        return kwargs
 | 
					        return kwargs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
 | 
					class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Calculate a dictionary with operation target and sum of operations."""
 | 
				
			||||||
    Calculate a dictionary with operation target and sum of operations
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = GeneralJournal
 | 
					    model = GeneralJournal
 | 
				
			||||||
    pk_url_kwarg = "j_id"
 | 
					    pk_url_kwarg = "j_id"
 | 
				
			||||||
@@ -765,7 +717,7 @@ class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
        return sum(self.statement(movement_type).values())
 | 
					        return sum(self.statement(movement_type).values())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """Add journal to the context"""
 | 
					        """Add journal to the context."""
 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        kwargs["credit_statement"] = self.statement("CREDIT")
 | 
					        kwargs["credit_statement"] = self.statement("CREDIT")
 | 
				
			||||||
        kwargs["debit_statement"] = self.statement("DEBIT")
 | 
					        kwargs["debit_statement"] = self.statement("DEBIT")
 | 
				
			||||||
@@ -775,9 +727,7 @@ class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView):
 | 
					class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Calculate a dictionary with operation type and sum of operations."""
 | 
				
			||||||
    Calculate a dictionary with operation type and sum of operations
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = GeneralJournal
 | 
					    model = GeneralJournal
 | 
				
			||||||
    pk_url_kwarg = "j_id"
 | 
					    pk_url_kwarg = "j_id"
 | 
				
			||||||
@@ -795,7 +745,7 @@ class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView)
 | 
				
			|||||||
        return statement
 | 
					        return statement
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """Add journal to the context"""
 | 
					        """Add journal to the context."""
 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        kwargs["statement"] = self.statement()
 | 
					        kwargs["statement"] = self.statement()
 | 
				
			||||||
        return kwargs
 | 
					        return kwargs
 | 
				
			||||||
@@ -810,9 +760,7 @@ class CompanyListView(CanViewMixin, ListView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CompanyCreateView(CanCreateMixin, CreateView):
 | 
					class CompanyCreateView(CanCreateMixin, CreateView):
 | 
				
			||||||
    """
 | 
					    """Create a company."""
 | 
				
			||||||
    Create a company
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Company
 | 
					    model = Company
 | 
				
			||||||
    fields = ["name"]
 | 
					    fields = ["name"]
 | 
				
			||||||
@@ -821,9 +769,7 @@ class CompanyCreateView(CanCreateMixin, CreateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CompanyEditView(CanCreateMixin, UpdateView):
 | 
					class CompanyEditView(CanCreateMixin, UpdateView):
 | 
				
			||||||
    """
 | 
					    """Edit a company."""
 | 
				
			||||||
    Edit a company
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Company
 | 
					    model = Company
 | 
				
			||||||
    pk_url_kwarg = "co_id"
 | 
					    pk_url_kwarg = "co_id"
 | 
				
			||||||
@@ -882,9 +828,7 @@ class CloseCustomerAccountForm(forms.Form):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RefoundAccountView(FormView):
 | 
					class RefoundAccountView(FormView):
 | 
				
			||||||
    """
 | 
					    """Create a selling with the same amount than the current user money."""
 | 
				
			||||||
    Create a selling with the same amount than the current user money
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    template_name = "accounting/refound_account.jinja"
 | 
					    template_name = "accounting/refound_account.jinja"
 | 
				
			||||||
    form_class = CloseCustomerAccountForm
 | 
					    form_class = CloseCustomerAccountForm
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,9 +23,9 @@ from core.views import can_edit, can_view
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def check_if(obj, user, test):
 | 
					def check_if(obj, user, test):
 | 
				
			||||||
    """
 | 
					    """Detect if it's a single object or a queryset.
 | 
				
			||||||
    Detect if it's a single object or a queryset
 | 
					
 | 
				
			||||||
    aply a given test on individual object and return global permission
 | 
					    Apply a given test on individual object and return global permission.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    if isinstance(obj, QuerySet):
 | 
					    if isinstance(obj, QuerySet):
 | 
				
			||||||
        for o in obj:
 | 
					        for o in obj:
 | 
				
			||||||
@@ -39,9 +39,7 @@ def check_if(obj, user, test):
 | 
				
			|||||||
class ManageModelMixin:
 | 
					class ManageModelMixin:
 | 
				
			||||||
    @action(detail=True)
 | 
					    @action(detail=True)
 | 
				
			||||||
    def id(self, request, pk=None):
 | 
					    def id(self, request, pk=None):
 | 
				
			||||||
        """
 | 
					        """Get by id (api/v1/router/{pk}/id/)."""
 | 
				
			||||||
        Get by id (api/v1/router/{pk}/id/)
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.queryset = get_object_or_404(self.queryset.filter(id=pk))
 | 
					        self.queryset = get_object_or_404(self.queryset.filter(id=pk))
 | 
				
			||||||
        serializer = self.get_serializer(self.queryset)
 | 
					        serializer = self.get_serializer(self.queryset)
 | 
				
			||||||
        return Response(serializer.data)
 | 
					        return Response(serializer.data)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,9 +23,7 @@ from core.templatetags.renderer import markdown
 | 
				
			|||||||
@api_view(["POST"])
 | 
					@api_view(["POST"])
 | 
				
			||||||
@renderer_classes((StaticHTMLRenderer,))
 | 
					@renderer_classes((StaticHTMLRenderer,))
 | 
				
			||||||
def RenderMarkdown(request):
 | 
					def RenderMarkdown(request):
 | 
				
			||||||
    """
 | 
					    """Render Markdown."""
 | 
				
			||||||
    Render Markdown
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        data = markdown(request.POST["text"])
 | 
					        data = markdown(request.POST["text"])
 | 
				
			||||||
    except:
 | 
					    except:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,9 +31,7 @@ class ClubSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClubViewSet(RightModelViewSet):
 | 
					class ClubViewSet(RightModelViewSet):
 | 
				
			||||||
    """
 | 
					    """Manage Clubs (api/v1/club/)."""
 | 
				
			||||||
    Manage Clubs (api/v1/club/)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    serializer_class = ClubSerializer
 | 
					    serializer_class = ClubSerializer
 | 
				
			||||||
    queryset = Club.objects.all()
 | 
					    queryset = Club.objects.all()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -33,18 +33,14 @@ class CounterSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CounterViewSet(RightModelViewSet):
 | 
					class CounterViewSet(RightModelViewSet):
 | 
				
			||||||
    """
 | 
					    """Manage Counters (api/v1/counter/)."""
 | 
				
			||||||
    Manage Counters (api/v1/counter/)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    serializer_class = CounterSerializer
 | 
					    serializer_class = CounterSerializer
 | 
				
			||||||
    queryset = Counter.objects.all()
 | 
					    queryset = Counter.objects.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @action(detail=False)
 | 
					    @action(detail=False)
 | 
				
			||||||
    def bar(self, request):
 | 
					    def bar(self, request):
 | 
				
			||||||
        """
 | 
					        """Return all bars (api/v1/counter/bar/)."""
 | 
				
			||||||
        Return all bars (api/v1/counter/bar/)
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.queryset = self.queryset.filter(type="BAR")
 | 
					        self.queryset = self.queryset.filter(type="BAR")
 | 
				
			||||||
        serializer = self.get_serializer(self.queryset, many=True)
 | 
					        serializer = self.get_serializer(self.queryset, many=True)
 | 
				
			||||||
        return Response(serializer.data)
 | 
					        return Response(serializer.data)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,9 +25,7 @@ class GroupSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GroupViewSet(RightModelViewSet):
 | 
					class GroupViewSet(RightModelViewSet):
 | 
				
			||||||
    """
 | 
					    """Manage Groups (api/v1/group/)."""
 | 
				
			||||||
    Manage Groups (api/v1/group/)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    serializer_class = GroupSerializer
 | 
					    serializer_class = GroupSerializer
 | 
				
			||||||
    queryset = RealGroup.objects.all()
 | 
					    queryset = RealGroup.objects.all()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -60,54 +60,42 @@ class LaunderetteTokenSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LaunderettePlaceViewSet(RightModelViewSet):
 | 
					class LaunderettePlaceViewSet(RightModelViewSet):
 | 
				
			||||||
    """
 | 
					    """Manage Launderette (api/v1/launderette/place/)."""
 | 
				
			||||||
    Manage Launderette (api/v1/launderette/place/)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    serializer_class = LaunderettePlaceSerializer
 | 
					    serializer_class = LaunderettePlaceSerializer
 | 
				
			||||||
    queryset = Launderette.objects.all()
 | 
					    queryset = Launderette.objects.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LaunderetteMachineViewSet(RightModelViewSet):
 | 
					class LaunderetteMachineViewSet(RightModelViewSet):
 | 
				
			||||||
    """
 | 
					    """Manage Washing Machines (api/v1/launderette/machine/)."""
 | 
				
			||||||
    Manage Washing Machines (api/v1/launderette/machine/)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    serializer_class = LaunderetteMachineSerializer
 | 
					    serializer_class = LaunderetteMachineSerializer
 | 
				
			||||||
    queryset = Machine.objects.all()
 | 
					    queryset = Machine.objects.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LaunderetteTokenViewSet(RightModelViewSet):
 | 
					class LaunderetteTokenViewSet(RightModelViewSet):
 | 
				
			||||||
    """
 | 
					    """Manage Launderette's tokens (api/v1/launderette/token/)."""
 | 
				
			||||||
    Manage Launderette's tokens (api/v1/launderette/token/)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    serializer_class = LaunderetteTokenSerializer
 | 
					    serializer_class = LaunderetteTokenSerializer
 | 
				
			||||||
    queryset = Token.objects.all()
 | 
					    queryset = Token.objects.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @action(detail=False)
 | 
					    @action(detail=False)
 | 
				
			||||||
    def washing(self, request):
 | 
					    def washing(self, request):
 | 
				
			||||||
        """
 | 
					        """Return all washing tokens (api/v1/launderette/token/washing)."""
 | 
				
			||||||
        Return all washing tokens (api/v1/launderette/token/washing)
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.queryset = self.queryset.filter(type="WASHING")
 | 
					        self.queryset = self.queryset.filter(type="WASHING")
 | 
				
			||||||
        serializer = self.get_serializer(self.queryset, many=True)
 | 
					        serializer = self.get_serializer(self.queryset, many=True)
 | 
				
			||||||
        return Response(serializer.data)
 | 
					        return Response(serializer.data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @action(detail=False)
 | 
					    @action(detail=False)
 | 
				
			||||||
    def drying(self, request):
 | 
					    def drying(self, request):
 | 
				
			||||||
        """
 | 
					        """Return all drying tokens (api/v1/launderette/token/drying)."""
 | 
				
			||||||
        Return all drying tokens (api/v1/launderette/token/drying)
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.queryset = self.queryset.filter(type="DRYING")
 | 
					        self.queryset = self.queryset.filter(type="DRYING")
 | 
				
			||||||
        serializer = self.get_serializer(self.queryset, many=True)
 | 
					        serializer = self.get_serializer(self.queryset, many=True)
 | 
				
			||||||
        return Response(serializer.data)
 | 
					        return Response(serializer.data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @action(detail=False)
 | 
					    @action(detail=False)
 | 
				
			||||||
    def avaliable(self, request):
 | 
					    def avaliable(self, request):
 | 
				
			||||||
        """
 | 
					        """Return all avaliable tokens (api/v1/launderette/token/avaliable)."""
 | 
				
			||||||
        Return all avaliable tokens (api/v1/launderette/token/avaliable)
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.queryset = self.queryset.filter(
 | 
					        self.queryset = self.queryset.filter(
 | 
				
			||||||
            borrow_date__isnull=True, user__isnull=True
 | 
					            borrow_date__isnull=True, user__isnull=True
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
@@ -116,9 +104,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @action(detail=False)
 | 
					    @action(detail=False)
 | 
				
			||||||
    def unavaliable(self, request):
 | 
					    def unavaliable(self, request):
 | 
				
			||||||
        """
 | 
					        """Return all unavaliable tokens (api/v1/launderette/token/unavaliable)."""
 | 
				
			||||||
        Return all unavaliable tokens (api/v1/launderette/token/unavaliable)
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.queryset = self.queryset.filter(
 | 
					        self.queryset = self.queryset.filter(
 | 
				
			||||||
            borrow_date__isnull=False, user__isnull=False
 | 
					            borrow_date__isnull=False, user__isnull=False
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -39,9 +39,9 @@ class UserSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserViewSet(RightModelViewSet):
 | 
					class UserViewSet(RightModelViewSet):
 | 
				
			||||||
    """
 | 
					    """Manage Users (api/v1/user/).
 | 
				
			||||||
    Manage Users (api/v1/user/)
 | 
					
 | 
				
			||||||
    Only show active users
 | 
					    Only show active users.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    serializer_class = UserSerializer
 | 
					    serializer_class = UserSerializer
 | 
				
			||||||
@@ -49,9 +49,7 @@ class UserViewSet(RightModelViewSet):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @action(detail=False)
 | 
					    @action(detail=False)
 | 
				
			||||||
    def birthday(self, request):
 | 
					    def birthday(self, request):
 | 
				
			||||||
        """
 | 
					        """Return all users born today (api/v1/user/birstdays)."""
 | 
				
			||||||
        Return all users born today (api/v1/user/birstdays)
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        date = datetime.datetime.today()
 | 
					        date = datetime.datetime.today()
 | 
				
			||||||
        self.queryset = self.queryset.filter(date_of_birth=date)
 | 
					        self.queryset = self.queryset.filter(date_of_birth=date)
 | 
				
			||||||
        serializer = self.get_serializer(self.queryset, many=True)
 | 
					        serializer = self.get_serializer(self.queryset, many=True)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,10 +28,10 @@ def uv_endpoint(request):
 | 
				
			|||||||
    return Response(make_clean_uv(short_uv, full_uv))
 | 
					    return Response(make_clean_uv(short_uv, full_uv))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def find_uv(lang, year, code):
 | 
					def find_uv(lang: str, year: int | str, code: str) -> tuple[dict | None, dict | None]:
 | 
				
			||||||
    """
 | 
					    """Uses the UTBM API to find an UV.
 | 
				
			||||||
    Uses the UTBM API to find an UV.
 | 
					
 | 
				
			||||||
    short_uv is the UV entry in the UV list. It is returned as it contains
 | 
					    Short_uv is the UV entry in the UV list. It is returned as it contains
 | 
				
			||||||
    information which are not in full_uv.
 | 
					    information which are not in full_uv.
 | 
				
			||||||
    full_uv is the detailed representation of an UV.
 | 
					    full_uv is the detailed representation of an UV.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
@@ -44,7 +44,7 @@ def find_uv(lang, year, code):
 | 
				
			|||||||
        # find the first UV which matches the code
 | 
					        # find the first UV which matches the code
 | 
				
			||||||
        short_uv = next(uv for uv in uvs if uv["code"] == code)
 | 
					        short_uv = next(uv for uv in uvs if uv["code"] == code)
 | 
				
			||||||
    except StopIteration:
 | 
					    except StopIteration:
 | 
				
			||||||
        return (None, None)
 | 
					        return None, None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # get detailed information about the UV
 | 
					    # get detailed information about the UV
 | 
				
			||||||
    uv_url = settings.SITH_PEDAGOGY_UTBM_API + "/uv/{}/{}/{}/{}".format(
 | 
					    uv_url = settings.SITH_PEDAGOGY_UTBM_API + "/uv/{}/{}/{}/{}".format(
 | 
				
			||||||
@@ -53,13 +53,11 @@ def find_uv(lang, year, code):
 | 
				
			|||||||
    response = urllib.request.urlopen(uv_url)
 | 
					    response = urllib.request.urlopen(uv_url)
 | 
				
			||||||
    full_uv = json.loads(response.read().decode("utf-8"))
 | 
					    full_uv = json.loads(response.read().decode("utf-8"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (short_uv, full_uv)
 | 
					    return short_uv, full_uv
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def make_clean_uv(short_uv, full_uv):
 | 
					def make_clean_uv(short_uv: dict, full_uv: dict):
 | 
				
			||||||
    """
 | 
					    """Cleans the data up so that it corresponds to our data representation."""
 | 
				
			||||||
    Cleans the data up so that it corresponds to our data representation.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    res = {}
 | 
					    res = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res["credit_type"] = short_uv["codeCategorie"]
 | 
					    res["credit_type"] = short_uv["codeCategorie"]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -44,9 +44,7 @@ class ClubEditForm(forms.ModelForm):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MailingForm(forms.Form):
 | 
					class MailingForm(forms.Form):
 | 
				
			||||||
    """
 | 
					    """Form handling mailing lists right."""
 | 
				
			||||||
    Form handling mailing lists right
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ACTION_NEW_MAILING = 1
 | 
					    ACTION_NEW_MAILING = 1
 | 
				
			||||||
    ACTION_NEW_SUBSCRIPTION = 2
 | 
					    ACTION_NEW_SUBSCRIPTION = 2
 | 
				
			||||||
@@ -105,16 +103,12 @@ class MailingForm(forms.Form):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def check_required(self, cleaned_data, field):
 | 
					    def check_required(self, cleaned_data, field):
 | 
				
			||||||
        """
 | 
					        """If the given field doesn't exist or has no value, add a required error on it."""
 | 
				
			||||||
        If the given field doesn't exist or has no value, add a required error on it
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if not cleaned_data.get(field, None):
 | 
					        if not cleaned_data.get(field, None):
 | 
				
			||||||
            self.add_error(field, _("This field is required"))
 | 
					            self.add_error(field, _("This field is required"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def clean_subscription_users(self):
 | 
					    def clean_subscription_users(self):
 | 
				
			||||||
        """
 | 
					        """Convert given users into real users and check their validity."""
 | 
				
			||||||
        Convert given users into real users and check their validity
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        cleaned_data = super().clean()
 | 
					        cleaned_data = super().clean()
 | 
				
			||||||
        users = []
 | 
					        users = []
 | 
				
			||||||
        for user in cleaned_data["subscription_users"]:
 | 
					        for user in cleaned_data["subscription_users"]:
 | 
				
			||||||
@@ -177,9 +171,7 @@ class SellingsForm(forms.Form):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClubMemberForm(forms.Form):
 | 
					class ClubMemberForm(forms.Form):
 | 
				
			||||||
    """
 | 
					    """Form handling the members of a club."""
 | 
				
			||||||
    Form handling the members of a club
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    error_css_class = "error"
 | 
					    error_css_class = "error"
 | 
				
			||||||
    required_css_class = "required"
 | 
					    required_css_class = "required"
 | 
				
			||||||
@@ -236,9 +228,9 @@ class ClubMemberForm(forms.Form):
 | 
				
			|||||||
            self.fields.pop("start_date")
 | 
					            self.fields.pop("start_date")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def clean_users(self):
 | 
					    def clean_users(self):
 | 
				
			||||||
        """
 | 
					        """Check that the user is not trying to add an user already in the club.
 | 
				
			||||||
        Check that the user is not trying to add an user already in the club
 | 
					
 | 
				
			||||||
        Also check that the user is valid and has a valid subscription
 | 
					        Also check that the user is valid and has a valid subscription.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        cleaned_data = super().clean()
 | 
					        cleaned_data = super().clean()
 | 
				
			||||||
        users = []
 | 
					        users = []
 | 
				
			||||||
@@ -260,9 +252,7 @@ class ClubMemberForm(forms.Form):
 | 
				
			|||||||
        return users
 | 
					        return users
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def clean(self):
 | 
					    def clean(self):
 | 
				
			||||||
        """
 | 
					        """Check user rights for adding an user."""
 | 
				
			||||||
        Check user rights for adding an user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        cleaned_data = super().clean()
 | 
					        cleaned_data = super().clean()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if "start_date" in cleaned_data and not cleaned_data["start_date"]:
 | 
					        if "start_date" in cleaned_data and not cleaned_data["start_date"]:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,7 +21,7 @@
 | 
				
			|||||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
 | 
					# Place - Suite 330, Boston, MA 02111-1307, USA.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
from typing import Optional
 | 
					from __future__ import annotations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.core import validators
 | 
					from django.core import validators
 | 
				
			||||||
@@ -46,9 +46,7 @@ def get_default_owner_group():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Club(models.Model):
 | 
					class Club(models.Model):
 | 
				
			||||||
    """
 | 
					    """The Club class, made as a tree to allow nice tidy organization."""
 | 
				
			||||||
    The Club class, made as a tree to allow nice tidy organization
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    id = models.AutoField(primary_key=True, db_index=True)
 | 
					    id = models.AutoField(primary_key=True, db_index=True)
 | 
				
			||||||
    name = models.CharField(_("name"), max_length=64)
 | 
					    name = models.CharField(_("name"), max_length=64)
 | 
				
			||||||
@@ -141,7 +139,7 @@ class Club(models.Model):
 | 
				
			|||||||
        ).first()
 | 
					        ).first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def check_loop(self):
 | 
					    def check_loop(self):
 | 
				
			||||||
        """Raise a validation error when a loop is found within the parent list"""
 | 
					        """Raise a validation error when a loop is found within the parent list."""
 | 
				
			||||||
        objs = []
 | 
					        objs = []
 | 
				
			||||||
        cur = self
 | 
					        cur = self
 | 
				
			||||||
        while cur.parent is not None:
 | 
					        while cur.parent is not None:
 | 
				
			||||||
@@ -223,9 +221,7 @@ class Club(models.Model):
 | 
				
			|||||||
        return self.name
 | 
					        return self.name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_owned_by(self, user):
 | 
					    def is_owned_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Method to see if that object can be super edited by the given user."""
 | 
				
			||||||
        Method to see if that object can be super edited by the given user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if user.is_anonymous:
 | 
					        if user.is_anonymous:
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
        return user.is_board_member
 | 
					        return user.is_board_member
 | 
				
			||||||
@@ -234,24 +230,21 @@ class Club(models.Model):
 | 
				
			|||||||
        return "https://%s%s" % (settings.SITH_URL, self.logo.url)
 | 
					        return "https://%s%s" % (settings.SITH_URL, self.logo.url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def can_be_edited_by(self, user):
 | 
					    def can_be_edited_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Method to see if that object can be edited by the given user."""
 | 
				
			||||||
        Method to see if that object can be edited by the given user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return self.has_rights_in_club(user)
 | 
					        return self.has_rights_in_club(user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def can_be_viewed_by(self, user):
 | 
					    def can_be_viewed_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Method to see if that object can be seen by the given user."""
 | 
				
			||||||
        Method to see if that object can be seen by the given user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        sub = User.objects.filter(pk=user.pk).first()
 | 
					        sub = User.objects.filter(pk=user.pk).first()
 | 
				
			||||||
        if sub is None:
 | 
					        if sub is None:
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
        return sub.was_subscribed
 | 
					        return sub.was_subscribed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_membership_for(self, user: User) -> Optional["Membership"]:
 | 
					    def get_membership_for(self, user: User) -> Membership | None:
 | 
				
			||||||
        """
 | 
					        """Return the current membership the given user.
 | 
				
			||||||
        Return the current membership the given user.
 | 
					
 | 
				
			||||||
        The result is cached.
 | 
					        Note:
 | 
				
			||||||
 | 
					            The result is cached.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if user.is_anonymous:
 | 
					        if user.is_anonymous:
 | 
				
			||||||
            return None
 | 
					            return None
 | 
				
			||||||
@@ -273,15 +266,12 @@ class Club(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class MembershipQuerySet(models.QuerySet):
 | 
					class MembershipQuerySet(models.QuerySet):
 | 
				
			||||||
    def ongoing(self) -> "MembershipQuerySet":
 | 
					    def ongoing(self) -> "MembershipQuerySet":
 | 
				
			||||||
        """
 | 
					        """Filter all memberships which are not finished yet."""
 | 
				
			||||||
        Filter all memberships which are not finished yet
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        # noinspection PyTypeChecker
 | 
					        # noinspection PyTypeChecker
 | 
				
			||||||
        return self.filter(Q(end_date=None) | Q(end_date__gte=timezone.now()))
 | 
					        return self.filter(Q(end_date=None) | Q(end_date__gte=timezone.now()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def board(self) -> "MembershipQuerySet":
 | 
					    def board(self) -> "MembershipQuerySet":
 | 
				
			||||||
        """
 | 
					        """Filter all memberships where the user is/was in the board.
 | 
				
			||||||
        Filter all memberships where the user is/was in the board.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Be aware that users who were in the board in the past
 | 
					        Be aware that users who were in the board in the past
 | 
				
			||||||
        are included, even if there are no more members.
 | 
					        are included, even if there are no more members.
 | 
				
			||||||
@@ -293,9 +283,9 @@ class MembershipQuerySet(models.QuerySet):
 | 
				
			|||||||
        return self.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE)
 | 
					        return self.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def update(self, **kwargs):
 | 
					    def update(self, **kwargs):
 | 
				
			||||||
        """
 | 
					        """Refresh the cache for the elements of the queryset.
 | 
				
			||||||
        Work just like the default Django's update() method,
 | 
					
 | 
				
			||||||
        but add a cache refresh for the elements of the queryset.
 | 
					        Besides that, does the same job as a regular update method.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Be aware that this adds a db query to retrieve the updated objects
 | 
					        Be aware that this adds a db query to retrieve the updated objects
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@@ -315,8 +305,7 @@ class MembershipQuerySet(models.QuerySet):
 | 
				
			|||||||
                    )
 | 
					                    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete(self):
 | 
					    def delete(self):
 | 
				
			||||||
        """
 | 
					        """Work just like the default Django's delete() method,
 | 
				
			||||||
        Work just like the default Django's delete() method,
 | 
					 | 
				
			||||||
        but add a cache invalidation for the elements of the queryset
 | 
					        but add a cache invalidation for the elements of the queryset
 | 
				
			||||||
        before the deletion.
 | 
					        before the deletion.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -332,8 +321,7 @@ class MembershipQuerySet(models.QuerySet):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Membership(models.Model):
 | 
					class Membership(models.Model):
 | 
				
			||||||
    """
 | 
					    """The Membership class makes the connection between User and Clubs.
 | 
				
			||||||
    The Membership class makes the connection between User and Clubs
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Both Users and Clubs can have many Membership objects:
 | 
					    Both Users and Clubs can have many Membership objects:
 | 
				
			||||||
       - a user can be a member of many clubs at a time
 | 
					       - a user can be a member of many clubs at a time
 | 
				
			||||||
@@ -390,17 +378,13 @@ class Membership(models.Model):
 | 
				
			|||||||
        return reverse("club:club_members", kwargs={"club_id": self.club_id})
 | 
					        return reverse("club:club_members", kwargs={"club_id": self.club_id})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_owned_by(self, user):
 | 
					    def is_owned_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Method to see if that object can be super edited by the given user."""
 | 
				
			||||||
        Method to see if that object can be super edited by the given user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if user.is_anonymous:
 | 
					        if user.is_anonymous:
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
        return user.is_board_member
 | 
					        return user.is_board_member
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def can_be_edited_by(self, user: User) -> bool:
 | 
					    def can_be_edited_by(self, user: User) -> bool:
 | 
				
			||||||
        """
 | 
					        """Check if that object can be edited by the given user."""
 | 
				
			||||||
        Check if that object can be edited by the given user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if user.is_root or user.is_board_member:
 | 
					        if user.is_root or user.is_board_member:
 | 
				
			||||||
            return True
 | 
					            return True
 | 
				
			||||||
        membership = self.club.get_membership_for(user)
 | 
					        membership = self.club.get_membership_for(user)
 | 
				
			||||||
@@ -414,9 +398,10 @@ class Membership(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Mailing(models.Model):
 | 
					class Mailing(models.Model):
 | 
				
			||||||
    """
 | 
					    """A Mailing list for a club.
 | 
				
			||||||
    This class correspond to a mailing list
 | 
					
 | 
				
			||||||
    Remember that mailing lists should be validated by UTBM
 | 
					    Warning:
 | 
				
			||||||
 | 
					        Remember that mailing lists should be validated by UTBM.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    club = models.ForeignKey(
 | 
					    club = models.ForeignKey(
 | 
				
			||||||
@@ -508,9 +493,7 @@ class Mailing(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MailingSubscription(models.Model):
 | 
					class MailingSubscription(models.Model):
 | 
				
			||||||
    """
 | 
					    """Link between user and mailing list."""
 | 
				
			||||||
    This class makes the link between user and mailing list
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    mailing = models.ForeignKey(
 | 
					    mailing = models.ForeignKey(
 | 
				
			||||||
        Mailing,
 | 
					        Mailing,
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										126
									
								
								club/tests.py
									
									
									
									
									
								
							
							
						
						
									
										126
									
								
								club/tests.py
									
									
									
									
									
								
							@@ -29,8 +29,8 @@ from sith.settings import SITH_BAR_MANAGER, SITH_MAIN_CLUB_ID
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClubTest(TestCase):
 | 
					class ClubTest(TestCase):
 | 
				
			||||||
    """
 | 
					    """Set up data for test cases related to clubs and membership.
 | 
				
			||||||
    Set up data for test cases related to clubs and membership
 | 
					
 | 
				
			||||||
    The generated dataset is the one created by the populate command,
 | 
					    The generated dataset is the one created by the populate command,
 | 
				
			||||||
    plus the following modifications :
 | 
					    plus the following modifications :
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -94,8 +94,7 @@ class ClubTest(TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class MembershipQuerySetTest(ClubTest):
 | 
					class MembershipQuerySetTest(ClubTest):
 | 
				
			||||||
    def test_ongoing(self):
 | 
					    def test_ongoing(self):
 | 
				
			||||||
        """
 | 
					        """Test that the ongoing queryset method returns the memberships that
 | 
				
			||||||
        Test that the ongoing queryset method returns the memberships that
 | 
					 | 
				
			||||||
        are not ended.
 | 
					        are not ended.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        current_members = list(self.club.members.ongoing().order_by("id"))
 | 
					        current_members = list(self.club.members.ongoing().order_by("id"))
 | 
				
			||||||
@@ -108,9 +107,8 @@ class MembershipQuerySetTest(ClubTest):
 | 
				
			|||||||
        assert current_members == expected
 | 
					        assert current_members == expected
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_board(self):
 | 
					    def test_board(self):
 | 
				
			||||||
        """
 | 
					        """Test that the board queryset method returns the memberships
 | 
				
			||||||
        Test that the board queryset method returns the memberships
 | 
					        of user in the club board.
 | 
				
			||||||
        of user in the club board
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        board_members = list(self.club.members.board().order_by("id"))
 | 
					        board_members = list(self.club.members.board().order_by("id"))
 | 
				
			||||||
        expected = [
 | 
					        expected = [
 | 
				
			||||||
@@ -123,9 +121,8 @@ class MembershipQuerySetTest(ClubTest):
 | 
				
			|||||||
        assert board_members == expected
 | 
					        assert board_members == expected
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_ongoing_board(self):
 | 
					    def test_ongoing_board(self):
 | 
				
			||||||
        """
 | 
					        """Test that combining ongoing and board returns users
 | 
				
			||||||
        Test that combining ongoing and board returns users
 | 
					        who are currently board members of the club.
 | 
				
			||||||
        who are currently board members of the club
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        members = list(self.club.members.ongoing().board().order_by("id"))
 | 
					        members = list(self.club.members.ongoing().board().order_by("id"))
 | 
				
			||||||
        expected = [
 | 
					        expected = [
 | 
				
			||||||
@@ -136,9 +133,7 @@ class MembershipQuerySetTest(ClubTest):
 | 
				
			|||||||
        assert members == expected
 | 
					        assert members == expected
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_update_invalidate_cache(self):
 | 
					    def test_update_invalidate_cache(self):
 | 
				
			||||||
        """
 | 
					        """Test that the `update` queryset method properly invalidate cache."""
 | 
				
			||||||
        Test that the `update` queryset method properly invalidate cache
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        mem_skia = self.skia.memberships.get(club=self.club)
 | 
					        mem_skia = self.skia.memberships.get(club=self.club)
 | 
				
			||||||
        cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia)
 | 
					        cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia)
 | 
				
			||||||
        self.skia.memberships.update(end_date=localtime(now()).date())
 | 
					        self.skia.memberships.update(end_date=localtime(now()).date())
 | 
				
			||||||
@@ -157,10 +152,7 @@ class MembershipQuerySetTest(ClubTest):
 | 
				
			|||||||
        assert new_mem.role == 5
 | 
					        assert new_mem.role == 5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_delete_invalidate_cache(self):
 | 
					    def test_delete_invalidate_cache(self):
 | 
				
			||||||
        """
 | 
					        """Test that the `delete` queryset properly invalidate cache."""
 | 
				
			||||||
        Test that the `delete` queryset properly invalidate cache
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        mem_skia = self.skia.memberships.get(club=self.club)
 | 
					        mem_skia = self.skia.memberships.get(club=self.club)
 | 
				
			||||||
        mem_comptable = self.comptable.memberships.get(club=self.club)
 | 
					        mem_comptable = self.comptable.memberships.get(club=self.club)
 | 
				
			||||||
        cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia)
 | 
					        cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia)
 | 
				
			||||||
@@ -180,9 +172,7 @@ class MembershipQuerySetTest(ClubTest):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class ClubModelTest(ClubTest):
 | 
					class ClubModelTest(ClubTest):
 | 
				
			||||||
    def assert_membership_started_today(self, user: User, role: int):
 | 
					    def assert_membership_started_today(self, user: User, role: int):
 | 
				
			||||||
        """
 | 
					        """Assert that the given membership is active and started today."""
 | 
				
			||||||
        Assert that the given membership is active and started today
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        membership = user.memberships.ongoing().filter(club=self.club).first()
 | 
					        membership = user.memberships.ongoing().filter(club=self.club).first()
 | 
				
			||||||
        assert membership is not None
 | 
					        assert membership is not None
 | 
				
			||||||
        assert localtime(now()).date() == membership.start_date
 | 
					        assert localtime(now()).date() == membership.start_date
 | 
				
			||||||
@@ -195,17 +185,14 @@ class ClubModelTest(ClubTest):
 | 
				
			|||||||
        assert user.is_in_group(name=board_group)
 | 
					        assert user.is_in_group(name=board_group)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def assert_membership_ended_today(self, user: User):
 | 
					    def assert_membership_ended_today(self, user: User):
 | 
				
			||||||
        """
 | 
					        """Assert that the given user have a membership which ended today."""
 | 
				
			||||||
        Assert that the given user have a membership which ended today
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        today = localtime(now()).date()
 | 
					        today = localtime(now()).date()
 | 
				
			||||||
        assert user.memberships.filter(club=self.club, end_date=today).exists()
 | 
					        assert user.memberships.filter(club=self.club, end_date=today).exists()
 | 
				
			||||||
        assert self.club.get_membership_for(user) is None
 | 
					        assert self.club.get_membership_for(user) is None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_access_unauthorized(self):
 | 
					    def test_access_unauthorized(self):
 | 
				
			||||||
        """
 | 
					        """Test that users who never subscribed and anonymous users
 | 
				
			||||||
        Test that users who never subscribed and anonymous users
 | 
					        cannot see the page.
 | 
				
			||||||
        cannot see the page
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        response = self.client.post(self.members_url)
 | 
					        response = self.client.post(self.members_url)
 | 
				
			||||||
        assert response.status_code == 403
 | 
					        assert response.status_code == 403
 | 
				
			||||||
@@ -215,8 +202,7 @@ class ClubModelTest(ClubTest):
 | 
				
			|||||||
        assert response.status_code == 403
 | 
					        assert response.status_code == 403
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_display(self):
 | 
					    def test_display(self):
 | 
				
			||||||
        """
 | 
					        """Test that a GET request return a page where the requested
 | 
				
			||||||
        Test that a GET request return a page where the requested
 | 
					 | 
				
			||||||
        information are displayed.
 | 
					        information are displayed.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.client.force_login(self.skia)
 | 
					        self.client.force_login(self.skia)
 | 
				
			||||||
@@ -251,9 +237,7 @@ class ClubModelTest(ClubTest):
 | 
				
			|||||||
        self.assertInHTML(expected_html, response.content.decode())
 | 
					        self.assertInHTML(expected_html, response.content.decode())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_root_add_one_club_member(self):
 | 
					    def test_root_add_one_club_member(self):
 | 
				
			||||||
        """
 | 
					        """Test that root users can add members to clubs, one at a time."""
 | 
				
			||||||
        Test that root users can add members to clubs, one at a time
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.client.force_login(self.root)
 | 
					        self.client.force_login(self.root)
 | 
				
			||||||
        response = self.client.post(
 | 
					        response = self.client.post(
 | 
				
			||||||
            self.members_url,
 | 
					            self.members_url,
 | 
				
			||||||
@@ -264,9 +248,7 @@ class ClubModelTest(ClubTest):
 | 
				
			|||||||
        self.assert_membership_started_today(self.subscriber, role=3)
 | 
					        self.assert_membership_started_today(self.subscriber, role=3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_root_add_multiple_club_member(self):
 | 
					    def test_root_add_multiple_club_member(self):
 | 
				
			||||||
        """
 | 
					        """Test that root users can add multiple members at once to clubs."""
 | 
				
			||||||
        Test that root users can add multiple members at once to clubs
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.client.force_login(self.root)
 | 
					        self.client.force_login(self.root)
 | 
				
			||||||
        response = self.client.post(
 | 
					        response = self.client.post(
 | 
				
			||||||
            self.members_url,
 | 
					            self.members_url,
 | 
				
			||||||
@@ -281,8 +263,7 @@ class ClubModelTest(ClubTest):
 | 
				
			|||||||
        self.assert_membership_started_today(self.krophil, role=3)
 | 
					        self.assert_membership_started_today(self.krophil, role=3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_add_unauthorized_members(self):
 | 
					    def test_add_unauthorized_members(self):
 | 
				
			||||||
        """
 | 
					        """Test that users who are not currently subscribed
 | 
				
			||||||
        Test that users who are not currently subscribed
 | 
					 | 
				
			||||||
        cannot be members of clubs.
 | 
					        cannot be members of clubs.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.client.force_login(self.root)
 | 
					        self.client.force_login(self.root)
 | 
				
			||||||
@@ -302,9 +283,8 @@ class ClubModelTest(ClubTest):
 | 
				
			|||||||
        assert '<ul class="errorlist"><li>' in response.content.decode()
 | 
					        assert '<ul class="errorlist"><li>' in response.content.decode()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_add_members_already_members(self):
 | 
					    def test_add_members_already_members(self):
 | 
				
			||||||
        """
 | 
					        """Test that users who are already members of a club
 | 
				
			||||||
        Test that users who are already members of a club
 | 
					        cannot be added again to this club.
 | 
				
			||||||
        cannot be added again to this club
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.client.force_login(self.root)
 | 
					        self.client.force_login(self.root)
 | 
				
			||||||
        current_membership = self.skia.memberships.ongoing().get(club=self.club)
 | 
					        current_membership = self.skia.memberships.ongoing().get(club=self.club)
 | 
				
			||||||
@@ -320,8 +300,7 @@ class ClubModelTest(ClubTest):
 | 
				
			|||||||
        assert self.club.get_membership_for(self.skia) == new_membership
 | 
					        assert self.club.get_membership_for(self.skia) == new_membership
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_add_not_existing_users(self):
 | 
					    def test_add_not_existing_users(self):
 | 
				
			||||||
        """
 | 
					        """Test that not existing users cannot be added in clubs.
 | 
				
			||||||
        Test that not existing users cannot be added in clubs.
 | 
					 | 
				
			||||||
        If one user in the request is invalid, no membership creation at all
 | 
					        If one user in the request is invalid, no membership creation at all
 | 
				
			||||||
        can take place.
 | 
					        can take place.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@@ -349,9 +328,7 @@ class ClubModelTest(ClubTest):
 | 
				
			|||||||
        assert self.club.members.count() == nb_memberships
 | 
					        assert self.club.members.count() == nb_memberships
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_president_add_members(self):
 | 
					    def test_president_add_members(self):
 | 
				
			||||||
        """
 | 
					        """Test that the president of the club can add members."""
 | 
				
			||||||
        Test that the president of the club can add members
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        president = self.club.members.get(role=10).user
 | 
					        president = self.club.members.get(role=10).user
 | 
				
			||||||
        nb_club_membership = self.club.members.count()
 | 
					        nb_club_membership = self.club.members.count()
 | 
				
			||||||
        nb_subscriber_memberships = self.subscriber.memberships.count()
 | 
					        nb_subscriber_memberships = self.subscriber.memberships.count()
 | 
				
			||||||
@@ -368,8 +345,7 @@ class ClubModelTest(ClubTest):
 | 
				
			|||||||
        self.assert_membership_started_today(self.subscriber, role=9)
 | 
					        self.assert_membership_started_today(self.subscriber, role=9)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_add_member_greater_role(self):
 | 
					    def test_add_member_greater_role(self):
 | 
				
			||||||
        """
 | 
					        """Test that a member of the club member cannot create
 | 
				
			||||||
        Test that a member of the club member cannot create
 | 
					 | 
				
			||||||
        a membership with a greater role than its own.
 | 
					        a membership with a greater role than its own.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.client.force_login(self.skia)
 | 
					        self.client.force_login(self.skia)
 | 
				
			||||||
@@ -388,9 +364,7 @@ class ClubModelTest(ClubTest):
 | 
				
			|||||||
        assert not self.subscriber.memberships.filter(club=self.club).exists()
 | 
					        assert not self.subscriber.memberships.filter(club=self.club).exists()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_add_member_without_role(self):
 | 
					    def test_add_member_without_role(self):
 | 
				
			||||||
        """
 | 
					        """Test that trying to add members without specifying their role fails."""
 | 
				
			||||||
        Test that trying to add members without specifying their role fails
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.client.force_login(self.root)
 | 
					        self.client.force_login(self.root)
 | 
				
			||||||
        response = self.client.post(
 | 
					        response = self.client.post(
 | 
				
			||||||
            self.members_url,
 | 
					            self.members_url,
 | 
				
			||||||
@@ -402,9 +376,7 @@ class ClubModelTest(ClubTest):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_end_membership_self(self):
 | 
					    def test_end_membership_self(self):
 | 
				
			||||||
        """
 | 
					        """Test that a member can end its own membership."""
 | 
				
			||||||
        Test that a member can end its own membership
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.client.force_login(self.skia)
 | 
					        self.client.force_login(self.skia)
 | 
				
			||||||
        self.client.post(
 | 
					        self.client.post(
 | 
				
			||||||
            self.members_url,
 | 
					            self.members_url,
 | 
				
			||||||
@@ -414,9 +386,8 @@ class ClubModelTest(ClubTest):
 | 
				
			|||||||
        self.assert_membership_ended_today(self.skia)
 | 
					        self.assert_membership_ended_today(self.skia)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_end_membership_lower_role(self):
 | 
					    def test_end_membership_lower_role(self):
 | 
				
			||||||
        """
 | 
					        """Test that board members of the club can end memberships
 | 
				
			||||||
        Test that board members of the club can end memberships
 | 
					        of users with lower roles.
 | 
				
			||||||
        of users with lower roles
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        # remainder : skia has role 3, comptable has role 10, richard has role 1
 | 
					        # remainder : skia has role 3, comptable has role 10, richard has role 1
 | 
				
			||||||
        self.client.force_login(self.skia)
 | 
					        self.client.force_login(self.skia)
 | 
				
			||||||
@@ -429,9 +400,8 @@ class ClubModelTest(ClubTest):
 | 
				
			|||||||
        self.assert_membership_ended_today(self.richard)
 | 
					        self.assert_membership_ended_today(self.richard)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_end_membership_higher_role(self):
 | 
					    def test_end_membership_higher_role(self):
 | 
				
			||||||
        """
 | 
					        """Test that board members of the club cannot end memberships
 | 
				
			||||||
        Test that board members of the club cannot end memberships
 | 
					        of users with higher roles.
 | 
				
			||||||
        of users with higher roles
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        membership = self.comptable.memberships.filter(club=self.club).first()
 | 
					        membership = self.comptable.memberships.filter(club=self.club).first()
 | 
				
			||||||
        self.client.force_login(self.skia)
 | 
					        self.client.force_login(self.skia)
 | 
				
			||||||
@@ -448,9 +418,8 @@ class ClubModelTest(ClubTest):
 | 
				
			|||||||
        assert membership.end_date is None
 | 
					        assert membership.end_date is None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_end_membership_as_main_club_board(self):
 | 
					    def test_end_membership_as_main_club_board(self):
 | 
				
			||||||
        """
 | 
					        """Test that board members of the main club can end the membership
 | 
				
			||||||
        Test that board members of the main club can end the membership
 | 
					        of anyone.
 | 
				
			||||||
        of anyone
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        # make subscriber a board member
 | 
					        # make subscriber a board member
 | 
				
			||||||
        self.subscriber.memberships.all().delete()
 | 
					        self.subscriber.memberships.all().delete()
 | 
				
			||||||
@@ -467,9 +436,7 @@ class ClubModelTest(ClubTest):
 | 
				
			|||||||
        assert self.club.members.ongoing().count() == nb_memberships - 1
 | 
					        assert self.club.members.ongoing().count() == nb_memberships - 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_end_membership_as_root(self):
 | 
					    def test_end_membership_as_root(self):
 | 
				
			||||||
        """
 | 
					        """Test that root users can end the membership of anyone."""
 | 
				
			||||||
        Test that root users can end the membership of anyone
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        nb_memberships = self.club.members.count()
 | 
					        nb_memberships = self.club.members.count()
 | 
				
			||||||
        self.client.force_login(self.root)
 | 
					        self.client.force_login(self.root)
 | 
				
			||||||
        response = self.client.post(
 | 
					        response = self.client.post(
 | 
				
			||||||
@@ -482,9 +449,7 @@ class ClubModelTest(ClubTest):
 | 
				
			|||||||
        assert self.club.members.count() == nb_memberships
 | 
					        assert self.club.members.count() == nb_memberships
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_end_membership_as_foreigner(self):
 | 
					    def test_end_membership_as_foreigner(self):
 | 
				
			||||||
        """
 | 
					        """Test that users who are not in this club cannot end its memberships."""
 | 
				
			||||||
        Test that users who are not in this club cannot end its memberships
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        nb_memberships = self.club.members.count()
 | 
					        nb_memberships = self.club.members.count()
 | 
				
			||||||
        membership = self.richard.memberships.filter(club=self.club).first()
 | 
					        membership = self.richard.memberships.filter(club=self.club).first()
 | 
				
			||||||
        self.client.force_login(self.subscriber)
 | 
					        self.client.force_login(self.subscriber)
 | 
				
			||||||
@@ -498,9 +463,8 @@ class ClubModelTest(ClubTest):
 | 
				
			|||||||
        assert membership == new_mem
 | 
					        assert membership == new_mem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_delete_remove_from_meta_group(self):
 | 
					    def test_delete_remove_from_meta_group(self):
 | 
				
			||||||
        """
 | 
					        """Test that when a club is deleted, all its members are removed from the
 | 
				
			||||||
        Test that when a club is deleted, all its members are removed from the
 | 
					        associated metagroup.
 | 
				
			||||||
        associated metagroup
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        memberships = self.club.members.select_related("user")
 | 
					        memberships = self.club.members.select_related("user")
 | 
				
			||||||
        users = [membership.user for membership in memberships]
 | 
					        users = [membership.user for membership in memberships]
 | 
				
			||||||
@@ -511,9 +475,7 @@ class ClubModelTest(ClubTest):
 | 
				
			|||||||
            assert not user.is_in_group(name=meta_group)
 | 
					            assert not user.is_in_group(name=meta_group)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_add_to_meta_group(self):
 | 
					    def test_add_to_meta_group(self):
 | 
				
			||||||
        """
 | 
					        """Test that when a membership begins, the user is added to the meta group."""
 | 
				
			||||||
        Test that when a membership begins, the user is added to the meta group
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        group_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX
 | 
					        group_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX
 | 
				
			||||||
        board_members = self.club.unix_name + settings.SITH_BOARD_SUFFIX
 | 
					        board_members = self.club.unix_name + settings.SITH_BOARD_SUFFIX
 | 
				
			||||||
        assert not self.subscriber.is_in_group(name=group_members)
 | 
					        assert not self.subscriber.is_in_group(name=group_members)
 | 
				
			||||||
@@ -523,9 +485,7 @@ class ClubModelTest(ClubTest):
 | 
				
			|||||||
        assert self.subscriber.is_in_group(name=board_members)
 | 
					        assert self.subscriber.is_in_group(name=board_members)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_remove_from_meta_group(self):
 | 
					    def test_remove_from_meta_group(self):
 | 
				
			||||||
        """
 | 
					        """Test that when a membership ends, the user is removed from meta group."""
 | 
				
			||||||
        Test that when a membership ends, the user is removed from meta group
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        group_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX
 | 
					        group_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX
 | 
				
			||||||
        board_members = self.club.unix_name + settings.SITH_BOARD_SUFFIX
 | 
					        board_members = self.club.unix_name + settings.SITH_BOARD_SUFFIX
 | 
				
			||||||
        assert self.comptable.is_in_group(name=group_members)
 | 
					        assert self.comptable.is_in_group(name=group_members)
 | 
				
			||||||
@@ -535,9 +495,7 @@ class ClubModelTest(ClubTest):
 | 
				
			|||||||
        assert not self.comptable.is_in_group(name=board_members)
 | 
					        assert not self.comptable.is_in_group(name=board_members)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_club_owner(self):
 | 
					    def test_club_owner(self):
 | 
				
			||||||
        """
 | 
					        """Test that a club is owned only by board members of the main club."""
 | 
				
			||||||
        Test that a club is owned only by board members of the main club
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        anonymous = AnonymousUser()
 | 
					        anonymous = AnonymousUser()
 | 
				
			||||||
        assert not self.club.is_owned_by(anonymous)
 | 
					        assert not self.club.is_owned_by(anonymous)
 | 
				
			||||||
        assert not self.club.is_owned_by(self.subscriber)
 | 
					        assert not self.club.is_owned_by(self.subscriber)
 | 
				
			||||||
@@ -549,7 +507,7 @@ class ClubModelTest(ClubTest):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MailingFormTest(TestCase):
 | 
					class MailingFormTest(TestCase):
 | 
				
			||||||
    """Perform validation tests for MailingForm"""
 | 
					    """Perform validation tests for MailingForm."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def setUpTestData(cls):
 | 
					    def setUpTestData(cls):
 | 
				
			||||||
@@ -865,9 +823,7 @@ class MailingFormTest(TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClubSellingViewTest(TestCase):
 | 
					class ClubSellingViewTest(TestCase):
 | 
				
			||||||
    """
 | 
					    """Perform basics tests to ensure that the page is available."""
 | 
				
			||||||
    Perform basics tests to ensure that the page is available
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def setUpTestData(cls):
 | 
					    def setUpTestData(cls):
 | 
				
			||||||
@@ -875,9 +831,7 @@ class ClubSellingViewTest(TestCase):
 | 
				
			|||||||
        cls.skia = User.objects.get(username="skia")
 | 
					        cls.skia = User.objects.get(username="skia")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_page_not_internal_error(self):
 | 
					    def test_page_not_internal_error(self):
 | 
				
			||||||
        """
 | 
					        """Test that the page does not return and internal error."""
 | 
				
			||||||
        Test that the page does not return and internal error
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.client.force_login(self.skia)
 | 
					        self.client.force_login(self.skia)
 | 
				
			||||||
        response = self.client.get(
 | 
					        response = self.client.get(
 | 
				
			||||||
            reverse("club:club_sellings", kwargs={"club_id": self.ae.id})
 | 
					            reverse("club:club_sellings", kwargs={"club_id": self.ae.id})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -175,18 +175,14 @@ class ClubTabsMixin(TabedViewMixin):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClubListView(ListView):
 | 
					class ClubListView(ListView):
 | 
				
			||||||
    """
 | 
					    """List the Clubs."""
 | 
				
			||||||
    List the Clubs
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Club
 | 
					    model = Club
 | 
				
			||||||
    template_name = "club/club_list.jinja"
 | 
					    template_name = "club/club_list.jinja"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClubView(ClubTabsMixin, DetailView):
 | 
					class ClubView(ClubTabsMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Front page of a Club."""
 | 
				
			||||||
    Front page of a Club
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Club
 | 
					    model = Club
 | 
				
			||||||
    pk_url_kwarg = "club_id"
 | 
					    pk_url_kwarg = "club_id"
 | 
				
			||||||
@@ -201,9 +197,7 @@ class ClubView(ClubTabsMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClubRevView(ClubView):
 | 
					class ClubRevView(ClubView):
 | 
				
			||||||
    """
 | 
					    """Display a specific page revision."""
 | 
				
			||||||
    Display a specific page revision
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def dispatch(self, request, *args, **kwargs):
 | 
					    def dispatch(self, request, *args, **kwargs):
 | 
				
			||||||
        obj = self.get_object()
 | 
					        obj = self.get_object()
 | 
				
			||||||
@@ -235,9 +229,7 @@ class ClubPageEditView(ClubTabsMixin, PageEditViewBase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClubPageHistView(ClubTabsMixin, CanViewMixin, DetailView):
 | 
					class ClubPageHistView(ClubTabsMixin, CanViewMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Modification hostory of the page."""
 | 
				
			||||||
    Modification hostory of the page
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Club
 | 
					    model = Club
 | 
				
			||||||
    pk_url_kwarg = "club_id"
 | 
					    pk_url_kwarg = "club_id"
 | 
				
			||||||
@@ -246,9 +238,7 @@ class ClubPageHistView(ClubTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView):
 | 
					class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Tools page of a Club."""
 | 
				
			||||||
    Tools page of a Club
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Club
 | 
					    model = Club
 | 
				
			||||||
    pk_url_kwarg = "club_id"
 | 
					    pk_url_kwarg = "club_id"
 | 
				
			||||||
@@ -257,9 +247,7 @@ class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
 | 
					class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
 | 
				
			||||||
    """
 | 
					    """View of a club's members."""
 | 
				
			||||||
    View of a club's members
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Club
 | 
					    model = Club
 | 
				
			||||||
    pk_url_kwarg = "club_id"
 | 
					    pk_url_kwarg = "club_id"
 | 
				
			||||||
@@ -280,9 +268,7 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
 | 
				
			|||||||
        return kwargs
 | 
					        return kwargs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def form_valid(self, form):
 | 
					    def form_valid(self, form):
 | 
				
			||||||
        """
 | 
					        """Check user rights."""
 | 
				
			||||||
        Check user rights
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        resp = super().form_valid(form)
 | 
					        resp = super().form_valid(form)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        data = form.clean()
 | 
					        data = form.clean()
 | 
				
			||||||
@@ -307,9 +293,7 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView):
 | 
					class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Old members of a club."""
 | 
				
			||||||
    Old members of a club
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Club
 | 
					    model = Club
 | 
				
			||||||
    pk_url_kwarg = "club_id"
 | 
					    pk_url_kwarg = "club_id"
 | 
				
			||||||
@@ -318,9 +302,7 @@ class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
 | 
					class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
 | 
				
			||||||
    """
 | 
					    """Sellings of a club."""
 | 
				
			||||||
    Sellings of a club
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Club
 | 
					    model = Club
 | 
				
			||||||
    pk_url_kwarg = "club_id"
 | 
					    pk_url_kwarg = "club_id"
 | 
				
			||||||
@@ -396,12 +378,10 @@ class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClubSellingCSVView(ClubSellingView):
 | 
					class ClubSellingCSVView(ClubSellingView):
 | 
				
			||||||
    """
 | 
					    """Generate sellings in csv for a given period."""
 | 
				
			||||||
    Generate sellings in csv for a given period
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class StreamWriter:
 | 
					    class StreamWriter:
 | 
				
			||||||
        """Implements a file-like interface for streaming the CSV"""
 | 
					        """Implements a file-like interface for streaming the CSV."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        def write(self, value):
 | 
					        def write(self, value):
 | 
				
			||||||
            """Write the value by returning it, instead of storing in a buffer."""
 | 
					            """Write the value by returning it, instead of storing in a buffer."""
 | 
				
			||||||
@@ -475,9 +455,7 @@ class ClubSellingCSVView(ClubSellingView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClubEditView(ClubTabsMixin, CanEditMixin, UpdateView):
 | 
					class ClubEditView(ClubTabsMixin, CanEditMixin, UpdateView):
 | 
				
			||||||
    """
 | 
					    """Edit a Club's main informations (for the club's members)."""
 | 
				
			||||||
    Edit a Club's main informations (for the club's members)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Club
 | 
					    model = Club
 | 
				
			||||||
    pk_url_kwarg = "club_id"
 | 
					    pk_url_kwarg = "club_id"
 | 
				
			||||||
@@ -487,9 +465,7 @@ class ClubEditView(ClubTabsMixin, CanEditMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClubEditPropView(ClubTabsMixin, CanEditPropMixin, UpdateView):
 | 
					class ClubEditPropView(ClubTabsMixin, CanEditPropMixin, UpdateView):
 | 
				
			||||||
    """
 | 
					    """Edit the properties of a Club object (for the Sith admins)."""
 | 
				
			||||||
    Edit the properties of a Club object (for the Sith admins)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Club
 | 
					    model = Club
 | 
				
			||||||
    pk_url_kwarg = "club_id"
 | 
					    pk_url_kwarg = "club_id"
 | 
				
			||||||
@@ -499,9 +475,7 @@ class ClubEditPropView(ClubTabsMixin, CanEditPropMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClubCreateView(CanCreateMixin, CreateView):
 | 
					class ClubCreateView(CanCreateMixin, CreateView):
 | 
				
			||||||
    """
 | 
					    """Create a club (for the Sith admin)."""
 | 
				
			||||||
    Create a club (for the Sith admin)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Club
 | 
					    model = Club
 | 
				
			||||||
    pk_url_kwarg = "club_id"
 | 
					    pk_url_kwarg = "club_id"
 | 
				
			||||||
@@ -510,9 +484,7 @@ class ClubCreateView(CanCreateMixin, CreateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MembershipSetOldView(CanEditMixin, DetailView):
 | 
					class MembershipSetOldView(CanEditMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Set a membership as beeing old."""
 | 
				
			||||||
    Set a membership as beeing old
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Membership
 | 
					    model = Membership
 | 
				
			||||||
    pk_url_kwarg = "membership_id"
 | 
					    pk_url_kwarg = "membership_id"
 | 
				
			||||||
@@ -541,9 +513,7 @@ class MembershipSetOldView(CanEditMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MembershipDeleteView(UserIsRootMixin, DeleteView):
 | 
					class MembershipDeleteView(UserIsRootMixin, DeleteView):
 | 
				
			||||||
    """
 | 
					    """Delete a membership (for admins only)."""
 | 
				
			||||||
    Delete a membership (for admins only)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Membership
 | 
					    model = Membership
 | 
				
			||||||
    pk_url_kwarg = "membership_id"
 | 
					    pk_url_kwarg = "membership_id"
 | 
				
			||||||
@@ -563,9 +533,7 @@ class ClubStatView(TemplateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
 | 
					class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
 | 
				
			||||||
    """
 | 
					    """A list of mailing for a given club."""
 | 
				
			||||||
    A list of mailing for a given club
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Club
 | 
					    model = Club
 | 
				
			||||||
    form_class = MailingForm
 | 
					    form_class = MailingForm
 | 
				
			||||||
@@ -603,9 +571,7 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
 | 
				
			|||||||
        return kwargs
 | 
					        return kwargs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def add_new_mailing(self, cleaned_data) -> ValidationError | None:
 | 
					    def add_new_mailing(self, cleaned_data) -> ValidationError | None:
 | 
				
			||||||
        """
 | 
					        """Create a new mailing list from the form."""
 | 
				
			||||||
        Create a new mailing list from the form
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        mailing = Mailing(
 | 
					        mailing = Mailing(
 | 
				
			||||||
            club=self.get_object(),
 | 
					            club=self.get_object(),
 | 
				
			||||||
            email=cleaned_data["mailing_email"],
 | 
					            email=cleaned_data["mailing_email"],
 | 
				
			||||||
@@ -620,9 +586,7 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
 | 
				
			|||||||
        return None
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def add_new_subscription(self, cleaned_data) -> ValidationError | None:
 | 
					    def add_new_subscription(self, cleaned_data) -> ValidationError | None:
 | 
				
			||||||
        """
 | 
					        """Add mailing subscriptions for each user given and/or for the specified email in form."""
 | 
				
			||||||
        Add mailing subscriptions for each user given and/or for the specified email in form
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        users_to_save = []
 | 
					        users_to_save = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for user in cleaned_data["subscription_users"]:
 | 
					        for user in cleaned_data["subscription_users"]:
 | 
				
			||||||
@@ -656,9 +620,7 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
 | 
				
			|||||||
        return None
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def remove_subscription(self, cleaned_data):
 | 
					    def remove_subscription(self, cleaned_data):
 | 
				
			||||||
        """
 | 
					        """Remove specified users from a mailing list."""
 | 
				
			||||||
        Remove specified users from a mailing list
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        fields = [
 | 
					        fields = [
 | 
				
			||||||
            cleaned_data[key]
 | 
					            cleaned_data[key]
 | 
				
			||||||
            for key in cleaned_data.keys()
 | 
					            for key in cleaned_data.keys()
 | 
				
			||||||
@@ -742,7 +704,7 @@ class MailingAutoGenerationView(View):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PosterListView(ClubTabsMixin, PosterListBaseView, CanViewMixin):
 | 
					class PosterListView(ClubTabsMixin, PosterListBaseView, CanViewMixin):
 | 
				
			||||||
    """List communication posters"""
 | 
					    """List communication posters."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_object(self):
 | 
					    def get_object(self):
 | 
				
			||||||
        return self.club
 | 
					        return self.club
 | 
				
			||||||
@@ -755,7 +717,7 @@ class PosterListView(ClubTabsMixin, PosterListBaseView, CanViewMixin):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PosterCreateView(PosterCreateBaseView, CanCreateMixin):
 | 
					class PosterCreateView(PosterCreateBaseView, CanCreateMixin):
 | 
				
			||||||
    """Create communication poster"""
 | 
					    """Create communication poster."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pk_url_kwarg = "club_id"
 | 
					    pk_url_kwarg = "club_id"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -770,7 +732,7 @@ class PosterCreateView(PosterCreateBaseView, CanCreateMixin):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PosterEditView(ClubTabsMixin, PosterEditBaseView, CanEditMixin):
 | 
					class PosterEditView(ClubTabsMixin, PosterEditBaseView, CanEditMixin):
 | 
				
			||||||
    """Edit communication poster"""
 | 
					    """Edit communication poster."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_success_url(self):
 | 
					    def get_success_url(self):
 | 
				
			||||||
        return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id})
 | 
					        return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id})
 | 
				
			||||||
@@ -782,7 +744,7 @@ class PosterEditView(ClubTabsMixin, PosterEditBaseView, CanEditMixin):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PosterDeleteView(PosterDeleteBaseView, ClubTabsMixin, CanEditMixin):
 | 
					class PosterDeleteView(PosterDeleteBaseView, ClubTabsMixin, CanEditMixin):
 | 
				
			||||||
    """Delete communication poster"""
 | 
					    """Delete communication poster."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_success_url(self):
 | 
					    def get_success_url(self):
 | 
				
			||||||
        return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id})
 | 
					        return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -39,7 +39,7 @@ from core.models import Notification, Preferences, RealGroup, User
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Sith(models.Model):
 | 
					class Sith(models.Model):
 | 
				
			||||||
    """A one instance class storing all the modifiable infos"""
 | 
					    """A one instance class storing all the modifiable infos."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    alert_msg = models.TextField(_("alert message"), default="", blank=True)
 | 
					    alert_msg = models.TextField(_("alert message"), default="", blank=True)
 | 
				
			||||||
    info_msg = models.TextField(_("info message"), default="", blank=True)
 | 
					    info_msg = models.TextField(_("info message"), default="", blank=True)
 | 
				
			||||||
@@ -64,7 +64,7 @@ NEWS_TYPES = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class News(models.Model):
 | 
					class News(models.Model):
 | 
				
			||||||
    """The news class"""
 | 
					    """The news class."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    title = models.CharField(_("title"), max_length=64)
 | 
					    title = models.CharField(_("title"), max_length=64)
 | 
				
			||||||
    summary = models.TextField(_("summary"))
 | 
					    summary = models.TextField(_("summary"))
 | 
				
			||||||
@@ -143,8 +143,7 @@ def news_notification_callback(notif):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class NewsDate(models.Model):
 | 
					class NewsDate(models.Model):
 | 
				
			||||||
    """
 | 
					    """A date class, useful for weekly events, or for events that just have no date.
 | 
				
			||||||
    A date class, useful for weekly events, or for events that just have no date
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    This class allows more flexibilty managing the dates related to a news, particularly when this news is weekly, since
 | 
					    This class allows more flexibilty managing the dates related to a news, particularly when this news is weekly, since
 | 
				
			||||||
    we don't have to make copies
 | 
					    we don't have to make copies
 | 
				
			||||||
@@ -164,8 +163,7 @@ class NewsDate(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Weekmail(models.Model):
 | 
					class Weekmail(models.Model):
 | 
				
			||||||
    """
 | 
					    """The weekmail class.
 | 
				
			||||||
    The weekmail class
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :ivar title: Title of the weekmail
 | 
					    :ivar title: Title of the weekmail
 | 
				
			||||||
    :ivar intro: Introduction of the weekmail
 | 
					    :ivar intro: Introduction of the weekmail
 | 
				
			||||||
@@ -189,8 +187,8 @@ class Weekmail(models.Model):
 | 
				
			|||||||
        return f"Weekmail {self.id} (sent: {self.sent}) - {self.title}"
 | 
					        return f"Weekmail {self.id} (sent: {self.sent}) - {self.title}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def send(self):
 | 
					    def send(self):
 | 
				
			||||||
        """
 | 
					        """Send the weekmail to all users with the receive weekmail option opt-in.
 | 
				
			||||||
        Send the weekmail to all users with the receive weekmail option opt-in.
 | 
					
 | 
				
			||||||
        Also send the weekmail to the mailing list in settings.SITH_COM_EMAIL.
 | 
					        Also send the weekmail to the mailing list in settings.SITH_COM_EMAIL.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        dest = [
 | 
					        dest = [
 | 
				
			||||||
@@ -214,33 +212,25 @@ class Weekmail(models.Model):
 | 
				
			|||||||
            Weekmail().save()
 | 
					            Weekmail().save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def render_text(self):
 | 
					    def render_text(self):
 | 
				
			||||||
        """
 | 
					        """Renders a pure text version of the mail for readers without HTML support."""
 | 
				
			||||||
        Renders a pure text version of the mail for readers without HTML support.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return render(
 | 
					        return render(
 | 
				
			||||||
            None, "com/weekmail_renderer_text.jinja", context={"weekmail": self}
 | 
					            None, "com/weekmail_renderer_text.jinja", context={"weekmail": self}
 | 
				
			||||||
        ).content.decode("utf-8")
 | 
					        ).content.decode("utf-8")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def render_html(self):
 | 
					    def render_html(self):
 | 
				
			||||||
        """
 | 
					        """Renders an HTML version of the mail with images and fancy CSS."""
 | 
				
			||||||
        Renders an HTML version of the mail with images and fancy CSS.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return render(
 | 
					        return render(
 | 
				
			||||||
            None, "com/weekmail_renderer_html.jinja", context={"weekmail": self}
 | 
					            None, "com/weekmail_renderer_html.jinja", context={"weekmail": self}
 | 
				
			||||||
        ).content.decode("utf-8")
 | 
					        ).content.decode("utf-8")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_banner(self):
 | 
					    def get_banner(self):
 | 
				
			||||||
        """
 | 
					        """Return an absolute link to the banner."""
 | 
				
			||||||
        Return an absolute link to the banner.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return (
 | 
					        return (
 | 
				
			||||||
            "http://" + settings.SITH_URL + static("com/img/weekmail_bannerV2P22.png")
 | 
					            "http://" + settings.SITH_URL + static("com/img/weekmail_bannerV2P22.png")
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_footer(self):
 | 
					    def get_footer(self):
 | 
				
			||||||
        """
 | 
					        """Return an absolute link to the footer."""
 | 
				
			||||||
        Return an absolute link to the footer.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return "http://" + settings.SITH_URL + static("com/img/weekmail_footerP22.png")
 | 
					        return "http://" + settings.SITH_URL + static("com/img/weekmail_footerP22.png")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_owned_by(self, user):
 | 
					    def is_owned_by(self, user):
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										28
									
								
								com/tests.py
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								com/tests.py
									
									
									
									
									
								
							@@ -115,10 +115,7 @@ class ComTest(TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class SithTest(TestCase):
 | 
					class SithTest(TestCase):
 | 
				
			||||||
    def test_sith_owner(self):
 | 
					    def test_sith_owner(self):
 | 
				
			||||||
        """
 | 
					        """Test that the sith instance is owned by com admins and nobody else."""
 | 
				
			||||||
        Test that the sith instance is owned by com admins
 | 
					 | 
				
			||||||
        and nobody else
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        sith: Sith = Sith.objects.first()
 | 
					        sith: Sith = Sith.objects.first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        com_admin = User.objects.get(username="comunity")
 | 
					        com_admin = User.objects.get(username="comunity")
 | 
				
			||||||
@@ -148,20 +145,17 @@ class NewsTest(TestCase):
 | 
				
			|||||||
        cls.anonymous = AnonymousUser()
 | 
					        cls.anonymous = AnonymousUser()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_news_owner(self):
 | 
					    def test_news_owner(self):
 | 
				
			||||||
 | 
					        """Test that news are owned by com admins
 | 
				
			||||||
 | 
					        or by their author but nobody else.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Test that news are owned by com admins
 | 
					 | 
				
			||||||
        or by their author but nobody else
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        assert self.new.is_owned_by(self.com_admin)
 | 
					        assert self.new.is_owned_by(self.com_admin)
 | 
				
			||||||
        assert self.new.is_owned_by(self.author)
 | 
					        assert self.new.is_owned_by(self.author)
 | 
				
			||||||
        assert not self.new.is_owned_by(self.anonymous)
 | 
					        assert not self.new.is_owned_by(self.anonymous)
 | 
				
			||||||
        assert not self.new.is_owned_by(self.sli)
 | 
					        assert not self.new.is_owned_by(self.sli)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_news_viewer(self):
 | 
					    def test_news_viewer(self):
 | 
				
			||||||
        """
 | 
					        """Test that moderated news can be viewed by anyone
 | 
				
			||||||
        Test that moderated news can be viewed by anyone
 | 
					        and not moderated news only by com admins.
 | 
				
			||||||
        and not moderated news only by com admins
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        # by default a news isn't moderated
 | 
					        # by default a news isn't moderated
 | 
				
			||||||
        assert self.new.can_be_viewed_by(self.com_admin)
 | 
					        assert self.new.can_be_viewed_by(self.com_admin)
 | 
				
			||||||
@@ -177,9 +171,7 @@ class NewsTest(TestCase):
 | 
				
			|||||||
        assert self.new.can_be_viewed_by(self.author)
 | 
					        assert self.new.can_be_viewed_by(self.author)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_news_editor(self):
 | 
					    def test_news_editor(self):
 | 
				
			||||||
        """
 | 
					        """Test that only com admins can edit news."""
 | 
				
			||||||
        Test that only com admins can edit news
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        assert self.new.can_be_edited_by(self.com_admin)
 | 
					        assert self.new.can_be_edited_by(self.com_admin)
 | 
				
			||||||
        assert not self.new.can_be_edited_by(self.sli)
 | 
					        assert not self.new.can_be_edited_by(self.sli)
 | 
				
			||||||
        assert not self.new.can_be_edited_by(self.anonymous)
 | 
					        assert not self.new.can_be_edited_by(self.anonymous)
 | 
				
			||||||
@@ -203,9 +195,7 @@ class WeekmailArticleTest(TestCase):
 | 
				
			|||||||
        cls.anonymous = AnonymousUser()
 | 
					        cls.anonymous = AnonymousUser()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_weekmail_owner(self):
 | 
					    def test_weekmail_owner(self):
 | 
				
			||||||
        """
 | 
					        """Test that weekmails are owned only by com admins."""
 | 
				
			||||||
        Test that weekmails are owned only by com admins
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        assert self.article.is_owned_by(self.com_admin)
 | 
					        assert self.article.is_owned_by(self.com_admin)
 | 
				
			||||||
        assert not self.article.is_owned_by(self.author)
 | 
					        assert not self.article.is_owned_by(self.author)
 | 
				
			||||||
        assert not self.article.is_owned_by(self.anonymous)
 | 
					        assert not self.article.is_owned_by(self.anonymous)
 | 
				
			||||||
@@ -229,9 +219,7 @@ class PosterTest(TestCase):
 | 
				
			|||||||
        cls.anonymous = AnonymousUser()
 | 
					        cls.anonymous = AnonymousUser()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_poster_owner(self):
 | 
					    def test_poster_owner(self):
 | 
				
			||||||
        """
 | 
					        """Test that poster are owned by com admins and board members in clubs."""
 | 
				
			||||||
        Test that poster are owned by com admins and board members in clubs
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        assert self.poster.is_owned_by(self.com_admin)
 | 
					        assert self.poster.is_owned_by(self.com_admin)
 | 
				
			||||||
        assert not self.poster.is_owned_by(self.anonymous)
 | 
					        assert not self.poster.is_owned_by(self.anonymous)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										40
									
								
								com/views.py
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								com/views.py
									
									
									
									
									
								
							@@ -427,7 +427,7 @@ class WeekmailPreviewView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, Detai
 | 
				
			|||||||
        return self.model.objects.filter(sent=False).order_by("-id").first()
 | 
					        return self.model.objects.filter(sent=False).order_by("-id").first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """Add rendered weekmail"""
 | 
					        """Add rendered weekmail."""
 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        kwargs["weekmail_rendered"] = self.object.render_html()
 | 
					        kwargs["weekmail_rendered"] = self.object.render_html()
 | 
				
			||||||
        kwargs["bad_recipients"] = self.bad_recipients
 | 
					        kwargs["bad_recipients"] = self.bad_recipients
 | 
				
			||||||
@@ -507,7 +507,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
 | 
				
			|||||||
        return super().get(request, *args, **kwargs)
 | 
					        return super().get(request, *args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """Add orphan articles"""
 | 
					        """Add orphan articles."""
 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        kwargs["orphans"] = WeekmailArticle.objects.filter(weekmail=None)
 | 
					        kwargs["orphans"] = WeekmailArticle.objects.filter(weekmail=None)
 | 
				
			||||||
        return kwargs
 | 
					        return kwargs
 | 
				
			||||||
@@ -516,7 +516,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
 | 
				
			|||||||
class WeekmailArticleEditView(
 | 
					class WeekmailArticleEditView(
 | 
				
			||||||
    ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateView
 | 
					    ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateView
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
    """Edit an article"""
 | 
					    """Edit an article."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = WeekmailArticle
 | 
					    model = WeekmailArticle
 | 
				
			||||||
    form_class = modelform_factory(
 | 
					    form_class = modelform_factory(
 | 
				
			||||||
@@ -532,7 +532,7 @@ class WeekmailArticleEditView(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class WeekmailArticleCreateView(QuickNotifMixin, CreateView):
 | 
					class WeekmailArticleCreateView(QuickNotifMixin, CreateView):
 | 
				
			||||||
    """Post an article"""
 | 
					    """Post an article."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = WeekmailArticle
 | 
					    model = WeekmailArticle
 | 
				
			||||||
    form_class = modelform_factory(
 | 
					    form_class = modelform_factory(
 | 
				
			||||||
@@ -574,7 +574,7 @@ class WeekmailArticleCreateView(QuickNotifMixin, CreateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class WeekmailArticleDeleteView(CanEditPropMixin, DeleteView):
 | 
					class WeekmailArticleDeleteView(CanEditPropMixin, DeleteView):
 | 
				
			||||||
    """Delete an article"""
 | 
					    """Delete an article."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = WeekmailArticle
 | 
					    model = WeekmailArticle
 | 
				
			||||||
    template_name = "core/delete_confirm.jinja"
 | 
					    template_name = "core/delete_confirm.jinja"
 | 
				
			||||||
@@ -614,7 +614,7 @@ class MailingModerateView(View):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PosterListBaseView(ListView):
 | 
					class PosterListBaseView(ListView):
 | 
				
			||||||
    """List communication posters"""
 | 
					    """List communication posters."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    current_tab = "posters"
 | 
					    current_tab = "posters"
 | 
				
			||||||
    model = Poster
 | 
					    model = Poster
 | 
				
			||||||
@@ -641,7 +641,7 @@ class PosterListBaseView(ListView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PosterCreateBaseView(CreateView):
 | 
					class PosterCreateBaseView(CreateView):
 | 
				
			||||||
    """Create communication poster"""
 | 
					    """Create communication poster."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    current_tab = "posters"
 | 
					    current_tab = "posters"
 | 
				
			||||||
    form_class = PosterForm
 | 
					    form_class = PosterForm
 | 
				
			||||||
@@ -673,7 +673,7 @@ class PosterCreateBaseView(CreateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PosterEditBaseView(UpdateView):
 | 
					class PosterEditBaseView(UpdateView):
 | 
				
			||||||
    """Edit communication poster"""
 | 
					    """Edit communication poster."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pk_url_kwarg = "poster_id"
 | 
					    pk_url_kwarg = "poster_id"
 | 
				
			||||||
    current_tab = "posters"
 | 
					    current_tab = "posters"
 | 
				
			||||||
@@ -721,7 +721,7 @@ class PosterEditBaseView(UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PosterDeleteBaseView(DeleteView):
 | 
					class PosterDeleteBaseView(DeleteView):
 | 
				
			||||||
    """Edit communication poster"""
 | 
					    """Edit communication poster."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pk_url_kwarg = "poster_id"
 | 
					    pk_url_kwarg = "poster_id"
 | 
				
			||||||
    current_tab = "posters"
 | 
					    current_tab = "posters"
 | 
				
			||||||
@@ -738,7 +738,7 @@ class PosterDeleteBaseView(DeleteView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PosterListView(IsComAdminMixin, ComTabsMixin, PosterListBaseView):
 | 
					class PosterListView(IsComAdminMixin, ComTabsMixin, PosterListBaseView):
 | 
				
			||||||
    """List communication posters"""
 | 
					    """List communication posters."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
@@ -747,7 +747,7 @@ class PosterListView(IsComAdminMixin, ComTabsMixin, PosterListBaseView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PosterCreateView(IsComAdminMixin, ComTabsMixin, PosterCreateBaseView):
 | 
					class PosterCreateView(IsComAdminMixin, ComTabsMixin, PosterCreateBaseView):
 | 
				
			||||||
    """Create communication poster"""
 | 
					    """Create communication poster."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    success_url = reverse_lazy("com:poster_list")
 | 
					    success_url = reverse_lazy("com:poster_list")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -758,7 +758,7 @@ class PosterCreateView(IsComAdminMixin, ComTabsMixin, PosterCreateBaseView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PosterEditView(IsComAdminMixin, ComTabsMixin, PosterEditBaseView):
 | 
					class PosterEditView(IsComAdminMixin, ComTabsMixin, PosterEditBaseView):
 | 
				
			||||||
    """Edit communication poster"""
 | 
					    """Edit communication poster."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    success_url = reverse_lazy("com:poster_list")
 | 
					    success_url = reverse_lazy("com:poster_list")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -769,13 +769,13 @@ class PosterEditView(IsComAdminMixin, ComTabsMixin, PosterEditBaseView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PosterDeleteView(IsComAdminMixin, ComTabsMixin, PosterDeleteBaseView):
 | 
					class PosterDeleteView(IsComAdminMixin, ComTabsMixin, PosterDeleteBaseView):
 | 
				
			||||||
    """Delete communication poster"""
 | 
					    """Delete communication poster."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    success_url = reverse_lazy("com:poster_list")
 | 
					    success_url = reverse_lazy("com:poster_list")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PosterModerateListView(IsComAdminMixin, ComTabsMixin, ListView):
 | 
					class PosterModerateListView(IsComAdminMixin, ComTabsMixin, ListView):
 | 
				
			||||||
    """Moderate list communication poster"""
 | 
					    """Moderate list communication poster."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    current_tab = "posters"
 | 
					    current_tab = "posters"
 | 
				
			||||||
    model = Poster
 | 
					    model = Poster
 | 
				
			||||||
@@ -789,7 +789,7 @@ class PosterModerateListView(IsComAdminMixin, ComTabsMixin, ListView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PosterModerateView(IsComAdminMixin, ComTabsMixin, View):
 | 
					class PosterModerateView(IsComAdminMixin, ComTabsMixin, View):
 | 
				
			||||||
    """Moderate communication poster"""
 | 
					    """Moderate communication poster."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get(self, request, *args, **kwargs):
 | 
					    def get(self, request, *args, **kwargs):
 | 
				
			||||||
        obj = get_object_or_404(Poster, pk=kwargs["object_id"])
 | 
					        obj = get_object_or_404(Poster, pk=kwargs["object_id"])
 | 
				
			||||||
@@ -807,7 +807,7 @@ class PosterModerateView(IsComAdminMixin, ComTabsMixin, View):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ScreenListView(IsComAdminMixin, ComTabsMixin, ListView):
 | 
					class ScreenListView(IsComAdminMixin, ComTabsMixin, ListView):
 | 
				
			||||||
    """List communication screens"""
 | 
					    """List communication screens."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    current_tab = "screens"
 | 
					    current_tab = "screens"
 | 
				
			||||||
    model = Screen
 | 
					    model = Screen
 | 
				
			||||||
@@ -815,7 +815,7 @@ class ScreenListView(IsComAdminMixin, ComTabsMixin, ListView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ScreenSlideshowView(DetailView):
 | 
					class ScreenSlideshowView(DetailView):
 | 
				
			||||||
    """Slideshow of actives posters"""
 | 
					    """Slideshow of actives posters."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pk_url_kwarg = "screen_id"
 | 
					    pk_url_kwarg = "screen_id"
 | 
				
			||||||
    model = Screen
 | 
					    model = Screen
 | 
				
			||||||
@@ -828,7 +828,7 @@ class ScreenSlideshowView(DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ScreenCreateView(IsComAdminMixin, ComTabsMixin, CreateView):
 | 
					class ScreenCreateView(IsComAdminMixin, ComTabsMixin, CreateView):
 | 
				
			||||||
    """Create communication screen"""
 | 
					    """Create communication screen."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    current_tab = "screens"
 | 
					    current_tab = "screens"
 | 
				
			||||||
    model = Screen
 | 
					    model = Screen
 | 
				
			||||||
@@ -838,7 +838,7 @@ class ScreenCreateView(IsComAdminMixin, ComTabsMixin, CreateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ScreenEditView(IsComAdminMixin, ComTabsMixin, UpdateView):
 | 
					class ScreenEditView(IsComAdminMixin, ComTabsMixin, UpdateView):
 | 
				
			||||||
    """Edit communication screen"""
 | 
					    """Edit communication screen."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pk_url_kwarg = "screen_id"
 | 
					    pk_url_kwarg = "screen_id"
 | 
				
			||||||
    current_tab = "screens"
 | 
					    current_tab = "screens"
 | 
				
			||||||
@@ -849,7 +849,7 @@ class ScreenEditView(IsComAdminMixin, ComTabsMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ScreenDeleteView(IsComAdminMixin, ComTabsMixin, DeleteView):
 | 
					class ScreenDeleteView(IsComAdminMixin, ComTabsMixin, DeleteView):
 | 
				
			||||||
    """Delete communication screen"""
 | 
					    """Delete communication screen."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pk_url_kwarg = "screen_id"
 | 
					    pk_url_kwarg = "screen_id"
 | 
				
			||||||
    current_tab = "screens"
 | 
					    current_tab = "screens"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,9 +19,7 @@ class TwoDigitMonthConverter:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BooleanStringConverter:
 | 
					class BooleanStringConverter:
 | 
				
			||||||
    """
 | 
					    """Converter whose regex match either True or False."""
 | 
				
			||||||
    Converter whose regex match either True or False
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    regex = r"(True)|(False)"
 | 
					    regex = r"(True)|(False)"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -90,12 +90,15 @@ def list_tags(s):
 | 
				
			|||||||
            yield parts[1][len(tag_prefix) :]
 | 
					            yield parts[1][len(tag_prefix) :]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def parse_semver(s):
 | 
					def parse_semver(s) -> tuple[int, int, int] | None:
 | 
				
			||||||
    """
 | 
					    """Parse a semver string.
 | 
				
			||||||
    Turns a semver string into a 3-tuple or None if the parsing failed, it is a
 | 
					 | 
				
			||||||
    prerelease or it has build metadata.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    See https://semver.org
 | 
					    See https://semver.org
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        A tuple, if the parsing was successful, else None.
 | 
				
			||||||
 | 
					        In the latter case, it must probably be a prerelease
 | 
				
			||||||
 | 
					        or include build metadata.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    m = semver_regex.match(s)
 | 
					    m = semver_regex.match(s)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -106,7 +109,7 @@ def parse_semver(s):
 | 
				
			|||||||
    ):
 | 
					    ):
 | 
				
			||||||
        return None
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (int(m.group("major")), int(m.group("minor")), int(m.group("patch")))
 | 
					    return int(m.group("major")), int(m.group("minor")), int(m.group("patch"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def semver_to_s(t):
 | 
					def semver_to_s(t):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,9 +29,7 @@ from django.core.management.commands import compilemessages
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Command(compilemessages.Command):
 | 
					class Command(compilemessages.Command):
 | 
				
			||||||
    """
 | 
					    """Wrap call to compilemessages to avoid building whole env."""
 | 
				
			||||||
    Wrap call to compilemessages to avoid building whole env
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    help = """
 | 
					    help = """
 | 
				
			||||||
        The usage is the same as the real compilemessages
 | 
					        The usage is the same as the real compilemessages
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,9 +30,7 @@ from django.core.management.base import BaseCommand
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Command(BaseCommand):
 | 
					class Command(BaseCommand):
 | 
				
			||||||
    """
 | 
					    """Compiles scss in static folder for production."""
 | 
				
			||||||
    Compiles scss in static folder for production
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    help = "Compile scss files from static folder"
 | 
					    help = "Compile scss files from static folder"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -53,13 +53,14 @@ _threadlocal = threading.local()
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_signal_request():
 | 
					def get_signal_request():
 | 
				
			||||||
    """
 | 
					    """Allow to access current request in signals.
 | 
				
			||||||
    !!! Do not use if your operation is asynchronus !!!
 | 
					 | 
				
			||||||
    Allow to access current request in signals
 | 
					 | 
				
			||||||
    This is a hack that looks into the thread
 | 
					 | 
				
			||||||
    Mainly used for log purpose
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    This is a hack that looks into the thread
 | 
				
			||||||
 | 
					    Mainly used for log purpose.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    !!!danger
 | 
				
			||||||
 | 
					        Do not use if your operation is asynchronous.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
    return getattr(_threadlocal, "request", None)
 | 
					    return getattr(_threadlocal, "request", None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										224
									
								
								core/models.py
									
									
									
									
									
								
							
							
						
						
									
										224
									
								
								core/models.py
									
									
									
									
									
								
							@@ -21,11 +21,13 @@
 | 
				
			|||||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
 | 
					# Place - Suite 330, Boston, MA 02111-1307, USA.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
 | 
					from __future__ import annotations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import importlib
 | 
					import importlib
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import unicodedata
 | 
					import unicodedata
 | 
				
			||||||
from datetime import date, timedelta
 | 
					from datetime import date, timedelta
 | 
				
			||||||
from typing import List, Optional, Union
 | 
					from typing import TYPE_CHECKING, Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.contrib.auth.models import (
 | 
					from django.contrib.auth.models import (
 | 
				
			||||||
@@ -56,6 +58,9 @@ from phonenumber_field.modelfields import PhoneNumberField
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from core import utils
 | 
					from core import utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if TYPE_CHECKING:
 | 
				
			||||||
 | 
					    from club.models import Club
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RealGroupManager(AuthGroupManager):
 | 
					class RealGroupManager(AuthGroupManager):
 | 
				
			||||||
    def get_queryset(self):
 | 
					    def get_queryset(self):
 | 
				
			||||||
@@ -68,8 +73,7 @@ class MetaGroupManager(AuthGroupManager):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Group(AuthGroup):
 | 
					class Group(AuthGroup):
 | 
				
			||||||
    """
 | 
					    """Implement both RealGroups and Meta groups.
 | 
				
			||||||
    Implement both RealGroups and Meta groups
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Groups are sorted by their is_meta property
 | 
					    Groups are sorted by their is_meta property
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
@@ -87,9 +91,6 @@ class Group(AuthGroup):
 | 
				
			|||||||
        ordering = ["name"]
 | 
					        ordering = ["name"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_absolute_url(self):
 | 
					    def get_absolute_url(self):
 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        This is needed for black magic powered UpdateView's children
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return reverse("core:group_list")
 | 
					        return reverse("core:group_list")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def save(self, *args, **kwargs):
 | 
					    def save(self, *args, **kwargs):
 | 
				
			||||||
@@ -104,8 +105,8 @@ class Group(AuthGroup):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MetaGroup(Group):
 | 
					class MetaGroup(Group):
 | 
				
			||||||
    """
 | 
					    """MetaGroups are dynamically created groups.
 | 
				
			||||||
    MetaGroups are dynamically created groups.
 | 
					
 | 
				
			||||||
    Generally used with clubs where creating a club creates two groups:
 | 
					    Generally used with clubs where creating a club creates two groups:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    * club-SITH_BOARD_SUFFIX
 | 
					    * club-SITH_BOARD_SUFFIX
 | 
				
			||||||
@@ -123,14 +124,14 @@ class MetaGroup(Group):
 | 
				
			|||||||
        self.is_meta = True
 | 
					        self.is_meta = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @cached_property
 | 
					    @cached_property
 | 
				
			||||||
    def associated_club(self):
 | 
					    def associated_club(self) -> Club | None:
 | 
				
			||||||
        """
 | 
					        """Return the group associated with this meta group.
 | 
				
			||||||
        Return the group associated with this meta group
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        The result of this function is cached
 | 
					        The result of this function is cached
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: The associated club if it exists, else None
 | 
					
 | 
				
			||||||
        :rtype: club.models.Club | None
 | 
					        Returns:
 | 
				
			||||||
 | 
					            The associated club if it exists, else None
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        from club.models import Club
 | 
					        from club.models import Club
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -150,8 +151,8 @@ class MetaGroup(Group):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RealGroup(Group):
 | 
					class RealGroup(Group):
 | 
				
			||||||
    """
 | 
					    """RealGroups are created by the developer.
 | 
				
			||||||
    RealGroups are created by the developer.
 | 
					
 | 
				
			||||||
    Most of the time they match a number in settings to be easily used for permissions.
 | 
					    Most of the time they match a number in settings to be easily used for permissions.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -173,22 +174,26 @@ def validate_promo(value):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_group(*, pk: int = None, name: str = None) -> Optional[Group]:
 | 
					def get_group(*, pk: int = None, name: str = None) -> Optional[Group]:
 | 
				
			||||||
    """
 | 
					    """Search for a group by its primary key or its name.
 | 
				
			||||||
    Search for a group by its primary key or its name.
 | 
					 | 
				
			||||||
    Either one of the two must be set.
 | 
					    Either one of the two must be set.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    The result is cached for the default duration (should be 5 minutes).
 | 
					    The result is cached for the default duration (should be 5 minutes).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param pk: The primary key of the group
 | 
					    Args:
 | 
				
			||||||
    :param name: The name of the group
 | 
					        pk: The primary key of the group
 | 
				
			||||||
    :return: The group if it exists, else None
 | 
					        name: The name of the group
 | 
				
			||||||
    :raise ValueError: If no group matches the criteria
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        The group if it exists, else None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Raises:
 | 
				
			||||||
 | 
					        ValueError: If no group matches the criteria
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    if pk is None and name is None:
 | 
					    if pk is None and name is None:
 | 
				
			||||||
        raise ValueError("Either pk or name must be set")
 | 
					        raise ValueError("Either pk or name must be set")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # replace space characters to hide warnings with memcached backend
 | 
					    # replace space characters to hide warnings with memcached backend
 | 
				
			||||||
    pk_or_name: Union[str, int] = pk if pk is not None else name.replace(" ", "_")
 | 
					    pk_or_name: str | int = pk if pk is not None else name.replace(" ", "_")
 | 
				
			||||||
    group = cache.get(f"sith_group_{pk_or_name}")
 | 
					    group = cache.get(f"sith_group_{pk_or_name}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if group == "not_found":
 | 
					    if group == "not_found":
 | 
				
			||||||
@@ -211,8 +216,7 @@ def get_group(*, pk: int = None, name: str = None) -> Optional[Group]:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class User(AbstractBaseUser):
 | 
					class User(AbstractBaseUser):
 | 
				
			||||||
    """
 | 
					    """Defines the base user class, useable in every app.
 | 
				
			||||||
    Defines the base user class, useable in every app
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    This is almost the same as the auth module AbstractUser since it inherits from it,
 | 
					    This is almost the same as the auth module AbstractUser since it inherits from it,
 | 
				
			||||||
    but some fields are required, and the username is generated automatically with the
 | 
					    but some fields are required, and the username is generated automatically with the
 | 
				
			||||||
@@ -382,9 +386,6 @@ class User(AbstractBaseUser):
 | 
				
			|||||||
        return self.is_active and self.is_superuser
 | 
					        return self.is_active and self.is_superuser
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_absolute_url(self):
 | 
					    def get_absolute_url(self):
 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        This is needed for black magic powered UpdateView's children
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return reverse("core:user_profile", kwargs={"user_id": self.pk})
 | 
					        return reverse("core:user_profile", kwargs={"user_id": self.pk})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
@@ -412,8 +413,7 @@ class User(AbstractBaseUser):
 | 
				
			|||||||
            return 0
 | 
					            return 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_in_group(self, *, pk: int = None, name: str = None) -> bool:
 | 
					    def is_in_group(self, *, pk: int = None, name: str = None) -> bool:
 | 
				
			||||||
        """
 | 
					        """Check if this user is in the given group.
 | 
				
			||||||
        Check if this user is in the given group.
 | 
					 | 
				
			||||||
        Either a group id or a group name must be provided.
 | 
					        Either a group id or a group name must be provided.
 | 
				
			||||||
        If both are passed, only the id will be considered.
 | 
					        If both are passed, only the id will be considered.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -421,7 +421,8 @@ class User(AbstractBaseUser):
 | 
				
			|||||||
        If no group is found, return False.
 | 
					        If no group is found, return False.
 | 
				
			||||||
        If a group is found, check if this user is in the latter.
 | 
					        If a group is found, check if this user is in the latter.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: True if the user is the group, else False
 | 
					        Returns:
 | 
				
			||||||
 | 
					             True if the user is the group, else False
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if pk is not None:
 | 
					        if pk is not None:
 | 
				
			||||||
            group: Optional[Group] = get_group(pk=pk)
 | 
					            group: Optional[Group] = get_group(pk=pk)
 | 
				
			||||||
@@ -454,11 +455,12 @@ class User(AbstractBaseUser):
 | 
				
			|||||||
        return group in self.cached_groups
 | 
					        return group in self.cached_groups
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def cached_groups(self) -> List[Group]:
 | 
					    def cached_groups(self) -> list[Group]:
 | 
				
			||||||
        """
 | 
					        """Get the list of groups this user is in.
 | 
				
			||||||
        Get the list of groups this user is in.
 | 
					
 | 
				
			||||||
        The result is cached for the default duration (should be 5 minutes)
 | 
					        The result is cached for the default duration (should be 5 minutes)
 | 
				
			||||||
        :return: A list of all the groups this user is in
 | 
					
 | 
				
			||||||
 | 
					        Returns: A list of all the groups this user is in.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        groups = cache.get(f"user_{self.id}_groups")
 | 
					        groups = cache.get(f"user_{self.id}_groups")
 | 
				
			||||||
        if groups is None:
 | 
					        if groups is None:
 | 
				
			||||||
@@ -523,9 +525,8 @@ class User(AbstractBaseUser):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @cached_property
 | 
					    @cached_property
 | 
				
			||||||
    def age(self) -> int:
 | 
					    def age(self) -> int:
 | 
				
			||||||
        """
 | 
					        """Return the age this user has the day the method is called.
 | 
				
			||||||
        Return the age this user has the day the method is called.
 | 
					        If the user has not filled his age, return 0.
 | 
				
			||||||
        If the user has not filled his age, return 0
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if self.date_of_birth is None:
 | 
					        if self.date_of_birth is None:
 | 
				
			||||||
            return 0
 | 
					            return 0
 | 
				
			||||||
@@ -576,31 +577,27 @@ class User(AbstractBaseUser):
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_full_name(self):
 | 
					    def get_full_name(self):
 | 
				
			||||||
        """
 | 
					        """Returns the first_name plus the last_name, with a space in between."""
 | 
				
			||||||
        Returns the first_name plus the last_name, with a space in between.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        full_name = "%s %s" % (self.first_name, self.last_name)
 | 
					        full_name = "%s %s" % (self.first_name, self.last_name)
 | 
				
			||||||
        return full_name.strip()
 | 
					        return full_name.strip()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_short_name(self):
 | 
					    def get_short_name(self):
 | 
				
			||||||
        "Returns the short name for the user."
 | 
					        """Returns the short name for the user."""
 | 
				
			||||||
        if self.nick_name:
 | 
					        if self.nick_name:
 | 
				
			||||||
            return self.nick_name
 | 
					            return self.nick_name
 | 
				
			||||||
        return self.first_name + " " + self.last_name
 | 
					        return self.first_name + " " + self.last_name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_display_name(self):
 | 
					    def get_display_name(self) -> str:
 | 
				
			||||||
        """
 | 
					        """Returns the display name of the user.
 | 
				
			||||||
        Returns the display name of the user.
 | 
					
 | 
				
			||||||
        A nickname if possible, otherwise, the full name
 | 
					        A nickname if possible, otherwise, the full name.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if self.nick_name:
 | 
					        if self.nick_name:
 | 
				
			||||||
            return "%s (%s)" % (self.get_full_name(), self.nick_name)
 | 
					            return "%s (%s)" % (self.get_full_name(), self.nick_name)
 | 
				
			||||||
        return self.get_full_name()
 | 
					        return self.get_full_name()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_age(self):
 | 
					    def get_age(self):
 | 
				
			||||||
        """
 | 
					        """Returns the age."""
 | 
				
			||||||
        Returns the age
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        today = timezone.now()
 | 
					        today = timezone.now()
 | 
				
			||||||
        born = self.date_of_birth
 | 
					        born = self.date_of_birth
 | 
				
			||||||
        return (
 | 
					        return (
 | 
				
			||||||
@@ -608,18 +605,18 @@ class User(AbstractBaseUser):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def email_user(self, subject, message, from_email=None, **kwargs):
 | 
					    def email_user(self, subject, message, from_email=None, **kwargs):
 | 
				
			||||||
        """
 | 
					        """Sends an email to this User."""
 | 
				
			||||||
        Sends an email to this User.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if from_email is None:
 | 
					        if from_email is None:
 | 
				
			||||||
            from_email = settings.DEFAULT_FROM_EMAIL
 | 
					            from_email = settings.DEFAULT_FROM_EMAIL
 | 
				
			||||||
        send_mail(subject, message, from_email, [self.email], **kwargs)
 | 
					        send_mail(subject, message, from_email, [self.email], **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def generate_username(self):
 | 
					    def generate_username(self) -> str:
 | 
				
			||||||
        """
 | 
					        """Generates a unique username based on the first and last names.
 | 
				
			||||||
        Generates a unique username based on the first and last names.
 | 
					
 | 
				
			||||||
        For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists
 | 
					        For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.
 | 
				
			||||||
        Returns the generated username
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            The generated username.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        def remove_accents(data):
 | 
					        def remove_accents(data):
 | 
				
			||||||
@@ -644,9 +641,7 @@ class User(AbstractBaseUser):
 | 
				
			|||||||
        return user_name
 | 
					        return user_name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_owner(self, obj):
 | 
					    def is_owner(self, obj):
 | 
				
			||||||
        """
 | 
					        """Determine if the object is owned by the user."""
 | 
				
			||||||
        Determine if the object is owned by the user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if hasattr(obj, "is_owned_by") and obj.is_owned_by(self):
 | 
					        if hasattr(obj, "is_owned_by") and obj.is_owned_by(self):
 | 
				
			||||||
            return True
 | 
					            return True
 | 
				
			||||||
        if hasattr(obj, "owner_group") and self.is_in_group(pk=obj.owner_group.id):
 | 
					        if hasattr(obj, "owner_group") and self.is_in_group(pk=obj.owner_group.id):
 | 
				
			||||||
@@ -656,9 +651,7 @@ class User(AbstractBaseUser):
 | 
				
			|||||||
        return False
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def can_edit(self, obj):
 | 
					    def can_edit(self, obj):
 | 
				
			||||||
        """
 | 
					        """Determine if the object can be edited by the user."""
 | 
				
			||||||
        Determine if the object can be edited by the user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if hasattr(obj, "can_be_edited_by") and obj.can_be_edited_by(self):
 | 
					        if hasattr(obj, "can_be_edited_by") and obj.can_be_edited_by(self):
 | 
				
			||||||
            return True
 | 
					            return True
 | 
				
			||||||
        if hasattr(obj, "edit_groups"):
 | 
					        if hasattr(obj, "edit_groups"):
 | 
				
			||||||
@@ -672,9 +665,7 @@ class User(AbstractBaseUser):
 | 
				
			|||||||
        return False
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def can_view(self, obj):
 | 
					    def can_view(self, obj):
 | 
				
			||||||
        """
 | 
					        """Determine if the object can be viewed by the user."""
 | 
				
			||||||
        Determine if the object can be viewed by the user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self):
 | 
					        if hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self):
 | 
				
			||||||
            return True
 | 
					            return True
 | 
				
			||||||
        if hasattr(obj, "view_groups"):
 | 
					        if hasattr(obj, "view_groups"):
 | 
				
			||||||
@@ -730,11 +721,8 @@ class User(AbstractBaseUser):
 | 
				
			|||||||
            return infos
 | 
					            return infos
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @cached_property
 | 
					    @cached_property
 | 
				
			||||||
    def clubs_with_rights(self):
 | 
					    def clubs_with_rights(self) -> list[Club]:
 | 
				
			||||||
        """
 | 
					        """The list of clubs where the user has rights"""
 | 
				
			||||||
        :return: the list of clubs where the user has rights
 | 
					 | 
				
			||||||
        :rtype: list[club.models.Club]
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        memberships = self.memberships.ongoing().board().select_related("club")
 | 
					        memberships = self.memberships.ongoing().board().select_related("club")
 | 
				
			||||||
        return [m.club for m in memberships]
 | 
					        return [m.club for m in memberships]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -796,9 +784,7 @@ class AnonymousUser(AuthAnonymousUser):
 | 
				
			|||||||
        raise PermissionDenied
 | 
					        raise PermissionDenied
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_in_group(self, *, pk: int = None, name: str = None) -> bool:
 | 
					    def is_in_group(self, *, pk: int = None, name: str = None) -> bool:
 | 
				
			||||||
        """
 | 
					        """The anonymous user is only in the public group."""
 | 
				
			||||||
        The anonymous user is only in the public group
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        allowed_id = settings.SITH_GROUP_PUBLIC_ID
 | 
					        allowed_id = settings.SITH_GROUP_PUBLIC_ID
 | 
				
			||||||
        if pk is not None:
 | 
					        if pk is not None:
 | 
				
			||||||
            return pk == allowed_id
 | 
					            return pk == allowed_id
 | 
				
			||||||
@@ -957,16 +943,15 @@ class SithFile(models.Model):
 | 
				
			|||||||
                ).save()
 | 
					                ).save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def can_be_managed_by(self, user: User) -> bool:
 | 
					    def can_be_managed_by(self, user: User) -> bool:
 | 
				
			||||||
        """
 | 
					        """Tell if the user can manage the file (edit, delete, etc.) or not.
 | 
				
			||||||
        Tell if the user can manage the file (edit, delete, etc.) or not.
 | 
					 | 
				
			||||||
        Apply the following rules:
 | 
					        Apply the following rules:
 | 
				
			||||||
            - If the file is not in the SAS nor in the profiles directory, it can be "managed" by anyone -> return True
 | 
					            - If the file is not in the SAS nor in the profiles directory, it can be "managed" by anyone -> return True
 | 
				
			||||||
            - If the file is in the SAS, only the SAS admins (or roots) can manage it -> return True if the user is in the SAS admin group or is a root
 | 
					            - If the file is in the SAS, only the SAS admins (or roots) can manage it -> return True if the user is in the SAS admin group or is a root
 | 
				
			||||||
            - If the file is in the profiles directory, only the roots can manage it -> return True if the user is a root
 | 
					            - If the file is in the profiles directory, only the roots can manage it -> return True if the user is a root.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :returns: True if the file is managed by the SAS or within the profiles directory, False otherwise
 | 
					        Returns:
 | 
				
			||||||
 | 
					            True if the file is managed by the SAS or within the profiles directory, False otherwise
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					 | 
				
			||||||
        # If the file is not in the SAS nor in the profiles directory, it can be "managed" by anyone
 | 
					        # If the file is not in the SAS nor in the profiles directory, it can be "managed" by anyone
 | 
				
			||||||
        profiles_dir = SithFile.objects.filter(name="profiles").first()
 | 
					        profiles_dir = SithFile.objects.filter(name="profiles").first()
 | 
				
			||||||
        if not self.is_in_sas and not profiles_dir in self.get_parent_list():
 | 
					        if not self.is_in_sas and not profiles_dir in self.get_parent_list():
 | 
				
			||||||
@@ -1017,9 +1002,7 @@ class SithFile(models.Model):
 | 
				
			|||||||
        return super().delete()
 | 
					        return super().delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def clean(self):
 | 
					    def clean(self):
 | 
				
			||||||
        """
 | 
					        """Cleans up the file."""
 | 
				
			||||||
        Cleans up the file
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        super().clean()
 | 
					        super().clean()
 | 
				
			||||||
        if "/" in self.name:
 | 
					        if "/" in self.name:
 | 
				
			||||||
            raise ValidationError(_("Character '/' not authorized in name"))
 | 
					            raise ValidationError(_("Character '/' not authorized in name"))
 | 
				
			||||||
@@ -1070,15 +1053,14 @@ class SithFile(models.Model):
 | 
				
			|||||||
            c.apply_rights_recursively(only_folders=only_folders)
 | 
					            c.apply_rights_recursively(only_folders=only_folders)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def copy_rights(self):
 | 
					    def copy_rights(self):
 | 
				
			||||||
        """Copy, if possible, the rights of the parent folder"""
 | 
					        """Copy, if possible, the rights of the parent folder."""
 | 
				
			||||||
        if self.parent is not None:
 | 
					        if self.parent is not None:
 | 
				
			||||||
            self.edit_groups.set(self.parent.edit_groups.all())
 | 
					            self.edit_groups.set(self.parent.edit_groups.all())
 | 
				
			||||||
            self.view_groups.set(self.parent.view_groups.all())
 | 
					            self.view_groups.set(self.parent.view_groups.all())
 | 
				
			||||||
            self.save()
 | 
					            self.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def move_to(self, parent):
 | 
					    def move_to(self, parent):
 | 
				
			||||||
        """
 | 
					        """Move a file to a new parent.
 | 
				
			||||||
        Move a file to a new parent.
 | 
					 | 
				
			||||||
        `parent` must be a SithFile with the `is_folder=True` property. Otherwise, this function doesn't change
 | 
					        `parent` must be a SithFile with the `is_folder=True` property. Otherwise, this function doesn't change
 | 
				
			||||||
        anything.
 | 
					        anything.
 | 
				
			||||||
        This is done only at the DB level, so that it's very fast for the user. Indeed, this function doesn't modify
 | 
					        This is done only at the DB level, so that it's very fast for the user. Indeed, this function doesn't modify
 | 
				
			||||||
@@ -1091,10 +1073,7 @@ class SithFile(models.Model):
 | 
				
			|||||||
        self.save()
 | 
					        self.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _repair_fs(self):
 | 
					    def _repair_fs(self):
 | 
				
			||||||
        """
 | 
					        """Rebuilds recursively the filesystem as it should be regarding the DB tree."""
 | 
				
			||||||
        This function rebuilds recursively the filesystem as it should be
 | 
					 | 
				
			||||||
        regarding the DB tree.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if self.is_folder:
 | 
					        if self.is_folder:
 | 
				
			||||||
            for c in self.children.all():
 | 
					            for c in self.children.all():
 | 
				
			||||||
                c._repair_fs()
 | 
					                c._repair_fs()
 | 
				
			||||||
@@ -1197,19 +1176,19 @@ class SithFile(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LockError(Exception):
 | 
					class LockError(Exception):
 | 
				
			||||||
    """There was a lock error on the object"""
 | 
					    """There was a lock error on the object."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pass
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AlreadyLocked(LockError):
 | 
					class AlreadyLocked(LockError):
 | 
				
			||||||
    """The object is already locked"""
 | 
					    """The object is already locked."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pass
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class NotLocked(LockError):
 | 
					class NotLocked(LockError):
 | 
				
			||||||
    """The object is not locked"""
 | 
					    """The object is not locked."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pass
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1220,12 +1199,11 @@ def get_default_owner_group():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Page(models.Model):
 | 
					class Page(models.Model):
 | 
				
			||||||
    """
 | 
					    """The page class to build a Wiki
 | 
				
			||||||
    The page class to build a Wiki
 | 
					 | 
				
			||||||
    Each page may have a parent and it's URL is of the form my.site/page/<grd_pa>/<parent>/<mypage>
 | 
					    Each page may have a parent and it's URL is of the form my.site/page/<grd_pa>/<parent>/<mypage>
 | 
				
			||||||
    It has an ID field, but don't use it, since it's only there for DB part, and because compound primary key is
 | 
					    It has an ID field, but don't use it, since it's only there for DB part, and because compound primary key is
 | 
				
			||||||
    awkward!
 | 
					    awkward!
 | 
				
			||||||
    Prefere querying pages with Page.get_page_by_full_name()
 | 
					    Prefere querying pages with Page.get_page_by_full_name().
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Be careful with the _full_name attribute: this field may not be valid until you call save(). It's made for fast
 | 
					    Be careful with the _full_name attribute: this field may not be valid until you call save(). It's made for fast
 | 
				
			||||||
    query, but don't rely on it when playing with a Page object, use get_full_name() instead!
 | 
					    query, but don't rely on it when playing with a Page object, use get_full_name() instead!
 | 
				
			||||||
@@ -1294,9 +1272,7 @@ class Page(models.Model):
 | 
				
			|||||||
        return self.get_full_name()
 | 
					        return self.get_full_name()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def save(self, *args, **kwargs):
 | 
					    def save(self, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """Performs some needed actions before and after saving a page in database."""
 | 
				
			||||||
        Performs some needed actions before and after saving a page in database
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        locked = kwargs.pop("force_lock", False)
 | 
					        locked = kwargs.pop("force_lock", False)
 | 
				
			||||||
        if not locked:
 | 
					        if not locked:
 | 
				
			||||||
            locked = self.is_locked()
 | 
					            locked = self.is_locked()
 | 
				
			||||||
@@ -1317,22 +1293,15 @@ class Page(models.Model):
 | 
				
			|||||||
        self.unset_lock()
 | 
					        self.unset_lock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_absolute_url(self):
 | 
					    def get_absolute_url(self):
 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        This is needed for black magic powered UpdateView's children
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return reverse("core:page", kwargs={"page_name": self._full_name})
 | 
					        return reverse("core:page", kwargs={"page_name": self._full_name})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def get_page_by_full_name(name):
 | 
					    def get_page_by_full_name(name):
 | 
				
			||||||
        """
 | 
					        """Quicker to get a page with that method rather than building the request every time."""
 | 
				
			||||||
        Quicker to get a page with that method rather than building the request every time
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return Page.objects.filter(_full_name=name).first()
 | 
					        return Page.objects.filter(_full_name=name).first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def clean(self):
 | 
					    def clean(self):
 | 
				
			||||||
        """
 | 
					        """Cleans up only the name for the moment, but this can be used to make any treatment before saving the object."""
 | 
				
			||||||
        Cleans up only the name for the moment, but this can be used to make any treatment before saving the object
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if "/" in self.name:
 | 
					        if "/" in self.name:
 | 
				
			||||||
            self.name = self.name.split("/")[-1]
 | 
					            self.name = self.name.split("/")[-1]
 | 
				
			||||||
        if (
 | 
					        if (
 | 
				
			||||||
@@ -1367,10 +1336,11 @@ class Page(models.Model):
 | 
				
			|||||||
        return l
 | 
					        return l
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_locked(self):
 | 
					    def is_locked(self):
 | 
				
			||||||
        """
 | 
					        """Is True if the page is locked, False otherwise.
 | 
				
			||||||
        Is True if the page is locked, False otherwise
 | 
					
 | 
				
			||||||
        This is where the timeout is handled, so a locked page for which the timeout is reach will be unlocked and this
 | 
					        This is where the timeout is handled,
 | 
				
			||||||
        function will return False
 | 
					        so a locked page for which the timeout is reach will be unlocked and this
 | 
				
			||||||
 | 
					        function will return False.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if self.lock_timeout and (
 | 
					        if self.lock_timeout and (
 | 
				
			||||||
            timezone.now() - self.lock_timeout > timedelta(minutes=5)
 | 
					            timezone.now() - self.lock_timeout > timedelta(minutes=5)
 | 
				
			||||||
@@ -1384,9 +1354,7 @@ class Page(models.Model):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def set_lock(self, user):
 | 
					    def set_lock(self, user):
 | 
				
			||||||
        """
 | 
					        """Sets a lock on the current page or raise an AlreadyLocked exception."""
 | 
				
			||||||
        Sets a lock on the current page or raise an AlreadyLocked exception
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if self.is_locked() and self.get_lock() != user:
 | 
					        if self.is_locked() and self.get_lock() != user:
 | 
				
			||||||
            raise AlreadyLocked("The page is already locked by someone else")
 | 
					            raise AlreadyLocked("The page is already locked by someone else")
 | 
				
			||||||
        self.lock_user = user
 | 
					        self.lock_user = user
 | 
				
			||||||
@@ -1395,41 +1363,34 @@ class Page(models.Model):
 | 
				
			|||||||
        # print("Locking page")
 | 
					        # print("Locking page")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def set_lock_recursive(self, user):
 | 
					    def set_lock_recursive(self, user):
 | 
				
			||||||
        """
 | 
					        """Locks recursively all the child pages for editing properties."""
 | 
				
			||||||
        Locks recursively all the child pages for editing properties
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        for p in self.children.all():
 | 
					        for p in self.children.all():
 | 
				
			||||||
            p.set_lock_recursive(user)
 | 
					            p.set_lock_recursive(user)
 | 
				
			||||||
        self.set_lock(user)
 | 
					        self.set_lock(user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def unset_lock_recursive(self):
 | 
					    def unset_lock_recursive(self):
 | 
				
			||||||
        """
 | 
					        """Unlocks recursively all the child pages."""
 | 
				
			||||||
        Unlocks recursively all the child pages
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        for p in self.children.all():
 | 
					        for p in self.children.all():
 | 
				
			||||||
            p.unset_lock_recursive()
 | 
					            p.unset_lock_recursive()
 | 
				
			||||||
        self.unset_lock()
 | 
					        self.unset_lock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def unset_lock(self):
 | 
					    def unset_lock(self):
 | 
				
			||||||
        """Always try to unlock, even if there is no lock"""
 | 
					        """Always try to unlock, even if there is no lock."""
 | 
				
			||||||
        self.lock_user = None
 | 
					        self.lock_user = None
 | 
				
			||||||
        self.lock_timeout = None
 | 
					        self.lock_timeout = None
 | 
				
			||||||
        super().save()
 | 
					        super().save()
 | 
				
			||||||
        # print("Unlocking page")
 | 
					        # print("Unlocking page")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_lock(self):
 | 
					    def get_lock(self):
 | 
				
			||||||
        """
 | 
					        """Returns the page's mutex containing the time and the user in a dict."""
 | 
				
			||||||
        Returns the page's mutex containing the time and the user in a dict
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if self.lock_user:
 | 
					        if self.lock_user:
 | 
				
			||||||
            return self.lock_user
 | 
					            return self.lock_user
 | 
				
			||||||
        raise NotLocked("The page is not locked and thus can not return its user")
 | 
					        raise NotLocked("The page is not locked and thus can not return its user")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_full_name(self):
 | 
					    def get_full_name(self):
 | 
				
			||||||
        """
 | 
					        """Computes the real full_name of the page based on its name and its parent's name
 | 
				
			||||||
        Computes the real full_name of the page based on its name and its parent's name
 | 
					 | 
				
			||||||
        You can and must rely on this function when working on a page object that is not freshly fetched from the DB
 | 
					        You can and must rely on this function when working on a page object that is not freshly fetched from the DB
 | 
				
			||||||
        (For example when treating a Page object coming from a form)
 | 
					        (For example when treating a Page object coming from a form).
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if self.parent is None:
 | 
					        if self.parent is None:
 | 
				
			||||||
            return self.name
 | 
					            return self.name
 | 
				
			||||||
@@ -1463,8 +1424,8 @@ class Page(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PageRev(models.Model):
 | 
					class PageRev(models.Model):
 | 
				
			||||||
    """
 | 
					    """True content of the page.
 | 
				
			||||||
    This is the true content of the page.
 | 
					
 | 
				
			||||||
    Each page object has a revisions field that is a list of PageRev, ordered by date.
 | 
					    Each page object has a revisions field that is a list of PageRev, ordered by date.
 | 
				
			||||||
    my_page.revisions.last() gives the PageRev object that is the most up-to-date, and thus,
 | 
					    my_page.revisions.last() gives the PageRev object that is the most up-to-date, and thus,
 | 
				
			||||||
    is the real content of the page.
 | 
					    is the real content of the page.
 | 
				
			||||||
@@ -1492,9 +1453,6 @@ class PageRev(models.Model):
 | 
				
			|||||||
        self.page.unset_lock()
 | 
					        self.page.unset_lock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_absolute_url(self):
 | 
					    def get_absolute_url(self):
 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        This is needed for black magic powered UpdateView's children
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return reverse("core:page", kwargs={"page_name": self.page._full_name})
 | 
					        return reverse("core:page", kwargs={"page_name": self.page._full_name})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __getattribute__(self, attr):
 | 
					    def __getattribute__(self, attr):
 | 
				
			||||||
@@ -1573,9 +1531,7 @@ class Gift(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class OperationLog(models.Model):
 | 
					class OperationLog(models.Model):
 | 
				
			||||||
    """
 | 
					    """General purpose log object to register operations."""
 | 
				
			||||||
    General purpose log object to register operations
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    date = models.DateTimeField(_("date"), auto_now_add=True)
 | 
					    date = models.DateTimeField(_("date"), auto_now_add=True)
 | 
				
			||||||
    label = models.CharField(_("label"), max_length=255)
 | 
					    label = models.CharField(_("label"), max_length=255)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,24 +21,26 @@
 | 
				
			|||||||
#
 | 
					#
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""Collection of utils for custom migration tricks.
 | 
				
			||||||
This page is useful for custom migration tricks.
 | 
					
 | 
				
			||||||
Sometimes, when you need to have a migration hack and you think it can be
 | 
					Sometimes, when you need to have a migration hack,
 | 
				
			||||||
useful again, put it there, we never know if we might need the hack again.
 | 
					 and you think it can be useful again,
 | 
				
			||||||
 | 
					put it there, we never know if we might need the hack again.
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.db import connection, migrations
 | 
					from django.db import connection, migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PsqlRunOnly(migrations.RunSQL):
 | 
					class PsqlRunOnly(migrations.RunSQL):
 | 
				
			||||||
    """
 | 
					    """SQL runner for PostgreSQL-only queries.
 | 
				
			||||||
    This is an SQL runner that will launch the given command only if
 | 
					
 | 
				
			||||||
    the used DBMS is PostgreSQL.
 | 
					 | 
				
			||||||
    It may be useful to run Postgres' specific SQL, or to take actions
 | 
					    It may be useful to run Postgres' specific SQL, or to take actions
 | 
				
			||||||
    that would be non-senses with backends other than Postgre, such
 | 
					    that would be non-senses with backends other than Postgre, such
 | 
				
			||||||
    as disabling particular constraints that would prevent the migration
 | 
					    as disabling particular constraints that would prevent the migration
 | 
				
			||||||
    to run successfully.
 | 
					    to run successfully.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    If used on another DBMS than Postgres, it will be a noop.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    See `club/migrations/0010_auto_20170912_2028.py` as an example.
 | 
					    See `club/migrations/0010_auto_20170912_2028.py` as an example.
 | 
				
			||||||
    Some explanations can be found here too:
 | 
					    Some explanations can be found here too:
 | 
				
			||||||
    https://stackoverflow.com/questions/28429933/django-migrations-using-runpython-to-commit-changes
 | 
					    https://stackoverflow.com/questions/28429933/django-migrations-using-runpython-to-commit-changes
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,9 +31,7 @@ from django.core.files.storage import FileSystemStorage
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ScssFinder(FileSystemFinder):
 | 
					class ScssFinder(FileSystemFinder):
 | 
				
			||||||
    """
 | 
					    """Find static *.css files compiled on the fly."""
 | 
				
			||||||
    Find static *.css files compiled on the fly
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    locations = []
 | 
					    locations = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,10 +35,9 @@ from core.scss.storage import ScssFileStorage, find_file
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ScssProcessor(object):
 | 
					class ScssProcessor(object):
 | 
				
			||||||
    """
 | 
					    """If DEBUG mode enabled : compile the scss file
 | 
				
			||||||
    If DEBUG mode enabled : compile the scss file
 | 
					 | 
				
			||||||
    Else : give the path of the corresponding css supposed to already be compiled
 | 
					    Else : give the path of the corresponding css supposed to already be compiled
 | 
				
			||||||
    Don't forget to use compilestatics to compile scss for production
 | 
					    Don't forget to use compilestatics to compile scss for production.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    prefix = iri_to_uri(getattr(settings, "STATIC_URL", "/static/"))
 | 
					    prefix = iri_to_uri(getattr(settings, "STATIC_URL", "/static/"))
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -91,9 +91,9 @@ class IndexSignalProcessor(signals.BaseSignalProcessor):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BigCharFieldIndex(indexes.CharField):
 | 
					class BigCharFieldIndex(indexes.CharField):
 | 
				
			||||||
    """
 | 
					    """Workaround to avoid xapian.InvalidArgument: Term too long (> 245).
 | 
				
			||||||
    Workaround to avoid xapian.InvalidArgument: Term too long (> 245)
 | 
					
 | 
				
			||||||
    See https://groups.google.com/forum/#!topic/django-haystack/hRJKcPNPXqw/discussion
 | 
					    See https://groups.google.com/forum/#!topic/django-haystack/hRJKcPNPXqw/discussion.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def prepare(self, term):
 | 
					    def prepare(self, term):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,9 +7,7 @@ from core.models import User
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@receiver(m2m_changed, sender=User.groups.through, dispatch_uid="user_groups_changed")
 | 
					@receiver(m2m_changed, sender=User.groups.through, dispatch_uid="user_groups_changed")
 | 
				
			||||||
def user_groups_changed(sender, instance: User, **kwargs):
 | 
					def user_groups_changed(sender, instance: User, **kwargs):
 | 
				
			||||||
    """
 | 
					    """Clear the cached groups of the user."""
 | 
				
			||||||
    Clear the cached groups of the user
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    # As a m2m relationship doesn't live within the model
 | 
					    # As a m2m relationship doesn't live within the model
 | 
				
			||||||
    # but rather on an intermediary table, there is no
 | 
					    # but rather on an intermediary table, there is no
 | 
				
			||||||
    # model method to override, meaning we must use
 | 
					    # model method to override, meaning we must use
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,11 +30,11 @@ from jinja2.parser import Parser
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class HoneypotExtension(Extension):
 | 
					class HoneypotExtension(Extension):
 | 
				
			||||||
    """
 | 
					    """Wrapper around the honeypot extension tag.
 | 
				
			||||||
    Wrapper around the honeypot extension tag
 | 
					 | 
				
			||||||
    Known limitation: doesn't support arguments
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Usage: {% render_honeypot_field %}
 | 
					    Known limitation: doesn't support arguments.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Usage: `{% render_honeypot_field %}`
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    tags = {"render_honeypot_field"}
 | 
					    tags = {"render_honeypot_field"}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -46,9 +46,7 @@ def markdown(text):
 | 
				
			|||||||
def phonenumber(
 | 
					def phonenumber(
 | 
				
			||||||
    value, country="FR", number_format=phonenumbers.PhoneNumberFormat.NATIONAL
 | 
					    value, country="FR", number_format=phonenumbers.PhoneNumberFormat.NATIONAL
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
    """
 | 
					    # collectivised from https://github.com/foundertherapy/django-phonenumber-filter.
 | 
				
			||||||
    This filter is kindly borrowed from https://github.com/foundertherapy/django-phonenumber-filter
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    value = str(value)
 | 
					    value = str(value)
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        parsed = phonenumbers.parse(value, country)
 | 
					        parsed = phonenumbers.parse(value, country)
 | 
				
			||||||
@@ -59,6 +57,12 @@ def phonenumber(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@register.filter(name="truncate_time")
 | 
					@register.filter(name="truncate_time")
 | 
				
			||||||
def truncate_time(value, time_unit):
 | 
					def truncate_time(value, time_unit):
 | 
				
			||||||
 | 
					    """Remove everything in the time format lower than the specified unit.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        value: the value to truncate
 | 
				
			||||||
 | 
					        time_unit: the lowest unit to display
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
    value = str(value)
 | 
					    value = str(value)
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
        "millis": lambda: value.split(".")[0],
 | 
					        "millis": lambda: value.split(".")[0],
 | 
				
			||||||
@@ -81,8 +85,6 @@ def format_timedelta(value: datetime.timedelta) -> str:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@register.simple_tag()
 | 
					@register.simple_tag()
 | 
				
			||||||
def scss(path):
 | 
					def scss(path):
 | 
				
			||||||
    """
 | 
					    """Return path of the corresponding css file after compilation."""
 | 
				
			||||||
    Return path of the corresponding css file after compilation
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    processor = ScssProcessor(path)
 | 
					    processor = ScssProcessor(path)
 | 
				
			||||||
    return processor.get_converted_scss()
 | 
					    return processor.get_converted_scss()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -105,7 +105,7 @@ class TestUserRegistration:
 | 
				
			|||||||
    def test_register_fail_with_not_existing_email(
 | 
					    def test_register_fail_with_not_existing_email(
 | 
				
			||||||
        self, client: Client, valid_payload, monkeypatch
 | 
					        self, client: Client, valid_payload, monkeypatch
 | 
				
			||||||
    ):
 | 
					    ):
 | 
				
			||||||
        """Test that, when email is valid but doesn't actually exist, registration fails"""
 | 
					        """Test that, when email is valid but doesn't actually exist, registration fails."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        def always_fail(*_args, **_kwargs):
 | 
					        def always_fail(*_args, **_kwargs):
 | 
				
			||||||
            raise SMTPException
 | 
					            raise SMTPException
 | 
				
			||||||
@@ -127,10 +127,7 @@ class TestUserLogin:
 | 
				
			|||||||
        return User.objects.first()
 | 
					        return User.objects.first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_login_fail(self, client, user):
 | 
					    def test_login_fail(self, client, user):
 | 
				
			||||||
        """
 | 
					        """Should not login a user correctly."""
 | 
				
			||||||
        Should not login a user correctly
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        response = client.post(
 | 
					        response = client.post(
 | 
				
			||||||
            reverse("core:login"),
 | 
					            reverse("core:login"),
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -158,9 +155,7 @@ class TestUserLogin:
 | 
				
			|||||||
        assert response.wsgi_request.user.is_anonymous
 | 
					        assert response.wsgi_request.user.is_anonymous
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_login_success(self, client, user):
 | 
					    def test_login_success(self, client, user):
 | 
				
			||||||
        """
 | 
					        """Should login a user correctly."""
 | 
				
			||||||
        Should login a user correctly
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        response = client.post(
 | 
					        response = client.post(
 | 
				
			||||||
            reverse("core:login"),
 | 
					            reverse("core:login"),
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -210,7 +205,7 @@ class TestUserLogin:
 | 
				
			|||||||
    ],
 | 
					    ],
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
def test_custom_markdown_syntax(md, html):
 | 
					def test_custom_markdown_syntax(md, html):
 | 
				
			||||||
    """Test the homemade markdown syntax"""
 | 
					    """Test the homemade markdown syntax."""
 | 
				
			||||||
    assert markdown(md) == f"<p>{html}</p>\n"
 | 
					    assert markdown(md) == f"<p>{html}</p>\n"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -233,7 +228,6 @@ class PageHandlingTest(TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def test_create_page_ok(self):
 | 
					    def test_create_page_ok(self):
 | 
				
			||||||
        """Should create a page correctly."""
 | 
					        """Should create a page correctly."""
 | 
				
			||||||
 | 
					 | 
				
			||||||
        response = self.client.post(
 | 
					        response = self.client.post(
 | 
				
			||||||
            reverse("core:page_new"),
 | 
					            reverse("core:page_new"),
 | 
				
			||||||
            {"parent": "", "name": "guy", "owner_group": self.root_group.id},
 | 
					            {"parent": "", "name": "guy", "owner_group": self.root_group.id},
 | 
				
			||||||
@@ -274,9 +268,7 @@ class PageHandlingTest(TestCase):
 | 
				
			|||||||
        assert '<a href="/page/guy/bibou/">' in str(response.content)
 | 
					        assert '<a href="/page/guy/bibou/">' in str(response.content)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_access_child_page_ok(self):
 | 
					    def test_access_child_page_ok(self):
 | 
				
			||||||
        """
 | 
					        """Should display a page correctly."""
 | 
				
			||||||
        Should display a page correctly
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        parent = Page(name="guy", owner_group=self.root_group)
 | 
					        parent = Page(name="guy", owner_group=self.root_group)
 | 
				
			||||||
        parent.save(force_lock=True)
 | 
					        parent.save(force_lock=True)
 | 
				
			||||||
        page = Page(name="bibou", owner_group=self.root_group, parent=parent)
 | 
					        page = Page(name="bibou", owner_group=self.root_group, parent=parent)
 | 
				
			||||||
@@ -289,18 +281,14 @@ class PageHandlingTest(TestCase):
 | 
				
			|||||||
        self.assertIn('<a href="/page/guy/bibou/edit/">', html)
 | 
					        self.assertIn('<a href="/page/guy/bibou/edit/">', html)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_access_page_not_found(self):
 | 
					    def test_access_page_not_found(self):
 | 
				
			||||||
        """
 | 
					        """Should not display a page correctly."""
 | 
				
			||||||
        Should not display a page correctly
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        response = self.client.get(reverse("core:page", kwargs={"page_name": "swagg"}))
 | 
					        response = self.client.get(reverse("core:page", kwargs={"page_name": "swagg"}))
 | 
				
			||||||
        assert response.status_code == 200
 | 
					        assert response.status_code == 200
 | 
				
			||||||
        html = response.content.decode()
 | 
					        html = response.content.decode()
 | 
				
			||||||
        self.assertIn('<a href="/page/create/?page=swagg">', html)
 | 
					        self.assertIn('<a href="/page/create/?page=swagg">', html)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_create_page_markdown_safe(self):
 | 
					    def test_create_page_markdown_safe(self):
 | 
				
			||||||
        """
 | 
					        """Should format the markdown and escape html correctly."""
 | 
				
			||||||
        Should format the markdown and escape html correctly
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.client.post(
 | 
					        self.client.post(
 | 
				
			||||||
            reverse("core:page_new"), {"parent": "", "name": "guy", "owner_group": "1"}
 | 
					            reverse("core:page_new"), {"parent": "", "name": "guy", "owner_group": "1"}
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
@@ -335,13 +323,13 @@ http://git.an
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class UserToolsTest:
 | 
					class UserToolsTest:
 | 
				
			||||||
    def test_anonymous_user_unauthorized(self, client):
 | 
					    def test_anonymous_user_unauthorized(self, client):
 | 
				
			||||||
        """An anonymous user shouldn't have access to the tools page"""
 | 
					        """An anonymous user shouldn't have access to the tools page."""
 | 
				
			||||||
        response = client.get(reverse("core:user_tools"))
 | 
					        response = client.get(reverse("core:user_tools"))
 | 
				
			||||||
        assert response.status_code == 403
 | 
					        assert response.status_code == 403
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @pytest.mark.parametrize("username", ["guy", "root", "skia", "comunity"])
 | 
					    @pytest.mark.parametrize("username", ["guy", "root", "skia", "comunity"])
 | 
				
			||||||
    def test_page_is_working(self, client, username):
 | 
					    def test_page_is_working(self, client, username):
 | 
				
			||||||
        """All existing users should be able to see the test page"""
 | 
					        """All existing users should be able to see the test page."""
 | 
				
			||||||
        # Test for simple user
 | 
					        # Test for simple user
 | 
				
			||||||
        client.force_login(User.objects.get(username=username))
 | 
					        client.force_login(User.objects.get(username=username))
 | 
				
			||||||
        response = client.get(reverse("core:user_tools"))
 | 
					        response = client.get(reverse("core:user_tools"))
 | 
				
			||||||
@@ -391,9 +379,8 @@ class FileHandlingTest(TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserIsInGroupTest(TestCase):
 | 
					class UserIsInGroupTest(TestCase):
 | 
				
			||||||
    """
 | 
					    """Test that the User.is_in_group() and AnonymousUser.is_in_group()
 | 
				
			||||||
    Test that the User.is_in_group() and AnonymousUser.is_in_group()
 | 
					    work as intended.
 | 
				
			||||||
    work as intended
 | 
					 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
@@ -450,30 +437,24 @@ class UserIsInGroupTest(TestCase):
 | 
				
			|||||||
        assert user.is_in_group(name=meta_groups_members) is False
 | 
					        assert user.is_in_group(name=meta_groups_members) is False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_anonymous_user(self):
 | 
					    def test_anonymous_user(self):
 | 
				
			||||||
        """
 | 
					        """Test that anonymous users are only in the public group."""
 | 
				
			||||||
        Test that anonymous users are only in the public group
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        user = AnonymousUser()
 | 
					        user = AnonymousUser()
 | 
				
			||||||
        self.assert_only_in_public_group(user)
 | 
					        self.assert_only_in_public_group(user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_not_subscribed_user(self):
 | 
					    def test_not_subscribed_user(self):
 | 
				
			||||||
        """
 | 
					        """Test that users who never subscribed are only in the public group."""
 | 
				
			||||||
        Test that users who never subscribed are only in the public group
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.assert_only_in_public_group(self.toto)
 | 
					        self.assert_only_in_public_group(self.toto)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_wrong_parameter_fail(self):
 | 
					    def test_wrong_parameter_fail(self):
 | 
				
			||||||
        """
 | 
					        """Test that when neither the pk nor the name argument is given,
 | 
				
			||||||
        Test that when neither the pk nor the name argument is given,
 | 
					        the function raises a ValueError.
 | 
				
			||||||
        the function raises a ValueError
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        with self.assertRaises(ValueError):
 | 
					        with self.assertRaises(ValueError):
 | 
				
			||||||
            self.toto.is_in_group()
 | 
					            self.toto.is_in_group()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_number_queries(self):
 | 
					    def test_number_queries(self):
 | 
				
			||||||
        """
 | 
					        """Test that the number of db queries is stable
 | 
				
			||||||
        Test that the number of db queries is stable
 | 
					        and that less queries are made when making a new call.
 | 
				
			||||||
        and that less queries are made when making a new call
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        # make sure Skia is in at least one group
 | 
					        # make sure Skia is in at least one group
 | 
				
			||||||
        self.skia.groups.add(Group.objects.first().pk)
 | 
					        self.skia.groups.add(Group.objects.first().pk)
 | 
				
			||||||
@@ -497,9 +478,8 @@ class UserIsInGroupTest(TestCase):
 | 
				
			|||||||
            self.skia.is_in_group(pk=group_not_in.id)
 | 
					            self.skia.is_in_group(pk=group_not_in.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_cache_properly_cleared_membership(self):
 | 
					    def test_cache_properly_cleared_membership(self):
 | 
				
			||||||
        """
 | 
					        """Test that when the membership of a user end,
 | 
				
			||||||
        Test that when the membership of a user end,
 | 
					        the cache is properly invalidated.
 | 
				
			||||||
        the cache is properly invalidated
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        membership = Membership.objects.create(
 | 
					        membership = Membership.objects.create(
 | 
				
			||||||
            club=self.club, user=self.toto, end_date=None
 | 
					            club=self.club, user=self.toto, end_date=None
 | 
				
			||||||
@@ -515,9 +495,8 @@ class UserIsInGroupTest(TestCase):
 | 
				
			|||||||
        assert self.toto.is_in_group(name=meta_groups_members) is False
 | 
					        assert self.toto.is_in_group(name=meta_groups_members) is False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_cache_properly_cleared_group(self):
 | 
					    def test_cache_properly_cleared_group(self):
 | 
				
			||||||
        """
 | 
					        """Test that when a user is removed from a group,
 | 
				
			||||||
        Test that when a user is removed from a group,
 | 
					        the is_in_group_method return False when calling it again.
 | 
				
			||||||
        the is_in_group_method return False when calling it again
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        # testing with pk
 | 
					        # testing with pk
 | 
				
			||||||
        self.toto.groups.add(self.com_admin.pk)
 | 
					        self.toto.groups.add(self.com_admin.pk)
 | 
				
			||||||
@@ -534,9 +513,8 @@ class UserIsInGroupTest(TestCase):
 | 
				
			|||||||
        assert self.toto.is_in_group(name="SAS admin") is False
 | 
					        assert self.toto.is_in_group(name="SAS admin") is False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_not_existing_group(self):
 | 
					    def test_not_existing_group(self):
 | 
				
			||||||
        """
 | 
					        """Test that searching for a not existing group
 | 
				
			||||||
        Test that searching for a not existing group
 | 
					        returns False.
 | 
				
			||||||
        returns False
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        assert self.skia.is_in_group(name="This doesn't exist") is False
 | 
					        assert self.skia.is_in_group(name="This doesn't exist") is False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -557,9 +535,7 @@ class DateUtilsTest(TestCase):
 | 
				
			|||||||
        cls.spring_first_day = date(2023, cls.spring_month, cls.spring_day)
 | 
					        cls.spring_first_day = date(2023, cls.spring_month, cls.spring_day)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_get_semester(self):
 | 
					    def test_get_semester(self):
 | 
				
			||||||
        """
 | 
					        """Test that the get_semester function returns the correct semester string."""
 | 
				
			||||||
        Test that the get_semester function returns the correct semester string
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        assert get_semester_code(self.autumn_semester_january) == "A24"
 | 
					        assert get_semester_code(self.autumn_semester_january) == "A24"
 | 
				
			||||||
        assert get_semester_code(self.autumn_semester_september) == "A24"
 | 
					        assert get_semester_code(self.autumn_semester_september) == "A24"
 | 
				
			||||||
        assert get_semester_code(self.autumn_first_day) == "A24"
 | 
					        assert get_semester_code(self.autumn_first_day) == "A24"
 | 
				
			||||||
@@ -568,9 +544,7 @@ class DateUtilsTest(TestCase):
 | 
				
			|||||||
        assert get_semester_code(self.spring_first_day) == "P23"
 | 
					        assert get_semester_code(self.spring_first_day) == "P23"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_get_start_of_semester_fixed_date(self):
 | 
					    def test_get_start_of_semester_fixed_date(self):
 | 
				
			||||||
        """
 | 
					        """Test that the get_start_of_semester correctly the starting date of the semester."""
 | 
				
			||||||
        Test that the get_start_of_semester correctly the starting date of the semester.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        automn_2024 = date(2024, self.autumn_month, self.autumn_day)
 | 
					        automn_2024 = date(2024, self.autumn_month, self.autumn_day)
 | 
				
			||||||
        assert get_start_of_semester(self.autumn_semester_january) == automn_2024
 | 
					        assert get_start_of_semester(self.autumn_semester_january) == automn_2024
 | 
				
			||||||
        assert get_start_of_semester(self.autumn_semester_september) == automn_2024
 | 
					        assert get_start_of_semester(self.autumn_semester_september) == automn_2024
 | 
				
			||||||
@@ -581,9 +555,8 @@ class DateUtilsTest(TestCase):
 | 
				
			|||||||
        assert get_start_of_semester(self.spring_first_day) == spring_2023
 | 
					        assert get_start_of_semester(self.spring_first_day) == spring_2023
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_get_start_of_semester_today(self):
 | 
					    def test_get_start_of_semester_today(self):
 | 
				
			||||||
        """
 | 
					        """Test that the get_start_of_semester returns the start of the current semester
 | 
				
			||||||
        Test that the get_start_of_semester returns the start of the current semester
 | 
					        when no date is given.
 | 
				
			||||||
        when no date is given
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        with freezegun.freeze_time(self.autumn_semester_september):
 | 
					        with freezegun.freeze_time(self.autumn_semester_september):
 | 
				
			||||||
            assert get_start_of_semester() == self.autumn_first_day
 | 
					            assert get_start_of_semester() == self.autumn_first_day
 | 
				
			||||||
@@ -592,8 +565,7 @@ class DateUtilsTest(TestCase):
 | 
				
			|||||||
            assert get_start_of_semester() == self.spring_first_day
 | 
					            assert get_start_of_semester() == self.spring_first_day
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_get_start_of_semester_changing_date(self):
 | 
					    def test_get_start_of_semester_changing_date(self):
 | 
				
			||||||
        """
 | 
					        """Test that the get_start_of_semester correctly gives the starting date of the semester,
 | 
				
			||||||
        Test that the get_start_of_semester correctly gives the starting date of the semester,
 | 
					 | 
				
			||||||
        even when the semester changes while the server isn't restarted.
 | 
					        even when the semester changes while the server isn't restarted.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        spring_2023 = date(2023, self.spring_month, self.spring_day)
 | 
					        spring_2023 = date(2023, self.spring_month, self.spring_day)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,9 +31,7 @@ from PIL.Image import Resampling
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_git_revision_short_hash() -> str:
 | 
					def get_git_revision_short_hash() -> str:
 | 
				
			||||||
    """
 | 
					    """Return the short hash of the current commit."""
 | 
				
			||||||
    Return the short hash of the current commit
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        output = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"])
 | 
					        output = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"])
 | 
				
			||||||
        if isinstance(output, bytes):
 | 
					        if isinstance(output, bytes):
 | 
				
			||||||
@@ -44,8 +42,7 @@ def get_git_revision_short_hash() -> str:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_start_of_semester(today: Optional[date] = None) -> date:
 | 
					def get_start_of_semester(today: Optional[date] = None) -> date:
 | 
				
			||||||
    """
 | 
					    """Return the date of the start of the semester of the given date.
 | 
				
			||||||
    Return the date of the start of the semester of the given date.
 | 
					 | 
				
			||||||
    If no date is given, return the start date of the current semester.
 | 
					    If no date is given, return the start date of the current semester.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    The current semester is computed as follows:
 | 
					    The current semester is computed as follows:
 | 
				
			||||||
@@ -54,8 +51,11 @@ def get_start_of_semester(today: Optional[date] = None) -> date:
 | 
				
			|||||||
    - If the date is between 01/01 and 15/02  => Autumn semester of the previous year.
 | 
					    - If the date is between 01/01 and 15/02  => Autumn semester of the previous year.
 | 
				
			||||||
    - If the date is between 15/02 and 15/08  => Spring semester
 | 
					    - If the date is between 15/02 and 15/08  => Spring semester
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param today: the date to use to compute the semester. If None, use today's date.
 | 
					    Args:
 | 
				
			||||||
    :return: the date of the start of the semester
 | 
					        today: the date to use to compute the semester. If None, use today's date.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        the date of the start of the semester
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    if today is None:
 | 
					    if today is None:
 | 
				
			||||||
        today = timezone.now().date()
 | 
					        today = timezone.now().date()
 | 
				
			||||||
@@ -72,16 +72,18 @@ def get_start_of_semester(today: Optional[date] = None) -> date:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_semester_code(d: Optional[date] = None) -> str:
 | 
					def get_semester_code(d: Optional[date] = None) -> str:
 | 
				
			||||||
    """
 | 
					    """Return the semester code of the given date.
 | 
				
			||||||
    Return the semester code of the given date.
 | 
					 | 
				
			||||||
    If no date is given, return the semester code of the current semester.
 | 
					    If no date is given, return the semester code of the current semester.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    The semester code is an upper letter (A for autumn, P for spring),
 | 
					    The semester code is an upper letter (A for autumn, P for spring),
 | 
				
			||||||
    followed by the last two digits of the year.
 | 
					    followed by the last two digits of the year.
 | 
				
			||||||
    For example, the autumn semester of 2018 is "A18".
 | 
					    For example, the autumn semester of 2018 is "A18".
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param d: the date to use to compute the semester. If None, use today's date.
 | 
					    Args:
 | 
				
			||||||
    :return: the semester code corresponding to the given date
 | 
					        d: the date to use to compute the semester. If None, use today's date.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        the semester code corresponding to the given date
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    if d is None:
 | 
					    if d is None:
 | 
				
			||||||
        d = timezone.now().date()
 | 
					        d = timezone.now().date()
 | 
				
			||||||
@@ -147,8 +149,15 @@ def exif_auto_rotate(image):
 | 
				
			|||||||
    return image
 | 
					    return image
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def doku_to_markdown(text):
 | 
					def doku_to_markdown(text: str) -> str:
 | 
				
			||||||
    """This is a quite correct doku translator"""
 | 
					    """Convert doku text to the corresponding markdown.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        text: the doku text to convert
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        The converted markdown text
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
    text = re.sub(
 | 
					    text = re.sub(
 | 
				
			||||||
        r"([^:]|^)\/\/(.*?)\/\/", r"*\2*", text
 | 
					        r"([^:]|^)\/\/(.*?)\/\/", r"*\2*", text
 | 
				
			||||||
    )  # Italic (prevents protocol:// conflict)
 | 
					    )  # Italic (prevents protocol:// conflict)
 | 
				
			||||||
@@ -235,7 +244,14 @@ def doku_to_markdown(text):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def bbcode_to_markdown(text):
 | 
					def bbcode_to_markdown(text):
 | 
				
			||||||
    """This is a very basic BBcode translator"""
 | 
					    """Convert bbcode text to the corresponding markdown.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        text: the bbcode text to convert
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        The converted markdown text
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
    text = re.sub(r"\[b\](.*?)\[\/b\]", r"**\1**", text, flags=re.DOTALL)  # Bold
 | 
					    text = re.sub(r"\[b\](.*?)\[\/b\]", r"**\1**", text, flags=re.DOTALL)  # Bold
 | 
				
			||||||
    text = re.sub(r"\[i\](.*?)\[\/i\]", r"*\1*", text, flags=re.DOTALL)  # Italic
 | 
					    text = re.sub(r"\[i\](.*?)\[\/i\]", r"*\1*", text, flags=re.DOTALL)  # Italic
 | 
				
			||||||
    text = re.sub(r"\[u\](.*?)\[\/u\]", r"__\1__", text, flags=re.DOTALL)  # Underline
 | 
					    text = re.sub(r"\[u\](.*?)\[\/u\]", r"__\1__", text, flags=re.DOTALL)  # Underline
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,6 +23,7 @@
 | 
				
			|||||||
#
 | 
					#
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import types
 | 
					import types
 | 
				
			||||||
 | 
					from typing import Any
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.core.exceptions import (
 | 
					from django.core.exceptions import (
 | 
				
			||||||
    ImproperlyConfigured,
 | 
					    ImproperlyConfigured,
 | 
				
			||||||
@@ -39,6 +40,7 @@ from django.views.generic.detail import SingleObjectMixin
 | 
				
			|||||||
from django.views.generic.edit import FormView
 | 
					from django.views.generic.edit import FormView
 | 
				
			||||||
from sentry_sdk import last_event_id
 | 
					from sentry_sdk import last_event_id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from core.models import User
 | 
				
			||||||
from core.views.forms import LoginForm
 | 
					from core.views.forms import LoginForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -60,60 +62,63 @@ def internal_servor_error(request):
 | 
				
			|||||||
    return HttpResponseServerError(render(request, "core/500.jinja"))
 | 
					    return HttpResponseServerError(render(request, "core/500.jinja"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def can_edit_prop(obj, user):
 | 
					def can_edit_prop(obj: Any, user: User) -> bool:
 | 
				
			||||||
    """
 | 
					    """Can the user edit the properties of the object.
 | 
				
			||||||
    :param obj: Object to test for permission
 | 
					 | 
				
			||||||
    :param user: core.models.User to test permissions against
 | 
					 | 
				
			||||||
    :return: if user is authorized to edit object properties
 | 
					 | 
				
			||||||
    :rtype: bool
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :Example:
 | 
					    Args:
 | 
				
			||||||
 | 
					        obj: Object to test for permission
 | 
				
			||||||
 | 
					        user: core.models.User to test permissions against
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .. code-block:: python
 | 
					    Returns:
 | 
				
			||||||
 | 
					        True if user is authorized to edit object properties else False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Examples:
 | 
				
			||||||
 | 
					        ```python
 | 
				
			||||||
        if not can_edit_prop(self.object ,request.user):
 | 
					        if not can_edit_prop(self.object ,request.user):
 | 
				
			||||||
            raise PermissionDenied
 | 
					            raise PermissionDenied
 | 
				
			||||||
 | 
					        ```
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    if obj is None or user.is_owner(obj):
 | 
					    if obj is None or user.is_owner(obj):
 | 
				
			||||||
        return True
 | 
					        return True
 | 
				
			||||||
    return False
 | 
					    return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def can_edit(obj, user):
 | 
					def can_edit(obj: Any, user: User):
 | 
				
			||||||
    """
 | 
					    """Can the user edit the object.
 | 
				
			||||||
    :param obj: Object to test for permission
 | 
					 | 
				
			||||||
    :param user: core.models.User to test permissions against
 | 
					 | 
				
			||||||
    :return: if user is authorized to edit object
 | 
					 | 
				
			||||||
    :rtype: bool
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :Example:
 | 
					    Args:
 | 
				
			||||||
 | 
					        obj: Object to test for permission
 | 
				
			||||||
 | 
					        user: core.models.User to test permissions against
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .. code-block:: python
 | 
					    Returns:
 | 
				
			||||||
 | 
					        True if user is authorized to edit object else False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if not can_edit(self.object ,request.user):
 | 
					    Examples:
 | 
				
			||||||
 | 
					        ```python
 | 
				
			||||||
 | 
					        if not can_edit(self.object, request.user):
 | 
				
			||||||
            raise PermissionDenied
 | 
					            raise PermissionDenied
 | 
				
			||||||
 | 
					        ```
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    if obj is None or user.can_edit(obj):
 | 
					    if obj is None or user.can_edit(obj):
 | 
				
			||||||
        return True
 | 
					        return True
 | 
				
			||||||
    return can_edit_prop(obj, user)
 | 
					    return can_edit_prop(obj, user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def can_view(obj, user):
 | 
					def can_view(obj: Any, user: User):
 | 
				
			||||||
    """
 | 
					    """Can the user see the object.
 | 
				
			||||||
    :param obj: Object to test for permission
 | 
					 | 
				
			||||||
    :param user: core.models.User to test permissions against
 | 
					 | 
				
			||||||
    :return: if user is authorized to see object
 | 
					 | 
				
			||||||
    :rtype: bool
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :Example:
 | 
					    Args:
 | 
				
			||||||
 | 
					        obj: Object to test for permission
 | 
				
			||||||
 | 
					        user: core.models.User to test permissions against
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .. code-block:: python
 | 
					    Returns:
 | 
				
			||||||
 | 
					        True if user is authorized to see object else False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Examples:
 | 
				
			||||||
 | 
					        ```python
 | 
				
			||||||
        if not can_view(self.object ,request.user):
 | 
					        if not can_view(self.object ,request.user):
 | 
				
			||||||
            raise PermissionDenied
 | 
					            raise PermissionDenied
 | 
				
			||||||
 | 
					        ```
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    if obj is None or user.can_view(obj):
 | 
					    if obj is None or user.can_view(obj):
 | 
				
			||||||
        return True
 | 
					        return True
 | 
				
			||||||
@@ -121,20 +126,22 @@ def can_view(obj, user):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GenericContentPermissionMixinBuilder(View):
 | 
					class GenericContentPermissionMixinBuilder(View):
 | 
				
			||||||
    """
 | 
					    """Used to build permission mixins.
 | 
				
			||||||
    Used to build permission mixins
 | 
					
 | 
				
			||||||
    This view protect any child view that would be showing an object that is restricted based
 | 
					    This view protect any child view that would be showing an object that is restricted based
 | 
				
			||||||
      on two properties
 | 
					      on two properties.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :prop permission_function: function to test permission with, takes an object and an user an return a bool
 | 
					    Attributes:
 | 
				
			||||||
    :prop raised_error: permission to be raised
 | 
					        raised_error: permission to be raised
 | 
				
			||||||
 | 
					 | 
				
			||||||
    :raises: raised_error
 | 
					 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    permission_function = lambda obj, user: False
 | 
					 | 
				
			||||||
    raised_error = PermissionDenied
 | 
					    raised_error = PermissionDenied
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def permission_function(obj: Any, user: User) -> bool:
 | 
				
			||||||
 | 
					        """Function to test permission with."""
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def get_permission_function(cls, obj, user):
 | 
					    def get_permission_function(cls, obj, user):
 | 
				
			||||||
        return cls.permission_function(obj, user)
 | 
					        return cls.permission_function(obj, user)
 | 
				
			||||||
@@ -162,11 +169,12 @@ class GenericContentPermissionMixinBuilder(View):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CanCreateMixin(View):
 | 
					class CanCreateMixin(View):
 | 
				
			||||||
    """
 | 
					    """Protect any child view that would create an object.
 | 
				
			||||||
    This view is made to protect any child view that would create an object, and thus, that can not be protected by any
 | 
					 | 
				
			||||||
    of the following mixin
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :raises: PermissionDenied
 | 
					    Raises:
 | 
				
			||||||
 | 
					        PermissionDenied:
 | 
				
			||||||
 | 
					            If the user has not the necessary permission
 | 
				
			||||||
 | 
					            to create the object of the view.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def dispatch(self, request, *arg, **kwargs):
 | 
					    def dispatch(self, request, *arg, **kwargs):
 | 
				
			||||||
@@ -183,55 +191,54 @@ class CanCreateMixin(View):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CanEditPropMixin(GenericContentPermissionMixinBuilder):
 | 
					class CanEditPropMixin(GenericContentPermissionMixinBuilder):
 | 
				
			||||||
    """
 | 
					    """Ensure the user has owner permissions on the child view object.
 | 
				
			||||||
    This view is made to protect any child view that would be showing some properties of an object that are restricted
 | 
					 | 
				
			||||||
    to only the owner group of the given object.
 | 
					 | 
				
			||||||
    In other word, you can make a view with this view as parent, and it would be retricted to the users that are in the
 | 
					 | 
				
			||||||
    object's owner_group
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :raises: PermissionDenied
 | 
					    In other word, you can make a view with this view as parent,
 | 
				
			||||||
 | 
					    and it will be retricted to the users that are in the
 | 
				
			||||||
 | 
					    object's owner_group or that pass the `obj.can_be_viewed_by` test.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Raises:
 | 
				
			||||||
 | 
					        PermissionDenied: If the user cannot see the object
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    permission_function = can_edit_prop
 | 
					    permission_function = can_edit_prop
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CanEditMixin(GenericContentPermissionMixinBuilder):
 | 
					class CanEditMixin(GenericContentPermissionMixinBuilder):
 | 
				
			||||||
    """
 | 
					    """Ensure the user has permission to edit this view's object.
 | 
				
			||||||
    This view makes exactly the same thing as its direct parent, but checks the group on the edit_groups field of the
 | 
					 | 
				
			||||||
    object
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :raises: PermissionDenied
 | 
					    Raises:
 | 
				
			||||||
 | 
					        PermissionDenied: if the user cannot edit this view's object.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    permission_function = can_edit
 | 
					    permission_function = can_edit
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CanViewMixin(GenericContentPermissionMixinBuilder):
 | 
					class CanViewMixin(GenericContentPermissionMixinBuilder):
 | 
				
			||||||
    """
 | 
					    """Ensure the user has permission to view this view's object.
 | 
				
			||||||
    This view still makes exactly the same thing as its direct parent, but checks the group on the view_groups field of
 | 
					 | 
				
			||||||
    the object
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :raises: PermissionDenied
 | 
					    Raises:
 | 
				
			||||||
 | 
					        PermissionDenied: if the user cannot edit this view's object.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    permission_function = can_view
 | 
					    permission_function = can_view
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserIsRootMixin(GenericContentPermissionMixinBuilder):
 | 
					class UserIsRootMixin(GenericContentPermissionMixinBuilder):
 | 
				
			||||||
    """
 | 
					    """Allow only root admins.
 | 
				
			||||||
    This view check if the user is root
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :raises: PermissionDenied
 | 
					    Raises:
 | 
				
			||||||
 | 
					        PermissionDenied: if the user isn't root
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    permission_function = lambda obj, user: user.is_root
 | 
					    permission_function = lambda obj, user: user.is_root
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FormerSubscriberMixin(View):
 | 
					class FormerSubscriberMixin(View):
 | 
				
			||||||
    """
 | 
					    """Check if the user was at least an old subscriber.
 | 
				
			||||||
    This view check if the user was at least an old subscriber
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :raises: PermissionDenied
 | 
					    Raises:
 | 
				
			||||||
 | 
					        PermissionDenied: if the user never subscribed.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def dispatch(self, request, *args, **kwargs):
 | 
					    def dispatch(self, request, *args, **kwargs):
 | 
				
			||||||
@@ -241,10 +248,10 @@ class FormerSubscriberMixin(View):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserIsLoggedMixin(View):
 | 
					class UserIsLoggedMixin(View):
 | 
				
			||||||
    """
 | 
					    """Check if the user is logged.
 | 
				
			||||||
    This view check if the user is logged
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :raises: PermissionDenied
 | 
					    Raises:
 | 
				
			||||||
 | 
					        PermissionDenied:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def dispatch(self, request, *args, **kwargs):
 | 
					    def dispatch(self, request, *args, **kwargs):
 | 
				
			||||||
@@ -254,9 +261,7 @@ class UserIsLoggedMixin(View):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TabedViewMixin(View):
 | 
					class TabedViewMixin(View):
 | 
				
			||||||
    """
 | 
					    """Basic functions for displaying tabs in the template."""
 | 
				
			||||||
    This view provide the basic functions for displaying tabs in the template
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_tabs_title(self):
 | 
					    def get_tabs_title(self):
 | 
				
			||||||
        if hasattr(self, "tabs_title"):
 | 
					        if hasattr(self, "tabs_title"):
 | 
				
			||||||
@@ -299,7 +304,7 @@ class QuickNotifMixin:
 | 
				
			|||||||
        return ret
 | 
					        return ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """Add quick notifications to context"""
 | 
					        """Add quick notifications to context."""
 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        kwargs["quick_notifs"] = []
 | 
					        kwargs["quick_notifs"] = []
 | 
				
			||||||
        for n in self.quick_notif_list:
 | 
					        for n in self.quick_notif_list:
 | 
				
			||||||
@@ -312,21 +317,15 @@ class QuickNotifMixin:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DetailFormView(SingleObjectMixin, FormView):
 | 
					class DetailFormView(SingleObjectMixin, FormView):
 | 
				
			||||||
    """
 | 
					    """Class that allow both a detail view and a form view."""
 | 
				
			||||||
    Class that allow both a detail view and a form view
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_object(self):
 | 
					    def get_object(self):
 | 
				
			||||||
        """
 | 
					        """Get current group from id in url."""
 | 
				
			||||||
        Get current group from id in url
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return self.cached_object
 | 
					        return self.cached_object
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @cached_property
 | 
					    @cached_property
 | 
				
			||||||
    def cached_object(self):
 | 
					    def cached_object(self):
 | 
				
			||||||
        """
 | 
					        """Optimisation on group retrieval."""
 | 
				
			||||||
        Optimisation on group retrieval
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return super().get_object()
 | 
					        return super().get_object()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -42,8 +42,7 @@ from counter.models import Counter
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def send_file(request, file_id, file_class=SithFile, file_attr="file"):
 | 
					def send_file(request, file_id, file_class=SithFile, file_attr="file"):
 | 
				
			||||||
    """
 | 
					    """Send a file through Django without loading the whole file into
 | 
				
			||||||
    Send a file through Django without loading the whole file into
 | 
					 | 
				
			||||||
    memory at once. The FileWrapper will turn the file object into an
 | 
					    memory at once. The FileWrapper will turn the file object into an
 | 
				
			||||||
    iterator for chunks of 8KB.
 | 
					    iterator for chunks of 8KB.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
@@ -268,7 +267,7 @@ class FileEditPropView(CanEditPropMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FileView(CanViewMixin, DetailView, FormMixin):
 | 
					class FileView(CanViewMixin, DetailView, FormMixin):
 | 
				
			||||||
    """This class handle the upload of new files into a folder"""
 | 
					    """Handle the upload of new files into a folder."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = SithFile
 | 
					    model = SithFile
 | 
				
			||||||
    pk_url_kwarg = "file_id"
 | 
					    pk_url_kwarg = "file_id"
 | 
				
			||||||
@@ -278,8 +277,8 @@ class FileView(CanViewMixin, DetailView, FormMixin):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def handle_clipboard(request, obj):
 | 
					    def handle_clipboard(request, obj):
 | 
				
			||||||
        """
 | 
					        """Handle the clipboard in the view.
 | 
				
			||||||
        This method handles the clipboard in the view.
 | 
					
 | 
				
			||||||
        This method can fail, since it does not catch the exceptions coming from
 | 
					        This method can fail, since it does not catch the exceptions coming from
 | 
				
			||||||
        below, allowing proper handling in the calling view.
 | 
					        below, allowing proper handling in the calling view.
 | 
				
			||||||
        Use this method like this:
 | 
					        Use this method like this:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -196,10 +196,9 @@ class RegisteringForm(UserCreationForm):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserProfileForm(forms.ModelForm):
 | 
					class UserProfileForm(forms.ModelForm):
 | 
				
			||||||
    """
 | 
					    """Form handling the user profile, managing the files
 | 
				
			||||||
    Form handling the user profile, managing the files
 | 
					 | 
				
			||||||
    This form is actually pretty bad and was made in the rush before the migration. It should be refactored.
 | 
					    This form is actually pretty bad and was made in the rush before the migration. It should be refactored.
 | 
				
			||||||
    TODO: refactor this form
 | 
					    TODO: refactor this form.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,9 +13,7 @@
 | 
				
			|||||||
#
 | 
					#
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""Views to manage Groups."""
 | 
				
			||||||
This module contains views to manage Groups
 | 
					 | 
				
			||||||
"""
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ajax_select.fields import AutoCompleteSelectMultipleField
 | 
					from ajax_select.fields import AutoCompleteSelectMultipleField
 | 
				
			||||||
from django import forms
 | 
					from django import forms
 | 
				
			||||||
@@ -31,9 +29,7 @@ from core.views import CanCreateMixin, CanEditMixin, DetailFormView
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EditMembersForm(forms.Form):
 | 
					class EditMembersForm(forms.Form):
 | 
				
			||||||
    """
 | 
					    """Add and remove members from a Group."""
 | 
				
			||||||
    Add and remove members from a Group
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
        self.current_users = kwargs.pop("users", [])
 | 
					        self.current_users = kwargs.pop("users", [])
 | 
				
			||||||
@@ -53,9 +49,7 @@ class EditMembersForm(forms.Form):
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def clean_users_added(self):
 | 
					    def clean_users_added(self):
 | 
				
			||||||
        """
 | 
					        """Check that the user is not trying to add an user already in the group."""
 | 
				
			||||||
        Check that the user is not trying to add an user already in the group
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        cleaned_data = super().clean()
 | 
					        cleaned_data = super().clean()
 | 
				
			||||||
        users_added = cleaned_data.get("users_added", None)
 | 
					        users_added = cleaned_data.get("users_added", None)
 | 
				
			||||||
        if not users_added:
 | 
					        if not users_added:
 | 
				
			||||||
@@ -77,9 +71,7 @@ class EditMembersForm(forms.Form):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GroupListView(CanEditMixin, ListView):
 | 
					class GroupListView(CanEditMixin, ListView):
 | 
				
			||||||
    """
 | 
					    """Displays the Group list."""
 | 
				
			||||||
    Displays the Group list
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = RealGroup
 | 
					    model = RealGroup
 | 
				
			||||||
    ordering = ["name"]
 | 
					    ordering = ["name"]
 | 
				
			||||||
@@ -87,9 +79,7 @@ class GroupListView(CanEditMixin, ListView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GroupEditView(CanEditMixin, UpdateView):
 | 
					class GroupEditView(CanEditMixin, UpdateView):
 | 
				
			||||||
    """
 | 
					    """Edit infos of a Group."""
 | 
				
			||||||
    Edit infos of a Group
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = RealGroup
 | 
					    model = RealGroup
 | 
				
			||||||
    pk_url_kwarg = "group_id"
 | 
					    pk_url_kwarg = "group_id"
 | 
				
			||||||
@@ -98,9 +88,7 @@ class GroupEditView(CanEditMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GroupCreateView(CanCreateMixin, CreateView):
 | 
					class GroupCreateView(CanCreateMixin, CreateView):
 | 
				
			||||||
    """
 | 
					    """Add a new Group."""
 | 
				
			||||||
    Add a new Group
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = RealGroup
 | 
					    model = RealGroup
 | 
				
			||||||
    template_name = "core/create.jinja"
 | 
					    template_name = "core/create.jinja"
 | 
				
			||||||
@@ -108,9 +96,8 @@ class GroupCreateView(CanCreateMixin, CreateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GroupTemplateView(CanEditMixin, DetailFormView):
 | 
					class GroupTemplateView(CanEditMixin, DetailFormView):
 | 
				
			||||||
    """
 | 
					    """Display all users in a given Group
 | 
				
			||||||
    Display all users in a given Group
 | 
					    Allow adding and removing users from it.
 | 
				
			||||||
    Allow adding and removing users from it
 | 
					 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = RealGroup
 | 
					    model = RealGroup
 | 
				
			||||||
@@ -143,9 +130,7 @@ class GroupTemplateView(CanEditMixin, DetailFormView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GroupDeleteView(CanEditMixin, DeleteView):
 | 
					class GroupDeleteView(CanEditMixin, DeleteView):
 | 
				
			||||||
    """
 | 
					    """Delete a Group."""
 | 
				
			||||||
    Delete a Group
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = RealGroup
 | 
					    model = RealGroup
 | 
				
			||||||
    pk_url_kwarg = "group_id"
 | 
					    pk_url_kwarg = "group_id"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -74,9 +74,7 @@ from trombi.views import UserTrombiForm
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@method_decorator(check_honeypot, name="post")
 | 
					@method_decorator(check_honeypot, name="post")
 | 
				
			||||||
class SithLoginView(views.LoginView):
 | 
					class SithLoginView(views.LoginView):
 | 
				
			||||||
    """
 | 
					    """The login View."""
 | 
				
			||||||
    The login View
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    template_name = "core/login.jinja"
 | 
					    template_name = "core/login.jinja"
 | 
				
			||||||
    authentication_form = LoginForm
 | 
					    authentication_form = LoginForm
 | 
				
			||||||
@@ -85,33 +83,25 @@ class SithLoginView(views.LoginView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SithPasswordChangeView(views.PasswordChangeView):
 | 
					class SithPasswordChangeView(views.PasswordChangeView):
 | 
				
			||||||
    """
 | 
					    """Allows a user to change its password."""
 | 
				
			||||||
    Allows a user to change its password
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    template_name = "core/password_change.jinja"
 | 
					    template_name = "core/password_change.jinja"
 | 
				
			||||||
    success_url = reverse_lazy("core:password_change_done")
 | 
					    success_url = reverse_lazy("core:password_change_done")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SithPasswordChangeDoneView(views.PasswordChangeDoneView):
 | 
					class SithPasswordChangeDoneView(views.PasswordChangeDoneView):
 | 
				
			||||||
    """
 | 
					    """Allows a user to change its password."""
 | 
				
			||||||
    Allows a user to change its password
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    template_name = "core/password_change_done.jinja"
 | 
					    template_name = "core/password_change_done.jinja"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def logout(request):
 | 
					def logout(request):
 | 
				
			||||||
    """
 | 
					    """The logout view."""
 | 
				
			||||||
    The logout view
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    return views.logout_then_login(request)
 | 
					    return views.logout_then_login(request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def password_root_change(request, user_id):
 | 
					def password_root_change(request, user_id):
 | 
				
			||||||
    """
 | 
					    """Allows a root user to change someone's password."""
 | 
				
			||||||
    Allows a root user to change someone's password
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    if not request.user.is_root:
 | 
					    if not request.user.is_root:
 | 
				
			||||||
        raise PermissionDenied
 | 
					        raise PermissionDenied
 | 
				
			||||||
    user = User.objects.filter(id=user_id).first()
 | 
					    user = User.objects.filter(id=user_id).first()
 | 
				
			||||||
@@ -131,9 +121,7 @@ def password_root_change(request, user_id):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@method_decorator(check_honeypot, name="post")
 | 
					@method_decorator(check_honeypot, name="post")
 | 
				
			||||||
class SithPasswordResetView(views.PasswordResetView):
 | 
					class SithPasswordResetView(views.PasswordResetView):
 | 
				
			||||||
    """
 | 
					    """Allows someone to enter an email address for resetting password."""
 | 
				
			||||||
    Allows someone to enter an email address for resetting password
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    template_name = "core/password_reset.jinja"
 | 
					    template_name = "core/password_reset.jinja"
 | 
				
			||||||
    email_template_name = "core/password_reset_email.jinja"
 | 
					    email_template_name = "core/password_reset_email.jinja"
 | 
				
			||||||
@@ -141,26 +129,20 @@ class SithPasswordResetView(views.PasswordResetView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SithPasswordResetDoneView(views.PasswordResetDoneView):
 | 
					class SithPasswordResetDoneView(views.PasswordResetDoneView):
 | 
				
			||||||
    """
 | 
					    """Confirm that the reset email has been sent."""
 | 
				
			||||||
    Confirm that the reset email has been sent
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    template_name = "core/password_reset_done.jinja"
 | 
					    template_name = "core/password_reset_done.jinja"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SithPasswordResetConfirmView(views.PasswordResetConfirmView):
 | 
					class SithPasswordResetConfirmView(views.PasswordResetConfirmView):
 | 
				
			||||||
    """
 | 
					    """Provide a reset password form."""
 | 
				
			||||||
    Provide a reset password form
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    template_name = "core/password_reset_confirm.jinja"
 | 
					    template_name = "core/password_reset_confirm.jinja"
 | 
				
			||||||
    success_url = reverse_lazy("core:password_reset_complete")
 | 
					    success_url = reverse_lazy("core:password_reset_complete")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SithPasswordResetCompleteView(views.PasswordResetCompleteView):
 | 
					class SithPasswordResetCompleteView(views.PasswordResetCompleteView):
 | 
				
			||||||
    """
 | 
					    """Confirm the password has successfully been reset."""
 | 
				
			||||||
    Confirm the password has successfully been reset
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    template_name = "core/password_reset_complete.jinja"
 | 
					    template_name = "core/password_reset_complete.jinja"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -302,9 +284,7 @@ class UserTabsMixin(TabedViewMixin):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserView(UserTabsMixin, CanViewMixin, DetailView):
 | 
					class UserView(UserTabsMixin, CanViewMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Display a user's profile."""
 | 
				
			||||||
    Display a user's profile
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = User
 | 
					    model = User
 | 
				
			||||||
    pk_url_kwarg = "user_id"
 | 
					    pk_url_kwarg = "user_id"
 | 
				
			||||||
@@ -321,9 +301,7 @@ class UserView(UserTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView):
 | 
					class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Display a user's pictures."""
 | 
				
			||||||
    Display a user's pictures
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = User
 | 
					    model = User
 | 
				
			||||||
    pk_url_kwarg = "user_id"
 | 
					    pk_url_kwarg = "user_id"
 | 
				
			||||||
@@ -361,9 +339,7 @@ def delete_user_godfather(request, user_id, godfather_id, is_father):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserGodfathersView(UserTabsMixin, CanViewMixin, DetailView):
 | 
					class UserGodfathersView(UserTabsMixin, CanViewMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Display a user's godfathers."""
 | 
				
			||||||
    Display a user's godfathers
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = User
 | 
					    model = User
 | 
				
			||||||
    pk_url_kwarg = "user_id"
 | 
					    pk_url_kwarg = "user_id"
 | 
				
			||||||
@@ -394,9 +370,7 @@ class UserGodfathersView(UserTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserGodfathersTreeView(UserTabsMixin, CanViewMixin, DetailView):
 | 
					class UserGodfathersTreeView(UserTabsMixin, CanViewMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Display a user's family tree."""
 | 
				
			||||||
    Display a user's family tree
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = User
 | 
					    model = User
 | 
				
			||||||
    pk_url_kwarg = "user_id"
 | 
					    pk_url_kwarg = "user_id"
 | 
				
			||||||
@@ -415,9 +389,7 @@ class UserGodfathersTreeView(UserTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserGodfathersTreePictureView(CanViewMixin, DetailView):
 | 
					class UserGodfathersTreePictureView(CanViewMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Display a user's tree as a picture."""
 | 
				
			||||||
    Display a user's tree as a picture
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = User
 | 
					    model = User
 | 
				
			||||||
    pk_url_kwarg = "user_id"
 | 
					    pk_url_kwarg = "user_id"
 | 
				
			||||||
@@ -489,9 +461,7 @@ class UserGodfathersTreePictureView(CanViewMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserStatsView(UserTabsMixin, CanViewMixin, DetailView):
 | 
					class UserStatsView(UserTabsMixin, CanViewMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Display a user's stats."""
 | 
				
			||||||
    Display a user's stats
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = User
 | 
					    model = User
 | 
				
			||||||
    pk_url_kwarg = "user_id"
 | 
					    pk_url_kwarg = "user_id"
 | 
				
			||||||
@@ -591,9 +561,7 @@ class UserStatsView(UserTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserMiniView(CanViewMixin, DetailView):
 | 
					class UserMiniView(CanViewMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Display a user's profile."""
 | 
				
			||||||
    Display a user's profile
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = User
 | 
					    model = User
 | 
				
			||||||
    pk_url_kwarg = "user_id"
 | 
					    pk_url_kwarg = "user_id"
 | 
				
			||||||
@@ -602,18 +570,14 @@ class UserMiniView(CanViewMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserListView(ListView, CanEditPropMixin):
 | 
					class UserListView(ListView, CanEditPropMixin):
 | 
				
			||||||
    """
 | 
					    """Displays the user list."""
 | 
				
			||||||
    Displays the user list
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = User
 | 
					    model = User
 | 
				
			||||||
    template_name = "core/user_list.jinja"
 | 
					    template_name = "core/user_list.jinja"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserUploadProfilePictView(CanEditMixin, DetailView):
 | 
					class UserUploadProfilePictView(CanEditMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Handle the upload of the profile picture taken with webcam in navigator."""
 | 
				
			||||||
    Handle the upload of the profile picture taken with webcam in navigator
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = User
 | 
					    model = User
 | 
				
			||||||
    pk_url_kwarg = "user_id"
 | 
					    pk_url_kwarg = "user_id"
 | 
				
			||||||
@@ -650,9 +614,7 @@ class UserUploadProfilePictView(CanEditMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView):
 | 
					class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView):
 | 
				
			||||||
    """
 | 
					    """Edit a user's profile."""
 | 
				
			||||||
    Edit a user's profile
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = User
 | 
					    model = User
 | 
				
			||||||
    pk_url_kwarg = "user_id"
 | 
					    pk_url_kwarg = "user_id"
 | 
				
			||||||
@@ -663,9 +625,7 @@ class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView):
 | 
				
			|||||||
    board_only = []
 | 
					    board_only = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def remove_restricted_fields(self, request):
 | 
					    def remove_restricted_fields(self, request):
 | 
				
			||||||
        """
 | 
					        """Removes edit_once and board_only fields."""
 | 
				
			||||||
        Removes edit_once and board_only fields
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        for i in self.edit_once:
 | 
					        for i in self.edit_once:
 | 
				
			||||||
            if getattr(self.form.instance, i) and not (
 | 
					            if getattr(self.form.instance, i) and not (
 | 
				
			||||||
                request.user.is_board_member or request.user.is_root
 | 
					                request.user.is_board_member or request.user.is_root
 | 
				
			||||||
@@ -703,9 +663,7 @@ class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserClubView(UserTabsMixin, CanViewMixin, DetailView):
 | 
					class UserClubView(UserTabsMixin, CanViewMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Display the user's club(s)."""
 | 
				
			||||||
    Display the user's club(s)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = User
 | 
					    model = User
 | 
				
			||||||
    context_object_name = "profile"
 | 
					    context_object_name = "profile"
 | 
				
			||||||
@@ -715,9 +673,7 @@ class UserClubView(UserTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView):
 | 
					class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView):
 | 
				
			||||||
    """
 | 
					    """Edit a user's preferences."""
 | 
				
			||||||
    Edit a user's preferences
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = User
 | 
					    model = User
 | 
				
			||||||
    pk_url_kwarg = "user_id"
 | 
					    pk_url_kwarg = "user_id"
 | 
				
			||||||
@@ -752,9 +708,7 @@ class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserUpdateGroupView(UserTabsMixin, CanEditPropMixin, UpdateView):
 | 
					class UserUpdateGroupView(UserTabsMixin, CanEditPropMixin, UpdateView):
 | 
				
			||||||
    """
 | 
					    """Edit a user's groups."""
 | 
				
			||||||
    Edit a user's groups
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = User
 | 
					    model = User
 | 
				
			||||||
    pk_url_kwarg = "user_id"
 | 
					    pk_url_kwarg = "user_id"
 | 
				
			||||||
@@ -767,9 +721,7 @@ class UserUpdateGroupView(UserTabsMixin, CanEditPropMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserToolsView(QuickNotifMixin, UserTabsMixin, UserIsLoggedMixin, TemplateView):
 | 
					class UserToolsView(QuickNotifMixin, UserTabsMixin, UserIsLoggedMixin, TemplateView):
 | 
				
			||||||
    """
 | 
					    """Displays the logged user's tools."""
 | 
				
			||||||
    Displays the logged user's tools
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    template_name = "core/user_tools.jinja"
 | 
					    template_name = "core/user_tools.jinja"
 | 
				
			||||||
    current_tab = "tools"
 | 
					    current_tab = "tools"
 | 
				
			||||||
@@ -786,9 +738,7 @@ class UserToolsView(QuickNotifMixin, UserTabsMixin, UserIsLoggedMixin, TemplateV
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserAccountBase(UserTabsMixin, DetailView):
 | 
					class UserAccountBase(UserTabsMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Base class for UserAccount."""
 | 
				
			||||||
    Base class for UserAccount
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = User
 | 
					    model = User
 | 
				
			||||||
    pk_url_kwarg = "user_id"
 | 
					    pk_url_kwarg = "user_id"
 | 
				
			||||||
@@ -809,9 +759,7 @@ class UserAccountBase(UserTabsMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserAccountView(UserAccountBase):
 | 
					class UserAccountView(UserAccountBase):
 | 
				
			||||||
    """
 | 
					    """Display a user's account."""
 | 
				
			||||||
    Display a user's account
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    template_name = "core/user_account.jinja"
 | 
					    template_name = "core/user_account.jinja"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -858,9 +806,7 @@ class UserAccountView(UserAccountBase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserAccountDetailView(UserAccountBase, YearMixin, MonthMixin):
 | 
					class UserAccountDetailView(UserAccountBase, YearMixin, MonthMixin):
 | 
				
			||||||
    """
 | 
					    """Display a user's account for month."""
 | 
				
			||||||
    Display a user's account for month
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    template_name = "core/user_account_detail.jinja"
 | 
					    template_name = "core/user_account_detail.jinja"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,9 +30,8 @@ class BillingInfoForm(forms.ModelForm):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StudentCardForm(forms.ModelForm):
 | 
					class StudentCardForm(forms.ModelForm):
 | 
				
			||||||
    """
 | 
					    """Form for adding student cards
 | 
				
			||||||
    Form for adding student cards
 | 
					    Only used for user profile since CounterClick is to complicated.
 | 
				
			||||||
    Only used for user profile since CounterClick is to complicated
 | 
					 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
@@ -48,8 +47,7 @@ class StudentCardForm(forms.ModelForm):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GetUserForm(forms.Form):
 | 
					class GetUserForm(forms.Form):
 | 
				
			||||||
    """
 | 
					    """The Form class aims at providing a valid user_id field in its cleaned data, in order to pass it to some view,
 | 
				
			||||||
    The Form class aims at providing a valid user_id field in its cleaned data, in order to pass it to some view,
 | 
					 | 
				
			||||||
    reverse function, or any other use.
 | 
					    reverse function, or any other use.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    The Form implements a nice JS widget allowing the user to type a customer account id, or search the database with
 | 
					    The Form implements a nice JS widget allowing the user to type a customer account id, or search the database with
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -44,9 +44,10 @@ from subscription.models import Subscription
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Customer(models.Model):
 | 
					class Customer(models.Model):
 | 
				
			||||||
    """
 | 
					    """Customer data of a User.
 | 
				
			||||||
    This class extends a user to make a customer. It adds some basic customers' information, such as the account ID, and
 | 
					
 | 
				
			||||||
    is used by other accounting classes as reference to the customer, rather than using User
 | 
					    It adds some basic customers' information, such as the account ID, and
 | 
				
			||||||
 | 
					    is used by other accounting classes as reference to the customer, rather than using User.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    user = models.OneToOneField(User, primary_key=True, on_delete=models.CASCADE)
 | 
					    user = models.OneToOneField(User, primary_key=True, on_delete=models.CASCADE)
 | 
				
			||||||
@@ -63,10 +64,9 @@ class Customer(models.Model):
 | 
				
			|||||||
        return "%s - %s" % (self.user.username, self.account_id)
 | 
					        return "%s - %s" % (self.user.username, self.account_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def save(self, *args, allow_negative=False, is_selling=False, **kwargs):
 | 
					    def save(self, *args, allow_negative=False, is_selling=False, **kwargs):
 | 
				
			||||||
        """
 | 
					        """is_selling : tell if the current action is a selling
 | 
				
			||||||
        is_selling : tell if the current action is a selling
 | 
					 | 
				
			||||||
        allow_negative : ignored if not a selling. Allow a selling to put the account in negative
 | 
					        allow_negative : ignored if not a selling. Allow a selling to put the account in negative
 | 
				
			||||||
        Those two parameters avoid blocking the save method of a customer if his account is negative
 | 
					        Those two parameters avoid blocking the save method of a customer if his account is negative.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if self.amount < 0 and (is_selling and not allow_negative):
 | 
					        if self.amount < 0 and (is_selling and not allow_negative):
 | 
				
			||||||
            raise ValidationError(_("Not enough money"))
 | 
					            raise ValidationError(_("Not enough money"))
 | 
				
			||||||
@@ -84,9 +84,8 @@ class Customer(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def can_buy(self) -> bool:
 | 
					    def can_buy(self) -> bool:
 | 
				
			||||||
        """
 | 
					        """Check if whether this customer has the right to purchase any item.
 | 
				
			||||||
        Check if whether this customer has the right to
 | 
					
 | 
				
			||||||
        purchase any item.
 | 
					 | 
				
			||||||
        This must be not confused with the Product.can_be_sold_to(user)
 | 
					        This must be not confused with the Product.can_be_sold_to(user)
 | 
				
			||||||
        method as the present method returns an information
 | 
					        method as the present method returns an information
 | 
				
			||||||
        about a customer whereas the other tells something
 | 
					        about a customer whereas the other tells something
 | 
				
			||||||
@@ -100,8 +99,7 @@ class Customer(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def get_or_create(cls, user: User) -> Tuple[Customer, bool]:
 | 
					    def get_or_create(cls, user: User) -> Tuple[Customer, bool]:
 | 
				
			||||||
        """
 | 
					        """Work in pretty much the same way as the usual get_or_create method,
 | 
				
			||||||
        Work in pretty much the same way as the usual get_or_create method,
 | 
					 | 
				
			||||||
        but with the default field replaced by some under the hood.
 | 
					        but with the default field replaced by some under the hood.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        If the user has an account, return it as is.
 | 
					        If the user has an account, return it as is.
 | 
				
			||||||
@@ -158,9 +156,8 @@ class Customer(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BillingInfo(models.Model):
 | 
					class BillingInfo(models.Model):
 | 
				
			||||||
    """
 | 
					    """Represent the billing information of a user, which are required
 | 
				
			||||||
    Represent the billing information of a user, which are required
 | 
					    by the 3D-Secure v2 system used by the etransaction module.
 | 
				
			||||||
    by the 3D-Secure v2 system used by the etransaction module
 | 
					 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    customer = models.OneToOneField(
 | 
					    customer = models.OneToOneField(
 | 
				
			||||||
@@ -182,10 +179,9 @@ class BillingInfo(models.Model):
 | 
				
			|||||||
        return f"{self.first_name} {self.last_name}"
 | 
					        return f"{self.first_name} {self.last_name}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def to_3dsv2_xml(self) -> str:
 | 
					    def to_3dsv2_xml(self) -> str:
 | 
				
			||||||
        """
 | 
					        """Convert the data from this model into a xml usable
 | 
				
			||||||
        Convert the data from this model into a xml usable
 | 
					 | 
				
			||||||
        by the online paying service of the Crédit Agricole bank.
 | 
					        by the online paying service of the Crédit Agricole bank.
 | 
				
			||||||
        see : `https://www.ca-moncommerce.com/espace-client-mon-commerce/up2pay-e-transactions/ma-documentation/manuel-dintegration-focus-3ds-v2/principes-generaux/#integration-3dsv2-developpeur-webmaster`
 | 
					        see : `https://www.ca-moncommerce.com/espace-client-mon-commerce/up2pay-e-transactions/ma-documentation/manuel-dintegration-focus-3ds-v2/principes-generaux/#integration-3dsv2-developpeur-webmaster`.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        data = {
 | 
					        data = {
 | 
				
			||||||
            "Address": {
 | 
					            "Address": {
 | 
				
			||||||
@@ -204,9 +200,9 @@ class BillingInfo(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProductType(models.Model):
 | 
					class ProductType(models.Model):
 | 
				
			||||||
    """
 | 
					    """A product type.
 | 
				
			||||||
    This describes a product type
 | 
					
 | 
				
			||||||
    Useful only for categorizing, changes are made at the product level for now
 | 
					    Useful only for categorizing.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    name = models.CharField(_("name"), max_length=30)
 | 
					    name = models.CharField(_("name"), max_length=30)
 | 
				
			||||||
@@ -229,9 +225,7 @@ class ProductType(models.Model):
 | 
				
			|||||||
        return reverse("counter:producttype_list")
 | 
					        return reverse("counter:producttype_list")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_owned_by(self, user):
 | 
					    def is_owned_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Method to see if that object can be edited by the given user."""
 | 
				
			||||||
        Method to see if that object can be edited by the given user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if user.is_anonymous:
 | 
					        if user.is_anonymous:
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
        if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
 | 
					        if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
 | 
				
			||||||
@@ -240,9 +234,7 @@ class ProductType(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Product(models.Model):
 | 
					class Product(models.Model):
 | 
				
			||||||
    """
 | 
					    """A product, with all its related information."""
 | 
				
			||||||
    This describes a product, with all its related informations
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    name = models.CharField(_("name"), max_length=64)
 | 
					    name = models.CharField(_("name"), max_length=64)
 | 
				
			||||||
    description = models.TextField(_("description"), blank=True)
 | 
					    description = models.TextField(_("description"), blank=True)
 | 
				
			||||||
@@ -297,9 +289,7 @@ class Product(models.Model):
 | 
				
			|||||||
        return settings.SITH_ECOCUP_DECO == self.id
 | 
					        return settings.SITH_ECOCUP_DECO == self.id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_owned_by(self, user):
 | 
					    def is_owned_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Method to see if that object can be edited by the given user."""
 | 
				
			||||||
        Method to see if that object can be edited by the given user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if user.is_anonymous:
 | 
					        if user.is_anonymous:
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
        if user.is_in_group(
 | 
					        if user.is_in_group(
 | 
				
			||||||
@@ -309,8 +299,7 @@ class Product(models.Model):
 | 
				
			|||||||
        return False
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def can_be_sold_to(self, user: User) -> bool:
 | 
					    def can_be_sold_to(self, user: User) -> bool:
 | 
				
			||||||
        """
 | 
					        """Check if whether the user given in parameter has the right to buy
 | 
				
			||||||
        Check if whether the user given in parameter has the right to buy
 | 
					 | 
				
			||||||
        this product or not.
 | 
					        this product or not.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        This must be not confused with the Customer.can_buy()
 | 
					        This must be not confused with the Customer.can_buy()
 | 
				
			||||||
@@ -319,7 +308,8 @@ class Product(models.Model):
 | 
				
			|||||||
        whereas the other tells something about a Customer
 | 
					        whereas the other tells something about a Customer
 | 
				
			||||||
        (and not a user, they are not the same model).
 | 
					        (and not a user, they are not the same model).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: True if the user can buy this product else False
 | 
					        Returns:
 | 
				
			||||||
 | 
					            True if the user can buy this product else False
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if not self.buying_groups.exists():
 | 
					        if not self.buying_groups.exists():
 | 
				
			||||||
            return True
 | 
					            return True
 | 
				
			||||||
@@ -335,15 +325,16 @@ class Product(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class CounterQuerySet(models.QuerySet):
 | 
					class CounterQuerySet(models.QuerySet):
 | 
				
			||||||
    def annotate_has_barman(self, user: User) -> CounterQuerySet:
 | 
					    def annotate_has_barman(self, user: User) -> CounterQuerySet:
 | 
				
			||||||
        """
 | 
					        """Annotate the queryset with the `user_is_barman` field.
 | 
				
			||||||
        Annotate the queryset with the `user_is_barman` field.
 | 
					
 | 
				
			||||||
        For each counter, this field has value True if the user
 | 
					        For each counter, this field has value True if the user
 | 
				
			||||||
        is a barman of this counter, else False.
 | 
					        is a barman of this counter, else False.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param user: the user we want to check if he is a barman
 | 
					        Args:
 | 
				
			||||||
 | 
					            user: the user we want to check if he is a barman
 | 
				
			||||||
        Example::
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Examples:
 | 
				
			||||||
 | 
					            ```python
 | 
				
			||||||
            sli = User.objects.get(username="sli")
 | 
					            sli = User.objects.get(username="sli")
 | 
				
			||||||
            counters = (
 | 
					            counters = (
 | 
				
			||||||
                Counter.objects
 | 
					                Counter.objects
 | 
				
			||||||
@@ -353,6 +344,7 @@ class CounterQuerySet(models.QuerySet):
 | 
				
			|||||||
            print("Sli est barman dans les comptoirs suivants :")
 | 
					            print("Sli est barman dans les comptoirs suivants :")
 | 
				
			||||||
            for counter in counters:
 | 
					            for counter in counters:
 | 
				
			||||||
                print(f"- {counter.name}")
 | 
					                print(f"- {counter.name}")
 | 
				
			||||||
 | 
					            ```
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        subquery = user.counters.filter(pk=OuterRef("pk"))
 | 
					        subquery = user.counters.filter(pk=OuterRef("pk"))
 | 
				
			||||||
        # noinspection PyTypeChecker
 | 
					        # noinspection PyTypeChecker
 | 
				
			||||||
@@ -417,23 +409,21 @@ class Counter(models.Model):
 | 
				
			|||||||
        return user.is_board_member or user in self.sellers.all()
 | 
					        return user.is_board_member or user in self.sellers.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def gen_token(self):
 | 
					    def gen_token(self):
 | 
				
			||||||
        """Generate a new token for this counter"""
 | 
					        """Generate a new token for this counter."""
 | 
				
			||||||
        self.token = "".join(
 | 
					        self.token = "".join(
 | 
				
			||||||
            random.choice(string.ascii_letters + string.digits) for x in range(30)
 | 
					            random.choice(string.ascii_letters + string.digits) for x in range(30)
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.save()
 | 
					        self.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def add_barman(self, user):
 | 
					    def add_barman(self, user):
 | 
				
			||||||
        """
 | 
					        """Logs a barman in to the given counter.
 | 
				
			||||||
        Logs a barman in to the given counter
 | 
					
 | 
				
			||||||
        A user is stored as a tuple with its login time
 | 
					        A user is stored as a tuple with its login time.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Permanency(user=user, counter=self, start=timezone.now(), end=None).save()
 | 
					        Permanency(user=user, counter=self, start=timezone.now(), end=None).save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def del_barman(self, user):
 | 
					    def del_barman(self, user):
 | 
				
			||||||
        """
 | 
					        """Logs a barman out and store its permanency."""
 | 
				
			||||||
        Logs a barman out and store its permanency
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        perm = Permanency.objects.filter(counter=self, user=user, end=None).all()
 | 
					        perm = Permanency.objects.filter(counter=self, user=user, end=None).all()
 | 
				
			||||||
        for p in perm:
 | 
					        for p in perm:
 | 
				
			||||||
            p.end = p.activity
 | 
					            p.end = p.activity
 | 
				
			||||||
@@ -444,8 +434,7 @@ class Counter(models.Model):
 | 
				
			|||||||
        return self.get_barmen_list()
 | 
					        return self.get_barmen_list()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_barmen_list(self):
 | 
					    def get_barmen_list(self):
 | 
				
			||||||
        """
 | 
					        """Returns the barman list as list of User.
 | 
				
			||||||
        Returns the barman list as list of User
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Also handle the timeout of the barmen
 | 
					        Also handle the timeout of the barmen
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@@ -462,16 +451,12 @@ class Counter(models.Model):
 | 
				
			|||||||
        return bl
 | 
					        return bl
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_random_barman(self):
 | 
					    def get_random_barman(self):
 | 
				
			||||||
        """
 | 
					        """Return a random user being currently a barman."""
 | 
				
			||||||
        Return a random user being currently a barman
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        bl = self.get_barmen_list()
 | 
					        bl = self.get_barmen_list()
 | 
				
			||||||
        return bl[random.randrange(0, len(bl))]
 | 
					        return bl[random.randrange(0, len(bl))]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def update_activity(self):
 | 
					    def update_activity(self):
 | 
				
			||||||
        """
 | 
					        """Update the barman activity to prevent timeout."""
 | 
				
			||||||
        Update the barman activity to prevent timeout
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        for p in Permanency.objects.filter(counter=self, end=None).all():
 | 
					        for p in Permanency.objects.filter(counter=self, end=None).all():
 | 
				
			||||||
            p.save()  # Update activity
 | 
					            p.save()  # Update activity
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -479,25 +464,18 @@ class Counter(models.Model):
 | 
				
			|||||||
        return len(self.barmen_list) > 0
 | 
					        return len(self.barmen_list) > 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_inactive(self):
 | 
					    def is_inactive(self):
 | 
				
			||||||
        """
 | 
					        """Returns True if the counter self is inactive from SITH_COUNTER_MINUTE_INACTIVE's value minutes, else False."""
 | 
				
			||||||
        Returns True if the counter self is inactive from SITH_COUNTER_MINUTE_INACTIVE's value minutes, else False
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return self.is_open() and (
 | 
					        return self.is_open() and (
 | 
				
			||||||
            (timezone.now() - self.permanencies.order_by("-activity").first().activity)
 | 
					            (timezone.now() - self.permanencies.order_by("-activity").first().activity)
 | 
				
			||||||
            > timedelta(minutes=settings.SITH_COUNTER_MINUTE_INACTIVE)
 | 
					            > timedelta(minutes=settings.SITH_COUNTER_MINUTE_INACTIVE)
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def barman_list(self):
 | 
					    def barman_list(self):
 | 
				
			||||||
        """
 | 
					        """Returns the barman id list."""
 | 
				
			||||||
        Returns the barman id list
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return [b.id for b in self.get_barmen_list()]
 | 
					        return [b.id for b in self.get_barmen_list()]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def can_refill(self):
 | 
					    def can_refill(self):
 | 
				
			||||||
        """
 | 
					        """Show if the counter authorize the refilling with physic money."""
 | 
				
			||||||
        Show if the counter authorize the refilling with physic money
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if self.type != "BAR":
 | 
					        if self.type != "BAR":
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
        if self.id in SITH_COUNTER_OFFICES:
 | 
					        if self.id in SITH_COUNTER_OFFICES:
 | 
				
			||||||
@@ -511,8 +489,7 @@ class Counter(models.Model):
 | 
				
			|||||||
        return is_ae_member
 | 
					        return is_ae_member
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_top_barmen(self) -> QuerySet:
 | 
					    def get_top_barmen(self) -> QuerySet:
 | 
				
			||||||
        """
 | 
					        """Return a QuerySet querying the office hours stats of all the barmen of all time
 | 
				
			||||||
        Return a QuerySet querying the office hours stats of all the barmen of all time
 | 
					 | 
				
			||||||
        of this counter, ordered by descending number of hours.
 | 
					        of this counter, ordered by descending number of hours.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Each element of the QuerySet corresponds to a barman and has the following data :
 | 
					        Each element of the QuerySet corresponds to a barman and has the following data :
 | 
				
			||||||
@@ -535,16 +512,17 @@ class Counter(models.Model):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_top_customers(self, since: datetime | date | None = None) -> QuerySet:
 | 
					    def get_top_customers(self, since: datetime | date | None = None) -> QuerySet:
 | 
				
			||||||
        """
 | 
					        """Return a QuerySet querying the money spent by customers of this counter
 | 
				
			||||||
        Return a QuerySet querying the money spent by customers of this counter
 | 
					 | 
				
			||||||
        since the specified date, ordered by descending amount of money spent.
 | 
					        since the specified date, ordered by descending amount of money spent.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Each element of the QuerySet corresponds to a customer and has the following data :
 | 
					        Each element of the QuerySet corresponds to a customer and has the following data :
 | 
				
			||||||
            - the full name (first name + last name) of the customer
 | 
					 | 
				
			||||||
            - the nickname of the customer
 | 
					 | 
				
			||||||
            - the amount of money spent by the customer
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param since: timestamp from which to perform the calculation
 | 
					        - the full name (first name + last name) of the customer
 | 
				
			||||||
 | 
					        - the nickname of the customer
 | 
				
			||||||
 | 
					        - the amount of money spent by the customer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            since: timestamp from which to perform the calculation
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if since is None:
 | 
					        if since is None:
 | 
				
			||||||
            since = get_start_of_semester()
 | 
					            since = get_start_of_semester()
 | 
				
			||||||
@@ -573,12 +551,15 @@ class Counter(models.Model):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_total_sales(self, since: datetime | date | None = None) -> CurrencyField:
 | 
					    def get_total_sales(self, since: datetime | date | None = None) -> CurrencyField:
 | 
				
			||||||
        """
 | 
					        """Compute and return the total turnover of this counter since the given date.
 | 
				
			||||||
        Compute and return the total turnover of this counter
 | 
					
 | 
				
			||||||
        since the date specified in parameter (by default, since the start of the current
 | 
					        By default, the date is the start of the current semester.
 | 
				
			||||||
        semester)
 | 
					
 | 
				
			||||||
        :param since: timestamp from which to perform the calculation
 | 
					        Args:
 | 
				
			||||||
        :return: Total revenue earned at this counter
 | 
					            since: timestamp from which to perform the calculation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            Total revenue earned at this counter.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if since is None:
 | 
					        if since is None:
 | 
				
			||||||
            since = get_start_of_semester()
 | 
					            since = get_start_of_semester()
 | 
				
			||||||
@@ -591,9 +572,7 @@ class Counter(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Refilling(models.Model):
 | 
					class Refilling(models.Model):
 | 
				
			||||||
    """
 | 
					    """Handle the refilling."""
 | 
				
			||||||
    Handle the refilling
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    counter = models.ForeignKey(
 | 
					    counter = models.ForeignKey(
 | 
				
			||||||
        Counter, related_name="refillings", blank=False, on_delete=models.CASCADE
 | 
					        Counter, related_name="refillings", blank=False, on_delete=models.CASCADE
 | 
				
			||||||
@@ -665,9 +644,7 @@ class Refilling(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Selling(models.Model):
 | 
					class Selling(models.Model):
 | 
				
			||||||
    """
 | 
					    """Handle the sellings."""
 | 
				
			||||||
    Handle the sellings
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    label = models.CharField(_("label"), max_length=64)
 | 
					    label = models.CharField(_("label"), max_length=64)
 | 
				
			||||||
    product = models.ForeignKey(
 | 
					    product = models.ForeignKey(
 | 
				
			||||||
@@ -724,9 +701,7 @@ class Selling(models.Model):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def save(self, *args, allow_negative=False, **kwargs):
 | 
					    def save(self, *args, allow_negative=False, **kwargs):
 | 
				
			||||||
        """
 | 
					        """allow_negative : Allow this selling to use more money than available for this user."""
 | 
				
			||||||
        allow_negative : Allow this selling to use more money than available for this user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if not self.date:
 | 
					        if not self.date:
 | 
				
			||||||
            self.date = timezone.now()
 | 
					            self.date = timezone.now()
 | 
				
			||||||
        self.full_clean()
 | 
					        self.full_clean()
 | 
				
			||||||
@@ -864,8 +839,10 @@ class Selling(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Permanency(models.Model):
 | 
					class Permanency(models.Model):
 | 
				
			||||||
    """
 | 
					    """A permanency of a barman, on a counter.
 | 
				
			||||||
    This class aims at storing a traceability of who was barman where and when
 | 
					
 | 
				
			||||||
 | 
					    This aims at storing a traceability of who was barman where and when.
 | 
				
			||||||
 | 
					    Mainly for ~~dick size contest~~ establishing the top 10 barmen of the semester.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    user = models.ForeignKey(
 | 
					    user = models.ForeignKey(
 | 
				
			||||||
@@ -971,9 +948,7 @@ class CashRegisterSummary(models.Model):
 | 
				
			|||||||
            return object.__getattribute__(self, name)
 | 
					            return object.__getattribute__(self, name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_owned_by(self, user):
 | 
					    def is_owned_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Method to see if that object can be edited by the given user."""
 | 
				
			||||||
        Method to see if that object can be edited by the given user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if user.is_anonymous:
 | 
					        if user.is_anonymous:
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
        if user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID):
 | 
					        if user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID):
 | 
				
			||||||
@@ -1010,9 +985,7 @@ class CashRegisterSummaryItem(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Eticket(models.Model):
 | 
					class Eticket(models.Model):
 | 
				
			||||||
    """
 | 
					    """Eticket can be linked to a product an allows PDF generation."""
 | 
				
			||||||
    Eticket can be linked to a product an allows PDF generation
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    product = models.OneToOneField(
 | 
					    product = models.OneToOneField(
 | 
				
			||||||
        Product,
 | 
					        Product,
 | 
				
			||||||
@@ -1041,9 +1014,7 @@ class Eticket(models.Model):
 | 
				
			|||||||
        return reverse("counter:eticket_list")
 | 
					        return reverse("counter:eticket_list")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_owned_by(self, user):
 | 
					    def is_owned_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Method to see if that object can be edited by the given user."""
 | 
				
			||||||
        Method to see if that object can be edited by the given user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if user.is_anonymous:
 | 
					        if user.is_anonymous:
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
        return user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)
 | 
					        return user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)
 | 
				
			||||||
@@ -1058,11 +1029,11 @@ class Eticket(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StudentCard(models.Model):
 | 
					class StudentCard(models.Model):
 | 
				
			||||||
    """
 | 
					    """Alternative way to connect a customer into a counter.
 | 
				
			||||||
    Alternative way to connect a customer into a counter
 | 
					
 | 
				
			||||||
    We are using Mifare DESFire EV1 specs since it's used for izly cards
 | 
					    We are using Mifare DESFire EV1 specs since it's used for izly cards
 | 
				
			||||||
    https://www.nxp.com/docs/en/application-note/AN10927.pdf
 | 
					    https://www.nxp.com/docs/en/application-note/AN10927.pdf
 | 
				
			||||||
    UID is 7 byte long that means 14 hexa characters
 | 
					    UID is 7 byte long that means 14 hexa characters.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    UID_SIZE = 14
 | 
					    UID_SIZE = 14
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -140,10 +140,7 @@ class CounterTest(TestCase):
 | 
				
			|||||||
        assert response.status_code == 200
 | 
					        assert response.status_code == 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_annotate_has_barman_queryset(self):
 | 
					    def test_annotate_has_barman_queryset(self):
 | 
				
			||||||
        """
 | 
					        """Test if the custom queryset method `annotate_has_barman` works as intended."""
 | 
				
			||||||
        Test if the custom queryset method ``annotate_has_barman``
 | 
					 | 
				
			||||||
        works as intended
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.sli.counters.set([self.foyer, self.mde])
 | 
					        self.sli.counters.set([self.foyer, self.mde])
 | 
				
			||||||
        counters = Counter.objects.annotate_has_barman(self.sli)
 | 
					        counters = Counter.objects.annotate_has_barman(self.sli)
 | 
				
			||||||
        for counter in counters:
 | 
					        for counter in counters:
 | 
				
			||||||
@@ -265,15 +262,11 @@ class CounterStatsTest(TestCase):
 | 
				
			|||||||
        assert response.status_code == 403
 | 
					        assert response.status_code == 403
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_get_total_sales(self):
 | 
					    def test_get_total_sales(self):
 | 
				
			||||||
        """
 | 
					        """Test the result of the Counter.get_total_sales() method."""
 | 
				
			||||||
        Test the result of the Counter.get_total_sales() method
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        assert self.counter.get_total_sales() == 3102
 | 
					        assert self.counter.get_total_sales() == 3102
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_top_barmen(self):
 | 
					    def test_top_barmen(self):
 | 
				
			||||||
        """
 | 
					        """Test the result of Counter.get_top_barmen() is correct."""
 | 
				
			||||||
        Test the result of Counter.get_top_barmen() is correct
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        users = [self.skia, self.root, self.sli]
 | 
					        users = [self.skia, self.root, self.sli]
 | 
				
			||||||
        perm_times = [
 | 
					        perm_times = [
 | 
				
			||||||
            timedelta(days=16, hours=2, minutes=35, seconds=54),
 | 
					            timedelta(days=16, hours=2, minutes=35, seconds=54),
 | 
				
			||||||
@@ -292,9 +285,7 @@ class CounterStatsTest(TestCase):
 | 
				
			|||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_top_customer(self):
 | 
					    def test_top_customer(self):
 | 
				
			||||||
        """
 | 
					        """Test the result of Counter.get_top_customers() is correct."""
 | 
				
			||||||
        Test the result of Counter.get_top_customers() is correct
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        users = [self.sli, self.skia, self.krophil, self.root]
 | 
					        users = [self.sli, self.skia, self.krophil, self.root]
 | 
				
			||||||
        sale_amounts = [2000, 1000, 100, 2]
 | 
					        sale_amounts = [2000, 1000, 100, 2]
 | 
				
			||||||
        assert list(self.counter.get_top_customers()) == [
 | 
					        assert list(self.counter.get_top_customers()) == [
 | 
				
			||||||
@@ -588,9 +579,8 @@ class BarmanConnectionTest(TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StudentCardTest(TestCase):
 | 
					class StudentCardTest(TestCase):
 | 
				
			||||||
    """
 | 
					    """Tests for adding and deleting Stundent Cards
 | 
				
			||||||
    Tests for adding and deleting Stundent Cards
 | 
					    Test that an user can be found with it's student card.
 | 
				
			||||||
    Test that an user can be found with it's student card
 | 
					 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										197
									
								
								counter/views.py
									
									
									
									
									
								
							
							
						
						
									
										197
									
								
								counter/views.py
									
									
									
									
									
								
							@@ -75,9 +75,7 @@ from counter.models import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CounterAdminMixin(View):
 | 
					class CounterAdminMixin(View):
 | 
				
			||||||
    """
 | 
					    """Protect counter admin section."""
 | 
				
			||||||
    This view is made to protect counter admin section
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    edit_group = [settings.SITH_GROUP_COUNTER_ADMIN_ID]
 | 
					    edit_group = [settings.SITH_GROUP_COUNTER_ADMIN_ID]
 | 
				
			||||||
    edit_club = []
 | 
					    edit_club = []
 | 
				
			||||||
@@ -105,9 +103,7 @@ class CounterAdminMixin(View):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StudentCardDeleteView(DeleteView, CanEditMixin):
 | 
					class StudentCardDeleteView(DeleteView, CanEditMixin):
 | 
				
			||||||
    """
 | 
					    """View used to delete a card from a user."""
 | 
				
			||||||
    View used to delete a card from a user
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = StudentCard
 | 
					    model = StudentCard
 | 
				
			||||||
    template_name = "core/delete_confirm.jinja"
 | 
					    template_name = "core/delete_confirm.jinja"
 | 
				
			||||||
@@ -210,9 +206,7 @@ class CounterTabsMixin(TabedViewMixin):
 | 
				
			|||||||
class CounterMain(
 | 
					class CounterMain(
 | 
				
			||||||
    CounterTabsMixin, CanViewMixin, DetailView, ProcessFormView, FormMixin
 | 
					    CounterTabsMixin, CanViewMixin, DetailView, ProcessFormView, FormMixin
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
    """
 | 
					    """The public (barman) view."""
 | 
				
			||||||
    The public (barman) view
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Counter
 | 
					    model = Counter
 | 
				
			||||||
    template_name = "counter/counter_main.jinja"
 | 
					    template_name = "counter/counter_main.jinja"
 | 
				
			||||||
@@ -239,9 +233,7 @@ class CounterMain(
 | 
				
			|||||||
        return super().post(request, *args, **kwargs)
 | 
					        return super().post(request, *args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """
 | 
					        """We handle here the login form for the barman."""
 | 
				
			||||||
        We handle here the login form for the barman
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if self.request.method == "POST":
 | 
					        if self.request.method == "POST":
 | 
				
			||||||
            self.object = self.get_object()
 | 
					            self.object = self.get_object()
 | 
				
			||||||
        self.object.update_activity()
 | 
					        self.object.update_activity()
 | 
				
			||||||
@@ -275,9 +267,7 @@ class CounterMain(
 | 
				
			|||||||
        return kwargs
 | 
					        return kwargs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def form_valid(self, form):
 | 
					    def form_valid(self, form):
 | 
				
			||||||
        """
 | 
					        """We handle here the redirection, passing the user id of the asked customer."""
 | 
				
			||||||
        We handle here the redirection, passing the user id of the asked customer
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.kwargs["user_id"] = form.cleaned_data["user_id"]
 | 
					        self.kwargs["user_id"] = form.cleaned_data["user_id"]
 | 
				
			||||||
        return super().form_valid(form)
 | 
					        return super().form_valid(form)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -286,10 +276,9 @@ class CounterMain(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
 | 
					class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """The click view
 | 
				
			||||||
    The click view
 | 
					 | 
				
			||||||
    This is a detail view not to have to worry about loading the counter
 | 
					    This is a detail view not to have to worry about loading the counter
 | 
				
			||||||
    Everything is made by hand in the post method
 | 
					    Everything is made by hand in the post method.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Counter
 | 
					    model = Counter
 | 
				
			||||||
@@ -347,7 +336,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
        return super().dispatch(request, *args, **kwargs)
 | 
					        return super().dispatch(request, *args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get(self, request, *args, **kwargs):
 | 
					    def get(self, request, *args, **kwargs):
 | 
				
			||||||
        """Simple get view"""
 | 
					        """Simple get view."""
 | 
				
			||||||
        if "basket" not in request.session.keys():  # Init the basket session entry
 | 
					        if "basket" not in request.session.keys():  # Init the basket session entry
 | 
				
			||||||
            request.session["basket"] = {}
 | 
					            request.session["basket"] = {}
 | 
				
			||||||
            request.session["basket_total"] = 0
 | 
					            request.session["basket_total"] = 0
 | 
				
			||||||
@@ -364,7 +353,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
        return ret
 | 
					        return ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def post(self, request, *args, **kwargs):
 | 
					    def post(self, request, *args, **kwargs):
 | 
				
			||||||
        """Handle the many possibilities of the post request"""
 | 
					        """Handle the many possibilities of the post request."""
 | 
				
			||||||
        self.object = self.get_object()
 | 
					        self.object = self.get_object()
 | 
				
			||||||
        self.refill_form = None
 | 
					        self.refill_form = None
 | 
				
			||||||
        if (self.object.type != "BAR" and not request.user.is_authenticated) or (
 | 
					        if (self.object.type != "BAR" and not request.user.is_authenticated) or (
 | 
				
			||||||
@@ -481,10 +470,9 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
        return len(request.POST) == 0 and len(request.body) != 0
 | 
					        return len(request.POST) == 0 and len(request.body) != 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def add_product(self, request, q=1, p=None):
 | 
					    def add_product(self, request, q=1, p=None):
 | 
				
			||||||
        """
 | 
					        """Add a product to the basket
 | 
				
			||||||
        Add a product to the basket
 | 
					 | 
				
			||||||
        q is the quantity passed as integer
 | 
					        q is the quantity passed as integer
 | 
				
			||||||
        p is the product id, passed as an integer
 | 
					        p is the product id, passed as an integer.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        pid = p or parse_qs(request.body.decode())["product_id"][0]
 | 
					        pid = p or parse_qs(request.body.decode())["product_id"][0]
 | 
				
			||||||
        pid = str(pid)
 | 
					        pid = str(pid)
 | 
				
			||||||
@@ -543,9 +531,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
        return True
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def add_student_card(self, request):
 | 
					    def add_student_card(self, request):
 | 
				
			||||||
        """
 | 
					        """Add a new student card on the customer account."""
 | 
				
			||||||
        Add a new student card on the customer account
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        uid = request.POST["student_card_uid"]
 | 
					        uid = request.POST["student_card_uid"]
 | 
				
			||||||
        uid = str(uid)
 | 
					        uid = str(uid)
 | 
				
			||||||
        if not StudentCard.is_valid(uid):
 | 
					        if not StudentCard.is_valid(uid):
 | 
				
			||||||
@@ -564,7 +550,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
        return True
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def del_product(self, request):
 | 
					    def del_product(self, request):
 | 
				
			||||||
        """Delete a product from the basket"""
 | 
					        """Delete a product from the basket."""
 | 
				
			||||||
        pid = parse_qs(request.body.decode())["product_id"][0]
 | 
					        pid = parse_qs(request.body.decode())["product_id"][0]
 | 
				
			||||||
        product = self.get_product(pid)
 | 
					        product = self.get_product(pid)
 | 
				
			||||||
        if pid in request.session["basket"]:
 | 
					        if pid in request.session["basket"]:
 | 
				
			||||||
@@ -581,11 +567,11 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
        request.session.modified = True
 | 
					        request.session.modified = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def parse_code(self, request):
 | 
					    def parse_code(self, request):
 | 
				
			||||||
        """
 | 
					        """Parse the string entered by the barman.
 | 
				
			||||||
        Parse the string entered by the barman
 | 
					
 | 
				
			||||||
        This can be of two forms :
 | 
					        This can be of two forms :
 | 
				
			||||||
            - <str>, where the string is the code of the product
 | 
					            - `<str>`, where the string is the code of the product
 | 
				
			||||||
            - <int>X<str>, where the integer is the quantity and str the code
 | 
					            - `<int>X<str>`, where the integer is the quantity and str the code.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        string = parse_qs(request.body.decode()).get("code", [""])[0].upper()
 | 
					        string = parse_qs(request.body.decode()).get("code", [""])[0].upper()
 | 
				
			||||||
        if string == "FIN":
 | 
					        if string == "FIN":
 | 
				
			||||||
@@ -605,7 +591,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
        return self.render_to_response(context)
 | 
					        return self.render_to_response(context)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def finish(self, request):
 | 
					    def finish(self, request):
 | 
				
			||||||
        """Finish the click session, and validate the basket"""
 | 
					        """Finish the click session, and validate the basket."""
 | 
				
			||||||
        with transaction.atomic():
 | 
					        with transaction.atomic():
 | 
				
			||||||
            request.session["last_basket"] = []
 | 
					            request.session["last_basket"] = []
 | 
				
			||||||
            if self.sum_basket(request) > self.customer.amount:
 | 
					            if self.sum_basket(request) > self.customer.amount:
 | 
				
			||||||
@@ -657,7 +643,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def cancel(self, request):
 | 
					    def cancel(self, request):
 | 
				
			||||||
        """Cancel the click session"""
 | 
					        """Cancel the click session."""
 | 
				
			||||||
        kwargs = {"counter_id": self.object.id}
 | 
					        kwargs = {"counter_id": self.object.id}
 | 
				
			||||||
        request.session.pop("basket", None)
 | 
					        request.session.pop("basket", None)
 | 
				
			||||||
        return HttpResponseRedirect(
 | 
					        return HttpResponseRedirect(
 | 
				
			||||||
@@ -665,7 +651,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def refill(self, request):
 | 
					    def refill(self, request):
 | 
				
			||||||
        """Refill the customer's account"""
 | 
					        """Refill the customer's account."""
 | 
				
			||||||
        if not self.object.can_refill():
 | 
					        if not self.object.can_refill():
 | 
				
			||||||
            raise PermissionDenied
 | 
					            raise PermissionDenied
 | 
				
			||||||
        form = RefillForm(request.POST)
 | 
					        form = RefillForm(request.POST)
 | 
				
			||||||
@@ -678,7 +664,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
            self.refill_form = form
 | 
					            self.refill_form = form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """Add customer to the context"""
 | 
					        """Add customer to the context."""
 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        products = self.object.products.select_related("product_type")
 | 
					        products = self.object.products.select_related("product_type")
 | 
				
			||||||
        if self.customer_is_barman():
 | 
					        if self.customer_is_barman():
 | 
				
			||||||
@@ -701,8 +687,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CounterLogin(RedirectView):
 | 
					class CounterLogin(RedirectView):
 | 
				
			||||||
    """
 | 
					    """Handle the login of a barman.
 | 
				
			||||||
    Handle the login of a barman
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Logged barmen are stored in the Permanency model
 | 
					    Logged barmen are stored in the Permanency model
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
@@ -710,9 +695,7 @@ class CounterLogin(RedirectView):
 | 
				
			|||||||
    permanent = False
 | 
					    permanent = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def post(self, request, *args, **kwargs):
 | 
					    def post(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """Register the logged user as barman for this counter."""
 | 
				
			||||||
        Register the logged user as barman for this counter
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.counter_id = kwargs["counter_id"]
 | 
					        self.counter_id = kwargs["counter_id"]
 | 
				
			||||||
        self.counter = Counter.objects.filter(id=kwargs["counter_id"]).first()
 | 
					        self.counter = Counter.objects.filter(id=kwargs["counter_id"]).first()
 | 
				
			||||||
        form = LoginForm(request, data=request.POST)
 | 
					        form = LoginForm(request, data=request.POST)
 | 
				
			||||||
@@ -745,9 +728,7 @@ class CounterLogout(RedirectView):
 | 
				
			|||||||
    permanent = False
 | 
					    permanent = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def post(self, request, *args, **kwargs):
 | 
					    def post(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """Unregister the user from the barman."""
 | 
				
			||||||
        Unregister the user from the barman
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.counter = Counter.objects.filter(id=kwargs["counter_id"]).first()
 | 
					        self.counter = Counter.objects.filter(id=kwargs["counter_id"]).first()
 | 
				
			||||||
        user = User.objects.filter(id=request.POST["user_id"]).first()
 | 
					        user = User.objects.filter(id=request.POST["user_id"]).first()
 | 
				
			||||||
        self.counter.del_barman(user)
 | 
					        self.counter.del_barman(user)
 | 
				
			||||||
@@ -803,9 +784,7 @@ class CounterAdminTabsMixin(TabedViewMixin):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView):
 | 
					class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView):
 | 
				
			||||||
    """
 | 
					    """A list view for the admins."""
 | 
				
			||||||
    A list view for the admins
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Counter
 | 
					    model = Counter
 | 
				
			||||||
    template_name = "counter/counter_list.jinja"
 | 
					    template_name = "counter/counter_list.jinja"
 | 
				
			||||||
@@ -813,9 +792,7 @@ class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CounterEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
 | 
					class CounterEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
 | 
				
			||||||
    """
 | 
					    """Edit a counter's main informations (for the counter's manager)."""
 | 
				
			||||||
    Edit a counter's main informations (for the counter's manager)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Counter
 | 
					    model = Counter
 | 
				
			||||||
    form_class = CounterEditForm
 | 
					    form_class = CounterEditForm
 | 
				
			||||||
@@ -833,9 +810,7 @@ class CounterEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CounterEditPropView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
 | 
					class CounterEditPropView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
 | 
				
			||||||
    """
 | 
					    """Edit a counter's main informations (for the counter's admin)."""
 | 
				
			||||||
    Edit a counter's main informations (for the counter's admin)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Counter
 | 
					    model = Counter
 | 
				
			||||||
    form_class = modelform_factory(Counter, fields=["name", "club", "type"])
 | 
					    form_class = modelform_factory(Counter, fields=["name", "club", "type"])
 | 
				
			||||||
@@ -845,9 +820,7 @@ class CounterEditPropView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
 | 
					class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
 | 
				
			||||||
    """
 | 
					    """Create a counter (for the admins)."""
 | 
				
			||||||
    Create a counter (for the admins)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Counter
 | 
					    model = Counter
 | 
				
			||||||
    form_class = modelform_factory(
 | 
					    form_class = modelform_factory(
 | 
				
			||||||
@@ -860,9 +833,7 @@ class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView):
 | 
					class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView):
 | 
				
			||||||
    """
 | 
					    """Delete a counter (for the admins)."""
 | 
				
			||||||
    Delete a counter (for the admins)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Counter
 | 
					    model = Counter
 | 
				
			||||||
    pk_url_kwarg = "counter_id"
 | 
					    pk_url_kwarg = "counter_id"
 | 
				
			||||||
@@ -875,9 +846,7 @@ class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
 | 
					class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
 | 
				
			||||||
    """
 | 
					    """A list view for the admins."""
 | 
				
			||||||
    A list view for the admins
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = ProductType
 | 
					    model = ProductType
 | 
				
			||||||
    template_name = "counter/producttype_list.jinja"
 | 
					    template_name = "counter/producttype_list.jinja"
 | 
				
			||||||
@@ -885,9 +854,7 @@ class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
 | 
					class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
 | 
				
			||||||
    """
 | 
					    """A create view for the admins."""
 | 
				
			||||||
    A create view for the admins
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = ProductType
 | 
					    model = ProductType
 | 
				
			||||||
    fields = ["name", "description", "comment", "icon", "priority"]
 | 
					    fields = ["name", "description", "comment", "icon", "priority"]
 | 
				
			||||||
@@ -896,9 +863,7 @@ class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
 | 
					class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
 | 
				
			||||||
    """
 | 
					    """An edit view for the admins."""
 | 
				
			||||||
    An edit view for the admins
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = ProductType
 | 
					    model = ProductType
 | 
				
			||||||
    template_name = "core/edit.jinja"
 | 
					    template_name = "core/edit.jinja"
 | 
				
			||||||
@@ -908,9 +873,7 @@ class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProductArchivedListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
 | 
					class ProductArchivedListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
 | 
				
			||||||
    """
 | 
					    """A list view for the admins."""
 | 
				
			||||||
    A list view for the admins
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Product
 | 
					    model = Product
 | 
				
			||||||
    template_name = "counter/product_list.jinja"
 | 
					    template_name = "counter/product_list.jinja"
 | 
				
			||||||
@@ -920,9 +883,7 @@ class ProductArchivedListView(CounterAdminTabsMixin, CounterAdminMixin, ListView
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
 | 
					class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
 | 
				
			||||||
    """
 | 
					    """A list view for the admins."""
 | 
				
			||||||
    A list view for the admins
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Product
 | 
					    model = Product
 | 
				
			||||||
    template_name = "counter/product_list.jinja"
 | 
					    template_name = "counter/product_list.jinja"
 | 
				
			||||||
@@ -932,9 +893,7 @@ class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
 | 
					class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
 | 
				
			||||||
    """
 | 
					    """A create view for the admins."""
 | 
				
			||||||
    A create view for the admins
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Product
 | 
					    model = Product
 | 
				
			||||||
    form_class = ProductEditForm
 | 
					    form_class = ProductEditForm
 | 
				
			||||||
@@ -943,9 +902,7 @@ class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
 | 
					class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
 | 
				
			||||||
    """
 | 
					    """An edit view for the admins."""
 | 
				
			||||||
    An edit view for the admins
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Product
 | 
					    model = Product
 | 
				
			||||||
    form_class = ProductEditForm
 | 
					    form_class = ProductEditForm
 | 
				
			||||||
@@ -955,18 +912,14 @@ class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RefillingDeleteView(DeleteView):
 | 
					class RefillingDeleteView(DeleteView):
 | 
				
			||||||
    """
 | 
					    """Delete a refilling (for the admins)."""
 | 
				
			||||||
    Delete a refilling (for the admins)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Refilling
 | 
					    model = Refilling
 | 
				
			||||||
    pk_url_kwarg = "refilling_id"
 | 
					    pk_url_kwarg = "refilling_id"
 | 
				
			||||||
    template_name = "core/delete_confirm.jinja"
 | 
					    template_name = "core/delete_confirm.jinja"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def dispatch(self, request, *args, **kwargs):
 | 
					    def dispatch(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """We have here a very particular right handling, we can't inherit from CanEditPropMixin."""
 | 
				
			||||||
        We have here a very particular right handling, we can't inherit from CanEditPropMixin
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.object = self.get_object()
 | 
					        self.object = self.get_object()
 | 
				
			||||||
        if (
 | 
					        if (
 | 
				
			||||||
            timezone.now() - self.object.date
 | 
					            timezone.now() - self.object.date
 | 
				
			||||||
@@ -990,18 +943,14 @@ class RefillingDeleteView(DeleteView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SellingDeleteView(DeleteView):
 | 
					class SellingDeleteView(DeleteView):
 | 
				
			||||||
    """
 | 
					    """Delete a selling (for the admins)."""
 | 
				
			||||||
    Delete a selling (for the admins)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Selling
 | 
					    model = Selling
 | 
				
			||||||
    pk_url_kwarg = "selling_id"
 | 
					    pk_url_kwarg = "selling_id"
 | 
				
			||||||
    template_name = "core/delete_confirm.jinja"
 | 
					    template_name = "core/delete_confirm.jinja"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def dispatch(self, request, *args, **kwargs):
 | 
					    def dispatch(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """We have here a very particular right handling, we can't inherit from CanEditPropMixin."""
 | 
				
			||||||
        We have here a very particular right handling, we can't inherit from CanEditPropMixin
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.object = self.get_object()
 | 
					        self.object = self.get_object()
 | 
				
			||||||
        if (
 | 
					        if (
 | 
				
			||||||
            timezone.now() - self.object.date
 | 
					            timezone.now() - self.object.date
 | 
				
			||||||
@@ -1028,9 +977,7 @@ class SellingDeleteView(DeleteView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CashRegisterSummaryForm(forms.Form):
 | 
					class CashRegisterSummaryForm(forms.Form):
 | 
				
			||||||
    """
 | 
					    """Provide the cash summary form."""
 | 
				
			||||||
    Provide the cash summary form
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ten_cents = forms.IntegerField(label=_("10 cents"), required=False, min_value=0)
 | 
					    ten_cents = forms.IntegerField(label=_("10 cents"), required=False, min_value=0)
 | 
				
			||||||
    twenty_cents = forms.IntegerField(label=_("20 cents"), required=False, min_value=0)
 | 
					    twenty_cents = forms.IntegerField(label=_("20 cents"), required=False, min_value=0)
 | 
				
			||||||
@@ -1238,9 +1185,7 @@ class CashRegisterSummaryForm(forms.Form):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
 | 
					class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Provide the last operations to allow barmen to delete them."""
 | 
				
			||||||
    Provide the last operations to allow barmen to delete them
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Counter
 | 
					    model = Counter
 | 
				
			||||||
    pk_url_kwarg = "counter_id"
 | 
					    pk_url_kwarg = "counter_id"
 | 
				
			||||||
@@ -1248,9 +1193,7 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
    current_tab = "last_ops"
 | 
					    current_tab = "last_ops"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def dispatch(self, request, *args, **kwargs):
 | 
					    def dispatch(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """We have here again a very particular right handling."""
 | 
				
			||||||
        We have here again a very particular right handling
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.object = self.get_object()
 | 
					        self.object = self.get_object()
 | 
				
			||||||
        if (
 | 
					        if (
 | 
				
			||||||
            self.object.get_barmen_list()
 | 
					            self.object.get_barmen_list()
 | 
				
			||||||
@@ -1267,7 +1210,7 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """Add form to the context"""
 | 
					        """Add form to the context."""
 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        threshold = timezone.now() - timedelta(
 | 
					        threshold = timezone.now() - timedelta(
 | 
				
			||||||
            minutes=settings.SITH_LAST_OPERATIONS_LIMIT
 | 
					            minutes=settings.SITH_LAST_OPERATIONS_LIMIT
 | 
				
			||||||
@@ -1282,9 +1225,7 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView):
 | 
					class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Provide the cash summary form."""
 | 
				
			||||||
    Provide the cash summary form
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Counter
 | 
					    model = Counter
 | 
				
			||||||
    pk_url_kwarg = "counter_id"
 | 
					    pk_url_kwarg = "counter_id"
 | 
				
			||||||
@@ -1292,9 +1233,7 @@ class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
    current_tab = "cash_summary"
 | 
					    current_tab = "cash_summary"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def dispatch(self, request, *args, **kwargs):
 | 
					    def dispatch(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """We have here again a very particular right handling."""
 | 
				
			||||||
        We have here again a very particular right handling
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.object = self.get_object()
 | 
					        self.object = self.get_object()
 | 
				
			||||||
        if (
 | 
					        if (
 | 
				
			||||||
            self.object.get_barmen_list()
 | 
					            self.object.get_barmen_list()
 | 
				
			||||||
@@ -1327,16 +1266,14 @@ class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView):
 | 
				
			|||||||
        return reverse_lazy("counter:details", kwargs={"counter_id": self.object.id})
 | 
					        return reverse_lazy("counter:details", kwargs={"counter_id": self.object.id})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """Add form to the context"""
 | 
					        """Add form to the context."""
 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        kwargs["form"] = self.form
 | 
					        kwargs["form"] = self.form
 | 
				
			||||||
        return kwargs
 | 
					        return kwargs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CounterActivityView(DetailView):
 | 
					class CounterActivityView(DetailView):
 | 
				
			||||||
    """
 | 
					    """Show the bar activity."""
 | 
				
			||||||
    Show the bar activity
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Counter
 | 
					    model = Counter
 | 
				
			||||||
    pk_url_kwarg = "counter_id"
 | 
					    pk_url_kwarg = "counter_id"
 | 
				
			||||||
@@ -1344,16 +1281,14 @@ class CounterActivityView(DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CounterStatView(DetailView, CounterAdminMixin):
 | 
					class CounterStatView(DetailView, CounterAdminMixin):
 | 
				
			||||||
    """
 | 
					    """Show the bar stats."""
 | 
				
			||||||
    Show the bar stats
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Counter
 | 
					    model = Counter
 | 
				
			||||||
    pk_url_kwarg = "counter_id"
 | 
					    pk_url_kwarg = "counter_id"
 | 
				
			||||||
    template_name = "counter/stats.jinja"
 | 
					    template_name = "counter/stats.jinja"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """Add stats to the context"""
 | 
					        """Add stats to the context."""
 | 
				
			||||||
        counter: Counter = self.object
 | 
					        counter: Counter = self.object
 | 
				
			||||||
        semester_start = get_start_of_semester()
 | 
					        semester_start = get_start_of_semester()
 | 
				
			||||||
        office_hours = counter.get_top_barmen()
 | 
					        office_hours = counter.get_top_barmen()
 | 
				
			||||||
@@ -1386,7 +1321,7 @@ class CounterStatView(DetailView, CounterAdminMixin):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
 | 
					class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
 | 
				
			||||||
    """Edit cash summaries"""
 | 
					    """Edit cash summaries."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = CashRegisterSummary
 | 
					    model = CashRegisterSummary
 | 
				
			||||||
    template_name = "counter/cash_register_summary.jinja"
 | 
					    template_name = "counter/cash_register_summary.jinja"
 | 
				
			||||||
@@ -1400,7 +1335,7 @@ class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
 | 
					class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
 | 
				
			||||||
    """Display a list of cash summaries"""
 | 
					    """Display a list of cash summaries."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = CashRegisterSummary
 | 
					    model = CashRegisterSummary
 | 
				
			||||||
    template_name = "counter/cash_summary_list.jinja"
 | 
					    template_name = "counter/cash_summary_list.jinja"
 | 
				
			||||||
@@ -1410,7 +1345,7 @@ class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
 | 
				
			|||||||
    paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH
 | 
					    paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """Add sums to the context"""
 | 
					        """Add sums to the context."""
 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        form = CashSummaryFormBase(self.request.GET)
 | 
					        form = CashSummaryFormBase(self.request.GET)
 | 
				
			||||||
        kwargs["form"] = form
 | 
					        kwargs["form"] = form
 | 
				
			||||||
@@ -1461,7 +1396,7 @@ class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView):
 | 
				
			|||||||
    current_tab = "invoices_call"
 | 
					    current_tab = "invoices_call"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """Add sums to the context"""
 | 
					        """Add sums to the context."""
 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC")
 | 
					        kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC")
 | 
				
			||||||
        if "month" in self.request.GET:
 | 
					        if "month" in self.request.GET:
 | 
				
			||||||
@@ -1522,9 +1457,7 @@ class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
 | 
					class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
 | 
				
			||||||
    """
 | 
					    """A list view for the admins."""
 | 
				
			||||||
    A list view for the admins
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Eticket
 | 
					    model = Eticket
 | 
				
			||||||
    template_name = "counter/eticket_list.jinja"
 | 
					    template_name = "counter/eticket_list.jinja"
 | 
				
			||||||
@@ -1533,9 +1466,7 @@ class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EticketCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
 | 
					class EticketCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
 | 
				
			||||||
    """
 | 
					    """Create an eticket."""
 | 
				
			||||||
    Create an eticket
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Eticket
 | 
					    model = Eticket
 | 
				
			||||||
    template_name = "core/create.jinja"
 | 
					    template_name = "core/create.jinja"
 | 
				
			||||||
@@ -1544,9 +1475,7 @@ class EticketCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EticketEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
 | 
					class EticketEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
 | 
				
			||||||
    """
 | 
					    """Edit an eticket."""
 | 
				
			||||||
    Edit an eticket
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Eticket
 | 
					    model = Eticket
 | 
				
			||||||
    template_name = "core/edit.jinja"
 | 
					    template_name = "core/edit.jinja"
 | 
				
			||||||
@@ -1556,9 +1485,7 @@ class EticketEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EticketPDFView(CanViewMixin, DetailView):
 | 
					class EticketPDFView(CanViewMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Display the PDF of an eticket."""
 | 
				
			||||||
    Display the PDF of an eticket
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Selling
 | 
					    model = Selling
 | 
				
			||||||
    pk_url_kwarg = "selling_id"
 | 
					    pk_url_kwarg = "selling_id"
 | 
				
			||||||
@@ -1647,9 +1574,7 @@ class EticketPDFView(CanViewMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
 | 
					class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
 | 
				
			||||||
    """
 | 
					    """List of refillings on a counter."""
 | 
				
			||||||
    List of refillings on a counter
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Refilling
 | 
					    model = Refilling
 | 
				
			||||||
    template_name = "counter/refilling_list.jinja"
 | 
					    template_name = "counter/refilling_list.jinja"
 | 
				
			||||||
@@ -1668,9 +1593,7 @@ class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListVie
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StudentCardFormView(FormView):
 | 
					class StudentCardFormView(FormView):
 | 
				
			||||||
    """
 | 
					    """Add a new student card."""
 | 
				
			||||||
    Add a new student card
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    form_class = StudentCardForm
 | 
					    form_class = StudentCardForm
 | 
				
			||||||
    template_name = "core/create.jinja"
 | 
					    template_name = "core/create.jinja"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,11 +21,10 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PaymentResultConverter:
 | 
					class PaymentResultConverter:
 | 
				
			||||||
    """
 | 
					    """Converter used for url mapping of the `eboutic.views.payment_result` view.
 | 
				
			||||||
    Converter used for url mapping of the ``eboutic.views.payment_result``
 | 
					
 | 
				
			||||||
    view.
 | 
					 | 
				
			||||||
    It's meant to build an url that can match
 | 
					    It's meant to build an url that can match
 | 
				
			||||||
    either ``/eboutic/pay/success/`` or ``/eboutic/pay/failure/``
 | 
					    either `/eboutic/pay/success/` or `/eboutic/pay/failure/`
 | 
				
			||||||
    but nothing else.
 | 
					    but nothing else.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -34,9 +34,8 @@ from eboutic.models import get_eboutic_products
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BasketForm:
 | 
					class BasketForm:
 | 
				
			||||||
    """
 | 
					    """Class intended to perform checks on the request sended to the server when
 | 
				
			||||||
    Class intended to perform checks on the request sended to the server when
 | 
					    the user submits his basket from /eboutic/.
 | 
				
			||||||
    the user submits his basket from /eboutic/
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Because it must check an unknown number of fields, coming from a cookie
 | 
					    Because it must check an unknown number of fields, coming from a cookie
 | 
				
			||||||
    and needing some databases checks to be performed, inheriting from forms.Form
 | 
					    and needing some databases checks to be performed, inheriting from forms.Form
 | 
				
			||||||
@@ -45,6 +44,7 @@ class BasketForm:
 | 
				
			|||||||
    However, it still tries to share some similarities with a standard django Form.
 | 
					    However, it still tries to share some similarities with a standard django Form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Example:
 | 
					    Example:
 | 
				
			||||||
 | 
					    -------
 | 
				
			||||||
        ::
 | 
					        ::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            def my_view(request):
 | 
					            def my_view(request):
 | 
				
			||||||
@@ -62,6 +62,7 @@ class BasketForm:
 | 
				
			|||||||
    You can also use a little shortcut by directly calling `form.is_valid()`
 | 
					    You can also use a little shortcut by directly calling `form.is_valid()`
 | 
				
			||||||
    without calling `form.clean()`. In this case, the latter method shall be
 | 
					    without calling `form.clean()`. In this case, the latter method shall be
 | 
				
			||||||
    implicitly called.
 | 
					    implicitly called.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # check the json is an array containing non-nested objects.
 | 
					    # check the json is an array containing non-nested objects.
 | 
				
			||||||
@@ -85,8 +86,7 @@ class BasketForm:
 | 
				
			|||||||
        self.correct_cookie = []
 | 
					        self.correct_cookie = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def clean(self) -> None:
 | 
					    def clean(self) -> None:
 | 
				
			||||||
        """
 | 
					        """Perform all the checks, but return nothing.
 | 
				
			||||||
        Perform all the checks, but return nothing.
 | 
					 | 
				
			||||||
        To know if the form is valid, the `is_valid()` method must be used.
 | 
					        To know if the form is valid, the `is_valid()` method must be used.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        The form shall be considered as valid if it meets all the following conditions :
 | 
					        The form shall be considered as valid if it meets all the following conditions :
 | 
				
			||||||
@@ -170,9 +170,9 @@ class BasketForm:
 | 
				
			|||||||
        # the form is invalid
 | 
					        # the form is invalid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_valid(self) -> bool:
 | 
					    def is_valid(self) -> bool:
 | 
				
			||||||
        """
 | 
					        """Return True if the form is correct else False.
 | 
				
			||||||
        return True if the form is correct else False.
 | 
					
 | 
				
			||||||
        If the `clean()` method has not been called beforehand, call it
 | 
					        If the `clean()` method has not been called beforehand, call it.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if self.error_messages == set() and self.correct_cookie == []:
 | 
					        if self.error_messages == set() and self.correct_cookie == []:
 | 
				
			||||||
            self.clean()
 | 
					            self.clean()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,10 +12,10 @@
 | 
				
			|||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
 | 
					# OR WITHIN THE LOCAL FILE "LICENSE"
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
 | 
					from __future__ import annotations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import hmac
 | 
					import hmac
 | 
				
			||||||
import typing
 | 
					 | 
				
			||||||
from datetime import datetime
 | 
					from datetime import datetime
 | 
				
			||||||
from typing import List
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from dict2xml import dict2xml
 | 
					from dict2xml import dict2xml
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
@@ -29,7 +29,7 @@ from core.models import User
 | 
				
			|||||||
from counter.models import BillingInfo, Counter, Customer, Product, Refilling, Selling
 | 
					from counter.models import BillingInfo, Counter, Customer, Product, Refilling, Selling
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_eboutic_products(user: User) -> List[Product]:
 | 
					def get_eboutic_products(user: User) -> list[Product]:
 | 
				
			||||||
    products = (
 | 
					    products = (
 | 
				
			||||||
        Counter.objects.get(type="EBOUTIC")
 | 
					        Counter.objects.get(type="EBOUTIC")
 | 
				
			||||||
        .products.filter(product_type__isnull=False)
 | 
					        .products.filter(product_type__isnull=False)
 | 
				
			||||||
@@ -43,9 +43,7 @@ def get_eboutic_products(user: User) -> List[Product]:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Basket(models.Model):
 | 
					class Basket(models.Model):
 | 
				
			||||||
    """
 | 
					    """Basket is built when the user connects to an eboutic page."""
 | 
				
			||||||
    Basket is built when the user connects to an eboutic page
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    user = models.ForeignKey(
 | 
					    user = models.ForeignKey(
 | 
				
			||||||
        User,
 | 
					        User,
 | 
				
			||||||
@@ -60,8 +58,7 @@ class Basket(models.Model):
 | 
				
			|||||||
        return f"{self.user}'s basket ({self.items.all().count()} items)"
 | 
					        return f"{self.user}'s basket ({self.items.all().count()} items)"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def add_product(self, p: Product, q: int = 1):
 | 
					    def add_product(self, p: Product, q: int = 1):
 | 
				
			||||||
        """
 | 
					        """Given p an object of the Product model and q an integer,
 | 
				
			||||||
        Given p an object of the Product model and q an integer,
 | 
					 | 
				
			||||||
        add q items corresponding to this Product from the basket.
 | 
					        add q items corresponding to this Product from the basket.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        If this function is called with a product not in the basket, no error will be raised
 | 
					        If this function is called with a product not in the basket, no error will be raised
 | 
				
			||||||
@@ -81,8 +78,7 @@ class Basket(models.Model):
 | 
				
			|||||||
            item.save()
 | 
					            item.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def del_product(self, p: Product, q: int = 1):
 | 
					    def del_product(self, p: Product, q: int = 1):
 | 
				
			||||||
        """
 | 
					        """Given p an object of the Product model and q an integer
 | 
				
			||||||
        Given p an object of the Product model and q an integer,
 | 
					 | 
				
			||||||
        remove q items corresponding to this Product from the basket.
 | 
					        remove q items corresponding to this Product from the basket.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        If this function is called with a product not in the basket, no error will be raised
 | 
					        If this function is called with a product not in the basket, no error will be raised
 | 
				
			||||||
@@ -98,9 +94,7 @@ class Basket(models.Model):
 | 
				
			|||||||
            item.save()
 | 
					            item.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def clear(self) -> None:
 | 
					    def clear(self) -> None:
 | 
				
			||||||
        """
 | 
					        """Remove all items from this basket without deleting the basket."""
 | 
				
			||||||
        Remove all items from this basket without deleting the basket
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.items.all().delete()
 | 
					        self.items.all().delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @cached_property
 | 
					    @cached_property
 | 
				
			||||||
@@ -116,11 +110,8 @@ class Basket(models.Model):
 | 
				
			|||||||
        return float(total) if total is not None else 0
 | 
					        return float(total) if total is not None else 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def from_session(cls, session) -> typing.Union["Basket", None]:
 | 
					    def from_session(cls, session) -> Basket | None:
 | 
				
			||||||
        """
 | 
					        """The basket stored in the session object, if it exists."""
 | 
				
			||||||
        Given an HttpRequest django object, return the basket used in the current session
 | 
					 | 
				
			||||||
        if it exists else None
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if "basket_id" in session:
 | 
					        if "basket_id" in session:
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                return cls.objects.get(id=session["basket_id"])
 | 
					                return cls.objects.get(id=session["basket_id"])
 | 
				
			||||||
@@ -129,23 +120,22 @@ class Basket(models.Model):
 | 
				
			|||||||
        return None
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def generate_sales(self, counter, seller: User, payment_method: str):
 | 
					    def generate_sales(self, counter, seller: User, payment_method: str):
 | 
				
			||||||
        """
 | 
					        """Generate a list of sold items corresponding to the items
 | 
				
			||||||
        Generate a list of sold items corresponding to the items
 | 
					        of this basket WITHOUT saving them NOR deleting the basket.
 | 
				
			||||||
        of this basket WITHOUT saving them NOR deleting the basket
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Example:
 | 
					        Example:
 | 
				
			||||||
            ::
 | 
					            ```python
 | 
				
			||||||
 | 
					            counter = Counter.objects.get(name="Eboutic")
 | 
				
			||||||
 | 
					            sales = basket.generate_sales(counter, "SITH_ACCOUNT")
 | 
				
			||||||
 | 
					            # here the basket is in the same state as before the method call
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                counter = Counter.objects.get(name="Eboutic")
 | 
					            with transaction.atomic():
 | 
				
			||||||
                sales = basket.generate_sales(counter, "SITH_ACCOUNT")
 | 
					                for sale in sales:
 | 
				
			||||||
                # here the basket is in the same state as before the method call
 | 
					                    sale.save()
 | 
				
			||||||
 | 
					                basket.delete()
 | 
				
			||||||
                with transaction.atomic():
 | 
					                # all the basket items are deleted by the on_delete=CASCADE relation
 | 
				
			||||||
                    for sale in sales:
 | 
					                # thus only the sales remain
 | 
				
			||||||
                        sale.save()
 | 
					            ```
 | 
				
			||||||
                    basket.delete()
 | 
					 | 
				
			||||||
                    # all the basket items are deleted by the on_delete=CASCADE relation
 | 
					 | 
				
			||||||
                    # thus only the sales remain
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        # I must proceed with two distinct requests instead of
 | 
					        # I must proceed with two distinct requests instead of
 | 
				
			||||||
        # only one with a join because the AbstractBaseItem model has been
 | 
					        # only one with a join because the AbstractBaseItem model has been
 | 
				
			||||||
@@ -212,9 +202,7 @@ class Basket(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Invoice(models.Model):
 | 
					class Invoice(models.Model):
 | 
				
			||||||
    """
 | 
					    """Invoices are generated once the payment has been validated."""
 | 
				
			||||||
    Invoices are generated once the payment has been validated
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    user = models.ForeignKey(
 | 
					    user = models.ForeignKey(
 | 
				
			||||||
        User,
 | 
					        User,
 | 
				
			||||||
@@ -297,11 +285,12 @@ class BasketItem(AbstractBaseItem):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def from_product(cls, product: Product, quantity: int):
 | 
					    def from_product(cls, product: Product, quantity: int):
 | 
				
			||||||
        """
 | 
					        """Create a BasketItem with the same characteristics as the
 | 
				
			||||||
        Create a BasketItem with the same characteristics as the
 | 
					        product passed in parameters, with the specified quantity.
 | 
				
			||||||
        product passed in parameters, with the specified quantity
 | 
					
 | 
				
			||||||
        WARNING : the basket field is not filled, so you must set
 | 
					        Warnings:
 | 
				
			||||||
        it yourself before saving the model
 | 
					            the basket field is not filled, so you must set
 | 
				
			||||||
 | 
					            it yourself before saving the model.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return cls(
 | 
					        return cls(
 | 
				
			||||||
            product_id=product.id,
 | 
					            product_id=product.id,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -52,9 +52,9 @@ class EbouticTest(TestCase):
 | 
				
			|||||||
        cls.public = User.objects.get(username="public")
 | 
					        cls.public = User.objects.get(username="public")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_busy_basket(self, user) -> Basket:
 | 
					    def get_busy_basket(self, user) -> Basket:
 | 
				
			||||||
        """
 | 
					        """Create and return a basket with 3 barbar and 1 cotis in it.
 | 
				
			||||||
        Create and return a basket with 3 barbar and 1 cotis in it.
 | 
					
 | 
				
			||||||
        Edit the client session to store the basket id in it
 | 
					        Edit the client session to store the basket id in it.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        session = self.client.session
 | 
					        session = self.client.session
 | 
				
			||||||
        basket = Basket.objects.create(user=user)
 | 
					        basket = Basket.objects.create(user=user)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -43,8 +43,8 @@ from eboutic.models import Basket, Invoice, InvoiceItem, get_eboutic_products
 | 
				
			|||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
@require_GET
 | 
					@require_GET
 | 
				
			||||||
def eboutic_main(request: HttpRequest) -> HttpResponse:
 | 
					def eboutic_main(request: HttpRequest) -> HttpResponse:
 | 
				
			||||||
    """
 | 
					    """Main view of the eboutic application.
 | 
				
			||||||
    Main view of the eboutic application.
 | 
					
 | 
				
			||||||
    Return an Http response whose content is of type text/html.
 | 
					    Return an Http response whose content is of type text/html.
 | 
				
			||||||
    The latter represents the page from which a user can see
 | 
					    The latter represents the page from which a user can see
 | 
				
			||||||
    the catalogue of products that he can buy and fill
 | 
					    the catalogue of products that he can buy and fill
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,9 +7,7 @@ from core.models import Group, User
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Election(models.Model):
 | 
					class Election(models.Model):
 | 
				
			||||||
    """
 | 
					    """This class allows to create a new election."""
 | 
				
			||||||
    This class allows to create a new election
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    title = models.CharField(_("title"), max_length=255)
 | 
					    title = models.CharField(_("title"), max_length=255)
 | 
				
			||||||
    description = models.TextField(_("description"), null=True, blank=True)
 | 
					    description = models.TextField(_("description"), null=True, blank=True)
 | 
				
			||||||
@@ -105,9 +103,7 @@ class Election(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Role(OrderedModel):
 | 
					class Role(OrderedModel):
 | 
				
			||||||
    """
 | 
					    """This class allows to create a new role avaliable for a candidature."""
 | 
				
			||||||
    This class allows to create a new role avaliable for a candidature
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    election = models.ForeignKey(
 | 
					    election = models.ForeignKey(
 | 
				
			||||||
        Election,
 | 
					        Election,
 | 
				
			||||||
@@ -151,9 +147,7 @@ class Role(OrderedModel):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ElectionList(models.Model):
 | 
					class ElectionList(models.Model):
 | 
				
			||||||
    """
 | 
					    """To allow per list vote."""
 | 
				
			||||||
    To allow per list vote
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    title = models.CharField(_("title"), max_length=255)
 | 
					    title = models.CharField(_("title"), max_length=255)
 | 
				
			||||||
    election = models.ForeignKey(
 | 
					    election = models.ForeignKey(
 | 
				
			||||||
@@ -176,9 +170,7 @@ class ElectionList(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Candidature(models.Model):
 | 
					class Candidature(models.Model):
 | 
				
			||||||
    """
 | 
					    """This class is a component of responsability."""
 | 
				
			||||||
    This class is a component of responsability
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    role = models.ForeignKey(
 | 
					    role = models.ForeignKey(
 | 
				
			||||||
        Role,
 | 
					        Role,
 | 
				
			||||||
@@ -214,9 +206,7 @@ class Candidature(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Vote(models.Model):
 | 
					class Vote(models.Model):
 | 
				
			||||||
    """
 | 
					    """This class allows to vote for candidates."""
 | 
				
			||||||
    This class allows to vote for candidates
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    role = models.ForeignKey(
 | 
					    role = models.ForeignKey(
 | 
				
			||||||
        Role, related_name="votes", verbose_name=_("role"), on_delete=models.CASCADE
 | 
					        Role, related_name="votes", verbose_name=_("role"), on_delete=models.CASCADE
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,10 +19,7 @@ from election.models import Candidature, Election, ElectionList, Role, Vote
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LimitedCheckboxField(forms.ModelMultipleChoiceField):
 | 
					class LimitedCheckboxField(forms.ModelMultipleChoiceField):
 | 
				
			||||||
    """
 | 
					    """A `ModelMultipleChoiceField`, with a max limit of selectable inputs."""
 | 
				
			||||||
    Used to replace ModelMultipleChoiceField but with
 | 
					 | 
				
			||||||
    automatic backend verification
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, queryset, max_choice, **kwargs):
 | 
					    def __init__(self, queryset, max_choice, **kwargs):
 | 
				
			||||||
        self.max_choice = max_choice
 | 
					        self.max_choice = max_choice
 | 
				
			||||||
@@ -45,7 +42,7 @@ class LimitedCheckboxField(forms.ModelMultipleChoiceField):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CandidateForm(forms.ModelForm):
 | 
					class CandidateForm(forms.ModelForm):
 | 
				
			||||||
    """Form to candidate"""
 | 
					    """Form to candidate."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Candidature
 | 
					        model = Candidature
 | 
				
			||||||
@@ -91,7 +88,7 @@ class VoteForm(forms.Form):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RoleForm(forms.ModelForm):
 | 
					class RoleForm(forms.ModelForm):
 | 
				
			||||||
    """Form for creating a role"""
 | 
					    """Form for creating a role."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Role
 | 
					        model = Role
 | 
				
			||||||
@@ -175,9 +172,7 @@ class ElectionForm(forms.ModelForm):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ElectionsListView(CanViewMixin, ListView):
 | 
					class ElectionsListView(CanViewMixin, ListView):
 | 
				
			||||||
    """
 | 
					    """A list of all non archived elections visible."""
 | 
				
			||||||
    A list of all non archived elections visible
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Election
 | 
					    model = Election
 | 
				
			||||||
    ordering = ["-id"]
 | 
					    ordering = ["-id"]
 | 
				
			||||||
@@ -189,9 +184,7 @@ class ElectionsListView(CanViewMixin, ListView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ElectionListArchivedView(CanViewMixin, ListView):
 | 
					class ElectionListArchivedView(CanViewMixin, ListView):
 | 
				
			||||||
    """
 | 
					    """A list of all archived elections visible."""
 | 
				
			||||||
    A list of all archived elections visible
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Election
 | 
					    model = Election
 | 
				
			||||||
    ordering = ["-id"]
 | 
					    ordering = ["-id"]
 | 
				
			||||||
@@ -203,9 +196,7 @@ class ElectionListArchivedView(CanViewMixin, ListView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ElectionDetailView(CanViewMixin, DetailView):
 | 
					class ElectionDetailView(CanViewMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Details an election responsability by responsability."""
 | 
				
			||||||
    Details an election responsability by responsability
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Election
 | 
					    model = Election
 | 
				
			||||||
    template_name = "election/election_detail.jinja"
 | 
					    template_name = "election/election_detail.jinja"
 | 
				
			||||||
@@ -232,7 +223,7 @@ class ElectionDetailView(CanViewMixin, DetailView):
 | 
				
			|||||||
        return response
 | 
					        return response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """Add additionnal data to the template"""
 | 
					        """Add additionnal data to the template."""
 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        kwargs["election_form"] = VoteForm(self.object, self.request.user)
 | 
					        kwargs["election_form"] = VoteForm(self.object, self.request.user)
 | 
				
			||||||
        kwargs["election_results"] = self.object.results
 | 
					        kwargs["election_results"] = self.object.results
 | 
				
			||||||
@@ -243,9 +234,7 @@ class ElectionDetailView(CanViewMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class VoteFormView(CanCreateMixin, FormView):
 | 
					class VoteFormView(CanCreateMixin, FormView):
 | 
				
			||||||
    """
 | 
					    """Alows users to vote."""
 | 
				
			||||||
    Alows users to vote
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    form_class = VoteForm
 | 
					    form_class = VoteForm
 | 
				
			||||||
    template_name = "election/election_detail.jinja"
 | 
					    template_name = "election/election_detail.jinja"
 | 
				
			||||||
@@ -278,9 +267,7 @@ class VoteFormView(CanCreateMixin, FormView):
 | 
				
			|||||||
        return kwargs
 | 
					        return kwargs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def form_valid(self, form):
 | 
					    def form_valid(self, form):
 | 
				
			||||||
        """
 | 
					        """Verify that the user is part in a vote group."""
 | 
				
			||||||
        Verify that the user is part in a vote group
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        data = form.clean()
 | 
					        data = form.clean()
 | 
				
			||||||
        res = super(FormView, self).form_valid(form)
 | 
					        res = super(FormView, self).form_valid(form)
 | 
				
			||||||
        for grp_id in self.election.vote_groups.values_list("pk", flat=True):
 | 
					        for grp_id in self.election.vote_groups.values_list("pk", flat=True):
 | 
				
			||||||
@@ -293,7 +280,7 @@ class VoteFormView(CanCreateMixin, FormView):
 | 
				
			|||||||
        return reverse_lazy("election:detail", kwargs={"election_id": self.election.id})
 | 
					        return reverse_lazy("election:detail", kwargs={"election_id": self.election.id})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """Add additionnal data to the template"""
 | 
					        """Add additionnal data to the template."""
 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        kwargs["object"] = self.election
 | 
					        kwargs["object"] = self.election
 | 
				
			||||||
        kwargs["election"] = self.election
 | 
					        kwargs["election"] = self.election
 | 
				
			||||||
@@ -305,9 +292,7 @@ class VoteFormView(CanCreateMixin, FormView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CandidatureCreateView(CanCreateMixin, CreateView):
 | 
					class CandidatureCreateView(CanCreateMixin, CreateView):
 | 
				
			||||||
    """
 | 
					    """View dedicated to a cundidature creation."""
 | 
				
			||||||
    View dedicated to a cundidature creation
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    form_class = CandidateForm
 | 
					    form_class = CandidateForm
 | 
				
			||||||
    model = Candidature
 | 
					    model = Candidature
 | 
				
			||||||
@@ -330,9 +315,7 @@ class CandidatureCreateView(CanCreateMixin, CreateView):
 | 
				
			|||||||
        return kwargs
 | 
					        return kwargs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def form_valid(self, form):
 | 
					    def form_valid(self, form):
 | 
				
			||||||
        """
 | 
					        """Verify that the selected user is in candidate group."""
 | 
				
			||||||
        Verify that the selected user is in candidate group
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        obj = form.instance
 | 
					        obj = form.instance
 | 
				
			||||||
        obj.election = Election.objects.get(id=self.election.id)
 | 
					        obj.election = Election.objects.get(id=self.election.id)
 | 
				
			||||||
        if (obj.election.can_candidate(obj.user)) and (
 | 
					        if (obj.election.can_candidate(obj.user)) and (
 | 
				
			||||||
@@ -361,10 +344,7 @@ class ElectionCreateView(CanCreateMixin, CreateView):
 | 
				
			|||||||
        return super().dispatch(request, *args, **kwargs)
 | 
					        return super().dispatch(request, *args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def form_valid(self, form):
 | 
					    def form_valid(self, form):
 | 
				
			||||||
        """
 | 
					        """Allow every user that had passed the dispatch to create an election."""
 | 
				
			||||||
        Allow every users that had passed the dispatch
 | 
					 | 
				
			||||||
        to create an election
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return super(CreateView, self).form_valid(form)
 | 
					        return super(CreateView, self).form_valid(form)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_success_url(self, **kwargs):
 | 
					    def get_success_url(self, **kwargs):
 | 
				
			||||||
@@ -388,9 +368,7 @@ class RoleCreateView(CanCreateMixin, CreateView):
 | 
				
			|||||||
        return init
 | 
					        return init
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def form_valid(self, form):
 | 
					    def form_valid(self, form):
 | 
				
			||||||
        """
 | 
					        """Verify that the user can edit properly."""
 | 
				
			||||||
        Verify that the user can edit properly
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        obj: Role = form.instance
 | 
					        obj: Role = form.instance
 | 
				
			||||||
        user: User = self.request.user
 | 
					        user: User = self.request.user
 | 
				
			||||||
        if obj.election:
 | 
					        if obj.election:
 | 
				
			||||||
@@ -432,9 +410,7 @@ class ElectionListCreateView(CanCreateMixin, CreateView):
 | 
				
			|||||||
        return kwargs
 | 
					        return kwargs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def form_valid(self, form):
 | 
					    def form_valid(self, form):
 | 
				
			||||||
        """
 | 
					        """Verify that the user can vote on this election."""
 | 
				
			||||||
        Verify that the user can vote on this election
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        obj: ElectionList = form.instance
 | 
					        obj: ElectionList = form.instance
 | 
				
			||||||
        user: User = self.request.user
 | 
					        user: User = self.request.user
 | 
				
			||||||
        if obj.election:
 | 
					        if obj.election:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -48,8 +48,7 @@ def get_default_view_group():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Forum(models.Model):
 | 
					class Forum(models.Model):
 | 
				
			||||||
    """
 | 
					    """The Forum class, made as a tree to allow nice tidy organization.
 | 
				
			||||||
    The Forum class, made as a tree to allow nice tidy organization
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    owner_club allows club members to moderate there own topics
 | 
					    owner_club allows club members to moderate there own topics
 | 
				
			||||||
    edit_groups allows to put any group as a forum admin
 | 
					    edit_groups allows to put any group as a forum admin
 | 
				
			||||||
@@ -157,7 +156,7 @@ class Forum(models.Model):
 | 
				
			|||||||
            c.apply_rights_recursively()
 | 
					            c.apply_rights_recursively()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def copy_rights(self):
 | 
					    def copy_rights(self):
 | 
				
			||||||
        """Copy, if possible, the rights of the parent folder"""
 | 
					        """Copy, if possible, the rights of the parent folder."""
 | 
				
			||||||
        if self.parent is not None:
 | 
					        if self.parent is not None:
 | 
				
			||||||
            self.owner_club = self.parent.owner_club
 | 
					            self.owner_club = self.parent.owner_club
 | 
				
			||||||
            self.edit_groups.set(self.parent.edit_groups.all())
 | 
					            self.edit_groups.set(self.parent.edit_groups.all())
 | 
				
			||||||
@@ -187,7 +186,7 @@ class Forum(models.Model):
 | 
				
			|||||||
        return False
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def check_loop(self):
 | 
					    def check_loop(self):
 | 
				
			||||||
        """Raise a validation error when a loop is found within the parent list"""
 | 
					        """Raise a validation error when a loop is found within the parent list."""
 | 
				
			||||||
        objs = []
 | 
					        objs = []
 | 
				
			||||||
        cur = self
 | 
					        cur = self
 | 
				
			||||||
        while cur.parent is not None:
 | 
					        while cur.parent is not None:
 | 
				
			||||||
@@ -299,9 +298,7 @@ class ForumTopic(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ForumMessage(models.Model):
 | 
					class ForumMessage(models.Model):
 | 
				
			||||||
    """
 | 
					    """A message in the forum (thx Cpt. Obvious.)."""
 | 
				
			||||||
    "A ForumMessage object represents a message in the forum" -- Cpt. Obvious
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    topic = models.ForeignKey(
 | 
					    topic = models.ForeignKey(
 | 
				
			||||||
        ForumTopic, related_name="messages", on_delete=models.CASCADE
 | 
					        ForumTopic, related_name="messages", on_delete=models.CASCADE
 | 
				
			||||||
@@ -425,7 +422,8 @@ class ForumMessageMeta(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ForumUserInfo(models.Model):
 | 
					class ForumUserInfo(models.Model):
 | 
				
			||||||
    """
 | 
					    """The forum infos of a user.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    This currently stores only the last date a user clicked "Mark all as read".
 | 
					    This currently stores only the last date a user clicked "Mark all as read".
 | 
				
			||||||
    However, this can be extended with lot of user preferences dedicated to a
 | 
					    However, this can be extended with lot of user preferences dedicated to a
 | 
				
			||||||
    user, such as the favourite topics, the signature, and so on...
 | 
					    user, such as the favourite topics, the signature, and so on...
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -118,11 +118,11 @@ class Command(BaseCommand):
 | 
				
			|||||||
            self.make_important_citizen(u)
 | 
					            self.make_important_citizen(u)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def make_clubs(self):
 | 
					    def make_clubs(self):
 | 
				
			||||||
        """
 | 
					        """Create all the clubs (:class:`club.models.Club`).
 | 
				
			||||||
        Create all the clubs (:class:`club.models.Club`)
 | 
					
 | 
				
			||||||
        and store them in `self.clubs` for fast access later.
 | 
					        After creation, the clubs are stored in `self.clubs` for fast access later.
 | 
				
			||||||
        Don't create the meta groups (:class:`core.models.MetaGroup`)
 | 
					        Don't create the meta groups (:class:`core.models.MetaGroup`)
 | 
				
			||||||
        nor the pages of the clubs (:class:`core.models.Page`)
 | 
					        nor the pages of the clubs (:class:`core.models.Page`).
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.clubs = []
 | 
					        self.clubs = []
 | 
				
			||||||
        for i in range(self.NB_CLUBS):
 | 
					        for i in range(self.NB_CLUBS):
 | 
				
			||||||
@@ -132,8 +132,7 @@ class Command(BaseCommand):
 | 
				
			|||||||
        self.clubs = list(Club.objects.filter(unix_name__startswith="galaxy-").all())
 | 
					        self.clubs = list(Club.objects.filter(unix_name__startswith="galaxy-").all())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def make_users(self):
 | 
					    def make_users(self):
 | 
				
			||||||
        """
 | 
					        """Create all the users and store them in `self.users` for fast access later.
 | 
				
			||||||
        Create all the users and store them in `self.users` for fast access later.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Also create a subscription for all the generated users.
 | 
					        Also create a subscription for all the generated users.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@@ -167,8 +166,7 @@ class Command(BaseCommand):
 | 
				
			|||||||
        Subscription.objects.bulk_create(subs)
 | 
					        Subscription.objects.bulk_create(subs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def make_families(self):
 | 
					    def make_families(self):
 | 
				
			||||||
        """
 | 
					        """Generate the godfather/godchild relations for the users contained in :attr:`self.users`.
 | 
				
			||||||
        Generate the godfather/godchild relations for the users contained in :attr:`self.users`.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        The :meth:`make_users` method must have been called beforehand.
 | 
					        The :meth:`make_users` method must have been called beforehand.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -194,8 +192,7 @@ class Command(BaseCommand):
 | 
				
			|||||||
        User.godfathers.through.objects.bulk_create(godfathers)
 | 
					        User.godfathers.through.objects.bulk_create(godfathers)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def make_club_memberships(self):
 | 
					    def make_club_memberships(self):
 | 
				
			||||||
        """
 | 
					        """Assign users to clubs and give them a role in a pseudo-random way.
 | 
				
			||||||
        Assign users to clubs and give them a role in a pseudo-random way.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        The :meth:`make_users` and :meth:`make_clubs` methods
 | 
					        The :meth:`make_users` and :meth:`make_clubs` methods
 | 
				
			||||||
        must have been called beforehand.
 | 
					        must have been called beforehand.
 | 
				
			||||||
@@ -265,8 +262,7 @@ class Command(BaseCommand):
 | 
				
			|||||||
        Membership.objects.bulk_create(memberships)
 | 
					        Membership.objects.bulk_create(memberships)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def make_pictures(self):
 | 
					    def make_pictures(self):
 | 
				
			||||||
        """
 | 
					        """Create pictures for users to be tagged on later.
 | 
				
			||||||
        Create pictures for users to be tagged on later.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        The :meth:`make_users` method must have been called beforehand.
 | 
					        The :meth:`make_users` method must have been called beforehand.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@@ -301,11 +297,10 @@ class Command(BaseCommand):
 | 
				
			|||||||
        self.picts = list(Picture.objects.filter(name__startswith="galaxy-").all())
 | 
					        self.picts = list(Picture.objects.filter(name__startswith="galaxy-").all())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def make_pictures_memberships(self):
 | 
					    def make_pictures_memberships(self):
 | 
				
			||||||
        """
 | 
					        """Assign users to pictures and make enough of them for our
 | 
				
			||||||
        Assign users to pictures and make enough of them for our
 | 
					 | 
				
			||||||
        created users to be eligible for promotion as citizen.
 | 
					        created users to be eligible for promotion as citizen.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        See :meth:`galaxy.models.Galaxy.rule` for details on promotion to citizen.
 | 
					        See `galaxy.models.Galaxy.rule` for details on promotion to citizen.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.pictures_tags = []
 | 
					        self.pictures_tags = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -363,9 +358,9 @@ class Command(BaseCommand):
 | 
				
			|||||||
        PeoplePictureRelation.objects.bulk_create(self.pictures_tags)
 | 
					        PeoplePictureRelation.objects.bulk_create(self.pictures_tags)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def make_important_citizen(self, uid: int):
 | 
					    def make_important_citizen(self, uid: int):
 | 
				
			||||||
        """
 | 
					        """Make the user whose uid is given in parameter a more important citizen.
 | 
				
			||||||
        Make the user whose uid is given in parameter a more important citizen,
 | 
					
 | 
				
			||||||
        thus triggering many more connections to others (lanes)
 | 
					        This will trigger many more connections to others (lanes)
 | 
				
			||||||
        and dragging him towards the center of the Galaxy.
 | 
					        and dragging him towards the center of the Galaxy.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        This promotion is obtained by adding more family links
 | 
					        This promotion is obtained by adding more family links
 | 
				
			||||||
@@ -375,7 +370,8 @@ class Command(BaseCommand):
 | 
				
			|||||||
        also be tagged in more pictures, thus making them also
 | 
					        also be tagged in more pictures, thus making them also
 | 
				
			||||||
        more important.
 | 
					        more important.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param uid: the id of the user to make more important
 | 
					        Args:
 | 
				
			||||||
 | 
					            uid: the id of the user to make more important
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        u1 = self.users[uid]
 | 
					        u1 = self.users[uid]
 | 
				
			||||||
        u2 = self.users[uid - 100]
 | 
					        u2 = self.users[uid - 100]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,7 +26,7 @@ from __future__ import annotations
 | 
				
			|||||||
import logging
 | 
					import logging
 | 
				
			||||||
import math
 | 
					import math
 | 
				
			||||||
import time
 | 
					import time
 | 
				
			||||||
from typing import List, NamedTuple, Optional, TypedDict, Union
 | 
					from typing import NamedTuple, TypedDict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
from django.db.models import Case, Count, F, Q, Value, When
 | 
					from django.db.models import Case, Count, F, Q, Value, When
 | 
				
			||||||
@@ -40,9 +40,9 @@ from sas.models import Picture
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GalaxyStar(models.Model):
 | 
					class GalaxyStar(models.Model):
 | 
				
			||||||
    """
 | 
					    """Define a star (vertex -> user) in the galaxy graph.
 | 
				
			||||||
    Define a star (vertex -> user) in the galaxy graph,
 | 
					
 | 
				
			||||||
    storing a reference to its owner citizen.
 | 
					    Store a reference to its owner citizen.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Stars are linked to each others through the :class:`GalaxyLane` model.
 | 
					    Stars are linked to each others through the :class:`GalaxyLane` model.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -74,13 +74,14 @@ class GalaxyStar(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@property
 | 
					@property
 | 
				
			||||||
def current_star(self) -> Optional[GalaxyStar]:
 | 
					def current_star(self) -> GalaxyStar | None:
 | 
				
			||||||
    """
 | 
					    """The star of this user in the :class:`Galaxy`.
 | 
				
			||||||
    The star of this user in the :class:`Galaxy`.
 | 
					
 | 
				
			||||||
    Only take into account the most recent active galaxy.
 | 
					    Only take into account the most recent active galaxy.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :return: The star of this user if there is an active Galaxy
 | 
					    Returns:
 | 
				
			||||||
             and this user is a citizen of it, else ``None``
 | 
					        The star of this user if there is an active Galaxy
 | 
				
			||||||
 | 
					        and this user is a citizen of it, else `None`
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    return self.stars.filter(galaxy=Galaxy.get_current_galaxy()).last()
 | 
					    return self.stars.filter(galaxy=Galaxy.get_current_galaxy()).last()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -90,10 +91,9 @@ User.current_star = current_star
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GalaxyLane(models.Model):
 | 
					class GalaxyLane(models.Model):
 | 
				
			||||||
    """
 | 
					    """Define a lane (edge -> link between galaxy citizen) in the galaxy map.
 | 
				
			||||||
    Define a lane (edge -> link between galaxy citizen)
 | 
					
 | 
				
			||||||
    in the galaxy map, storing a reference to both its
 | 
					    Store a reference to both its ends and the distance it covers.
 | 
				
			||||||
    ends and the distance it covers.
 | 
					 | 
				
			||||||
    Score details between citizen owning the stars is also stored here.
 | 
					    Score details between citizen owning the stars is also stored here.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -138,8 +138,8 @@ class StarDict(TypedDict):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GalaxyDict(TypedDict):
 | 
					class GalaxyDict(TypedDict):
 | 
				
			||||||
    nodes: List[StarDict]
 | 
					    nodes: list[StarDict]
 | 
				
			||||||
    links: List
 | 
					    links: list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RelationScore(NamedTuple):
 | 
					class RelationScore(NamedTuple):
 | 
				
			||||||
@@ -149,8 +149,8 @@ class RelationScore(NamedTuple):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Galaxy(models.Model):
 | 
					class Galaxy(models.Model):
 | 
				
			||||||
    """
 | 
					    """The Galaxy, a graph linking the active users between each others.
 | 
				
			||||||
    The Galaxy, a graph linking the active users between each others.
 | 
					
 | 
				
			||||||
    The distance between two users is given by a relation score which takes
 | 
					    The distance between two users is given by a relation score which takes
 | 
				
			||||||
    into account a few parameter like the number of pictures they are both tagged on,
 | 
					    into account a few parameter like the number of pictures they are both tagged on,
 | 
				
			||||||
    the time during which they were in the same clubs and whether they are
 | 
					    the time during which they were in the same clubs and whether they are
 | 
				
			||||||
@@ -204,8 +204,8 @@ class Galaxy(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def compute_user_score(cls, user: User) -> int:
 | 
					    def compute_user_score(cls, user: User) -> int:
 | 
				
			||||||
        """
 | 
					        """Compute an individual score for each citizen.
 | 
				
			||||||
        Compute an individual score for each citizen.
 | 
					
 | 
				
			||||||
        It will later be used by the graph algorithm to push
 | 
					        It will later be used by the graph algorithm to push
 | 
				
			||||||
        higher scores towards the center of the galaxy.
 | 
					        higher scores towards the center of the galaxy.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -231,10 +231,7 @@ class Galaxy(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def query_user_score(cls, user: User) -> int:
 | 
					    def query_user_score(cls, user: User) -> int:
 | 
				
			||||||
        """
 | 
					        """Get the individual score of the given user in the galaxy."""
 | 
				
			||||||
        Perform the db query to get the  individual score
 | 
					 | 
				
			||||||
        of the given user in the galaxy.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        score_query = (
 | 
					        score_query = (
 | 
				
			||||||
            User.objects.filter(id=user.id)
 | 
					            User.objects.filter(id=user.id)
 | 
				
			||||||
            .annotate(
 | 
					            .annotate(
 | 
				
			||||||
@@ -262,9 +259,9 @@ class Galaxy(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def compute_users_score(cls, user1: User, user2: User) -> RelationScore:
 | 
					    def compute_users_score(cls, user1: User, user2: User) -> RelationScore:
 | 
				
			||||||
        """
 | 
					        """Compute the relationship scores of the two given users.
 | 
				
			||||||
        Compute the relationship scores of the two given users
 | 
					
 | 
				
			||||||
        in the following fields :
 | 
					        The computation is done with the following fields :
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        - family: if they have some godfather/godchild relation
 | 
					        - family: if they have some godfather/godchild relation
 | 
				
			||||||
        - pictures: in how many pictures are both tagged
 | 
					        - pictures: in how many pictures are both tagged
 | 
				
			||||||
@@ -277,11 +274,12 @@ class Galaxy(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def compute_users_family_score(cls, user1: User, user2: User) -> int:
 | 
					    def compute_users_family_score(cls, user1: User, user2: User) -> int:
 | 
				
			||||||
        """
 | 
					        """Compute the family score of the relation between the given users.
 | 
				
			||||||
        Compute the family score of the relation between the given users.
 | 
					
 | 
				
			||||||
        This takes into account mutual godfathers.
 | 
					        This takes into account mutual godfathers.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: 366 if user1 is the godfather of user2 (or vice versa) else 0
 | 
					        Returns:
 | 
				
			||||||
 | 
					             366 if user1 is the godfather of user2 (or vice versa) else 0
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        link_count = User.objects.filter(
 | 
					        link_count = User.objects.filter(
 | 
				
			||||||
            Q(id=user1.id, godfathers=user2) | Q(id=user2.id, godfathers=user1)
 | 
					            Q(id=user1.id, godfathers=user2) | Q(id=user2.id, godfathers=user1)
 | 
				
			||||||
@@ -294,14 +292,14 @@ class Galaxy(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def compute_users_pictures_score(cls, user1: User, user2: User) -> int:
 | 
					    def compute_users_pictures_score(cls, user1: User, user2: User) -> int:
 | 
				
			||||||
        """
 | 
					        """Compute the pictures score of the relation between the given users.
 | 
				
			||||||
        Compute the pictures score of the relation between the given users.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        The pictures score is obtained by counting the number
 | 
					        The pictures score is obtained by counting the number
 | 
				
			||||||
        of :class:`Picture` in which they have been both identified.
 | 
					        of :class:`Picture` in which they have been both identified.
 | 
				
			||||||
        This score is then multiplied by 2.
 | 
					        This score is then multiplied by 2.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: The number of pictures both users have in common, times 2
 | 
					        Returns:
 | 
				
			||||||
 | 
					             The number of pictures both users have in common, times 2
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        picture_count = (
 | 
					        picture_count = (
 | 
				
			||||||
            Picture.objects.filter(people__user__in=(user1,))
 | 
					            Picture.objects.filter(people__user__in=(user1,))
 | 
				
			||||||
@@ -316,8 +314,7 @@ class Galaxy(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def compute_users_clubs_score(cls, user1: User, user2: User) -> int:
 | 
					    def compute_users_clubs_score(cls, user1: User, user2: User) -> int:
 | 
				
			||||||
        """
 | 
					        """Compute the clubs score of the relation between the given users.
 | 
				
			||||||
        Compute the clubs score of the relation between the given users.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        The club score is obtained by counting the number of days
 | 
					        The club score is obtained by counting the number of days
 | 
				
			||||||
        during which the memberships (see :class:`club.models.Membership`)
 | 
					        during which the memberships (see :class:`club.models.Membership`)
 | 
				
			||||||
@@ -328,7 +325,8 @@ class Galaxy(models.Model):
 | 
				
			|||||||
        31/12/2022 (also two years, but with an offset of one year), then their
 | 
					        31/12/2022 (also two years, but with an offset of one year), then their
 | 
				
			||||||
        club score is 365.
 | 
					        club score is 365.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: the number of days during which both users were in the same club
 | 
					        Returns:
 | 
				
			||||||
 | 
					            the number of days during which both users were in the same club
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        common_clubs = Club.objects.filter(members__in=user1.memberships.all()).filter(
 | 
					        common_clubs = Club.objects.filter(members__in=user1.memberships.all()).filter(
 | 
				
			||||||
            members__in=user2.memberships.all()
 | 
					            members__in=user2.memberships.all()
 | 
				
			||||||
@@ -380,13 +378,13 @@ class Galaxy(models.Model):
 | 
				
			|||||||
    ###################
 | 
					    ###################
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def scale_distance(cls, value: Union[int, float]) -> int:
 | 
					    def scale_distance(cls, value: int | float) -> int:
 | 
				
			||||||
        """
 | 
					        """Given a numeric value, return a scaled value which can
 | 
				
			||||||
        Given a numeric value, return a scaled value which can
 | 
					 | 
				
			||||||
        be used in the Galaxy's graphical interface to set the distance
 | 
					        be used in the Galaxy's graphical interface to set the distance
 | 
				
			||||||
        between two stars
 | 
					        between two stars.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: the scaled value usable in the Galaxy's 3d graph
 | 
					        Returns:
 | 
				
			||||||
 | 
					            the scaled value usable in the Galaxy's 3d graph
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        # TODO: this will need adjustements with the real, typical data on Taiste
 | 
					        # TODO: this will need adjustements with the real, typical data on Taiste
 | 
				
			||||||
        if value == 0:
 | 
					        if value == 0:
 | 
				
			||||||
@@ -409,8 +407,8 @@ class Galaxy(models.Model):
 | 
				
			|||||||
        return int(value)
 | 
					        return int(value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def rule(self, picture_count_threshold=10) -> None:
 | 
					    def rule(self, picture_count_threshold=10) -> None:
 | 
				
			||||||
        """
 | 
					        """Main function of the Galaxy.
 | 
				
			||||||
        Main function of the Galaxy.
 | 
					
 | 
				
			||||||
        Iterate over all the rulable users to promote them to citizens.
 | 
					        Iterate over all the rulable users to promote them to citizens.
 | 
				
			||||||
        A citizen is a user who has a corresponding star in the Galaxy.
 | 
					        A citizen is a user who has a corresponding star in the Galaxy.
 | 
				
			||||||
        Also build up the lanes, which are the links between the different citizen.
 | 
					        Also build up the lanes, which are the links between the different citizen.
 | 
				
			||||||
@@ -566,9 +564,7 @@ class Galaxy(models.Model):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def make_state(self) -> None:
 | 
					    def make_state(self) -> None:
 | 
				
			||||||
        """
 | 
					        """Compute JSON structure to send to 3d-force-graph: https://github.com/vasturiano/3d-force-graph/."""
 | 
				
			||||||
        Compute JSON structure to send to 3d-force-graph: https://github.com/vasturiano/3d-force-graph/
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.logger.info(
 | 
					        self.logger.info(
 | 
				
			||||||
            "Caching current Galaxy state for a quicker display of the Empire's power."
 | 
					            "Caching current Galaxy state for a quicker display of the Empire's power."
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -46,9 +46,7 @@ class GalaxyTestModel(TestCase):
 | 
				
			|||||||
        cls.com = User.objects.get(username="comunity")
 | 
					        cls.com = User.objects.get(username="comunity")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_user_self_score(self):
 | 
					    def test_user_self_score(self):
 | 
				
			||||||
        """
 | 
					        """Test that individual user scores are correct."""
 | 
				
			||||||
        Test that individual user scores are correct
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        with self.assertNumQueries(8):
 | 
					        with self.assertNumQueries(8):
 | 
				
			||||||
            assert Galaxy.compute_user_score(self.root) == 9
 | 
					            assert Galaxy.compute_user_score(self.root) == 9
 | 
				
			||||||
            assert Galaxy.compute_user_score(self.skia) == 10
 | 
					            assert Galaxy.compute_user_score(self.skia) == 10
 | 
				
			||||||
@@ -60,9 +58,8 @@ class GalaxyTestModel(TestCase):
 | 
				
			|||||||
            assert Galaxy.compute_user_score(self.com) == 1
 | 
					            assert Galaxy.compute_user_score(self.com) == 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_users_score(self):
 | 
					    def test_users_score(self):
 | 
				
			||||||
        """
 | 
					        """Test on the default dataset generated by the `populate` command
 | 
				
			||||||
        Test on the default dataset generated by the `populate` command
 | 
					        that the relation scores are correct.
 | 
				
			||||||
        that the relation scores are correct
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        expected_scores = {
 | 
					        expected_scores = {
 | 
				
			||||||
            "krophil": {
 | 
					            "krophil": {
 | 
				
			||||||
@@ -138,8 +135,7 @@ class GalaxyTestModel(TestCase):
 | 
				
			|||||||
        self.assertDictEqual(expected_scores, computed_scores)
 | 
					        self.assertDictEqual(expected_scores, computed_scores)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_rule(self):
 | 
					    def test_rule(self):
 | 
				
			||||||
        """
 | 
					        """Test on the default dataset generated by the `populate` command
 | 
				
			||||||
        Test on the default dataset generated by the `populate` command
 | 
					 | 
				
			||||||
        that the number of queries to rule the galaxy is stable.
 | 
					        that the number of queries to rule the galaxy is stable.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        galaxy = Galaxy.objects.create()
 | 
					        galaxy = Galaxy.objects.create()
 | 
				
			||||||
@@ -151,18 +147,14 @@ class GalaxyTestModel(TestCase):
 | 
				
			|||||||
class GalaxyTestView(TestCase):
 | 
					class GalaxyTestView(TestCase):
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def setUpTestData(cls):
 | 
					    def setUpTestData(cls):
 | 
				
			||||||
        """
 | 
					        """Generate a plausible Galaxy once for every test."""
 | 
				
			||||||
        Generate a plausible Galaxy once for every test
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        call_command("generate_galaxy_test_data", "-v", "0")
 | 
					        call_command("generate_galaxy_test_data", "-v", "0")
 | 
				
			||||||
        galaxy = Galaxy.objects.create()
 | 
					        galaxy = Galaxy.objects.create()
 | 
				
			||||||
        galaxy.rule(26)  # We want a fast test
 | 
					        galaxy.rule(26)  # We want a fast test
 | 
				
			||||||
        cls.root = User.objects.get(username="root")
 | 
					        cls.root = User.objects.get(username="root")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_page_is_citizen(self):
 | 
					    def test_page_is_citizen(self):
 | 
				
			||||||
        """
 | 
					        """Test that users can access the galaxy page of users who are citizens."""
 | 
				
			||||||
        Test that users can access the galaxy page of users who are citizens
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.client.force_login(self.root)
 | 
					        self.client.force_login(self.root)
 | 
				
			||||||
        user = User.objects.get(last_name="n°500")
 | 
					        user = User.objects.get(last_name="n°500")
 | 
				
			||||||
        response = self.client.get(reverse("galaxy:user", args=[user.id]))
 | 
					        response = self.client.get(reverse("galaxy:user", args=[user.id]))
 | 
				
			||||||
@@ -173,20 +165,19 @@ class GalaxyTestView(TestCase):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_page_not_citizen(self):
 | 
					    def test_page_not_citizen(self):
 | 
				
			||||||
        """
 | 
					        """Test that trying to access the galaxy page of non-citizen users return a 404."""
 | 
				
			||||||
        Test that trying to access the galaxy page of a user who is not
 | 
					 | 
				
			||||||
        citizens return a 404
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.client.force_login(self.root)
 | 
					        self.client.force_login(self.root)
 | 
				
			||||||
        user = User.objects.get(last_name="n°1")
 | 
					        user = User.objects.get(last_name="n°1")
 | 
				
			||||||
        response = self.client.get(reverse("galaxy:user", args=[user.id]))
 | 
					        response = self.client.get(reverse("galaxy:user", args=[user.id]))
 | 
				
			||||||
        assert response.status_code == 404
 | 
					        assert response.status_code == 404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_full_galaxy_state(self):
 | 
					    def test_full_galaxy_state(self):
 | 
				
			||||||
        """
 | 
					        """Test with a more complete galaxy.
 | 
				
			||||||
        Test on the more complex dataset generated by the `generate_galaxy_test_data`
 | 
					
 | 
				
			||||||
        command that the relation scores are correct, and that the view exposes the
 | 
					        This time, the test is done on
 | 
				
			||||||
        right data.
 | 
					        the more complex dataset generated by the `generate_galaxy_test_data`
 | 
				
			||||||
 | 
					        command that the relation scores are correct,
 | 
				
			||||||
 | 
					        and that the view exposes the right data.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.client.force_login(self.root)
 | 
					        self.client.force_login(self.root)
 | 
				
			||||||
        response = self.client.get(reverse("galaxy:data"))
 | 
					        response = self.client.get(reverse("galaxy:data"))
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -44,9 +44,7 @@ class Launderette(models.Model):
 | 
				
			|||||||
        return reverse("launderette:launderette_list")
 | 
					        return reverse("launderette:launderette_list")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_owned_by(self, user):
 | 
					    def is_owned_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Method to see if that object can be edited by the given user."""
 | 
				
			||||||
        Method to see if that object can be edited by the given user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if user.is_anonymous:
 | 
					        if user.is_anonymous:
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
        launderette_club = Club.objects.filter(
 | 
					        launderette_club = Club.objects.filter(
 | 
				
			||||||
@@ -108,9 +106,7 @@ class Machine(models.Model):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_owned_by(self, user):
 | 
					    def is_owned_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Method to see if that object can be edited by the given user."""
 | 
				
			||||||
        Method to see if that object can be edited by the given user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if user.is_anonymous:
 | 
					        if user.is_anonymous:
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
        launderette_club = Club.objects.filter(
 | 
					        launderette_club = Club.objects.filter(
 | 
				
			||||||
@@ -161,9 +157,7 @@ class Token(models.Model):
 | 
				
			|||||||
            super().save(*args, **kwargs)
 | 
					            super().save(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_owned_by(self, user):
 | 
					    def is_owned_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Method to see if that object can be edited by the given user."""
 | 
				
			||||||
        Method to see if that object can be edited by the given user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if user.is_anonymous:
 | 
					        if user.is_anonymous:
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
        launderette_club = Club.objects.filter(
 | 
					        launderette_club = Club.objects.filter(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,26 +38,26 @@ from launderette.models import Launderette, Machine, Slot, Token
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LaunderetteMainView(TemplateView):
 | 
					class LaunderetteMainView(TemplateView):
 | 
				
			||||||
    """Main presentation view"""
 | 
					    """Main presentation view."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    template_name = "launderette/launderette_main.jinja"
 | 
					    template_name = "launderette/launderette_main.jinja"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """Add page to the context"""
 | 
					        """Add page to the context."""
 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        kwargs["page"] = Page.objects.filter(name="launderette").first()
 | 
					        kwargs["page"] = Page.objects.filter(name="launderette").first()
 | 
				
			||||||
        return kwargs
 | 
					        return kwargs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LaunderetteBookMainView(CanViewMixin, ListView):
 | 
					class LaunderetteBookMainView(CanViewMixin, ListView):
 | 
				
			||||||
    """Choose which launderette to book"""
 | 
					    """Choose which launderette to book."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Launderette
 | 
					    model = Launderette
 | 
				
			||||||
    template_name = "launderette/launderette_book_choose.jinja"
 | 
					    template_name = "launderette/launderette_book_choose.jinja"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LaunderetteBookView(CanViewMixin, DetailView):
 | 
					class LaunderetteBookView(CanViewMixin, DetailView):
 | 
				
			||||||
    """Display the launderette schedule"""
 | 
					    """Display the launderette schedule."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Launderette
 | 
					    model = Launderette
 | 
				
			||||||
    pk_url_kwarg = "launderette_id"
 | 
					    pk_url_kwarg = "launderette_id"
 | 
				
			||||||
@@ -133,7 +133,7 @@ class LaunderetteBookView(CanViewMixin, DetailView):
 | 
				
			|||||||
            currentDate += delta
 | 
					            currentDate += delta
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """Add page to the context"""
 | 
					        """Add page to the context."""
 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        kwargs["planning"] = OrderedDict()
 | 
					        kwargs["planning"] = OrderedDict()
 | 
				
			||||||
        kwargs["slot_type"] = self.slot_type
 | 
					        kwargs["slot_type"] = self.slot_type
 | 
				
			||||||
@@ -166,7 +166,7 @@ class LaunderetteBookView(CanViewMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SlotDeleteView(CanEditPropMixin, DeleteView):
 | 
					class SlotDeleteView(CanEditPropMixin, DeleteView):
 | 
				
			||||||
    """Delete a slot"""
 | 
					    """Delete a slot."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Slot
 | 
					    model = Slot
 | 
				
			||||||
    pk_url_kwarg = "slot_id"
 | 
					    pk_url_kwarg = "slot_id"
 | 
				
			||||||
@@ -180,14 +180,14 @@ class SlotDeleteView(CanEditPropMixin, DeleteView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LaunderetteListView(CanEditPropMixin, ListView):
 | 
					class LaunderetteListView(CanEditPropMixin, ListView):
 | 
				
			||||||
    """Choose which launderette to administer"""
 | 
					    """Choose which launderette to administer."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Launderette
 | 
					    model = Launderette
 | 
				
			||||||
    template_name = "launderette/launderette_list.jinja"
 | 
					    template_name = "launderette/launderette_list.jinja"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LaunderetteEditView(CanEditPropMixin, UpdateView):
 | 
					class LaunderetteEditView(CanEditPropMixin, UpdateView):
 | 
				
			||||||
    """Edit a launderette"""
 | 
					    """Edit a launderette."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Launderette
 | 
					    model = Launderette
 | 
				
			||||||
    pk_url_kwarg = "launderette_id"
 | 
					    pk_url_kwarg = "launderette_id"
 | 
				
			||||||
@@ -196,7 +196,7 @@ class LaunderetteEditView(CanEditPropMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LaunderetteCreateView(CanCreateMixin, CreateView):
 | 
					class LaunderetteCreateView(CanCreateMixin, CreateView):
 | 
				
			||||||
    """Create a new launderette"""
 | 
					    """Create a new launderette."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Launderette
 | 
					    model = Launderette
 | 
				
			||||||
    fields = ["name"]
 | 
					    fields = ["name"]
 | 
				
			||||||
@@ -275,7 +275,7 @@ class ManageTokenForm(forms.Form):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LaunderetteAdminView(CanEditPropMixin, BaseFormView, DetailView):
 | 
					class LaunderetteAdminView(CanEditPropMixin, BaseFormView, DetailView):
 | 
				
			||||||
    """The admin page of the launderette"""
 | 
					    """The admin page of the launderette."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Launderette
 | 
					    model = Launderette
 | 
				
			||||||
    pk_url_kwarg = "launderette_id"
 | 
					    pk_url_kwarg = "launderette_id"
 | 
				
			||||||
@@ -297,9 +297,7 @@ class LaunderetteAdminView(CanEditPropMixin, BaseFormView, DetailView):
 | 
				
			|||||||
            return self.form_invalid(form)
 | 
					            return self.form_invalid(form)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def form_valid(self, form):
 | 
					    def form_valid(self, form):
 | 
				
			||||||
        """
 | 
					        """We handle here the redirection, passing the user id of the asked customer."""
 | 
				
			||||||
        We handle here the redirection, passing the user id of the asked customer
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        form.process(self.object)
 | 
					        form.process(self.object)
 | 
				
			||||||
        if form.is_valid():
 | 
					        if form.is_valid():
 | 
				
			||||||
            return super().form_valid(form)
 | 
					            return super().form_valid(form)
 | 
				
			||||||
@@ -307,9 +305,7 @@ class LaunderetteAdminView(CanEditPropMixin, BaseFormView, DetailView):
 | 
				
			|||||||
            return super().form_invalid(form)
 | 
					            return super().form_invalid(form)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """
 | 
					        """We handle here the login form for the barman."""
 | 
				
			||||||
        We handle here the login form for the barman
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        if self.request.method == "GET":
 | 
					        if self.request.method == "GET":
 | 
				
			||||||
            kwargs["form"] = self.get_form()
 | 
					            kwargs["form"] = self.get_form()
 | 
				
			||||||
@@ -331,7 +327,7 @@ class GetLaunderetteUserForm(GetUserForm):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LaunderetteMainClickView(CanEditMixin, BaseFormView, DetailView):
 | 
					class LaunderetteMainClickView(CanEditMixin, BaseFormView, DetailView):
 | 
				
			||||||
    """The click page of the launderette"""
 | 
					    """The click page of the launderette."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Launderette
 | 
					    model = Launderette
 | 
				
			||||||
    pk_url_kwarg = "launderette_id"
 | 
					    pk_url_kwarg = "launderette_id"
 | 
				
			||||||
@@ -347,16 +343,12 @@ class LaunderetteMainClickView(CanEditMixin, BaseFormView, DetailView):
 | 
				
			|||||||
        return super().post(request, *args, **kwargs)
 | 
					        return super().post(request, *args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def form_valid(self, form):
 | 
					    def form_valid(self, form):
 | 
				
			||||||
        """
 | 
					        """We handle here the redirection, passing the user id of the asked customer."""
 | 
				
			||||||
        We handle here the redirection, passing the user id of the asked customer
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.kwargs["user_id"] = form.cleaned_data["user_id"]
 | 
					        self.kwargs["user_id"] = form.cleaned_data["user_id"]
 | 
				
			||||||
        return super().form_valid(form)
 | 
					        return super().form_valid(form)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """
 | 
					        """We handle here the login form for the barman."""
 | 
				
			||||||
        We handle here the login form for the barman
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        kwargs["counter"] = self.object.counter
 | 
					        kwargs["counter"] = self.object.counter
 | 
				
			||||||
        kwargs["form"] = self.get_form()
 | 
					        kwargs["form"] = self.get_form()
 | 
				
			||||||
@@ -417,7 +409,7 @@ class ClickTokenForm(forms.BaseForm):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView):
 | 
					class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView):
 | 
				
			||||||
    """The click page of the launderette"""
 | 
					    """The click page of the launderette."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Launderette
 | 
					    model = Launderette
 | 
				
			||||||
    pk_url_kwarg = "launderette_id"
 | 
					    pk_url_kwarg = "launderette_id"
 | 
				
			||||||
@@ -465,14 +457,14 @@ class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView):
 | 
				
			|||||||
        return type("ClickForm", (ClickTokenForm,), kwargs)
 | 
					        return type("ClickForm", (ClickTokenForm,), kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get(self, request, *args, **kwargs):
 | 
					    def get(self, request, *args, **kwargs):
 | 
				
			||||||
        """Simple get view"""
 | 
					        """Simple get view."""
 | 
				
			||||||
        self.customer = Customer.objects.filter(user__id=self.kwargs["user_id"]).first()
 | 
					        self.customer = Customer.objects.filter(user__id=self.kwargs["user_id"]).first()
 | 
				
			||||||
        self.subscriber = self.customer.user
 | 
					        self.subscriber = self.customer.user
 | 
				
			||||||
        self.operator = request.user
 | 
					        self.operator = request.user
 | 
				
			||||||
        return super().get(request, *args, **kwargs)
 | 
					        return super().get(request, *args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def post(self, request, *args, **kwargs):
 | 
					    def post(self, request, *args, **kwargs):
 | 
				
			||||||
        """Handle the many possibilities of the post request"""
 | 
					        """Handle the many possibilities of the post request."""
 | 
				
			||||||
        self.object = self.get_object()
 | 
					        self.object = self.get_object()
 | 
				
			||||||
        self.customer = Customer.objects.filter(user__id=self.kwargs["user_id"]).first()
 | 
					        self.customer = Customer.objects.filter(user__id=self.kwargs["user_id"]).first()
 | 
				
			||||||
        self.subscriber = self.customer.user
 | 
					        self.subscriber = self.customer.user
 | 
				
			||||||
@@ -480,16 +472,12 @@ class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView):
 | 
				
			|||||||
        return super().post(request, *args, **kwargs)
 | 
					        return super().post(request, *args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def form_valid(self, form):
 | 
					    def form_valid(self, form):
 | 
				
			||||||
        """
 | 
					        """We handle here the redirection, passing the user id of the asked customer."""
 | 
				
			||||||
        We handle here the redirection, passing the user id of the asked customer
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.request.session.update(form.last_basket)
 | 
					        self.request.session.update(form.last_basket)
 | 
				
			||||||
        return super().form_valid(form)
 | 
					        return super().form_valid(form)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """
 | 
					        """We handle here the login form for the barman."""
 | 
				
			||||||
        We handle here the login form for the barman
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        if "form" not in kwargs.keys():
 | 
					        if "form" not in kwargs.keys():
 | 
				
			||||||
            kwargs["form"] = self.get_form()
 | 
					            kwargs["form"] = self.get_form()
 | 
				
			||||||
@@ -505,7 +493,7 @@ class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MachineEditView(CanEditPropMixin, UpdateView):
 | 
					class MachineEditView(CanEditPropMixin, UpdateView):
 | 
				
			||||||
    """Edit a machine"""
 | 
					    """Edit a machine."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Machine
 | 
					    model = Machine
 | 
				
			||||||
    pk_url_kwarg = "machine_id"
 | 
					    pk_url_kwarg = "machine_id"
 | 
				
			||||||
@@ -514,7 +502,7 @@ class MachineEditView(CanEditPropMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MachineDeleteView(CanEditPropMixin, DeleteView):
 | 
					class MachineDeleteView(CanEditPropMixin, DeleteView):
 | 
				
			||||||
    """Edit a machine"""
 | 
					    """Edit a machine."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Machine
 | 
					    model = Machine
 | 
				
			||||||
    pk_url_kwarg = "machine_id"
 | 
					    pk_url_kwarg = "machine_id"
 | 
				
			||||||
@@ -523,7 +511,7 @@ class MachineDeleteView(CanEditPropMixin, DeleteView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MachineCreateView(CanCreateMixin, CreateView):
 | 
					class MachineCreateView(CanCreateMixin, CreateView):
 | 
				
			||||||
    """Create a new machine"""
 | 
					    """Create a new machine."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Machine
 | 
					    model = Machine
 | 
				
			||||||
    fields = ["name", "launderette", "type"]
 | 
					    fields = ["name", "launderette", "type"]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -155,9 +155,7 @@ class SearchFormListView(FormerSubscriberMixin, SingleObjectMixin, ListView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SearchFormView(FormerSubscriberMixin, FormView):
 | 
					class SearchFormView(FormerSubscriberMixin, FormView):
 | 
				
			||||||
    """
 | 
					    """Allows users to search inside the user list."""
 | 
				
			||||||
    Allows users to search inside the user list
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    form_class = SearchForm
 | 
					    form_class = SearchForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -200,9 +198,7 @@ class SearchQuickFormView(SearchFormView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SearchClearFormView(FormerSubscriberMixin, View):
 | 
					class SearchClearFormView(FormerSubscriberMixin, View):
 | 
				
			||||||
    """
 | 
					    """Clear SearchFormView and redirect to it."""
 | 
				
			||||||
    Clear SearchFormView and redirect to it
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def dispatch(self, request, *args, **kwargs):
 | 
					    def dispatch(self, request, *args, **kwargs):
 | 
				
			||||||
        super().dispatch(request, *args, **kwargs)
 | 
					        super().dispatch(request, *args, **kwargs)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,9 +30,7 @@ from pedagogy.models import UV, UVComment, UVCommentReport
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UVForm(forms.ModelForm):
 | 
					class UVForm(forms.ModelForm):
 | 
				
			||||||
    """
 | 
					    """Form handeling creation and edit of an UV."""
 | 
				
			||||||
    Form handeling creation and edit of an UV
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = UV
 | 
					        model = UV
 | 
				
			||||||
@@ -85,9 +83,7 @@ class StarList(forms.NumberInput):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UVCommentForm(forms.ModelForm):
 | 
					class UVCommentForm(forms.ModelForm):
 | 
				
			||||||
    """
 | 
					    """Form handeling creation and edit of an UVComment."""
 | 
				
			||||||
    Form handeling creation and edit of an UVComment
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = UVComment
 | 
					        model = UVComment
 | 
				
			||||||
@@ -137,9 +133,7 @@ class UVCommentForm(forms.ModelForm):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UVCommentReportForm(forms.ModelForm):
 | 
					class UVCommentReportForm(forms.ModelForm):
 | 
				
			||||||
    """
 | 
					    """Form handeling creation and edit of an UVReport."""
 | 
				
			||||||
    Form handeling creation and edit of an UVReport
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = UVCommentReport
 | 
					        model = UVCommentReport
 | 
				
			||||||
@@ -159,9 +153,7 @@ class UVCommentReportForm(forms.ModelForm):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UVCommentModerationForm(forms.Form):
 | 
					class UVCommentModerationForm(forms.Form):
 | 
				
			||||||
    """
 | 
					    """Form handeling bulk comment deletion."""
 | 
				
			||||||
    Form handeling bulk comment deletion
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    accepted_reports = forms.ModelMultipleChoiceField(
 | 
					    accepted_reports = forms.ModelMultipleChoiceField(
 | 
				
			||||||
        UVCommentReport.objects.all(),
 | 
					        UVCommentReport.objects.all(),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -36,9 +36,7 @@ from core.models import User
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UV(models.Model):
 | 
					class UV(models.Model):
 | 
				
			||||||
    """
 | 
					    """Contains infos about an UV (course)."""
 | 
				
			||||||
    Contains infos about an UV (course)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    code = models.CharField(
 | 
					    code = models.CharField(
 | 
				
			||||||
        _("code"),
 | 
					        _("code"),
 | 
				
			||||||
@@ -148,15 +146,11 @@ class UV(models.Model):
 | 
				
			|||||||
        return reverse("pedagogy:uv_detail", kwargs={"uv_id": self.id})
 | 
					        return reverse("pedagogy:uv_detail", kwargs={"uv_id": self.id})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_owned_by(self, user):
 | 
					    def is_owned_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Can be created by superuser, root or pedagogy admin user."""
 | 
				
			||||||
        Can be created by superuser, root or pedagogy admin user
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return user.is_in_group(pk=settings.SITH_GROUP_PEDAGOGY_ADMIN_ID)
 | 
					        return user.is_in_group(pk=settings.SITH_GROUP_PEDAGOGY_ADMIN_ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def can_be_viewed_by(self, user):
 | 
					    def can_be_viewed_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Only visible by subscribers."""
 | 
				
			||||||
        Only visible by subscribers
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return user.is_subscribed
 | 
					        return user.is_subscribed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __grade_average_generic(self, field):
 | 
					    def __grade_average_generic(self, field):
 | 
				
			||||||
@@ -166,14 +160,13 @@ class UV(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return int(sum(comments.values_list(field, flat=True)) / comments.count())
 | 
					        return int(sum(comments.values_list(field, flat=True)) / comments.count())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def has_user_already_commented(self, user):
 | 
					    def has_user_already_commented(self, user: User) -> bool:
 | 
				
			||||||
        """
 | 
					        """Help prevent multiples comments from the same user.
 | 
				
			||||||
        Help prevent multiples comments from the same user
 | 
					 | 
				
			||||||
        This function checks that no other comment has been posted by a specified user
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param user: core.models.User
 | 
					        This function checks that no other comment has been posted by a specified user.
 | 
				
			||||||
        :return: if the user has already posted a comment on this UV
 | 
					
 | 
				
			||||||
        :rtype: bool
 | 
					        Returns:
 | 
				
			||||||
 | 
					            True if the user has already posted a comment on this UV, else False.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self.comments.filter(author=user).exists()
 | 
					        return self.comments.filter(author=user).exists()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -199,9 +192,7 @@ class UV(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UVComment(models.Model):
 | 
					class UVComment(models.Model):
 | 
				
			||||||
    """
 | 
					    """A comment about an UV."""
 | 
				
			||||||
    A comment about an UV
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    author = models.ForeignKey(
 | 
					    author = models.ForeignKey(
 | 
				
			||||||
        User,
 | 
					        User,
 | 
				
			||||||
@@ -261,28 +252,30 @@ class UVComment(models.Model):
 | 
				
			|||||||
        super().save(*args, **kwargs)
 | 
					        super().save(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_owned_by(self, user):
 | 
					    def is_owned_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Is owned by a pedagogy admin, a superuser or the author himself."""
 | 
				
			||||||
        Is owned by a pedagogy admin, a superuser or the author himself
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return self.author == user or user.is_owner(self.uv)
 | 
					        return self.author == user or user.is_owner(self.uv)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @cached_property
 | 
					    @cached_property
 | 
				
			||||||
    def is_reported(self):
 | 
					    def is_reported(self):
 | 
				
			||||||
        """
 | 
					        """Return True if someone reported this UV."""
 | 
				
			||||||
        Return True if someone reported this UV
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return self.reports.exists()
 | 
					        return self.reports.exists()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# TODO : it seems that some views were meant to be implemented
 | 
				
			||||||
 | 
					#        to use this model.
 | 
				
			||||||
 | 
					#        However, it seems that the implementation finally didn't happen.
 | 
				
			||||||
 | 
					#        It should be discussed, when possible, of what to do with that :
 | 
				
			||||||
 | 
					#        - go on and finally implement the UV results features ?
 | 
				
			||||||
 | 
					#        - or fuck go back and remove this model ?
 | 
				
			||||||
class UVResult(models.Model):
 | 
					class UVResult(models.Model):
 | 
				
			||||||
    """
 | 
					    """Results got to an UV.
 | 
				
			||||||
    Results got to an UV
 | 
					
 | 
				
			||||||
    Views will be implemented after the first release
 | 
					    Views will be implemented after the first release
 | 
				
			||||||
    Will list every UV done by an user
 | 
					    Will list every UV done by an user
 | 
				
			||||||
    Linked to user
 | 
					    Linked to user
 | 
				
			||||||
              uv
 | 
					              uv
 | 
				
			||||||
    Contains a grade settings.SITH_PEDAGOGY_UV_RESULT_GRADE
 | 
					    Contains a grade settings.SITH_PEDAGOGY_UV_RESULT_GRADE
 | 
				
			||||||
             a semester (P/A)20xx
 | 
					             a semester (P/A)20xx.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    uv = models.ForeignKey(
 | 
					    uv = models.ForeignKey(
 | 
				
			||||||
@@ -308,9 +301,7 @@ class UVResult(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UVCommentReport(models.Model):
 | 
					class UVCommentReport(models.Model):
 | 
				
			||||||
    """
 | 
					    """Report an inapropriate comment."""
 | 
				
			||||||
    Report an inapropriate comment
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    comment = models.ForeignKey(
 | 
					    comment = models.ForeignKey(
 | 
				
			||||||
        UVComment,
 | 
					        UVComment,
 | 
				
			||||||
@@ -334,9 +325,7 @@ class UVCommentReport(models.Model):
 | 
				
			|||||||
        return self.comment.uv
 | 
					        return self.comment.uv
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_owned_by(self, user):
 | 
					    def is_owned_by(self, user):
 | 
				
			||||||
        """
 | 
					        """Can be created by a pedagogy admin, a superuser or a subscriber."""
 | 
				
			||||||
        Can be created by a pedagogy admin, a superuser or a subscriber
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return user.is_subscribed or user.is_owner(self.comment.uv)
 | 
					        return user.is_subscribed or user.is_owner(self.comment.uv)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -344,9 +333,9 @@ class UVCommentReport(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UVSerializer(serializers.ModelSerializer):
 | 
					class UVSerializer(serializers.ModelSerializer):
 | 
				
			||||||
    """
 | 
					    """Custom seralizer for UVs.
 | 
				
			||||||
    Custom seralizer for UVs
 | 
					
 | 
				
			||||||
    Allow adding more informations like absolute_url
 | 
					    Allow adding more informations like absolute_url.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,9 +29,7 @@ from pedagogy.models import UV
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class IndexSignalProcessor(signals.BaseSignalProcessor):
 | 
					class IndexSignalProcessor(signals.BaseSignalProcessor):
 | 
				
			||||||
    """
 | 
					    """Auto update index on CRUD operations."""
 | 
				
			||||||
    Auto update index on CRUD operations
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def setup(self):
 | 
					    def setup(self):
 | 
				
			||||||
        # Listen only to the ``UV`` model.
 | 
					        # Listen only to the ``UV`` model.
 | 
				
			||||||
@@ -45,9 +43,7 @@ class IndexSignalProcessor(signals.BaseSignalProcessor):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UVIndex(indexes.SearchIndex, indexes.Indexable):
 | 
					class UVIndex(indexes.SearchIndex, indexes.Indexable):
 | 
				
			||||||
    """
 | 
					    """Indexer class for UVs."""
 | 
				
			||||||
    Indexer class for UVs
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    text = BigCharFieldIndex(document=True, use_template=True)
 | 
					    text = BigCharFieldIndex(document=True, use_template=True)
 | 
				
			||||||
    auto = indexes.EdgeNgramField(use_template=True)
 | 
					    auto = indexes.EdgeNgramField(use_template=True)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,9 +32,7 @@ from pedagogy.models import UV, UVComment, UVCommentReport
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def create_uv_template(user_id, code="IFC1", exclude_list=None):
 | 
					def create_uv_template(user_id, code="IFC1", exclude_list=None):
 | 
				
			||||||
    """
 | 
					    """Factory to help UV creation/update in post requests."""
 | 
				
			||||||
    Factory to help UV creation/update in post requests
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    if exclude_list is None:
 | 
					    if exclude_list is None:
 | 
				
			||||||
        exclude_list = []
 | 
					        exclude_list = []
 | 
				
			||||||
    uv = {
 | 
					    uv = {
 | 
				
			||||||
@@ -79,9 +77,7 @@ def create_uv_template(user_id, code="IFC1", exclude_list=None):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UVCreation(TestCase):
 | 
					class UVCreation(TestCase):
 | 
				
			||||||
    """
 | 
					    """Test uv creation."""
 | 
				
			||||||
    Test uv creation
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def setUpTestData(cls):
 | 
					    def setUpTestData(cls):
 | 
				
			||||||
@@ -291,9 +287,7 @@ class UVUpdateTest(TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def create_uv_comment_template(user_id, uv_code="PA00", exclude_list=None):
 | 
					def create_uv_comment_template(user_id, uv_code="PA00", exclude_list=None):
 | 
				
			||||||
    """
 | 
					    """Factory to help UVComment creation/update in post requests."""
 | 
				
			||||||
    Factory to help UVComment creation/update in post requests
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    if exclude_list is None:
 | 
					    if exclude_list is None:
 | 
				
			||||||
        exclude_list = []
 | 
					        exclude_list = []
 | 
				
			||||||
    comment = {
 | 
					    comment = {
 | 
				
			||||||
@@ -312,9 +306,9 @@ def create_uv_comment_template(user_id, uv_code="PA00", exclude_list=None):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UVCommentCreationAndDisplay(TestCase):
 | 
					class UVCommentCreationAndDisplay(TestCase):
 | 
				
			||||||
    """
 | 
					    """Test UVComment creation and its display.
 | 
				
			||||||
    Test UVComment creation and it's display
 | 
					
 | 
				
			||||||
    Display and creation are the same view
 | 
					    Display and creation are the same view.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
@@ -575,10 +569,7 @@ class UVCommentUpdateTest(TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UVSearchTest(TestCase):
 | 
					class UVSearchTest(TestCase):
 | 
				
			||||||
    """
 | 
					    """Test UV guide rights for view and API."""
 | 
				
			||||||
    Test UV guide rights for view and API
 | 
					 | 
				
			||||||
    Test that the API is working well
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def setUpTestData(cls):
 | 
					    def setUpTestData(cls):
 | 
				
			||||||
@@ -751,10 +742,7 @@ class UVSearchTest(TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UVModerationFormTest(TestCase):
 | 
					class UVModerationFormTest(TestCase):
 | 
				
			||||||
    """
 | 
					    """Assert access rights and if the form works well."""
 | 
				
			||||||
    Test moderation view
 | 
					 | 
				
			||||||
    Assert access rights and if the form works well
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def setUpTestData(cls):
 | 
					    def setUpTestData(cls):
 | 
				
			||||||
@@ -967,9 +955,9 @@ class UVModerationFormTest(TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UVCommentReportCreateTest(TestCase):
 | 
					class UVCommentReportCreateTest(TestCase):
 | 
				
			||||||
    """
 | 
					    """Test report creation view.
 | 
				
			||||||
    Test report creation view view
 | 
					
 | 
				
			||||||
    Assert access rights and if you can create with it
 | 
					    Assert access rights and if you can create with it.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def setUp(self):
 | 
					    def setUp(self):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -57,21 +57,15 @@ from pedagogy.models import UV, UVComment, UVCommentReport, UVSerializer
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CanCreateUVFunctionMixin(View):
 | 
					class CanCreateUVFunctionMixin(View):
 | 
				
			||||||
    """
 | 
					    """Add the function can_create_uv(user) into the template."""
 | 
				
			||||||
    Add the function can_create_uv(user) into the template
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def can_create_uv(user):
 | 
					    def can_create_uv(user):
 | 
				
			||||||
        """
 | 
					        """Creates a dummy instance of UV and test is_owner."""
 | 
				
			||||||
        Creates a dummy instance of UV and test is_owner
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return user.is_owner(UV())
 | 
					        return user.is_owner(UV())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """
 | 
					        """Pass the function to the template."""
 | 
				
			||||||
        Pass the function to the template
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        kwargs["can_create_uv"] = self.can_create_uv
 | 
					        kwargs["can_create_uv"] = self.can_create_uv
 | 
				
			||||||
        return kwargs
 | 
					        return kwargs
 | 
				
			||||||
@@ -81,9 +75,9 @@ class CanCreateUVFunctionMixin(View):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UVDetailFormView(CanViewMixin, CanCreateUVFunctionMixin, DetailFormView):
 | 
					class UVDetailFormView(CanViewMixin, CanCreateUVFunctionMixin, DetailFormView):
 | 
				
			||||||
    """
 | 
					    """Display every comment of an UV and detailed infos about it.
 | 
				
			||||||
    Dispaly every comment of an UV and detailed infos about it
 | 
					
 | 
				
			||||||
    Allow to comment the UV
 | 
					    Allow to comment the UV.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = UV
 | 
					    model = UV
 | 
				
			||||||
@@ -109,9 +103,7 @@ class UVDetailFormView(CanViewMixin, CanCreateUVFunctionMixin, DetailFormView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UVCommentUpdateView(CanEditPropMixin, UpdateView):
 | 
					class UVCommentUpdateView(CanEditPropMixin, UpdateView):
 | 
				
			||||||
    """
 | 
					    """Allow edit of a given comment."""
 | 
				
			||||||
    Allow edit of a given comment
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = UVComment
 | 
					    model = UVComment
 | 
				
			||||||
    form_class = UVCommentForm
 | 
					    form_class = UVCommentForm
 | 
				
			||||||
@@ -132,9 +124,7 @@ class UVCommentUpdateView(CanEditPropMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UVCommentDeleteView(CanEditPropMixin, DeleteView):
 | 
					class UVCommentDeleteView(CanEditPropMixin, DeleteView):
 | 
				
			||||||
    """
 | 
					    """Allow delete of a given comment."""
 | 
				
			||||||
    Allow delete of a given comment
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = UVComment
 | 
					    model = UVComment
 | 
				
			||||||
    pk_url_kwarg = "comment_id"
 | 
					    pk_url_kwarg = "comment_id"
 | 
				
			||||||
@@ -145,9 +135,7 @@ class UVCommentDeleteView(CanEditPropMixin, DeleteView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView):
 | 
					class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView):
 | 
				
			||||||
    """
 | 
					    """UV guide main page."""
 | 
				
			||||||
    UV guide main page
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # This is very basic and is prone to changment
 | 
					    # This is very basic and is prone to changment
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -208,9 +196,7 @@ class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UVCommentReportCreateView(CanCreateMixin, CreateView):
 | 
					class UVCommentReportCreateView(CanCreateMixin, CreateView):
 | 
				
			||||||
    """
 | 
					    """Create a new report for an inapropriate comment."""
 | 
				
			||||||
    Create a new report for an inapropriate comment
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = UVCommentReport
 | 
					    model = UVCommentReport
 | 
				
			||||||
    form_class = UVCommentReportForm
 | 
					    form_class = UVCommentReportForm
 | 
				
			||||||
@@ -253,9 +239,7 @@ class UVCommentReportCreateView(CanCreateMixin, CreateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UVModerationFormView(FormView):
 | 
					class UVModerationFormView(FormView):
 | 
				
			||||||
    """
 | 
					    """Moderation interface (Privileged)."""
 | 
				
			||||||
    Moderation interface (Privileged)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    form_class = UVCommentModerationForm
 | 
					    form_class = UVCommentModerationForm
 | 
				
			||||||
    template_name = "pedagogy/moderation.jinja"
 | 
					    template_name = "pedagogy/moderation.jinja"
 | 
				
			||||||
@@ -286,9 +270,7 @@ class UVModerationFormView(FormView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UVCreateView(CanCreateMixin, CreateView):
 | 
					class UVCreateView(CanCreateMixin, CreateView):
 | 
				
			||||||
    """
 | 
					    """Add a new UV (Privileged)."""
 | 
				
			||||||
    Add a new UV (Privileged)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = UV
 | 
					    model = UV
 | 
				
			||||||
    form_class = UVForm
 | 
					    form_class = UVForm
 | 
				
			||||||
@@ -304,9 +286,7 @@ class UVCreateView(CanCreateMixin, CreateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UVDeleteView(CanEditPropMixin, DeleteView):
 | 
					class UVDeleteView(CanEditPropMixin, DeleteView):
 | 
				
			||||||
    """
 | 
					    """Allow to delete an UV (Privileged)."""
 | 
				
			||||||
    Allow to delete an UV (Privileged)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = UV
 | 
					    model = UV
 | 
				
			||||||
    pk_url_kwarg = "uv_id"
 | 
					    pk_url_kwarg = "uv_id"
 | 
				
			||||||
@@ -317,9 +297,7 @@ class UVDeleteView(CanEditPropMixin, DeleteView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UVUpdateView(CanEditPropMixin, UpdateView):
 | 
					class UVUpdateView(CanEditPropMixin, UpdateView):
 | 
				
			||||||
    """
 | 
					    """Allow to edit an UV (Privilegied)."""
 | 
				
			||||||
    Allow to edit an UV (Privilegied)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = UV
 | 
					    model = UV
 | 
				
			||||||
    form_class = UVForm
 | 
					    form_class = UVForm
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -100,6 +100,9 @@ ignore = [
 | 
				
			|||||||
	"DJ001",  # null=True in CharField/TextField. this one would require a migration
 | 
						"DJ001",  # null=True in CharField/TextField. this one would require a migration
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[tool.ruff.lint.pydocstyle]
 | 
				
			||||||
 | 
					convention = "google"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[tool.pytest.ini_options]
 | 
					[tool.pytest.ini_options]
 | 
				
			||||||
DJANGO_SETTINGS_MODULE = "sith.settings"
 | 
					DJANGO_SETTINGS_MODULE = "sith.settings"
 | 
				
			||||||
python_files = ["tests.py", "test_*.py", "*_tests.py"]
 | 
					python_files = ["tests.py", "test_*.py", "*_tests.py"]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,9 +29,7 @@ from rootplace.views import delete_all_forum_user_messages
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Command(BaseCommand):
 | 
					class Command(BaseCommand):
 | 
				
			||||||
    """
 | 
					    """Delete all forum messages from a user."""
 | 
				
			||||||
    Delete all forum messages from a user
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    help = "Delete all user's forum message"
 | 
					    help = "Delete all user's forum message"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,8 +38,8 @@ from forum.models import ForumMessageMeta
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def __merge_subscriptions(u1: User, u2: User):
 | 
					def __merge_subscriptions(u1: User, u2: User):
 | 
				
			||||||
    """
 | 
					    """Give all the subscriptions of the second user to first one.
 | 
				
			||||||
    Give all the subscriptions of the second user to first one
 | 
					
 | 
				
			||||||
    If some subscriptions are still active, update their end date
 | 
					    If some subscriptions are still active, update their end date
 | 
				
			||||||
    to increase the overall subscription time of the first user.
 | 
					    to increase the overall subscription time of the first user.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -87,8 +87,8 @@ def __merge_pictures(u1: User, u2: User) -> None:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def merge_users(u1: User, u2: User) -> User:
 | 
					def merge_users(u1: User, u2: User) -> User:
 | 
				
			||||||
    """
 | 
					    """Merge u2 into u1.
 | 
				
			||||||
    Merge u2 into u1
 | 
					
 | 
				
			||||||
    This means that u1 shall receive everything that belonged to u2 :
 | 
					    This means that u1 shall receive everything that belonged to u2 :
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        - pictures
 | 
					        - pictures
 | 
				
			||||||
@@ -134,11 +134,12 @@ def merge_users(u1: User, u2: User) -> User:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def delete_all_forum_user_messages(user, moderator, *, verbose=False):
 | 
					def delete_all_forum_user_messages(user, moderator, *, verbose=False):
 | 
				
			||||||
    """
 | 
					    """Soft delete all messages of a user.
 | 
				
			||||||
    Create a ForumMessageMeta that says a forum
 | 
					
 | 
				
			||||||
        message is deleted on every forum message of an user
 | 
					    Args:
 | 
				
			||||||
    user: the user to delete messages from
 | 
					        user: the user to delete messages from
 | 
				
			||||||
    moderator: the one marked as the moderator
 | 
					        moderator: the one marked as the moderator.
 | 
				
			||||||
 | 
					        verbose: it True, print the deleted messages
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    for message in user.forum_messages.all():
 | 
					    for message in user.forum_messages.all():
 | 
				
			||||||
        if message.is_deleted():
 | 
					        if message.is_deleted():
 | 
				
			||||||
@@ -184,10 +185,10 @@ class MergeUsersView(FormView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DeleteAllForumUserMessagesView(FormView):
 | 
					class DeleteAllForumUserMessagesView(FormView):
 | 
				
			||||||
    """
 | 
					    """Delete all forum messages from an user.
 | 
				
			||||||
    Delete all forum messages from an user
 | 
					
 | 
				
			||||||
    Messages are soft deleted and are still visible from admins
 | 
					    Messages are soft deleted and are still visible from admins
 | 
				
			||||||
    GUI frontend to the dedicated command
 | 
					    GUI frontend to the dedicated command.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    template_name = "rootplace/delete_user_messages.jinja"
 | 
					    template_name = "rootplace/delete_user_messages.jinja"
 | 
				
			||||||
@@ -209,9 +210,7 @@ class DeleteAllForumUserMessagesView(FormView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class OperationLogListView(ListView, CanEditPropMixin):
 | 
					class OperationLogListView(ListView, CanEditPropMixin):
 | 
				
			||||||
    """
 | 
					    """List all logs."""
 | 
				
			||||||
    List all logs
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = OperationLog
 | 
					    model = OperationLog
 | 
				
			||||||
    template_name = "rootplace/logs.jinja"
 | 
					    template_name = "rootplace/logs.jinja"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -221,10 +221,7 @@ def sas_notification_callback(notif):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PeoplePictureRelation(models.Model):
 | 
					class PeoplePictureRelation(models.Model):
 | 
				
			||||||
    """
 | 
					    """The PeoplePictureRelation class makes the connection between User and Picture."""
 | 
				
			||||||
    The PeoplePictureRelation class makes the connection between User and Picture
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    user = models.ForeignKey(
 | 
					    user = models.ForeignKey(
 | 
				
			||||||
        User,
 | 
					        User,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,8 +22,7 @@
 | 
				
			|||||||
#
 | 
					#
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""Django settings for sith project.
 | 
				
			||||||
Django settings for sith project.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
Generated by 'django-admin startproject' using Django 1.8.6.
 | 
					Generated by 'django-admin startproject' using Django 1.8.6.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										16
									
								
								sith/urls.py
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								sith/urls.py
									
									
									
									
									
								
							@@ -13,22 +13,6 @@
 | 
				
			|||||||
#
 | 
					#
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""sith URL Configuration
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
The `urlpatterns` list routes URLs to views. For more information please see:
 | 
					 | 
				
			||||||
    https://docs.djangoproject.com/en/1.8/topics/http/urls/
 | 
					 | 
				
			||||||
Examples:
 | 
					 | 
				
			||||||
Function views
 | 
					 | 
				
			||||||
    1. Add an import:  from my_app import views
 | 
					 | 
				
			||||||
    2. Add a URL to urlpatterns:  url(r'^$', views.home, name='home')
 | 
					 | 
				
			||||||
Class-based views
 | 
					 | 
				
			||||||
    1. Add an import:  from other_app.views import Home
 | 
					 | 
				
			||||||
    2. Add a URL to urlpatterns:  url(r'^$', Home.as_view(), name='home')
 | 
					 | 
				
			||||||
Including another URLconf
 | 
					 | 
				
			||||||
    1. Add an import:  from blog import urls as blog_urls
 | 
					 | 
				
			||||||
    2. Add a URL to urlpatterns:  url(r'^blog/', include(blog_urls))
 | 
					 | 
				
			||||||
"""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from ajax_select import urls as ajax_select_urls
 | 
					from ajax_select import urls as ajax_select_urls
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.conf.urls.static import static
 | 
					from django.conf.urls.static import static
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,8 +13,7 @@
 | 
				
			|||||||
#
 | 
					#
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""WSGI config for sith project.
 | 
				
			||||||
WSGI config for sith project.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
It exposes the WSGI callable as a module-level variable named ``application``.
 | 
					It exposes the WSGI callable as a module-level variable named ``application``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,9 +31,7 @@ from counter.models import Counter, ProductType
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Stock(models.Model):
 | 
					class Stock(models.Model):
 | 
				
			||||||
    """
 | 
					    """The Stock class, this one is used to know how many products are left for a specific counter."""
 | 
				
			||||||
    The Stock class, this one is used to know how many products are left for a specific counter
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    name = models.CharField(_("name"), max_length=64)
 | 
					    name = models.CharField(_("name"), max_length=64)
 | 
				
			||||||
    counter = models.OneToOneField(
 | 
					    counter = models.OneToOneField(
 | 
				
			||||||
@@ -54,9 +52,7 @@ class Stock(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StockItem(models.Model):
 | 
					class StockItem(models.Model):
 | 
				
			||||||
    """
 | 
					    """The StockItem class, element of the stock."""
 | 
				
			||||||
    The StockItem class, element of the stock
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    name = models.CharField(_("name"), max_length=64)
 | 
					    name = models.CharField(_("name"), max_length=64)
 | 
				
			||||||
    unit_quantity = models.IntegerField(
 | 
					    unit_quantity = models.IntegerField(
 | 
				
			||||||
@@ -95,9 +91,7 @@ class StockItem(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ShoppingList(models.Model):
 | 
					class ShoppingList(models.Model):
 | 
				
			||||||
    """
 | 
					    """The ShoppingList class, used to make an history of the shopping lists."""
 | 
				
			||||||
    The ShoppingList class, used to make an history of the shopping lists
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    date = models.DateTimeField(_("date"))
 | 
					    date = models.DateTimeField(_("date"))
 | 
				
			||||||
    name = models.CharField(_("name"), max_length=64)
 | 
					    name = models.CharField(_("name"), max_length=64)
 | 
				
			||||||
@@ -118,7 +112,7 @@ class ShoppingList(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ShoppingListItem(models.Model):
 | 
					class ShoppingListItem(models.Model):
 | 
				
			||||||
    """"""
 | 
					    """An Item on a shopping list."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    shopping_lists = models.ManyToManyField(
 | 
					    shopping_lists = models.ManyToManyField(
 | 
				
			||||||
        ShoppingList,
 | 
					        ShoppingList,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -41,9 +41,7 @@ from stock.models import ShoppingList, ShoppingListItem, Stock, StockItem
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StockItemList(CounterAdminTabsMixin, CanCreateMixin, ListView):
 | 
					class StockItemList(CounterAdminTabsMixin, CanCreateMixin, ListView):
 | 
				
			||||||
    """
 | 
					    """The stockitems list view for the counter owner."""
 | 
				
			||||||
    The stockitems list view for the counter owner
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Stock
 | 
					    model = Stock
 | 
				
			||||||
    template_name = "stock/stock_item_list.jinja"
 | 
					    template_name = "stock/stock_item_list.jinja"
 | 
				
			||||||
@@ -58,9 +56,7 @@ class StockItemList(CounterAdminTabsMixin, CanCreateMixin, ListView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StockListView(CounterAdminTabsMixin, CanViewMixin, ListView):
 | 
					class StockListView(CounterAdminTabsMixin, CanViewMixin, ListView):
 | 
				
			||||||
    """
 | 
					    """A list view for the admins."""
 | 
				
			||||||
    A list view for the admins
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Stock
 | 
					    model = Stock
 | 
				
			||||||
    template_name = "stock/stock_list.jinja"
 | 
					    template_name = "stock/stock_list.jinja"
 | 
				
			||||||
@@ -68,9 +64,7 @@ class StockListView(CounterAdminTabsMixin, CanViewMixin, ListView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StockEditForm(forms.ModelForm):
 | 
					class StockEditForm(forms.ModelForm):
 | 
				
			||||||
    """
 | 
					    """A form to change stock's characteristics."""
 | 
				
			||||||
    A form to change stock's characteristics
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Stock
 | 
					        model = Stock
 | 
				
			||||||
@@ -84,9 +78,7 @@ class StockEditForm(forms.ModelForm):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StockEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView):
 | 
					class StockEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView):
 | 
				
			||||||
    """
 | 
					    """An edit view for the stock."""
 | 
				
			||||||
    An edit view for the stock
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Stock
 | 
					    model = Stock
 | 
				
			||||||
    form_class = modelform_factory(Stock, fields=["name", "counter"])
 | 
					    form_class = modelform_factory(Stock, fields=["name", "counter"])
 | 
				
			||||||
@@ -96,9 +88,7 @@ class StockEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StockItemEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView):
 | 
					class StockItemEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView):
 | 
				
			||||||
    """
 | 
					    """An edit view for a stock item."""
 | 
				
			||||||
    An edit view for a stock item
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = StockItem
 | 
					    model = StockItem
 | 
				
			||||||
    form_class = modelform_factory(
 | 
					    form_class = modelform_factory(
 | 
				
			||||||
@@ -118,9 +108,7 @@ class StockItemEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StockCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView):
 | 
					class StockCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView):
 | 
				
			||||||
    """
 | 
					    """A create view for a new Stock."""
 | 
				
			||||||
    A create view for a new Stock
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Stock
 | 
					    model = Stock
 | 
				
			||||||
    form_class = modelform_factory(Stock, fields=["name", "counter"])
 | 
					    form_class = modelform_factory(Stock, fields=["name", "counter"])
 | 
				
			||||||
@@ -137,9 +125,7 @@ class StockCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StockItemCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView):
 | 
					class StockItemCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView):
 | 
				
			||||||
    """
 | 
					    """A create view for a new StockItem."""
 | 
				
			||||||
    A create view for a new StockItem
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = StockItem
 | 
					    model = StockItem
 | 
				
			||||||
    form_class = modelform_factory(
 | 
					    form_class = modelform_factory(
 | 
				
			||||||
@@ -170,9 +156,7 @@ class StockItemCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StockShoppingListView(CounterAdminTabsMixin, CanViewMixin, ListView):
 | 
					class StockShoppingListView(CounterAdminTabsMixin, CanViewMixin, ListView):
 | 
				
			||||||
    """
 | 
					    """A list view for the people to know the item to buy."""
 | 
				
			||||||
    A list view for the people to know the item to buy
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Stock
 | 
					    model = Stock
 | 
				
			||||||
    template_name = "stock/stock_shopping_list.jinja"
 | 
					    template_name = "stock/stock_shopping_list.jinja"
 | 
				
			||||||
@@ -225,9 +209,7 @@ class StockItemQuantityForm(forms.BaseForm):
 | 
				
			|||||||
class StockItemQuantityBaseFormView(
 | 
					class StockItemQuantityBaseFormView(
 | 
				
			||||||
    CounterAdminTabsMixin, CanEditMixin, DetailView, BaseFormView
 | 
					    CounterAdminTabsMixin, CanEditMixin, DetailView, BaseFormView
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
    """
 | 
					    """docstring for StockItemOutList."""
 | 
				
			||||||
    docstring for StockItemOutList
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = StockItem
 | 
					    model = StockItem
 | 
				
			||||||
    template_name = "stock/shopping_list_quantity.jinja"
 | 
					    template_name = "stock/shopping_list_quantity.jinja"
 | 
				
			||||||
@@ -266,16 +248,12 @@ class StockItemQuantityBaseFormView(
 | 
				
			|||||||
        return type("StockItemQuantityForm", (StockItemQuantityForm,), kwargs)
 | 
					        return type("StockItemQuantityForm", (StockItemQuantityForm,), kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get(self, request, *args, **kwargs):
 | 
					    def get(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """Simple get view."""
 | 
				
			||||||
        Simple get view
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first()
 | 
					        self.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first()
 | 
				
			||||||
        return super().get(request, *args, **kwargs)
 | 
					        return super().get(request, *args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def post(self, request, *args, **kwargs):
 | 
					    def post(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """Handle the many possibilities of the post request."""
 | 
				
			||||||
        Handle the many possibilities of the post request
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.object = self.get_object()
 | 
					        self.object = self.get_object()
 | 
				
			||||||
        self.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first()
 | 
					        self.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first()
 | 
				
			||||||
        return super().post(request, *args, **kwargs)
 | 
					        return super().post(request, *args, **kwargs)
 | 
				
			||||||
@@ -297,7 +275,7 @@ class StockItemQuantityBaseFormView(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StockShoppingListItemListView(CounterAdminTabsMixin, CanViewMixin, ListView):
 | 
					class StockShoppingListItemListView(CounterAdminTabsMixin, CanViewMixin, ListView):
 | 
				
			||||||
    """docstring for StockShoppingListItemListView"""
 | 
					    """docstring for StockShoppingListItemListView."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = ShoppingList
 | 
					    model = ShoppingList
 | 
				
			||||||
    template_name = "stock/shopping_list_items.jinja"
 | 
					    template_name = "stock/shopping_list_items.jinja"
 | 
				
			||||||
@@ -314,9 +292,7 @@ class StockShoppingListItemListView(CounterAdminTabsMixin, CanViewMixin, ListVie
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StockShoppingListDeleteView(CounterAdminTabsMixin, CanEditMixin, DeleteView):
 | 
					class StockShoppingListDeleteView(CounterAdminTabsMixin, CanEditMixin, DeleteView):
 | 
				
			||||||
    """
 | 
					    """Delete a ShoppingList (for the resonsible account)."""
 | 
				
			||||||
    Delete a ShoppingList (for the resonsible account)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = ShoppingList
 | 
					    model = ShoppingList
 | 
				
			||||||
    pk_url_kwarg = "shoppinglist_id"
 | 
					    pk_url_kwarg = "shoppinglist_id"
 | 
				
			||||||
@@ -330,9 +306,7 @@ class StockShoppingListDeleteView(CounterAdminTabsMixin, CanEditMixin, DeleteVie
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StockShopppingListSetDone(CanEditMixin, DetailView):
 | 
					class StockShopppingListSetDone(CanEditMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Set a ShoppingList as done."""
 | 
				
			||||||
    Set a ShoppingList as done
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = ShoppingList
 | 
					    model = ShoppingList
 | 
				
			||||||
    pk_url_kwarg = "shoppinglist_id"
 | 
					    pk_url_kwarg = "shoppinglist_id"
 | 
				
			||||||
@@ -361,9 +335,7 @@ class StockShopppingListSetDone(CanEditMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StockShopppingListSetTodo(CanEditMixin, DetailView):
 | 
					class StockShopppingListSetTodo(CanEditMixin, DetailView):
 | 
				
			||||||
    """
 | 
					    """Set a ShoppingList as done."""
 | 
				
			||||||
    Set a ShoppingList as done
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = ShoppingList
 | 
					    model = ShoppingList
 | 
				
			||||||
    pk_url_kwarg = "shoppinglist_id"
 | 
					    pk_url_kwarg = "shoppinglist_id"
 | 
				
			||||||
@@ -415,9 +387,7 @@ class StockUpdateAfterShopppingForm(forms.BaseForm):
 | 
				
			|||||||
class StockUpdateAfterShopppingBaseFormView(
 | 
					class StockUpdateAfterShopppingBaseFormView(
 | 
				
			||||||
    CounterAdminTabsMixin, CanEditMixin, DetailView, BaseFormView
 | 
					    CounterAdminTabsMixin, CanEditMixin, DetailView, BaseFormView
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
    """
 | 
					    """docstring for StockUpdateAfterShopppingBaseFormView."""
 | 
				
			||||||
    docstring for StockUpdateAfterShopppingBaseFormView
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = ShoppingList
 | 
					    model = ShoppingList
 | 
				
			||||||
    template_name = "stock/update_after_shopping.jinja"
 | 
					    template_name = "stock/update_after_shopping.jinja"
 | 
				
			||||||
@@ -453,9 +423,7 @@ class StockUpdateAfterShopppingBaseFormView(
 | 
				
			|||||||
        return super().get(request, *args, **kwargs)
 | 
					        return super().get(request, *args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def post(self, request, *args, **kwargs):
 | 
					    def post(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """Handle the many possibilities of the post request."""
 | 
				
			||||||
        Handle the many possibilities of the post request
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.object = self.get_object()
 | 
					        self.object = self.get_object()
 | 
				
			||||||
        self.shoppinglist = ShoppingList.objects.filter(
 | 
					        self.shoppinglist = ShoppingList.objects.filter(
 | 
				
			||||||
            id=self.kwargs["shoppinglist_id"]
 | 
					            id=self.kwargs["shoppinglist_id"]
 | 
				
			||||||
@@ -463,9 +431,7 @@ class StockUpdateAfterShopppingBaseFormView(
 | 
				
			|||||||
        return super().post(request, *args, **kwargs)
 | 
					        return super().post(request, *args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def form_valid(self, form):
 | 
					    def form_valid(self, form):
 | 
				
			||||||
        """
 | 
					        """We handle here the redirection."""
 | 
				
			||||||
        We handle here the redirection
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return super().form_valid(form)
 | 
					        return super().form_valid(form)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
@@ -484,9 +450,7 @@ class StockUpdateAfterShopppingBaseFormView(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StockTakeItemsForm(forms.BaseForm):
 | 
					class StockTakeItemsForm(forms.BaseForm):
 | 
				
			||||||
    """
 | 
					    """docstring for StockTakeItemsFormView."""
 | 
				
			||||||
    docstring for StockTakeItemsFormView
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def clean(self):
 | 
					    def clean(self):
 | 
				
			||||||
        with transaction.atomic():
 | 
					        with transaction.atomic():
 | 
				
			||||||
@@ -502,9 +466,7 @@ class StockTakeItemsForm(forms.BaseForm):
 | 
				
			|||||||
class StockTakeItemsBaseFormView(
 | 
					class StockTakeItemsBaseFormView(
 | 
				
			||||||
    CounterTabsMixin, CanEditMixin, DetailView, BaseFormView
 | 
					    CounterTabsMixin, CanEditMixin, DetailView, BaseFormView
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
    """
 | 
					    """docstring for StockTakeItemsBaseFormView."""
 | 
				
			||||||
    docstring for StockTakeItemsBaseFormView
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = StockItem
 | 
					    model = StockItem
 | 
				
			||||||
    template_name = "stock/stock_take_items.jinja"
 | 
					    template_name = "stock/stock_take_items.jinja"
 | 
				
			||||||
@@ -535,16 +497,11 @@ class StockTakeItemsBaseFormView(
 | 
				
			|||||||
        return type("StockTakeItemsForm", (StockTakeItemsForm,), kwargs)
 | 
					        return type("StockTakeItemsForm", (StockTakeItemsForm,), kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get(self, request, *args, **kwargs):
 | 
					    def get(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Simple get view
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first()
 | 
					        self.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first()
 | 
				
			||||||
        return super().get(request, *args, **kwargs)
 | 
					        return super().get(request, *args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def post(self, request, *args, **kwargs):
 | 
					    def post(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """Handle the many possibilities of the post request."""
 | 
				
			||||||
        Handle the many possibilities of the post request
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.object = self.get_object()
 | 
					        self.object = self.get_object()
 | 
				
			||||||
        self.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first()
 | 
					        self.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first()
 | 
				
			||||||
        if self.stock.counter.type == "BAR" and not (
 | 
					        if self.stock.counter.type == "BAR" and not (
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -108,14 +108,15 @@ class Subscription(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def compute_start(d: date = None, duration: int = 1, user: User = None) -> date:
 | 
					    def compute_start(d: date = None, duration: int = 1, user: User = None) -> date:
 | 
				
			||||||
        """
 | 
					        """Computes the start date of the subscription.
 | 
				
			||||||
        This function computes the start date of the subscription with respect to the given date (default is today),
 | 
					
 | 
				
			||||||
 | 
					        The computation is done with respect to the given date (default is today)
 | 
				
			||||||
        and the start date given in settings.SITH_SEMESTER_START_AUTUMN.
 | 
					        and the start date given in settings.SITH_SEMESTER_START_AUTUMN.
 | 
				
			||||||
        It takes the nearest past start date.
 | 
					        It takes the nearest past start date.
 | 
				
			||||||
        Exemples: with SITH_SEMESTER_START_AUTUMN = (8, 15)
 | 
					        Exemples: with SITH_SEMESTER_START_AUTUMN = (8, 15)
 | 
				
			||||||
            Today      -> Start date
 | 
					            Today      -> Start date
 | 
				
			||||||
            2015-03-17 -> 2015-02-15
 | 
					            2015-03-17 -> 2015-02-15
 | 
				
			||||||
            2015-01-11 -> 2014-08-15
 | 
					            2015-01-11 -> 2014-08-15.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if not d:
 | 
					        if not d:
 | 
				
			||||||
            d = date.today()
 | 
					            d = date.today()
 | 
				
			||||||
@@ -129,14 +130,21 @@ class Subscription(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def compute_end(duration: int, start: date = None, user: User = None) -> date:
 | 
					    def compute_end(duration: int, start: date = None, user: User = None) -> date:
 | 
				
			||||||
        """
 | 
					        """Compute the end date of the subscription.
 | 
				
			||||||
        This function compute the end date of the subscription given a start date and a duration in number of semester
 | 
					
 | 
				
			||||||
        Exemple:
 | 
					        Args:
 | 
				
			||||||
 | 
					            duration:
 | 
				
			||||||
 | 
					                the duration of the subscription, in semester
 | 
				
			||||||
 | 
					                (for example, 2 => 2 semesters => 1 year)
 | 
				
			||||||
 | 
					            start: The start date of the subscription
 | 
				
			||||||
 | 
					            user: the user which is (or will be) subscribed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Exemples:
 | 
				
			||||||
            Start - Duration -> End date
 | 
					            Start - Duration -> End date
 | 
				
			||||||
            2015-09-18 - 1 -> 2016-03-18
 | 
					            2015-09-18 - 1 -> 2016-03-18
 | 
				
			||||||
            2015-09-18 - 2 -> 2016-09-18
 | 
					            2015-09-18 - 2 -> 2016-09-18
 | 
				
			||||||
            2015-09-18 - 3 -> 2017-03-18
 | 
					            2015-09-18 - 3 -> 2017-03-18
 | 
				
			||||||
            2015-09-18 - 4 -> 2017-09-18
 | 
					            2015-09-18 - 4 -> 2017-09-18.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if start is None:
 | 
					        if start is None:
 | 
				
			||||||
            start = Subscription.compute_start(duration=duration, user=user)
 | 
					            start = Subscription.compute_start(duration=duration, user=user)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -45,8 +45,8 @@ class AvailableTrombiManager(models.Manager):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Trombi(models.Model):
 | 
					class Trombi(models.Model):
 | 
				
			||||||
    """
 | 
					    """Main class of the trombi, the Trombi itself.
 | 
				
			||||||
    This is the main class, the Trombi itself.
 | 
					
 | 
				
			||||||
    It contains the deadlines for the users, and the link to the club that makes
 | 
					    It contains the deadlines for the users, and the link to the club that makes
 | 
				
			||||||
    its Trombi.
 | 
					    its Trombi.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
@@ -103,10 +103,10 @@ class Trombi(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TrombiUser(models.Model):
 | 
					class TrombiUser(models.Model):
 | 
				
			||||||
    """
 | 
					    """Bound between a `User` and a `Trombi`.
 | 
				
			||||||
    This class is only here to avoid cross references between the core, club,
 | 
					
 | 
				
			||||||
    and trombi modules. It binds a User to a Trombi without needing to import
 | 
					    This class is here to avoid cross-references between the core, club,
 | 
				
			||||||
    Trombi into the core.
 | 
					    and trombi modules.
 | 
				
			||||||
    It also adds the pictures to the profile without needing all the security
 | 
					    It also adds the pictures to the profile without needing all the security
 | 
				
			||||||
    like the other SithFiles.
 | 
					    like the other SithFiles.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
@@ -172,10 +172,7 @@ class TrombiUser(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TrombiComment(models.Model):
 | 
					class TrombiComment(models.Model):
 | 
				
			||||||
    """
 | 
					    """A comment given by someone to someone else in the same Trombi instance."""
 | 
				
			||||||
    This represent a comment given by someone to someone else in the same Trombi
 | 
					 | 
				
			||||||
    instance.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    author = models.ForeignKey(
 | 
					    author = models.ForeignKey(
 | 
				
			||||||
        TrombiUser,
 | 
					        TrombiUser,
 | 
				
			||||||
@@ -202,9 +199,7 @@ class TrombiComment(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TrombiClubMembership(models.Model):
 | 
					class TrombiClubMembership(models.Model):
 | 
				
			||||||
    """
 | 
					    """A membership in a club."""
 | 
				
			||||||
    This represent a membership to a club
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    user = models.ForeignKey(
 | 
					    user = models.ForeignKey(
 | 
				
			||||||
        TrombiUser,
 | 
					        TrombiUser,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -94,9 +94,7 @@ class TrombiTabsMixin(TabedViewMixin):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserIsInATrombiMixin(View):
 | 
					class UserIsInATrombiMixin(View):
 | 
				
			||||||
    """
 | 
					    """Check if the requested user has a trombi_user attribute."""
 | 
				
			||||||
    This view check if the requested user has a trombi_user attribute
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def dispatch(self, request, *args, **kwargs):
 | 
					    def dispatch(self, request, *args, **kwargs):
 | 
				
			||||||
        if not hasattr(self.request.user, "trombi_user"):
 | 
					        if not hasattr(self.request.user, "trombi_user"):
 | 
				
			||||||
@@ -118,18 +116,14 @@ class TrombiForm(forms.ModelForm):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TrombiCreateView(CanCreateMixin, CreateView):
 | 
					class TrombiCreateView(CanCreateMixin, CreateView):
 | 
				
			||||||
    """
 | 
					    """Create a trombi for a club."""
 | 
				
			||||||
    Create a trombi for a club
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = Trombi
 | 
					    model = Trombi
 | 
				
			||||||
    form_class = TrombiForm
 | 
					    form_class = TrombiForm
 | 
				
			||||||
    template_name = "core/create.jinja"
 | 
					    template_name = "core/create.jinja"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def post(self, request, *args, **kwargs):
 | 
					    def post(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """Affect club."""
 | 
				
			||||||
        Affect club
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        form = self.get_form()
 | 
					        form = self.get_form()
 | 
				
			||||||
        if form.is_valid():
 | 
					        if form.is_valid():
 | 
				
			||||||
            club = get_object_or_404(Club, id=self.kwargs["club_id"])
 | 
					            club = get_object_or_404(Club, id=self.kwargs["club_id"])
 | 
				
			||||||
@@ -304,9 +298,7 @@ class UserTrombiForm(forms.Form):
 | 
				
			|||||||
class UserTrombiToolsView(
 | 
					class UserTrombiToolsView(
 | 
				
			||||||
    QuickNotifMixin, TrombiTabsMixin, UserIsLoggedMixin, TemplateView
 | 
					    QuickNotifMixin, TrombiTabsMixin, UserIsLoggedMixin, TemplateView
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
    """
 | 
					    """Display a user's trombi tools."""
 | 
				
			||||||
    Display a user's trombi tools
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    template_name = "trombi/user_tools.jinja"
 | 
					    template_name = "trombi/user_tools.jinja"
 | 
				
			||||||
    current_tab = "tools"
 | 
					    current_tab = "tools"
 | 
				
			||||||
@@ -466,9 +458,7 @@ class UserTrombiProfileView(TrombiTabsMixin, DetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TrombiCommentFormView(LoginRequiredMixin, View):
 | 
					class TrombiCommentFormView(LoginRequiredMixin, View):
 | 
				
			||||||
    """
 | 
					    """Create/edit a trombi comment."""
 | 
				
			||||||
    Create/edit a trombi comment
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    model = TrombiComment
 | 
					    model = TrombiComment
 | 
				
			||||||
    fields = ["content"]
 | 
					    fields = ["content"]
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user