mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-24 10:04:34 +00:00
use google convention for docstrings
This commit is contained in:
parent
07b625d4aa
commit
8c69a94488
@ -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"]
|
||||||
|
Loading…
Reference in New Issue
Block a user