mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-10-31 00:53:08 +00:00 
			
		
		
		
	use google convention for docstrings
This commit is contained in:
		| @@ -29,9 +29,7 @@ from core.models import SithFile, User | ||||
|  | ||||
|  | ||||
| class CurrencyField(models.DecimalField): | ||||
|     """ | ||||
|     This is a custom database field used for currency | ||||
|     """ | ||||
|     """Custom database field used for currency.""" | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         kwargs["max_digits"] = 12 | ||||
| @@ -71,30 +69,22 @@ class Company(models.Model): | ||||
|         return self.name | ||||
|  | ||||
|     def is_owned_by(self, user): | ||||
|         """ | ||||
|         Method to see if that object can be edited by the given user | ||||
|         """ | ||||
|         """Check if that object can be edited by the given user.""" | ||||
|         if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): | ||||
|             return True | ||||
|         return False | ||||
|  | ||||
|     def can_be_edited_by(self, user): | ||||
|         """ | ||||
|         Method to see if that object can be edited by the given user | ||||
|         """ | ||||
|         for club in user.memberships.filter(end_date=None).all(): | ||||
|             if club and club.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]: | ||||
|                 return True | ||||
|         return False | ||||
|         """Check 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"] | ||||
|         ).exists() | ||||
|  | ||||
|     def can_be_viewed_by(self, user): | ||||
|         """ | ||||
|         Method to see if that object can be viewed by the given user | ||||
|         """ | ||||
|         for club in user.memberships.filter(end_date=None).all(): | ||||
|             if club and club.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]: | ||||
|                 return True | ||||
|         return False | ||||
|         """Check 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"] | ||||
|         ).exists() | ||||
|  | ||||
|  | ||||
| class BankAccount(models.Model): | ||||
| @@ -119,9 +109,7 @@ class BankAccount(models.Model): | ||||
|         return reverse("accounting:bank_details", kwargs={"b_account_id": self.id}) | ||||
|  | ||||
|     def is_owned_by(self, user): | ||||
|         """ | ||||
|         Method to see if that object can be edited by the given user | ||||
|         """ | ||||
|         """Check if that object can be edited by the given user.""" | ||||
|         if user.is_anonymous: | ||||
|             return False | ||||
|         if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): | ||||
| @@ -158,9 +146,7 @@ class ClubAccount(models.Model): | ||||
|         return reverse("accounting:club_details", kwargs={"c_account_id": self.id}) | ||||
|  | ||||
|     def is_owned_by(self, user): | ||||
|         """ | ||||
|         Method to see if that object can be edited by the given user | ||||
|         """ | ||||
|         """Check if that object can be edited by the given user.""" | ||||
|         if user.is_anonymous: | ||||
|             return False | ||||
|         if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): | ||||
| @@ -168,18 +154,14 @@ class ClubAccount(models.Model): | ||||
|         return False | ||||
|  | ||||
|     def can_be_edited_by(self, user): | ||||
|         """ | ||||
|         Method to see if that object can be edited by the given user | ||||
|         """ | ||||
|         """Check if that object can be edited by the given user.""" | ||||
|         m = self.club.get_membership_for(user) | ||||
|         if m and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]: | ||||
|             return True | ||||
|         return False | ||||
|  | ||||
|     def can_be_viewed_by(self, user): | ||||
|         """ | ||||
|         Method to see if that object can be viewed by the given user | ||||
|         """ | ||||
|         """Check if that object can be viewed by the given user.""" | ||||
|         m = self.club.get_membership_for(user) | ||||
|         if m and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]: | ||||
|             return True | ||||
| @@ -202,9 +184,7 @@ class ClubAccount(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")) | ||||
|     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}) | ||||
|  | ||||
|     def is_owned_by(self, user): | ||||
|         """ | ||||
|         Method to see if that object can be edited by the given user | ||||
|         """ | ||||
|         """Check if that object can be edited by the given user.""" | ||||
|         if user.is_anonymous: | ||||
|             return False | ||||
|         if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): | ||||
| @@ -243,9 +221,7 @@ class GeneralJournal(models.Model): | ||||
|         return False | ||||
|  | ||||
|     def can_be_edited_by(self, user): | ||||
|         """ | ||||
|         Method to see if that object can be edited by the given user | ||||
|         """ | ||||
|         """Check if that object can be edited by the given user.""" | ||||
|         if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): | ||||
|             return True | ||||
|         if self.club_account.can_be_edited_by(user): | ||||
| @@ -271,9 +247,7 @@ class GeneralJournal(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")) | ||||
|     journal = models.ForeignKey( | ||||
| @@ -422,9 +396,7 @@ class Operation(models.Model): | ||||
|         return tar | ||||
|  | ||||
|     def is_owned_by(self, user): | ||||
|         """ | ||||
|         Method to see if that object can be edited by the given user | ||||
|         """ | ||||
|         """Check if that object can be edited by the given user.""" | ||||
|         if user.is_anonymous: | ||||
|             return False | ||||
|         if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): | ||||
| @@ -437,9 +409,7 @@ class Operation(models.Model): | ||||
|         return False | ||||
|  | ||||
|     def can_be_edited_by(self, user): | ||||
|         """ | ||||
|         Method to see if that object can be edited by the given user | ||||
|         """ | ||||
|         """Check if that object can be edited by the given user.""" | ||||
|         if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): | ||||
|             return True | ||||
|         if self.journal.closed: | ||||
| @@ -451,10 +421,9 @@ class Operation(models.Model): | ||||
|  | ||||
|  | ||||
| class AccountingType(models.Model): | ||||
|     """ | ||||
|     Class describing the accounting types. | ||||
|     """Accounting types. | ||||
|  | ||||
|     Thoses are numbers used in accounting to classify operations | ||||
|     Those are numbers used in accounting to classify operations | ||||
|     """ | ||||
|  | ||||
|     code = models.CharField( | ||||
| @@ -488,9 +457,7 @@ class AccountingType(models.Model): | ||||
|         return reverse("accounting:type_list") | ||||
|  | ||||
|     def is_owned_by(self, user): | ||||
|         """ | ||||
|         Method to see if that object can be edited by the given user | ||||
|         """ | ||||
|         """Check if that object can be edited by the given user.""" | ||||
|         if user.is_anonymous: | ||||
|             return False | ||||
|         if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): | ||||
| @@ -499,9 +466,7 @@ class AccountingType(models.Model): | ||||
|  | ||||
|  | ||||
| class SimplifiedAccountingType(models.Model): | ||||
|     """ | ||||
|     Class describing the simplified accounting types. | ||||
|     """ | ||||
|     """Simplified version of `AccountingType`.""" | ||||
|  | ||||
|     label = models.CharField(_("label"), max_length=128) | ||||
|     accounting_type = models.ForeignKey( | ||||
| @@ -533,7 +498,7 @@ class SimplifiedAccountingType(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) | ||||
|     club_account = models.ForeignKey( | ||||
|   | ||||
| @@ -53,9 +53,7 @@ from counter.models import Counter, Product, Selling | ||||
|  | ||||
|  | ||||
| class BankAccountListView(CanViewMixin, ListView): | ||||
|     """ | ||||
|     A list view for the admins | ||||
|     """ | ||||
|     """A list view for the admins.""" | ||||
|  | ||||
|     model = BankAccount | ||||
|     template_name = "accounting/bank_account_list.jinja" | ||||
| @@ -66,18 +64,14 @@ class BankAccountListView(CanViewMixin, ListView): | ||||
|  | ||||
|  | ||||
| class SimplifiedAccountingTypeListView(CanViewMixin, ListView): | ||||
|     """ | ||||
|     A list view for the admins | ||||
|     """ | ||||
|     """A list view for the admins.""" | ||||
|  | ||||
|     model = SimplifiedAccountingType | ||||
|     template_name = "accounting/simplifiedaccountingtype_list.jinja" | ||||
|  | ||||
|  | ||||
| class SimplifiedAccountingTypeEditView(CanViewMixin, UpdateView): | ||||
|     """ | ||||
|     An edit view for the admins | ||||
|     """ | ||||
|     """An edit view for the admins.""" | ||||
|  | ||||
|     model = SimplifiedAccountingType | ||||
|     pk_url_kwarg = "type_id" | ||||
| @@ -86,9 +80,7 @@ class SimplifiedAccountingTypeEditView(CanViewMixin, UpdateView): | ||||
|  | ||||
|  | ||||
| class SimplifiedAccountingTypeCreateView(CanCreateMixin, CreateView): | ||||
|     """ | ||||
|     Create an accounting type (for the admins) | ||||
|     """ | ||||
|     """Create an accounting type (for the admins).""" | ||||
|  | ||||
|     model = SimplifiedAccountingType | ||||
|     fields = ["label", "accounting_type"] | ||||
| @@ -99,18 +91,14 @@ class SimplifiedAccountingTypeCreateView(CanCreateMixin, CreateView): | ||||
|  | ||||
|  | ||||
| class AccountingTypeListView(CanViewMixin, ListView): | ||||
|     """ | ||||
|     A list view for the admins | ||||
|     """ | ||||
|     """A list view for the admins.""" | ||||
|  | ||||
|     model = AccountingType | ||||
|     template_name = "accounting/accountingtype_list.jinja" | ||||
|  | ||||
|  | ||||
| class AccountingTypeEditView(CanViewMixin, UpdateView): | ||||
|     """ | ||||
|     An edit view for the admins | ||||
|     """ | ||||
|     """An edit view for the admins.""" | ||||
|  | ||||
|     model = AccountingType | ||||
|     pk_url_kwarg = "type_id" | ||||
| @@ -119,9 +107,7 @@ class AccountingTypeEditView(CanViewMixin, UpdateView): | ||||
|  | ||||
|  | ||||
| class AccountingTypeCreateView(CanCreateMixin, CreateView): | ||||
|     """ | ||||
|     Create an accounting type (for the admins) | ||||
|     """ | ||||
|     """Create an accounting type (for the admins).""" | ||||
|  | ||||
|     model = AccountingType | ||||
|     fields = ["code", "label", "movement_type"] | ||||
| @@ -132,9 +118,7 @@ class AccountingTypeCreateView(CanCreateMixin, CreateView): | ||||
|  | ||||
|  | ||||
| class BankAccountEditView(CanViewMixin, UpdateView): | ||||
|     """ | ||||
|     An edit view for the admins | ||||
|     """ | ||||
|     """An edit view for the admins.""" | ||||
|  | ||||
|     model = BankAccount | ||||
|     pk_url_kwarg = "b_account_id" | ||||
| @@ -143,9 +127,7 @@ class BankAccountEditView(CanViewMixin, UpdateView): | ||||
|  | ||||
|  | ||||
| class BankAccountDetailView(CanViewMixin, DetailView): | ||||
|     """ | ||||
|     A detail view, listing every club account | ||||
|     """ | ||||
|     """A detail view, listing every club account.""" | ||||
|  | ||||
|     model = BankAccount | ||||
|     pk_url_kwarg = "b_account_id" | ||||
| @@ -153,9 +135,7 @@ class BankAccountDetailView(CanViewMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class BankAccountCreateView(CanCreateMixin, CreateView): | ||||
|     """ | ||||
|     Create a bank account (for the admins) | ||||
|     """ | ||||
|     """Create a bank account (for the admins).""" | ||||
|  | ||||
|     model = BankAccount | ||||
|     fields = ["name", "club", "iban", "number"] | ||||
| @@ -165,9 +145,7 @@ class BankAccountCreateView(CanCreateMixin, CreateView): | ||||
| class BankAccountDeleteView( | ||||
|     CanEditPropMixin, DeleteView | ||||
| ):  # TODO change Delete to Close | ||||
|     """ | ||||
|     Delete a bank account (for the admins) | ||||
|     """ | ||||
|     """Delete a bank account (for the admins).""" | ||||
|  | ||||
|     model = BankAccount | ||||
|     pk_url_kwarg = "b_account_id" | ||||
| @@ -179,9 +157,7 @@ class BankAccountDeleteView( | ||||
|  | ||||
|  | ||||
| class ClubAccountEditView(CanViewMixin, UpdateView): | ||||
|     """ | ||||
|     An edit view for the admins | ||||
|     """ | ||||
|     """An edit view for the admins.""" | ||||
|  | ||||
|     model = ClubAccount | ||||
|     pk_url_kwarg = "c_account_id" | ||||
| @@ -190,9 +166,7 @@ class ClubAccountEditView(CanViewMixin, UpdateView): | ||||
|  | ||||
|  | ||||
| class ClubAccountDetailView(CanViewMixin, DetailView): | ||||
|     """ | ||||
|     A detail view, listing every journal | ||||
|     """ | ||||
|     """A detail view, listing every journal.""" | ||||
|  | ||||
|     model = ClubAccount | ||||
|     pk_url_kwarg = "c_account_id" | ||||
| @@ -200,9 +174,7 @@ class ClubAccountDetailView(CanViewMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class ClubAccountCreateView(CanCreateMixin, CreateView): | ||||
|     """ | ||||
|     Create a club account (for the admins) | ||||
|     """ | ||||
|     """Create a club account (for the admins).""" | ||||
|  | ||||
|     model = ClubAccount | ||||
|     fields = ["name", "club", "bank_account"] | ||||
| @@ -220,9 +192,7 @@ class ClubAccountCreateView(CanCreateMixin, CreateView): | ||||
| class ClubAccountDeleteView( | ||||
|     CanEditPropMixin, DeleteView | ||||
| ):  # TODO change Delete to Close | ||||
|     """ | ||||
|     Delete a club account (for the admins) | ||||
|     """ | ||||
|     """Delete a club account (for the admins).""" | ||||
|  | ||||
|     model = ClubAccount | ||||
|     pk_url_kwarg = "c_account_id" | ||||
| @@ -282,9 +252,7 @@ class JournalTabsMixin(TabedViewMixin): | ||||
|  | ||||
|  | ||||
| class JournalCreateView(CanCreateMixin, CreateView): | ||||
|     """ | ||||
|     Create a general journal | ||||
|     """ | ||||
|     """Create a general journal.""" | ||||
|  | ||||
|     model = GeneralJournal | ||||
|     form_class = modelform_factory( | ||||
| @@ -304,9 +272,7 @@ class JournalCreateView(CanCreateMixin, CreateView): | ||||
|  | ||||
|  | ||||
| class JournalDetailView(JournalTabsMixin, CanViewMixin, DetailView): | ||||
|     """ | ||||
|     A detail view, listing every operation | ||||
|     """ | ||||
|     """A detail view, listing every operation.""" | ||||
|  | ||||
|     model = GeneralJournal | ||||
|     pk_url_kwarg = "j_id" | ||||
| @@ -315,9 +281,7 @@ class JournalDetailView(JournalTabsMixin, CanViewMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class JournalEditView(CanEditMixin, UpdateView): | ||||
|     """ | ||||
|     Update a general journal | ||||
|     """ | ||||
|     """Update a general journal.""" | ||||
|  | ||||
|     model = GeneralJournal | ||||
|     pk_url_kwarg = "j_id" | ||||
| @@ -326,9 +290,7 @@ class JournalEditView(CanEditMixin, UpdateView): | ||||
|  | ||||
|  | ||||
| class JournalDeleteView(CanEditPropMixin, DeleteView): | ||||
|     """ | ||||
|     Delete a club account (for the admins) | ||||
|     """ | ||||
|     """Delete a club account (for the admins).""" | ||||
|  | ||||
|     model = GeneralJournal | ||||
|     pk_url_kwarg = "j_id" | ||||
| @@ -467,9 +429,7 @@ class OperationForm(forms.ModelForm): | ||||
|  | ||||
|  | ||||
| class OperationCreateView(CanCreateMixin, CreateView): | ||||
|     """ | ||||
|     Create an operation | ||||
|     """ | ||||
|     """Create an operation.""" | ||||
|  | ||||
|     model = Operation | ||||
|     form_class = OperationForm | ||||
| @@ -487,7 +447,7 @@ class OperationCreateView(CanCreateMixin, CreateView): | ||||
|         return ret | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         """Add journal to the context""" | ||||
|         """Add journal to the context.""" | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
|         if self.journal: | ||||
|             kwargs["object"] = self.journal | ||||
| @@ -495,9 +455,7 @@ class OperationCreateView(CanCreateMixin, CreateView): | ||||
|  | ||||
|  | ||||
| class OperationEditView(CanEditMixin, UpdateView): | ||||
|     """ | ||||
|     An edit view, working as detail for the moment | ||||
|     """ | ||||
|     """An edit view, working as detail for the moment.""" | ||||
|  | ||||
|     model = Operation | ||||
|     pk_url_kwarg = "op_id" | ||||
| @@ -505,16 +463,14 @@ class OperationEditView(CanEditMixin, UpdateView): | ||||
|     template_name = "accounting/operation_edit.jinja" | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         """Add journal to the context""" | ||||
|         """Add journal to the context.""" | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
|         kwargs["object"] = self.object.journal | ||||
|         return kwargs | ||||
|  | ||||
|  | ||||
| class OperationPDFView(CanViewMixin, DetailView): | ||||
|     """ | ||||
|     Display the PDF of a given operation | ||||
|     """ | ||||
|     """Display the PDF of a given operation.""" | ||||
|  | ||||
|     model = Operation | ||||
|     pk_url_kwarg = "op_id" | ||||
| @@ -666,9 +622,7 @@ class OperationPDFView(CanViewMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView): | ||||
|     """ | ||||
|     Display a statement sorted by labels | ||||
|     """ | ||||
|     """Display a statement sorted by labels.""" | ||||
|  | ||||
|     model = GeneralJournal | ||||
|     pk_url_kwarg = "j_id" | ||||
| @@ -726,16 +680,14 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView): | ||||
|         return statement | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         """Add infos to the context""" | ||||
|         """Add infos to the context.""" | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
|         kwargs["statement"] = self.big_statement() | ||||
|         return kwargs | ||||
|  | ||||
|  | ||||
| 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 | ||||
|     pk_url_kwarg = "j_id" | ||||
| @@ -765,7 +717,7 @@ class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView): | ||||
|         return sum(self.statement(movement_type).values()) | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         """Add journal to the context""" | ||||
|         """Add journal to the context.""" | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
|         kwargs["credit_statement"] = self.statement("CREDIT") | ||||
|         kwargs["debit_statement"] = self.statement("DEBIT") | ||||
| @@ -775,9 +727,7 @@ class JournalPersonStatementView(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 | ||||
|     pk_url_kwarg = "j_id" | ||||
| @@ -795,7 +745,7 @@ class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView) | ||||
|         return statement | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         """Add journal to the context""" | ||||
|         """Add journal to the context.""" | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
|         kwargs["statement"] = self.statement() | ||||
|         return kwargs | ||||
| @@ -810,9 +760,7 @@ class CompanyListView(CanViewMixin, ListView): | ||||
|  | ||||
|  | ||||
| class CompanyCreateView(CanCreateMixin, CreateView): | ||||
|     """ | ||||
|     Create a company | ||||
|     """ | ||||
|     """Create a company.""" | ||||
|  | ||||
|     model = Company | ||||
|     fields = ["name"] | ||||
| @@ -821,9 +769,7 @@ class CompanyCreateView(CanCreateMixin, CreateView): | ||||
|  | ||||
|  | ||||
| class CompanyEditView(CanCreateMixin, UpdateView): | ||||
|     """ | ||||
|     Edit a company | ||||
|     """ | ||||
|     """Edit a company.""" | ||||
|  | ||||
|     model = Company | ||||
|     pk_url_kwarg = "co_id" | ||||
| @@ -882,9 +828,7 @@ class CloseCustomerAccountForm(forms.Form): | ||||
|  | ||||
|  | ||||
| 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" | ||||
|     form_class = CloseCustomerAccountForm | ||||
|   | ||||
| @@ -23,9 +23,9 @@ from core.views import can_edit, can_view | ||||
|  | ||||
|  | ||||
| def check_if(obj, user, test): | ||||
|     """ | ||||
|     Detect if it's a single object or a queryset | ||||
|     aply a given test on individual object and return global permission | ||||
|     """Detect if it's a single object or a queryset. | ||||
|  | ||||
|     Apply a given test on individual object and return global permission. | ||||
|     """ | ||||
|     if isinstance(obj, QuerySet): | ||||
|         for o in obj: | ||||
| @@ -39,9 +39,7 @@ def check_if(obj, user, test): | ||||
| class ManageModelMixin: | ||||
|     @action(detail=True) | ||||
|     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)) | ||||
|         serializer = self.get_serializer(self.queryset) | ||||
|         return Response(serializer.data) | ||||
|   | ||||
| @@ -23,9 +23,7 @@ from core.templatetags.renderer import markdown | ||||
| @api_view(["POST"]) | ||||
| @renderer_classes((StaticHTMLRenderer,)) | ||||
| def RenderMarkdown(request): | ||||
|     """ | ||||
|     Render Markdown | ||||
|     """ | ||||
|     """Render Markdown.""" | ||||
|     try: | ||||
|         data = markdown(request.POST["text"]) | ||||
|     except: | ||||
|   | ||||
| @@ -31,9 +31,7 @@ class ClubSerializer(serializers.ModelSerializer): | ||||
|  | ||||
|  | ||||
| class ClubViewSet(RightModelViewSet): | ||||
|     """ | ||||
|     Manage Clubs (api/v1/club/) | ||||
|     """ | ||||
|     """Manage Clubs (api/v1/club/).""" | ||||
|  | ||||
|     serializer_class = ClubSerializer | ||||
|     queryset = Club.objects.all() | ||||
|   | ||||
| @@ -33,18 +33,14 @@ class CounterSerializer(serializers.ModelSerializer): | ||||
|  | ||||
|  | ||||
| class CounterViewSet(RightModelViewSet): | ||||
|     """ | ||||
|     Manage Counters (api/v1/counter/) | ||||
|     """ | ||||
|     """Manage Counters (api/v1/counter/).""" | ||||
|  | ||||
|     serializer_class = CounterSerializer | ||||
|     queryset = Counter.objects.all() | ||||
|  | ||||
|     @action(detail=False) | ||||
|     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") | ||||
|         serializer = self.get_serializer(self.queryset, many=True) | ||||
|         return Response(serializer.data) | ||||
|   | ||||
| @@ -25,9 +25,7 @@ class GroupSerializer(serializers.ModelSerializer): | ||||
|  | ||||
|  | ||||
| class GroupViewSet(RightModelViewSet): | ||||
|     """ | ||||
|     Manage Groups (api/v1/group/) | ||||
|     """ | ||||
|     """Manage Groups (api/v1/group/).""" | ||||
|  | ||||
|     serializer_class = GroupSerializer | ||||
|     queryset = RealGroup.objects.all() | ||||
|   | ||||
| @@ -60,54 +60,42 @@ class LaunderetteTokenSerializer(serializers.ModelSerializer): | ||||
|  | ||||
|  | ||||
| class LaunderettePlaceViewSet(RightModelViewSet): | ||||
|     """ | ||||
|     Manage Launderette (api/v1/launderette/place/) | ||||
|     """ | ||||
|     """Manage Launderette (api/v1/launderette/place/).""" | ||||
|  | ||||
|     serializer_class = LaunderettePlaceSerializer | ||||
|     queryset = Launderette.objects.all() | ||||
|  | ||||
|  | ||||
| class LaunderetteMachineViewSet(RightModelViewSet): | ||||
|     """ | ||||
|     Manage Washing Machines (api/v1/launderette/machine/) | ||||
|     """ | ||||
|     """Manage Washing Machines (api/v1/launderette/machine/).""" | ||||
|  | ||||
|     serializer_class = LaunderetteMachineSerializer | ||||
|     queryset = Machine.objects.all() | ||||
|  | ||||
|  | ||||
| class LaunderetteTokenViewSet(RightModelViewSet): | ||||
|     """ | ||||
|     Manage Launderette's tokens (api/v1/launderette/token/) | ||||
|     """ | ||||
|     """Manage Launderette's tokens (api/v1/launderette/token/).""" | ||||
|  | ||||
|     serializer_class = LaunderetteTokenSerializer | ||||
|     queryset = Token.objects.all() | ||||
|  | ||||
|     @action(detail=False) | ||||
|     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") | ||||
|         serializer = self.get_serializer(self.queryset, many=True) | ||||
|         return Response(serializer.data) | ||||
|  | ||||
|     @action(detail=False) | ||||
|     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") | ||||
|         serializer = self.get_serializer(self.queryset, many=True) | ||||
|         return Response(serializer.data) | ||||
|  | ||||
|     @action(detail=False) | ||||
|     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( | ||||
|             borrow_date__isnull=True, user__isnull=True | ||||
|         ) | ||||
| @@ -116,9 +104,7 @@ class LaunderetteTokenViewSet(RightModelViewSet): | ||||
|  | ||||
|     @action(detail=False) | ||||
|     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( | ||||
|             borrow_date__isnull=False, user__isnull=False | ||||
|         ) | ||||
|   | ||||
| @@ -39,9 +39,9 @@ class UserSerializer(serializers.ModelSerializer): | ||||
|  | ||||
|  | ||||
| class UserViewSet(RightModelViewSet): | ||||
|     """ | ||||
|     Manage Users (api/v1/user/) | ||||
|     Only show active users | ||||
|     """Manage Users (api/v1/user/). | ||||
|  | ||||
|     Only show active users. | ||||
|     """ | ||||
|  | ||||
|     serializer_class = UserSerializer | ||||
| @@ -49,9 +49,7 @@ class UserViewSet(RightModelViewSet): | ||||
|  | ||||
|     @action(detail=False) | ||||
|     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() | ||||
|         self.queryset = self.queryset.filter(date_of_birth=date) | ||||
|         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)) | ||||
|  | ||||
|  | ||||
| def find_uv(lang, year, code): | ||||
|     """ | ||||
|     Uses the UTBM API to find an UV. | ||||
|     short_uv is the UV entry in the UV list. It is returned as it contains | ||||
| def find_uv(lang: str, year: int | str, code: str) -> tuple[dict | None, dict | None]: | ||||
|     """Uses the UTBM API to find an UV. | ||||
|  | ||||
|     Short_uv is the UV entry in the UV list. It is returned as it contains | ||||
|     information which are not in full_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 | ||||
|         short_uv = next(uv for uv in uvs if uv["code"] == code) | ||||
|     except StopIteration: | ||||
|         return (None, None) | ||||
|         return None, None | ||||
|  | ||||
|     # get detailed information about the UV | ||||
|     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) | ||||
|     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): | ||||
|     """ | ||||
|     Cleans the data up so that it corresponds to our data representation. | ||||
|     """ | ||||
| def make_clean_uv(short_uv: dict, full_uv: dict): | ||||
|     """Cleans the data up so that it corresponds to our data representation.""" | ||||
|     res = {} | ||||
|  | ||||
|     res["credit_type"] = short_uv["codeCategorie"] | ||||
|   | ||||
| @@ -44,9 +44,7 @@ class ClubEditForm(forms.ModelForm): | ||||
|  | ||||
|  | ||||
| class MailingForm(forms.Form): | ||||
|     """ | ||||
|     Form handling mailing lists right | ||||
|     """ | ||||
|     """Form handling mailing lists right.""" | ||||
|  | ||||
|     ACTION_NEW_MAILING = 1 | ||||
|     ACTION_NEW_SUBSCRIPTION = 2 | ||||
| @@ -105,16 +103,12 @@ class MailingForm(forms.Form): | ||||
|         ) | ||||
|  | ||||
|     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): | ||||
|             self.add_error(field, _("This field is required")) | ||||
|  | ||||
|     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() | ||||
|         users = [] | ||||
|         for user in cleaned_data["subscription_users"]: | ||||
| @@ -177,9 +171,7 @@ class SellingsForm(forms.Form): | ||||
|  | ||||
|  | ||||
| class ClubMemberForm(forms.Form): | ||||
|     """ | ||||
|     Form handling the members of a club | ||||
|     """ | ||||
|     """Form handling the members of a club.""" | ||||
|  | ||||
|     error_css_class = "error" | ||||
|     required_css_class = "required" | ||||
| @@ -236,9 +228,9 @@ class ClubMemberForm(forms.Form): | ||||
|             self.fields.pop("start_date") | ||||
|  | ||||
|     def clean_users(self): | ||||
|         """ | ||||
|         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 | ||||
|         """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. | ||||
|         """ | ||||
|         cleaned_data = super().clean() | ||||
|         users = [] | ||||
| @@ -260,9 +252,7 @@ class ClubMemberForm(forms.Form): | ||||
|         return users | ||||
|  | ||||
|     def clean(self): | ||||
|         """ | ||||
|         Check user rights for adding an user | ||||
|         """ | ||||
|         """Check user rights for adding an user.""" | ||||
|         cleaned_data = super().clean() | ||||
|  | ||||
|         if "start_date" in cleaned_data and not cleaned_data["start_date"]: | ||||
|   | ||||
| @@ -21,7 +21,7 @@ | ||||
| # Place - Suite 330, Boston, MA 02111-1307, USA. | ||||
| # | ||||
| # | ||||
| from typing import Optional | ||||
| from __future__ import annotations | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.core import validators | ||||
| @@ -46,9 +46,7 @@ def get_default_owner_group(): | ||||
|  | ||||
|  | ||||
| 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) | ||||
|     name = models.CharField(_("name"), max_length=64) | ||||
| @@ -141,7 +139,7 @@ class Club(models.Model): | ||||
|         ).first() | ||||
|  | ||||
|     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 = [] | ||||
|         cur = self | ||||
|         while cur.parent is not None: | ||||
| @@ -223,9 +221,7 @@ class Club(models.Model): | ||||
|         return self.name | ||||
|  | ||||
|     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: | ||||
|             return False | ||||
|         return user.is_board_member | ||||
| @@ -234,24 +230,21 @@ class Club(models.Model): | ||||
|         return "https://%s%s" % (settings.SITH_URL, self.logo.url) | ||||
|  | ||||
|     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) | ||||
|  | ||||
|     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() | ||||
|         if sub is None: | ||||
|             return False | ||||
|         return sub.was_subscribed | ||||
|  | ||||
|     def get_membership_for(self, user: User) -> Optional["Membership"]: | ||||
|         """ | ||||
|         Return the current membership the given user. | ||||
|         The result is cached. | ||||
|     def get_membership_for(self, user: User) -> Membership | None: | ||||
|         """Return the current membership the given user. | ||||
|  | ||||
|         Note: | ||||
|             The result is cached. | ||||
|         """ | ||||
|         if user.is_anonymous: | ||||
|             return None | ||||
| @@ -273,15 +266,12 @@ class Club(models.Model): | ||||
|  | ||||
| class MembershipQuerySet(models.QuerySet): | ||||
|     def ongoing(self) -> "MembershipQuerySet": | ||||
|         """ | ||||
|         Filter all memberships which are not finished yet | ||||
|         """ | ||||
|         """Filter all memberships which are not finished yet.""" | ||||
|         # noinspection PyTypeChecker | ||||
|         return self.filter(Q(end_date=None) | Q(end_date__gte=timezone.now())) | ||||
|  | ||||
|     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 | ||||
|         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) | ||||
|  | ||||
|     def update(self, **kwargs): | ||||
|         """ | ||||
|         Work just like the default Django's update() method, | ||||
|         but add a cache refresh for the elements of the queryset. | ||||
|         """Refresh the cache 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 | ||||
|         """ | ||||
| @@ -315,8 +305,7 @@ class MembershipQuerySet(models.QuerySet): | ||||
|                     ) | ||||
|  | ||||
|     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 | ||||
|         before the deletion. | ||||
|  | ||||
| @@ -332,8 +321,7 @@ class MembershipQuerySet(models.QuerySet): | ||||
|  | ||||
|  | ||||
| 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: | ||||
|        - 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}) | ||||
|  | ||||
|     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: | ||||
|             return False | ||||
|         return user.is_board_member | ||||
|  | ||||
|     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: | ||||
|             return True | ||||
|         membership = self.club.get_membership_for(user) | ||||
| @@ -414,9 +398,10 @@ class Membership(models.Model): | ||||
|  | ||||
|  | ||||
| class Mailing(models.Model): | ||||
|     """ | ||||
|     This class correspond to a mailing list | ||||
|     Remember that mailing lists should be validated by UTBM | ||||
|     """A Mailing list for a club. | ||||
|  | ||||
|     Warning: | ||||
|         Remember that mailing lists should be validated by UTBM. | ||||
|     """ | ||||
|  | ||||
|     club = models.ForeignKey( | ||||
| @@ -508,9 +493,7 @@ class Mailing(models.Model): | ||||
|  | ||||
|  | ||||
| class MailingSubscription(models.Model): | ||||
|     """ | ||||
|     This class makes the link between user and mailing list | ||||
|     """ | ||||
|     """Link between user and mailing list.""" | ||||
|  | ||||
|     mailing = models.ForeignKey( | ||||
|         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): | ||||
|     """ | ||||
|     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, | ||||
|     plus the following modifications : | ||||
|  | ||||
| @@ -94,8 +94,7 @@ class ClubTest(TestCase): | ||||
|  | ||||
| class MembershipQuerySetTest(ClubTest): | ||||
|     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. | ||||
|         """ | ||||
|         current_members = list(self.club.members.ongoing().order_by("id")) | ||||
| @@ -108,9 +107,8 @@ class MembershipQuerySetTest(ClubTest): | ||||
|         assert current_members == expected | ||||
|  | ||||
|     def test_board(self): | ||||
|         """ | ||||
|         Test that the board queryset method returns the memberships | ||||
|         of user in the club board | ||||
|         """Test that the board queryset method returns the memberships | ||||
|         of user in the club board. | ||||
|         """ | ||||
|         board_members = list(self.club.members.board().order_by("id")) | ||||
|         expected = [ | ||||
| @@ -123,9 +121,8 @@ class MembershipQuerySetTest(ClubTest): | ||||
|         assert board_members == expected | ||||
|  | ||||
|     def test_ongoing_board(self): | ||||
|         """ | ||||
|         Test that combining ongoing and board returns users | ||||
|         who are currently board members of the club | ||||
|         """Test that combining ongoing and board returns users | ||||
|         who are currently board members of the club. | ||||
|         """ | ||||
|         members = list(self.club.members.ongoing().board().order_by("id")) | ||||
|         expected = [ | ||||
| @@ -136,9 +133,7 @@ class MembershipQuerySetTest(ClubTest): | ||||
|         assert members == expected | ||||
|  | ||||
|     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) | ||||
|         cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia) | ||||
|         self.skia.memberships.update(end_date=localtime(now()).date()) | ||||
| @@ -157,10 +152,7 @@ class MembershipQuerySetTest(ClubTest): | ||||
|         assert new_mem.role == 5 | ||||
|  | ||||
|     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_comptable = self.comptable.memberships.get(club=self.club) | ||||
|         cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia) | ||||
| @@ -180,9 +172,7 @@ class MembershipQuerySetTest(ClubTest): | ||||
|  | ||||
| class ClubModelTest(ClubTest): | ||||
|     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() | ||||
|         assert membership is not None | ||||
|         assert localtime(now()).date() == membership.start_date | ||||
| @@ -195,17 +185,14 @@ class ClubModelTest(ClubTest): | ||||
|         assert user.is_in_group(name=board_group) | ||||
|  | ||||
|     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() | ||||
|         assert user.memberships.filter(club=self.club, end_date=today).exists() | ||||
|         assert self.club.get_membership_for(user) is None | ||||
|  | ||||
|     def test_access_unauthorized(self): | ||||
|         """ | ||||
|         Test that users who never subscribed and anonymous users | ||||
|         cannot see the page | ||||
|         """Test that users who never subscribed and anonymous users | ||||
|         cannot see the page. | ||||
|         """ | ||||
|         response = self.client.post(self.members_url) | ||||
|         assert response.status_code == 403 | ||||
| @@ -215,8 +202,7 @@ class ClubModelTest(ClubTest): | ||||
|         assert response.status_code == 403 | ||||
|  | ||||
|     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. | ||||
|         """ | ||||
|         self.client.force_login(self.skia) | ||||
| @@ -251,9 +237,7 @@ class ClubModelTest(ClubTest): | ||||
|         self.assertInHTML(expected_html, response.content.decode()) | ||||
|  | ||||
|     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) | ||||
|         response = self.client.post( | ||||
|             self.members_url, | ||||
| @@ -264,9 +248,7 @@ class ClubModelTest(ClubTest): | ||||
|         self.assert_membership_started_today(self.subscriber, role=3) | ||||
|  | ||||
|     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) | ||||
|         response = self.client.post( | ||||
|             self.members_url, | ||||
| @@ -281,8 +263,7 @@ class ClubModelTest(ClubTest): | ||||
|         self.assert_membership_started_today(self.krophil, role=3) | ||||
|  | ||||
|     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. | ||||
|         """ | ||||
|         self.client.force_login(self.root) | ||||
| @@ -302,9 +283,8 @@ class ClubModelTest(ClubTest): | ||||
|         assert '<ul class="errorlist"><li>' in response.content.decode() | ||||
|  | ||||
|     def test_add_members_already_members(self): | ||||
|         """ | ||||
|         Test that users who are already members of a club | ||||
|         cannot be added again to this club | ||||
|         """Test that users who are already members of a club | ||||
|         cannot be added again to this club. | ||||
|         """ | ||||
|         self.client.force_login(self.root) | ||||
|         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 | ||||
|  | ||||
|     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 | ||||
|         can take place. | ||||
|         """ | ||||
| @@ -349,9 +328,7 @@ class ClubModelTest(ClubTest): | ||||
|         assert self.club.members.count() == nb_memberships | ||||
|  | ||||
|     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 | ||||
|         nb_club_membership = self.club.members.count() | ||||
|         nb_subscriber_memberships = self.subscriber.memberships.count() | ||||
| @@ -368,8 +345,7 @@ class ClubModelTest(ClubTest): | ||||
|         self.assert_membership_started_today(self.subscriber, role=9) | ||||
|  | ||||
|     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. | ||||
|         """ | ||||
|         self.client.force_login(self.skia) | ||||
| @@ -388,9 +364,7 @@ class ClubModelTest(ClubTest): | ||||
|         assert not self.subscriber.memberships.filter(club=self.club).exists() | ||||
|  | ||||
|     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) | ||||
|         response = self.client.post( | ||||
|             self.members_url, | ||||
| @@ -402,9 +376,7 @@ class ClubModelTest(ClubTest): | ||||
|         ) | ||||
|  | ||||
|     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.post( | ||||
|             self.members_url, | ||||
| @@ -414,9 +386,8 @@ class ClubModelTest(ClubTest): | ||||
|         self.assert_membership_ended_today(self.skia) | ||||
|  | ||||
|     def test_end_membership_lower_role(self): | ||||
|         """ | ||||
|         Test that board members of the club can end memberships | ||||
|         of users with lower roles | ||||
|         """Test that board members of the club can end memberships | ||||
|         of users with lower roles. | ||||
|         """ | ||||
|         # remainder : skia has role 3, comptable has role 10, richard has role 1 | ||||
|         self.client.force_login(self.skia) | ||||
| @@ -429,9 +400,8 @@ class ClubModelTest(ClubTest): | ||||
|         self.assert_membership_ended_today(self.richard) | ||||
|  | ||||
|     def test_end_membership_higher_role(self): | ||||
|         """ | ||||
|         Test that board members of the club cannot end memberships | ||||
|         of users with higher roles | ||||
|         """Test that board members of the club cannot end memberships | ||||
|         of users with higher roles. | ||||
|         """ | ||||
|         membership = self.comptable.memberships.filter(club=self.club).first() | ||||
|         self.client.force_login(self.skia) | ||||
| @@ -448,9 +418,8 @@ class ClubModelTest(ClubTest): | ||||
|         assert membership.end_date is None | ||||
|  | ||||
|     def test_end_membership_as_main_club_board(self): | ||||
|         """ | ||||
|         Test that board members of the main club can end the membership | ||||
|         of anyone | ||||
|         """Test that board members of the main club can end the membership | ||||
|         of anyone. | ||||
|         """ | ||||
|         # make subscriber a board member | ||||
|         self.subscriber.memberships.all().delete() | ||||
| @@ -467,9 +436,7 @@ class ClubModelTest(ClubTest): | ||||
|         assert self.club.members.ongoing().count() == nb_memberships - 1 | ||||
|  | ||||
|     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() | ||||
|         self.client.force_login(self.root) | ||||
|         response = self.client.post( | ||||
| @@ -482,9 +449,7 @@ class ClubModelTest(ClubTest): | ||||
|         assert self.club.members.count() == nb_memberships | ||||
|  | ||||
|     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() | ||||
|         membership = self.richard.memberships.filter(club=self.club).first() | ||||
|         self.client.force_login(self.subscriber) | ||||
| @@ -498,9 +463,8 @@ class ClubModelTest(ClubTest): | ||||
|         assert membership == new_mem | ||||
|  | ||||
|     def test_delete_remove_from_meta_group(self): | ||||
|         """ | ||||
|         Test that when a club is deleted, all its members are removed from the | ||||
|         associated metagroup | ||||
|         """Test that when a club is deleted, all its members are removed from the | ||||
|         associated metagroup. | ||||
|         """ | ||||
|         memberships = self.club.members.select_related("user") | ||||
|         users = [membership.user for membership in memberships] | ||||
| @@ -511,9 +475,7 @@ class ClubModelTest(ClubTest): | ||||
|             assert not user.is_in_group(name=meta_group) | ||||
|  | ||||
|     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 | ||||
|         board_members = self.club.unix_name + settings.SITH_BOARD_SUFFIX | ||||
|         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) | ||||
|  | ||||
|     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 | ||||
|         board_members = self.club.unix_name + settings.SITH_BOARD_SUFFIX | ||||
|         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) | ||||
|  | ||||
|     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() | ||||
|         assert not self.club.is_owned_by(anonymous) | ||||
|         assert not self.club.is_owned_by(self.subscriber) | ||||
| @@ -549,7 +507,7 @@ class ClubModelTest(ClubTest): | ||||
|  | ||||
|  | ||||
| class MailingFormTest(TestCase): | ||||
|     """Perform validation tests for MailingForm""" | ||||
|     """Perform validation tests for MailingForm.""" | ||||
|  | ||||
|     @classmethod | ||||
|     def setUpTestData(cls): | ||||
| @@ -865,9 +823,7 @@ class MailingFormTest(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 | ||||
|     def setUpTestData(cls): | ||||
| @@ -875,9 +831,7 @@ class ClubSellingViewTest(TestCase): | ||||
|         cls.skia = User.objects.get(username="skia") | ||||
|  | ||||
|     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) | ||||
|         response = self.client.get( | ||||
|             reverse("club:club_sellings", kwargs={"club_id": self.ae.id}) | ||||
|   | ||||
| @@ -175,18 +175,14 @@ class ClubTabsMixin(TabedViewMixin): | ||||
|  | ||||
|  | ||||
| class ClubListView(ListView): | ||||
|     """ | ||||
|     List the Clubs | ||||
|     """ | ||||
|     """List the Clubs.""" | ||||
|  | ||||
|     model = Club | ||||
|     template_name = "club/club_list.jinja" | ||||
|  | ||||
|  | ||||
| class ClubView(ClubTabsMixin, DetailView): | ||||
|     """ | ||||
|     Front page of a Club | ||||
|     """ | ||||
|     """Front page of a Club.""" | ||||
|  | ||||
|     model = Club | ||||
|     pk_url_kwarg = "club_id" | ||||
| @@ -201,9 +197,7 @@ class ClubView(ClubTabsMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class ClubRevView(ClubView): | ||||
|     """ | ||||
|     Display a specific page revision | ||||
|     """ | ||||
|     """Display a specific page revision.""" | ||||
|  | ||||
|     def dispatch(self, request, *args, **kwargs): | ||||
|         obj = self.get_object() | ||||
| @@ -235,9 +229,7 @@ class ClubPageEditView(ClubTabsMixin, PageEditViewBase): | ||||
|  | ||||
|  | ||||
| class ClubPageHistView(ClubTabsMixin, CanViewMixin, DetailView): | ||||
|     """ | ||||
|     Modification hostory of the page | ||||
|     """ | ||||
|     """Modification hostory of the page.""" | ||||
|  | ||||
|     model = Club | ||||
|     pk_url_kwarg = "club_id" | ||||
| @@ -246,9 +238,7 @@ class ClubPageHistView(ClubTabsMixin, CanViewMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView): | ||||
|     """ | ||||
|     Tools page of a Club | ||||
|     """ | ||||
|     """Tools page of a Club.""" | ||||
|  | ||||
|     model = Club | ||||
|     pk_url_kwarg = "club_id" | ||||
| @@ -257,9 +247,7 @@ class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView): | ||||
|     """ | ||||
|     View of a club's members | ||||
|     """ | ||||
|     """View of a club's members.""" | ||||
|  | ||||
|     model = Club | ||||
|     pk_url_kwarg = "club_id" | ||||
| @@ -280,9 +268,7 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView): | ||||
|         return kwargs | ||||
|  | ||||
|     def form_valid(self, form): | ||||
|         """ | ||||
|         Check user rights | ||||
|         """ | ||||
|         """Check user rights.""" | ||||
|         resp = super().form_valid(form) | ||||
|  | ||||
|         data = form.clean() | ||||
| @@ -307,9 +293,7 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView): | ||||
|  | ||||
|  | ||||
| class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView): | ||||
|     """ | ||||
|     Old members of a club | ||||
|     """ | ||||
|     """Old members of a club.""" | ||||
|  | ||||
|     model = Club | ||||
|     pk_url_kwarg = "club_id" | ||||
| @@ -318,9 +302,7 @@ class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView): | ||||
|     """ | ||||
|     Sellings of a club | ||||
|     """ | ||||
|     """Sellings of a club.""" | ||||
|  | ||||
|     model = Club | ||||
|     pk_url_kwarg = "club_id" | ||||
| @@ -396,12 +378,10 @@ class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView): | ||||
|  | ||||
|  | ||||
| class ClubSellingCSVView(ClubSellingView): | ||||
|     """ | ||||
|     Generate sellings in csv for a given period | ||||
|     """ | ||||
|     """Generate sellings in csv for a given period.""" | ||||
|  | ||||
|     class StreamWriter: | ||||
|         """Implements a file-like interface for streaming the CSV""" | ||||
|         """Implements a file-like interface for streaming the CSV.""" | ||||
|  | ||||
|         def write(self, value): | ||||
|             """Write the value by returning it, instead of storing in a buffer.""" | ||||
| @@ -475,9 +455,7 @@ class ClubSellingCSVView(ClubSellingView): | ||||
|  | ||||
|  | ||||
| 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 | ||||
|     pk_url_kwarg = "club_id" | ||||
| @@ -487,9 +465,7 @@ class ClubEditView(ClubTabsMixin, CanEditMixin, 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 | ||||
|     pk_url_kwarg = "club_id" | ||||
| @@ -499,9 +475,7 @@ class ClubEditPropView(ClubTabsMixin, CanEditPropMixin, UpdateView): | ||||
|  | ||||
|  | ||||
| class ClubCreateView(CanCreateMixin, CreateView): | ||||
|     """ | ||||
|     Create a club (for the Sith admin) | ||||
|     """ | ||||
|     """Create a club (for the Sith admin).""" | ||||
|  | ||||
|     model = Club | ||||
|     pk_url_kwarg = "club_id" | ||||
| @@ -510,9 +484,7 @@ class ClubCreateView(CanCreateMixin, CreateView): | ||||
|  | ||||
|  | ||||
| class MembershipSetOldView(CanEditMixin, DetailView): | ||||
|     """ | ||||
|     Set a membership as beeing old | ||||
|     """ | ||||
|     """Set a membership as beeing old.""" | ||||
|  | ||||
|     model = Membership | ||||
|     pk_url_kwarg = "membership_id" | ||||
| @@ -541,9 +513,7 @@ class MembershipSetOldView(CanEditMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class MembershipDeleteView(UserIsRootMixin, DeleteView): | ||||
|     """ | ||||
|     Delete a membership (for admins only) | ||||
|     """ | ||||
|     """Delete a membership (for admins only).""" | ||||
|  | ||||
|     model = Membership | ||||
|     pk_url_kwarg = "membership_id" | ||||
| @@ -563,9 +533,7 @@ class ClubStatView(TemplateView): | ||||
|  | ||||
|  | ||||
| class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView): | ||||
|     """ | ||||
|     A list of mailing for a given club | ||||
|     """ | ||||
|     """A list of mailing for a given club.""" | ||||
|  | ||||
|     model = Club | ||||
|     form_class = MailingForm | ||||
| @@ -603,9 +571,7 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView): | ||||
|         return kwargs | ||||
|  | ||||
|     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( | ||||
|             club=self.get_object(), | ||||
|             email=cleaned_data["mailing_email"], | ||||
| @@ -620,9 +586,7 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView): | ||||
|         return 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 = [] | ||||
|  | ||||
|         for user in cleaned_data["subscription_users"]: | ||||
| @@ -656,9 +620,7 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView): | ||||
|         return None | ||||
|  | ||||
|     def remove_subscription(self, cleaned_data): | ||||
|         """ | ||||
|         Remove specified users from a mailing list | ||||
|         """ | ||||
|         """Remove specified users from a mailing list.""" | ||||
|         fields = [ | ||||
|             cleaned_data[key] | ||||
|             for key in cleaned_data.keys() | ||||
| @@ -742,7 +704,7 @@ class MailingAutoGenerationView(View): | ||||
|  | ||||
|  | ||||
| class PosterListView(ClubTabsMixin, PosterListBaseView, CanViewMixin): | ||||
|     """List communication posters""" | ||||
|     """List communication posters.""" | ||||
|  | ||||
|     def get_object(self): | ||||
|         return self.club | ||||
| @@ -755,7 +717,7 @@ class PosterListView(ClubTabsMixin, PosterListBaseView, CanViewMixin): | ||||
|  | ||||
|  | ||||
| class PosterCreateView(PosterCreateBaseView, CanCreateMixin): | ||||
|     """Create communication poster""" | ||||
|     """Create communication poster.""" | ||||
|  | ||||
|     pk_url_kwarg = "club_id" | ||||
|  | ||||
| @@ -770,7 +732,7 @@ class PosterCreateView(PosterCreateBaseView, CanCreateMixin): | ||||
|  | ||||
|  | ||||
| class PosterEditView(ClubTabsMixin, PosterEditBaseView, CanEditMixin): | ||||
|     """Edit communication poster""" | ||||
|     """Edit communication poster.""" | ||||
|  | ||||
|     def get_success_url(self): | ||||
|         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): | ||||
|     """Delete communication poster""" | ||||
|     """Delete communication poster.""" | ||||
|  | ||||
|     def get_success_url(self): | ||||
|         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): | ||||
|     """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) | ||||
|     info_msg = models.TextField(_("info message"), default="", blank=True) | ||||
| @@ -64,7 +64,7 @@ NEWS_TYPES = [ | ||||
|  | ||||
|  | ||||
| class News(models.Model): | ||||
|     """The news class""" | ||||
|     """The news class.""" | ||||
|  | ||||
|     title = models.CharField(_("title"), max_length=64) | ||||
|     summary = models.TextField(_("summary")) | ||||
| @@ -143,8 +143,7 @@ def news_notification_callback(notif): | ||||
|  | ||||
|  | ||||
| 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 | ||||
|     we don't have to make copies | ||||
| @@ -164,8 +163,7 @@ class NewsDate(models.Model): | ||||
|  | ||||
|  | ||||
| class Weekmail(models.Model): | ||||
|     """ | ||||
|     The weekmail class | ||||
|     """The weekmail class. | ||||
|  | ||||
|     :ivar title: Title 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}" | ||||
|  | ||||
|     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. | ||||
|         """ | ||||
|         dest = [ | ||||
| @@ -214,33 +212,25 @@ class Weekmail(models.Model): | ||||
|             Weekmail().save() | ||||
|  | ||||
|     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( | ||||
|             None, "com/weekmail_renderer_text.jinja", context={"weekmail": self} | ||||
|         ).content.decode("utf-8") | ||||
|  | ||||
|     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( | ||||
|             None, "com/weekmail_renderer_html.jinja", context={"weekmail": self} | ||||
|         ).content.decode("utf-8") | ||||
|  | ||||
|     def get_banner(self): | ||||
|         """ | ||||
|         Return an absolute link to the banner. | ||||
|         """ | ||||
|         """Return an absolute link to the banner.""" | ||||
|         return ( | ||||
|             "http://" + settings.SITH_URL + static("com/img/weekmail_bannerV2P22.png") | ||||
|         ) | ||||
|  | ||||
|     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") | ||||
|  | ||||
|     def is_owned_by(self, user): | ||||
|   | ||||
							
								
								
									
										28
									
								
								com/tests.py
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								com/tests.py
									
									
									
									
									
								
							| @@ -115,10 +115,7 @@ class ComTest(TestCase): | ||||
|  | ||||
| class SithTest(TestCase): | ||||
|     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() | ||||
|  | ||||
|         com_admin = User.objects.get(username="comunity") | ||||
| @@ -148,20 +145,17 @@ class NewsTest(TestCase): | ||||
|         cls.anonymous = AnonymousUser() | ||||
|  | ||||
|     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.author) | ||||
|         assert not self.new.is_owned_by(self.anonymous) | ||||
|         assert not self.new.is_owned_by(self.sli) | ||||
|  | ||||
|     def test_news_viewer(self): | ||||
|         """ | ||||
|         Test that moderated news can be viewed by anyone | ||||
|         and not moderated news only by com admins | ||||
|         """Test that moderated news can be viewed by anyone | ||||
|         and not moderated news only by com admins. | ||||
|         """ | ||||
|         # by default a news isn't moderated | ||||
|         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) | ||||
|  | ||||
|     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 not self.new.can_be_edited_by(self.sli) | ||||
|         assert not self.new.can_be_edited_by(self.anonymous) | ||||
| @@ -203,9 +195,7 @@ class WeekmailArticleTest(TestCase): | ||||
|         cls.anonymous = AnonymousUser() | ||||
|  | ||||
|     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 not self.article.is_owned_by(self.author) | ||||
|         assert not self.article.is_owned_by(self.anonymous) | ||||
| @@ -229,9 +219,7 @@ class PosterTest(TestCase): | ||||
|         cls.anonymous = AnonymousUser() | ||||
|  | ||||
|     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 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() | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         """Add rendered weekmail""" | ||||
|         """Add rendered weekmail.""" | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
|         kwargs["weekmail_rendered"] = self.object.render_html() | ||||
|         kwargs["bad_recipients"] = self.bad_recipients | ||||
| @@ -507,7 +507,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi | ||||
|         return super().get(request, *args, **kwargs) | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         """Add orphan articles""" | ||||
|         """Add orphan articles.""" | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
|         kwargs["orphans"] = WeekmailArticle.objects.filter(weekmail=None) | ||||
|         return kwargs | ||||
| @@ -516,7 +516,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi | ||||
| class WeekmailArticleEditView( | ||||
|     ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateView | ||||
| ): | ||||
|     """Edit an article""" | ||||
|     """Edit an article.""" | ||||
|  | ||||
|     model = WeekmailArticle | ||||
|     form_class = modelform_factory( | ||||
| @@ -532,7 +532,7 @@ class WeekmailArticleEditView( | ||||
|  | ||||
|  | ||||
| class WeekmailArticleCreateView(QuickNotifMixin, CreateView): | ||||
|     """Post an article""" | ||||
|     """Post an article.""" | ||||
|  | ||||
|     model = WeekmailArticle | ||||
|     form_class = modelform_factory( | ||||
| @@ -574,7 +574,7 @@ class WeekmailArticleCreateView(QuickNotifMixin, CreateView): | ||||
|  | ||||
|  | ||||
| class WeekmailArticleDeleteView(CanEditPropMixin, DeleteView): | ||||
|     """Delete an article""" | ||||
|     """Delete an article.""" | ||||
|  | ||||
|     model = WeekmailArticle | ||||
|     template_name = "core/delete_confirm.jinja" | ||||
| @@ -614,7 +614,7 @@ class MailingModerateView(View): | ||||
|  | ||||
|  | ||||
| class PosterListBaseView(ListView): | ||||
|     """List communication posters""" | ||||
|     """List communication posters.""" | ||||
|  | ||||
|     current_tab = "posters" | ||||
|     model = Poster | ||||
| @@ -641,7 +641,7 @@ class PosterListBaseView(ListView): | ||||
|  | ||||
|  | ||||
| class PosterCreateBaseView(CreateView): | ||||
|     """Create communication poster""" | ||||
|     """Create communication poster.""" | ||||
|  | ||||
|     current_tab = "posters" | ||||
|     form_class = PosterForm | ||||
| @@ -673,7 +673,7 @@ class PosterCreateBaseView(CreateView): | ||||
|  | ||||
|  | ||||
| class PosterEditBaseView(UpdateView): | ||||
|     """Edit communication poster""" | ||||
|     """Edit communication poster.""" | ||||
|  | ||||
|     pk_url_kwarg = "poster_id" | ||||
|     current_tab = "posters" | ||||
| @@ -721,7 +721,7 @@ class PosterEditBaseView(UpdateView): | ||||
|  | ||||
|  | ||||
| class PosterDeleteBaseView(DeleteView): | ||||
|     """Edit communication poster""" | ||||
|     """Edit communication poster.""" | ||||
|  | ||||
|     pk_url_kwarg = "poster_id" | ||||
|     current_tab = "posters" | ||||
| @@ -738,7 +738,7 @@ class PosterDeleteBaseView(DeleteView): | ||||
|  | ||||
|  | ||||
| class PosterListView(IsComAdminMixin, ComTabsMixin, PosterListBaseView): | ||||
|     """List communication posters""" | ||||
|     """List communication posters.""" | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
| @@ -747,7 +747,7 @@ class PosterListView(IsComAdminMixin, ComTabsMixin, PosterListBaseView): | ||||
|  | ||||
|  | ||||
| class PosterCreateView(IsComAdminMixin, ComTabsMixin, PosterCreateBaseView): | ||||
|     """Create communication poster""" | ||||
|     """Create communication poster.""" | ||||
|  | ||||
|     success_url = reverse_lazy("com:poster_list") | ||||
|  | ||||
| @@ -758,7 +758,7 @@ class PosterCreateView(IsComAdminMixin, ComTabsMixin, PosterCreateBaseView): | ||||
|  | ||||
|  | ||||
| class PosterEditView(IsComAdminMixin, ComTabsMixin, PosterEditBaseView): | ||||
|     """Edit communication poster""" | ||||
|     """Edit communication poster.""" | ||||
|  | ||||
|     success_url = reverse_lazy("com:poster_list") | ||||
|  | ||||
| @@ -769,13 +769,13 @@ class PosterEditView(IsComAdminMixin, ComTabsMixin, PosterEditBaseView): | ||||
|  | ||||
|  | ||||
| class PosterDeleteView(IsComAdminMixin, ComTabsMixin, PosterDeleteBaseView): | ||||
|     """Delete communication poster""" | ||||
|     """Delete communication poster.""" | ||||
|  | ||||
|     success_url = reverse_lazy("com:poster_list") | ||||
|  | ||||
|  | ||||
| class PosterModerateListView(IsComAdminMixin, ComTabsMixin, ListView): | ||||
|     """Moderate list communication poster""" | ||||
|     """Moderate list communication poster.""" | ||||
|  | ||||
|     current_tab = "posters" | ||||
|     model = Poster | ||||
| @@ -789,7 +789,7 @@ class PosterModerateListView(IsComAdminMixin, ComTabsMixin, ListView): | ||||
|  | ||||
|  | ||||
| class PosterModerateView(IsComAdminMixin, ComTabsMixin, View): | ||||
|     """Moderate communication poster""" | ||||
|     """Moderate communication poster.""" | ||||
|  | ||||
|     def get(self, request, *args, **kwargs): | ||||
|         obj = get_object_or_404(Poster, pk=kwargs["object_id"]) | ||||
| @@ -807,7 +807,7 @@ class PosterModerateView(IsComAdminMixin, ComTabsMixin, View): | ||||
|  | ||||
|  | ||||
| class ScreenListView(IsComAdminMixin, ComTabsMixin, ListView): | ||||
|     """List communication screens""" | ||||
|     """List communication screens.""" | ||||
|  | ||||
|     current_tab = "screens" | ||||
|     model = Screen | ||||
| @@ -815,7 +815,7 @@ class ScreenListView(IsComAdminMixin, ComTabsMixin, ListView): | ||||
|  | ||||
|  | ||||
| class ScreenSlideshowView(DetailView): | ||||
|     """Slideshow of actives posters""" | ||||
|     """Slideshow of actives posters.""" | ||||
|  | ||||
|     pk_url_kwarg = "screen_id" | ||||
|     model = Screen | ||||
| @@ -828,7 +828,7 @@ class ScreenSlideshowView(DetailView): | ||||
|  | ||||
|  | ||||
| class ScreenCreateView(IsComAdminMixin, ComTabsMixin, CreateView): | ||||
|     """Create communication screen""" | ||||
|     """Create communication screen.""" | ||||
|  | ||||
|     current_tab = "screens" | ||||
|     model = Screen | ||||
| @@ -838,7 +838,7 @@ class ScreenCreateView(IsComAdminMixin, ComTabsMixin, CreateView): | ||||
|  | ||||
|  | ||||
| class ScreenEditView(IsComAdminMixin, ComTabsMixin, UpdateView): | ||||
|     """Edit communication screen""" | ||||
|     """Edit communication screen.""" | ||||
|  | ||||
|     pk_url_kwarg = "screen_id" | ||||
|     current_tab = "screens" | ||||
| @@ -849,7 +849,7 @@ class ScreenEditView(IsComAdminMixin, ComTabsMixin, UpdateView): | ||||
|  | ||||
|  | ||||
| class ScreenDeleteView(IsComAdminMixin, ComTabsMixin, DeleteView): | ||||
|     """Delete communication screen""" | ||||
|     """Delete communication screen.""" | ||||
|  | ||||
|     pk_url_kwarg = "screen_id" | ||||
|     current_tab = "screens" | ||||
|   | ||||
| @@ -19,9 +19,7 @@ class TwoDigitMonthConverter: | ||||
|  | ||||
|  | ||||
| class BooleanStringConverter: | ||||
|     """ | ||||
|     Converter whose regex match either True or False | ||||
|     """ | ||||
|     """Converter whose regex match either True or False.""" | ||||
|  | ||||
|     regex = r"(True)|(False)" | ||||
|  | ||||
|   | ||||
| @@ -90,12 +90,15 @@ def list_tags(s): | ||||
|             yield parts[1][len(tag_prefix) :] | ||||
|  | ||||
|  | ||||
| def parse_semver(s): | ||||
|     """ | ||||
|     Turns a semver string into a 3-tuple or None if the parsing failed, it is a | ||||
|     prerelease or it has build metadata. | ||||
| def parse_semver(s) -> tuple[int, int, int] | None: | ||||
|     """Parse a semver string. | ||||
|  | ||||
|     See https://semver.org | ||||
|  | ||||
|     Returns: | ||||
|         A tuple, if the parsing was successful, else None. | ||||
|         In the latter case, it must probably be a prerelease | ||||
|         or include build metadata. | ||||
|     """ | ||||
|     m = semver_regex.match(s) | ||||
|  | ||||
| @@ -106,7 +109,7 @@ def parse_semver(s): | ||||
|     ): | ||||
|         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): | ||||
|   | ||||
| @@ -29,9 +29,7 @@ from django.core.management.commands import compilemessages | ||||
|  | ||||
|  | ||||
| class Command(compilemessages.Command): | ||||
|     """ | ||||
|     Wrap call to compilemessages to avoid building whole env | ||||
|     """ | ||||
|     """Wrap call to compilemessages to avoid building whole env.""" | ||||
|  | ||||
|     help = """ | ||||
|         The usage is the same as the real compilemessages | ||||
|   | ||||
| @@ -30,9 +30,7 @@ from django.core.management.base import 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" | ||||
|  | ||||
|   | ||||
| @@ -53,13 +53,14 @@ _threadlocal = threading.local() | ||||
|  | ||||
|  | ||||
| def get_signal_request(): | ||||
|     """ | ||||
|     !!! 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 | ||||
|     """ | ||||
|     """Allow to access current request in signals. | ||||
|  | ||||
|     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) | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										224
									
								
								core/models.py
									
									
									
									
									
								
							
							
						
						
									
										224
									
								
								core/models.py
									
									
									
									
									
								
							| @@ -21,11 +21,13 @@ | ||||
| # Place - Suite 330, Boston, MA 02111-1307, USA. | ||||
| # | ||||
| # | ||||
| from __future__ import annotations | ||||
|  | ||||
| import importlib | ||||
| import os | ||||
| import unicodedata | ||||
| from datetime import date, timedelta | ||||
| from typing import List, Optional, Union | ||||
| from typing import TYPE_CHECKING, Optional | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.contrib.auth.models import ( | ||||
| @@ -56,6 +58,9 @@ from phonenumber_field.modelfields import PhoneNumberField | ||||
|  | ||||
| from core import utils | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from club.models import Club | ||||
|  | ||||
|  | ||||
| class RealGroupManager(AuthGroupManager): | ||||
|     def get_queryset(self): | ||||
| @@ -68,8 +73,7 @@ class MetaGroupManager(AuthGroupManager): | ||||
|  | ||||
|  | ||||
| class Group(AuthGroup): | ||||
|     """ | ||||
|     Implement both RealGroups and Meta groups | ||||
|     """Implement both RealGroups and Meta groups. | ||||
|  | ||||
|     Groups are sorted by their is_meta property | ||||
|     """ | ||||
| @@ -87,9 +91,6 @@ class Group(AuthGroup): | ||||
|         ordering = ["name"] | ||||
|  | ||||
|     def get_absolute_url(self): | ||||
|         """ | ||||
|         This is needed for black magic powered UpdateView's children | ||||
|         """ | ||||
|         return reverse("core:group_list") | ||||
|  | ||||
|     def save(self, *args, **kwargs): | ||||
| @@ -104,8 +105,8 @@ class Group(AuthGroup): | ||||
|  | ||||
|  | ||||
| class MetaGroup(Group): | ||||
|     """ | ||||
|     MetaGroups are dynamically created groups. | ||||
|     """MetaGroups are dynamically created groups. | ||||
|  | ||||
|     Generally used with clubs where creating a club creates two groups: | ||||
|  | ||||
|     * club-SITH_BOARD_SUFFIX | ||||
| @@ -123,14 +124,14 @@ class MetaGroup(Group): | ||||
|         self.is_meta = True | ||||
|  | ||||
|     @cached_property | ||||
|     def associated_club(self): | ||||
|         """ | ||||
|         Return the group associated with this meta group | ||||
|     def associated_club(self) -> Club | None: | ||||
|         """Return the group associated with this meta group. | ||||
|  | ||||
|         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 | ||||
|  | ||||
| @@ -150,8 +151,8 @@ class MetaGroup(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. | ||||
|     """ | ||||
|  | ||||
| @@ -173,22 +174,26 @@ def validate_promo(value): | ||||
|  | ||||
|  | ||||
| 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. | ||||
|  | ||||
|     The result is cached for the default duration (should be 5 minutes). | ||||
|  | ||||
|     :param pk: The primary key of the group | ||||
|     :param name: The name of the group | ||||
|     :return: The group if it exists, else None | ||||
|     :raise ValueError: If no group matches the criteria | ||||
|     Args: | ||||
|         pk: The primary key of the group | ||||
|         name: The name of the group | ||||
|  | ||||
|     Returns: | ||||
|         The group if it exists, else None | ||||
|  | ||||
|     Raises: | ||||
|         ValueError: If no group matches the criteria | ||||
|     """ | ||||
|     if pk is None and name is None: | ||||
|         raise ValueError("Either pk or name must be set") | ||||
|  | ||||
|     # 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}") | ||||
|  | ||||
|     if group == "not_found": | ||||
| @@ -211,8 +216,7 @@ def get_group(*, pk: int = None, name: str = None) -> Optional[Group]: | ||||
|  | ||||
|  | ||||
| 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, | ||||
|     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 | ||||
|  | ||||
|     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}) | ||||
|  | ||||
|     def __str__(self): | ||||
| @@ -412,8 +413,7 @@ class User(AbstractBaseUser): | ||||
|             return 0 | ||||
|  | ||||
|     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. | ||||
|         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 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: | ||||
|             group: Optional[Group] = get_group(pk=pk) | ||||
| @@ -454,11 +455,12 @@ class User(AbstractBaseUser): | ||||
|         return group in self.cached_groups | ||||
|  | ||||
|     @property | ||||
|     def cached_groups(self) -> List[Group]: | ||||
|         """ | ||||
|         Get the list of groups this user is in. | ||||
|     def cached_groups(self) -> list[Group]: | ||||
|         """Get the list of groups this user is in. | ||||
|  | ||||
|         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") | ||||
|         if groups is None: | ||||
| @@ -523,9 +525,8 @@ class User(AbstractBaseUser): | ||||
|  | ||||
|     @cached_property | ||||
|     def age(self) -> int: | ||||
|         """ | ||||
|         Return the age this user has the day the method is called. | ||||
|         If the user has not filled his age, return 0 | ||||
|         """Return the age this user has the day the method is called. | ||||
|         If the user has not filled his age, return 0. | ||||
|         """ | ||||
|         if self.date_of_birth is None: | ||||
|             return 0 | ||||
| @@ -576,31 +577,27 @@ class User(AbstractBaseUser): | ||||
|         } | ||||
|  | ||||
|     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) | ||||
|         return full_name.strip() | ||||
|  | ||||
|     def get_short_name(self): | ||||
|         "Returns the short name for the user." | ||||
|         """Returns the short name for the user.""" | ||||
|         if self.nick_name: | ||||
|             return self.nick_name | ||||
|         return self.first_name + " " + self.last_name | ||||
|  | ||||
|     def get_display_name(self): | ||||
|         """ | ||||
|         Returns the display name of the user. | ||||
|         A nickname if possible, otherwise, the full name | ||||
|     def get_display_name(self) -> str: | ||||
|         """Returns the display name of the user. | ||||
|  | ||||
|         A nickname if possible, otherwise, the full name. | ||||
|         """ | ||||
|         if self.nick_name: | ||||
|             return "%s (%s)" % (self.get_full_name(), self.nick_name) | ||||
|         return self.get_full_name() | ||||
|  | ||||
|     def get_age(self): | ||||
|         """ | ||||
|         Returns the age | ||||
|         """ | ||||
|         """Returns the age.""" | ||||
|         today = timezone.now() | ||||
|         born = self.date_of_birth | ||||
|         return ( | ||||
| @@ -608,18 +605,18 @@ class User(AbstractBaseUser): | ||||
|         ) | ||||
|  | ||||
|     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: | ||||
|             from_email = settings.DEFAULT_FROM_EMAIL | ||||
|         send_mail(subject, message, from_email, [self.email], **kwargs) | ||||
|  | ||||
|     def generate_username(self): | ||||
|         """ | ||||
|         Generates a unique username based on the first and last names. | ||||
|         For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists | ||||
|         Returns the generated username | ||||
|     def generate_username(self) -> str: | ||||
|         """Generates a unique username based on the first and last names. | ||||
|  | ||||
|         For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists. | ||||
|  | ||||
|         Returns: | ||||
|             The generated username. | ||||
|         """ | ||||
|  | ||||
|         def remove_accents(data): | ||||
| @@ -644,9 +641,7 @@ class User(AbstractBaseUser): | ||||
|         return user_name | ||||
|  | ||||
|     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): | ||||
|             return True | ||||
|         if hasattr(obj, "owner_group") and self.is_in_group(pk=obj.owner_group.id): | ||||
| @@ -656,9 +651,7 @@ class User(AbstractBaseUser): | ||||
|         return False | ||||
|  | ||||
|     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): | ||||
|             return True | ||||
|         if hasattr(obj, "edit_groups"): | ||||
| @@ -672,9 +665,7 @@ class User(AbstractBaseUser): | ||||
|         return False | ||||
|  | ||||
|     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): | ||||
|             return True | ||||
|         if hasattr(obj, "view_groups"): | ||||
| @@ -730,11 +721,8 @@ class User(AbstractBaseUser): | ||||
|             return infos | ||||
|  | ||||
|     @cached_property | ||||
|     def clubs_with_rights(self): | ||||
|         """ | ||||
|         :return: the list of clubs where the user has rights | ||||
|         :rtype: list[club.models.Club] | ||||
|         """ | ||||
|     def clubs_with_rights(self) -> list[Club]: | ||||
|         """The list of clubs where the user has rights""" | ||||
|         memberships = self.memberships.ongoing().board().select_related("club") | ||||
|         return [m.club for m in memberships] | ||||
|  | ||||
| @@ -796,9 +784,7 @@ class AnonymousUser(AuthAnonymousUser): | ||||
|         raise PermissionDenied | ||||
|  | ||||
|     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 | ||||
|         if pk is not None: | ||||
|             return pk == allowed_id | ||||
| @@ -957,16 +943,15 @@ class SithFile(models.Model): | ||||
|                 ).save() | ||||
|  | ||||
|     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: | ||||
|             - 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 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 | ||||
|         profiles_dir = SithFile.objects.filter(name="profiles").first() | ||||
|         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() | ||||
|  | ||||
|     def clean(self): | ||||
|         """ | ||||
|         Cleans up the file | ||||
|         """ | ||||
|         """Cleans up the file.""" | ||||
|         super().clean() | ||||
|         if "/" in self.name: | ||||
|             raise ValidationError(_("Character '/' not authorized in name")) | ||||
| @@ -1070,15 +1053,14 @@ class SithFile(models.Model): | ||||
|             c.apply_rights_recursively(only_folders=only_folders) | ||||
|  | ||||
|     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: | ||||
|             self.edit_groups.set(self.parent.edit_groups.all()) | ||||
|             self.view_groups.set(self.parent.view_groups.all()) | ||||
|             self.save() | ||||
|  | ||||
|     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 | ||||
|         anything. | ||||
|         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() | ||||
|  | ||||
|     def _repair_fs(self): | ||||
|         """ | ||||
|         This function rebuilds recursively the filesystem as it should be | ||||
|         regarding the DB tree. | ||||
|         """ | ||||
|         """Rebuilds recursively the filesystem as it should be regarding the DB tree.""" | ||||
|         if self.is_folder: | ||||
|             for c in self.children.all(): | ||||
|                 c._repair_fs() | ||||
| @@ -1197,19 +1176,19 @@ class SithFile(models.Model): | ||||
|  | ||||
|  | ||||
| class LockError(Exception): | ||||
|     """There was a lock error on the object""" | ||||
|     """There was a lock error on the object.""" | ||||
|  | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class AlreadyLocked(LockError): | ||||
|     """The object is already locked""" | ||||
|     """The object is already locked.""" | ||||
|  | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class NotLocked(LockError): | ||||
|     """The object is not locked""" | ||||
|     """The object is not locked.""" | ||||
|  | ||||
|     pass | ||||
|  | ||||
| @@ -1220,12 +1199,11 @@ def get_default_owner_group(): | ||||
|  | ||||
|  | ||||
| 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> | ||||
|     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! | ||||
|     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 | ||||
|     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() | ||||
|  | ||||
|     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) | ||||
|         if not locked: | ||||
|             locked = self.is_locked() | ||||
| @@ -1317,22 +1293,15 @@ class Page(models.Model): | ||||
|         self.unset_lock() | ||||
|  | ||||
|     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}) | ||||
|  | ||||
|     @staticmethod | ||||
|     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() | ||||
|  | ||||
|     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: | ||||
|             self.name = self.name.split("/")[-1] | ||||
|         if ( | ||||
| @@ -1367,10 +1336,11 @@ class Page(models.Model): | ||||
|         return l | ||||
|  | ||||
|     def is_locked(self): | ||||
|         """ | ||||
|         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 | ||||
|         function will return False | ||||
|         """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 | ||||
|         function will return False. | ||||
|         """ | ||||
|         if self.lock_timeout and ( | ||||
|             timezone.now() - self.lock_timeout > timedelta(minutes=5) | ||||
| @@ -1384,9 +1354,7 @@ class Page(models.Model): | ||||
|         ) | ||||
|  | ||||
|     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: | ||||
|             raise AlreadyLocked("The page is already locked by someone else") | ||||
|         self.lock_user = user | ||||
| @@ -1395,41 +1363,34 @@ class Page(models.Model): | ||||
|         # print("Locking page") | ||||
|  | ||||
|     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(): | ||||
|             p.set_lock_recursive(user) | ||||
|         self.set_lock(user) | ||||
|  | ||||
|     def unset_lock_recursive(self): | ||||
|         """ | ||||
|         Unlocks recursively all the child pages | ||||
|         """ | ||||
|         """Unlocks recursively all the child pages.""" | ||||
|         for p in self.children.all(): | ||||
|             p.unset_lock_recursive() | ||||
|         self.unset_lock() | ||||
|  | ||||
|     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_timeout = None | ||||
|         super().save() | ||||
|         # print("Unlocking page") | ||||
|  | ||||
|     def get_lock(self): | ||||
|         """ | ||||
|         Returns the page's mutex containing the time and the user in a dict | ||||
|         """ | ||||
|         """Returns the page's mutex containing the time and the user in a dict.""" | ||||
|         if self.lock_user: | ||||
|             return self.lock_user | ||||
|         raise NotLocked("The page is not locked and thus can not return its user") | ||||
|  | ||||
|     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 | ||||
|         (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: | ||||
|             return self.name | ||||
| @@ -1463,8 +1424,8 @@ class Page(models.Model): | ||||
|  | ||||
|  | ||||
| class PageRev(models.Model): | ||||
|     """ | ||||
|     This is the true content of the page. | ||||
|     """True content of the page. | ||||
|  | ||||
|     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, | ||||
|     is the real content of the page. | ||||
| @@ -1492,9 +1453,6 @@ class PageRev(models.Model): | ||||
|         self.page.unset_lock() | ||||
|  | ||||
|     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}) | ||||
|  | ||||
|     def __getattribute__(self, attr): | ||||
| @@ -1573,9 +1531,7 @@ class Gift(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) | ||||
|     label = models.CharField(_("label"), max_length=255) | ||||
|   | ||||
| @@ -21,24 +21,26 @@ | ||||
| # | ||||
| # | ||||
|  | ||||
| """ | ||||
| This page is useful for custom migration tricks. | ||||
| Sometimes, when you need to have a migration hack and you think it can be | ||||
| useful again, put it there, we never know if we might need the hack again. | ||||
| """Collection of utils for custom migration tricks. | ||||
|  | ||||
| Sometimes, when you need to have a migration hack, | ||||
|  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 | ||||
|  | ||||
|  | ||||
| class PsqlRunOnly(migrations.RunSQL): | ||||
|     """ | ||||
|     This is an SQL runner that will launch the given command only if | ||||
|     the used DBMS is PostgreSQL. | ||||
|     """SQL runner for PostgreSQL-only queries. | ||||
|  | ||||
|     It may be useful to run Postgres' specific SQL, or to take actions | ||||
|     that would be non-senses with backends other than Postgre, such | ||||
|     as disabling particular constraints that would prevent the migration | ||||
|     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. | ||||
|     Some explanations can be found here too: | ||||
|     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): | ||||
|     """ | ||||
|     Find static *.css files compiled on the fly | ||||
|     """ | ||||
|     """Find static *.css files compiled on the fly.""" | ||||
|  | ||||
|     locations = [] | ||||
|  | ||||
|   | ||||
| @@ -35,10 +35,9 @@ from core.scss.storage import ScssFileStorage, find_file | ||||
|  | ||||
|  | ||||
| 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 | ||||
|     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/")) | ||||
|   | ||||
| @@ -91,9 +91,9 @@ class IndexSignalProcessor(signals.BaseSignalProcessor): | ||||
|  | ||||
|  | ||||
| class BigCharFieldIndex(indexes.CharField): | ||||
|     """ | ||||
|     Workaround to avoid xapian.InvalidArgument: Term too long (> 245) | ||||
|     See https://groups.google.com/forum/#!topic/django-haystack/hRJKcPNPXqw/discussion | ||||
|     """Workaround to avoid xapian.InvalidArgument: Term too long (> 245). | ||||
|  | ||||
|     See https://groups.google.com/forum/#!topic/django-haystack/hRJKcPNPXqw/discussion. | ||||
|     """ | ||||
|  | ||||
|     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") | ||||
| 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 | ||||
|     # but rather on an intermediary table, there is no | ||||
|     # model method to override, meaning we must use | ||||
|   | ||||
| @@ -30,11 +30,11 @@ from jinja2.parser import Parser | ||||
|  | ||||
|  | ||||
| class HoneypotExtension(Extension): | ||||
|     """ | ||||
|     Wrapper around the honeypot extension tag | ||||
|     Known limitation: doesn't support arguments | ||||
|     """Wrapper around the honeypot extension tag. | ||||
|  | ||||
|     Usage: {% render_honeypot_field %} | ||||
|     Known limitation: doesn't support arguments. | ||||
|  | ||||
|     Usage: `{% render_honeypot_field %}` | ||||
|     """ | ||||
|  | ||||
|     tags = {"render_honeypot_field"} | ||||
|   | ||||
| @@ -46,9 +46,7 @@ def markdown(text): | ||||
| def phonenumber( | ||||
|     value, country="FR", number_format=phonenumbers.PhoneNumberFormat.NATIONAL | ||||
| ): | ||||
|     """ | ||||
|     This filter is kindly borrowed from https://github.com/foundertherapy/django-phonenumber-filter | ||||
|     """ | ||||
|     # collectivised from https://github.com/foundertherapy/django-phonenumber-filter. | ||||
|     value = str(value) | ||||
|     try: | ||||
|         parsed = phonenumbers.parse(value, country) | ||||
| @@ -59,6 +57,12 @@ def phonenumber( | ||||
|  | ||||
| @register.filter(name="truncate_time") | ||||
| 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) | ||||
|     return { | ||||
|         "millis": lambda: value.split(".")[0], | ||||
| @@ -81,8 +85,6 @@ def format_timedelta(value: datetime.timedelta) -> str: | ||||
|  | ||||
| @register.simple_tag() | ||||
| def scss(path): | ||||
|     """ | ||||
|     Return path of the corresponding css file after compilation | ||||
|     """ | ||||
|     """Return path of the corresponding css file after compilation.""" | ||||
|     processor = ScssProcessor(path) | ||||
|     return processor.get_converted_scss() | ||||
|   | ||||
| @@ -105,7 +105,7 @@ class TestUserRegistration: | ||||
|     def test_register_fail_with_not_existing_email( | ||||
|         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): | ||||
|             raise SMTPException | ||||
| @@ -127,10 +127,7 @@ class TestUserLogin: | ||||
|         return User.objects.first() | ||||
|  | ||||
|     def test_login_fail(self, client, user): | ||||
|         """ | ||||
|         Should not login a user correctly | ||||
|         """ | ||||
|  | ||||
|         """Should not login a user correctly.""" | ||||
|         response = client.post( | ||||
|             reverse("core:login"), | ||||
|             { | ||||
| @@ -158,9 +155,7 @@ class TestUserLogin: | ||||
|         assert response.wsgi_request.user.is_anonymous | ||||
|  | ||||
|     def test_login_success(self, client, user): | ||||
|         """ | ||||
|         Should login a user correctly | ||||
|         """ | ||||
|         """Should login a user correctly.""" | ||||
|         response = client.post( | ||||
|             reverse("core:login"), | ||||
|             { | ||||
| @@ -210,7 +205,7 @@ class TestUserLogin: | ||||
|     ], | ||||
| ) | ||||
| 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" | ||||
|  | ||||
|  | ||||
| @@ -233,7 +228,6 @@ class PageHandlingTest(TestCase): | ||||
|  | ||||
|     def test_create_page_ok(self): | ||||
|         """Should create a page correctly.""" | ||||
|  | ||||
|         response = self.client.post( | ||||
|             reverse("core:page_new"), | ||||
|             {"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) | ||||
|  | ||||
|     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.save(force_lock=True) | ||||
|         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) | ||||
|  | ||||
|     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"})) | ||||
|         assert response.status_code == 200 | ||||
|         html = response.content.decode() | ||||
|         self.assertIn('<a href="/page/create/?page=swagg">', html) | ||||
|  | ||||
|     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( | ||||
|             reverse("core:page_new"), {"parent": "", "name": "guy", "owner_group": "1"} | ||||
|         ) | ||||
| @@ -335,13 +323,13 @@ http://git.an | ||||
|  | ||||
| class UserToolsTest: | ||||
|     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")) | ||||
|         assert response.status_code == 403 | ||||
|  | ||||
|     @pytest.mark.parametrize("username", ["guy", "root", "skia", "comunity"]) | ||||
|     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 | ||||
|         client.force_login(User.objects.get(username=username)) | ||||
|         response = client.get(reverse("core:user_tools")) | ||||
| @@ -391,9 +379,8 @@ class FileHandlingTest(TestCase): | ||||
|  | ||||
|  | ||||
| class UserIsInGroupTest(TestCase): | ||||
|     """ | ||||
|     Test that the User.is_in_group() and AnonymousUser.is_in_group() | ||||
|     work as intended | ||||
|     """Test that the User.is_in_group() and AnonymousUser.is_in_group() | ||||
|     work as intended. | ||||
|     """ | ||||
|  | ||||
|     @classmethod | ||||
| @@ -450,30 +437,24 @@ class UserIsInGroupTest(TestCase): | ||||
|         assert user.is_in_group(name=meta_groups_members) is False | ||||
|  | ||||
|     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() | ||||
|         self.assert_only_in_public_group(user) | ||||
|  | ||||
|     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) | ||||
|  | ||||
|     def test_wrong_parameter_fail(self): | ||||
|         """ | ||||
|         Test that when neither the pk nor the name argument is given, | ||||
|         the function raises a ValueError | ||||
|         """Test that when neither the pk nor the name argument is given, | ||||
|         the function raises a ValueError. | ||||
|         """ | ||||
|         with self.assertRaises(ValueError): | ||||
|             self.toto.is_in_group() | ||||
|  | ||||
|     def test_number_queries(self): | ||||
|         """ | ||||
|         Test that the number of db queries is stable | ||||
|         and that less queries are made when making a new call | ||||
|         """Test that the number of db queries is stable | ||||
|         and that less queries are made when making a new call. | ||||
|         """ | ||||
|         # make sure Skia is in at least one group | ||||
|         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) | ||||
|  | ||||
|     def test_cache_properly_cleared_membership(self): | ||||
|         """ | ||||
|         Test that when the membership of a user end, | ||||
|         the cache is properly invalidated | ||||
|         """Test that when the membership of a user end, | ||||
|         the cache is properly invalidated. | ||||
|         """ | ||||
|         membership = Membership.objects.create( | ||||
|             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 | ||||
|  | ||||
|     def test_cache_properly_cleared_group(self): | ||||
|         """ | ||||
|         Test that when a user is removed from a group, | ||||
|         the is_in_group_method return False when calling it again | ||||
|         """Test that when a user is removed from a group, | ||||
|         the is_in_group_method return False when calling it again. | ||||
|         """ | ||||
|         # testing with 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 | ||||
|  | ||||
|     def test_not_existing_group(self): | ||||
|         """ | ||||
|         Test that searching for a not existing group | ||||
|         returns False | ||||
|         """Test that searching for a not existing group | ||||
|         returns 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) | ||||
|  | ||||
|     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_september) == "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" | ||||
|  | ||||
|     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) | ||||
|         assert get_start_of_semester(self.autumn_semester_january) == 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 | ||||
|  | ||||
|     def test_get_start_of_semester_today(self): | ||||
|         """ | ||||
|         Test that the get_start_of_semester returns the start of the current semester | ||||
|         when no date is given | ||||
|         """Test that the get_start_of_semester returns the start of the current semester | ||||
|         when no date is given. | ||||
|         """ | ||||
|         with freezegun.freeze_time(self.autumn_semester_september): | ||||
|             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 | ||||
|  | ||||
|     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. | ||||
|         """ | ||||
|         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: | ||||
|     """ | ||||
|     Return the short hash of the current commit | ||||
|     """ | ||||
|     """Return the short hash of the current commit.""" | ||||
|     try: | ||||
|         output = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]) | ||||
|         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: | ||||
|     """ | ||||
|     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. | ||||
|  | ||||
|     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 15/02 and 15/08  => Spring semester | ||||
|  | ||||
|     :param today: the date to use to compute the semester. If None, use today's date. | ||||
|     :return: the date of the start of the semester | ||||
|     Args: | ||||
|         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: | ||||
|         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: | ||||
|     """ | ||||
|     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. | ||||
|  | ||||
|     The semester code is an upper letter (A for autumn, P for spring), | ||||
|     followed by the last two digits of the year. | ||||
|     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. | ||||
|     :return: the semester code corresponding to the given date | ||||
|     Args: | ||||
|         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: | ||||
|         d = timezone.now().date() | ||||
| @@ -147,8 +149,15 @@ def exif_auto_rotate(image): | ||||
|     return image | ||||
|  | ||||
|  | ||||
| def doku_to_markdown(text): | ||||
|     """This is a quite correct doku translator""" | ||||
| def doku_to_markdown(text: str) -> str: | ||||
|     """Convert doku text to the corresponding markdown. | ||||
|  | ||||
|     Args: | ||||
|         text: the doku text to convert | ||||
|  | ||||
|     Returns: | ||||
|         The converted markdown text | ||||
|     """ | ||||
|     text = re.sub( | ||||
|         r"([^:]|^)\/\/(.*?)\/\/", r"*\2*", text | ||||
|     )  # Italic (prevents protocol:// conflict) | ||||
| @@ -235,7 +244,14 @@ def doku_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"\[i\](.*?)\[\/i\]", r"*\1*", text, flags=re.DOTALL)  # Italic | ||||
|     text = re.sub(r"\[u\](.*?)\[\/u\]", r"__\1__", text, flags=re.DOTALL)  # Underline | ||||
|   | ||||
| @@ -23,6 +23,7 @@ | ||||
| # | ||||
|  | ||||
| import types | ||||
| from typing import Any | ||||
|  | ||||
| from django.core.exceptions import ( | ||||
|     ImproperlyConfigured, | ||||
| @@ -39,6 +40,7 @@ from django.views.generic.detail import SingleObjectMixin | ||||
| from django.views.generic.edit import FormView | ||||
| from sentry_sdk import last_event_id | ||||
|  | ||||
| from core.models import User | ||||
| from core.views.forms import LoginForm | ||||
|  | ||||
|  | ||||
| @@ -60,60 +62,63 @@ def internal_servor_error(request): | ||||
|     return HttpResponseServerError(render(request, "core/500.jinja")) | ||||
|  | ||||
|  | ||||
| def can_edit_prop(obj, user): | ||||
|     """ | ||||
|     :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 | ||||
| def can_edit_prop(obj: Any, user: User) -> bool: | ||||
|     """Can the user edit the properties of the object. | ||||
|  | ||||
|     :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): | ||||
|             raise PermissionDenied | ||||
|  | ||||
|         ``` | ||||
|     """ | ||||
|     if obj is None or user.is_owner(obj): | ||||
|         return True | ||||
|     return False | ||||
|  | ||||
|  | ||||
| def can_edit(obj, user): | ||||
|     """ | ||||
|     :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 | ||||
| def can_edit(obj: Any, user: User): | ||||
|     """Can the user edit the object. | ||||
|  | ||||
|     :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 | ||||
|  | ||||
|         ``` | ||||
|     """ | ||||
|     if obj is None or user.can_edit(obj): | ||||
|         return True | ||||
|     return can_edit_prop(obj, user) | ||||
|  | ||||
|  | ||||
| def can_view(obj, user): | ||||
|     """ | ||||
|     :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 | ||||
| def can_view(obj: Any, user: User): | ||||
|     """Can the user see the object. | ||||
|  | ||||
|     :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): | ||||
|             raise PermissionDenied | ||||
|  | ||||
|         ``` | ||||
|     """ | ||||
|     if obj is None or user.can_view(obj): | ||||
|         return True | ||||
| @@ -121,20 +126,22 @@ def can_view(obj, user): | ||||
|  | ||||
|  | ||||
| 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 | ||||
|       on two properties | ||||
|       on two properties. | ||||
|  | ||||
|     :prop permission_function: function to test permission with, takes an object and an user an return a bool | ||||
|     :prop raised_error: permission to be raised | ||||
|  | ||||
|     :raises: raised_error | ||||
|     Attributes: | ||||
|         raised_error: permission to be raised | ||||
|     """ | ||||
|  | ||||
|     permission_function = lambda obj, user: False | ||||
|     raised_error = PermissionDenied | ||||
|  | ||||
|     @staticmethod | ||||
|     def permission_function(obj: Any, user: User) -> bool: | ||||
|         """Function to test permission with.""" | ||||
|         return False | ||||
|  | ||||
|     @classmethod | ||||
|     def get_permission_function(cls, obj, user): | ||||
|         return cls.permission_function(obj, user) | ||||
| @@ -162,11 +169,12 @@ class GenericContentPermissionMixinBuilder(View): | ||||
|  | ||||
|  | ||||
| class CanCreateMixin(View): | ||||
|     """ | ||||
|     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 | ||||
|     """Protect any child view that would create an object. | ||||
|  | ||||
|     :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): | ||||
| @@ -183,55 +191,54 @@ class CanCreateMixin(View): | ||||
|  | ||||
|  | ||||
| class CanEditPropMixin(GenericContentPermissionMixinBuilder): | ||||
|     """ | ||||
|     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 | ||||
|     """Ensure the user has owner permissions on the child view object. | ||||
|  | ||||
|     :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 | ||||
|  | ||||
|  | ||||
| class CanEditMixin(GenericContentPermissionMixinBuilder): | ||||
|     """ | ||||
|     This view makes exactly the same thing as its direct parent, but checks the group on the edit_groups field of the | ||||
|     object | ||||
|     """Ensure the user has permission to edit this view's object. | ||||
|  | ||||
|     :raises: PermissionDenied | ||||
|     Raises: | ||||
|         PermissionDenied: if the user cannot edit this view's object. | ||||
|     """ | ||||
|  | ||||
|     permission_function = can_edit | ||||
|  | ||||
|  | ||||
| class CanViewMixin(GenericContentPermissionMixinBuilder): | ||||
|     """ | ||||
|     This view still makes exactly the same thing as its direct parent, but checks the group on the view_groups field of | ||||
|     the object | ||||
|     """Ensure the user has permission to view this view's object. | ||||
|  | ||||
|     :raises: PermissionDenied | ||||
|     Raises: | ||||
|         PermissionDenied: if the user cannot edit this view's object. | ||||
|     """ | ||||
|  | ||||
|     permission_function = can_view | ||||
|  | ||||
|  | ||||
| class UserIsRootMixin(GenericContentPermissionMixinBuilder): | ||||
|     """ | ||||
|     This view check if the user is root | ||||
|     """Allow only root admins. | ||||
|  | ||||
|     :raises: PermissionDenied | ||||
|     Raises: | ||||
|         PermissionDenied: if the user isn't root | ||||
|     """ | ||||
|  | ||||
|     permission_function = lambda obj, user: user.is_root | ||||
|  | ||||
|  | ||||
| class FormerSubscriberMixin(View): | ||||
|     """ | ||||
|     This view check if the user was at least an old subscriber | ||||
|     """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): | ||||
| @@ -241,10 +248,10 @@ class FormerSubscriberMixin(View): | ||||
|  | ||||
|  | ||||
| class UserIsLoggedMixin(View): | ||||
|     """ | ||||
|     This view check if the user is logged | ||||
|     """Check if the user is logged. | ||||
|  | ||||
|     :raises: PermissionDenied | ||||
|     Raises: | ||||
|         PermissionDenied: | ||||
|     """ | ||||
|  | ||||
|     def dispatch(self, request, *args, **kwargs): | ||||
| @@ -254,9 +261,7 @@ class UserIsLoggedMixin(View): | ||||
|  | ||||
|  | ||||
| class TabedViewMixin(View): | ||||
|     """ | ||||
|     This view provide the basic functions for displaying tabs in the template | ||||
|     """ | ||||
|     """Basic functions for displaying tabs in the template.""" | ||||
|  | ||||
|     def get_tabs_title(self): | ||||
|         if hasattr(self, "tabs_title"): | ||||
| @@ -299,7 +304,7 @@ class QuickNotifMixin: | ||||
|         return ret | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         """Add quick notifications to context""" | ||||
|         """Add quick notifications to context.""" | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
|         kwargs["quick_notifs"] = [] | ||||
|         for n in self.quick_notif_list: | ||||
| @@ -312,21 +317,15 @@ class QuickNotifMixin: | ||||
|  | ||||
|  | ||||
| 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): | ||||
|         """ | ||||
|         Get current group from id in url | ||||
|         """ | ||||
|         """Get current group from id in url.""" | ||||
|         return self.cached_object | ||||
|  | ||||
|     @cached_property | ||||
|     def cached_object(self): | ||||
|         """ | ||||
|         Optimisation on group retrieval | ||||
|         """ | ||||
|         """Optimisation on group retrieval.""" | ||||
|         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"): | ||||
|     """ | ||||
|     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 | ||||
|     iterator for chunks of 8KB. | ||||
|     """ | ||||
| @@ -268,7 +267,7 @@ class FileEditPropView(CanEditPropMixin, UpdateView): | ||||
|  | ||||
|  | ||||
| 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 | ||||
|     pk_url_kwarg = "file_id" | ||||
| @@ -278,8 +277,8 @@ class FileView(CanViewMixin, DetailView, FormMixin): | ||||
|  | ||||
|     @staticmethod | ||||
|     def handle_clipboard(request, obj): | ||||
|         """ | ||||
|         This method handles the clipboard in the view. | ||||
|         """Handle the clipboard in the view. | ||||
|  | ||||
|         This method can fail, since it does not catch the exceptions coming from | ||||
|         below, allowing proper handling in the calling view. | ||||
|         Use this method like this: | ||||
|   | ||||
| @@ -196,10 +196,9 @@ class RegisteringForm(UserCreationForm): | ||||
|  | ||||
|  | ||||
| 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. | ||||
|     TODO: refactor this form | ||||
|     TODO: refactor this form. | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|   | ||||
| @@ -13,9 +13,7 @@ | ||||
| # | ||||
| # | ||||
|  | ||||
| """ | ||||
| This module contains views to manage Groups | ||||
| """ | ||||
| """Views to manage Groups.""" | ||||
|  | ||||
| from ajax_select.fields import AutoCompleteSelectMultipleField | ||||
| from django import forms | ||||
| @@ -31,9 +29,7 @@ from core.views import CanCreateMixin, CanEditMixin, DetailFormView | ||||
|  | ||||
|  | ||||
| class EditMembersForm(forms.Form): | ||||
|     """ | ||||
|     Add and remove members from a Group | ||||
|     """ | ||||
|     """Add and remove members from a Group.""" | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         self.current_users = kwargs.pop("users", []) | ||||
| @@ -53,9 +49,7 @@ class EditMembersForm(forms.Form): | ||||
|     ) | ||||
|  | ||||
|     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() | ||||
|         users_added = cleaned_data.get("users_added", None) | ||||
|         if not users_added: | ||||
| @@ -77,9 +71,7 @@ class EditMembersForm(forms.Form): | ||||
|  | ||||
|  | ||||
| class GroupListView(CanEditMixin, ListView): | ||||
|     """ | ||||
|     Displays the Group list | ||||
|     """ | ||||
|     """Displays the Group list.""" | ||||
|  | ||||
|     model = RealGroup | ||||
|     ordering = ["name"] | ||||
| @@ -87,9 +79,7 @@ class GroupListView(CanEditMixin, ListView): | ||||
|  | ||||
|  | ||||
| class GroupEditView(CanEditMixin, UpdateView): | ||||
|     """ | ||||
|     Edit infos of a Group | ||||
|     """ | ||||
|     """Edit infos of a Group.""" | ||||
|  | ||||
|     model = RealGroup | ||||
|     pk_url_kwarg = "group_id" | ||||
| @@ -98,9 +88,7 @@ class GroupEditView(CanEditMixin, UpdateView): | ||||
|  | ||||
|  | ||||
| class GroupCreateView(CanCreateMixin, CreateView): | ||||
|     """ | ||||
|     Add a new Group | ||||
|     """ | ||||
|     """Add a new Group.""" | ||||
|  | ||||
|     model = RealGroup | ||||
|     template_name = "core/create.jinja" | ||||
| @@ -108,9 +96,8 @@ class GroupCreateView(CanCreateMixin, CreateView): | ||||
|  | ||||
|  | ||||
| class GroupTemplateView(CanEditMixin, DetailFormView): | ||||
|     """ | ||||
|     Display all users in a given Group | ||||
|     Allow adding and removing users from it | ||||
|     """Display all users in a given Group | ||||
|     Allow adding and removing users from it. | ||||
|     """ | ||||
|  | ||||
|     model = RealGroup | ||||
| @@ -143,9 +130,7 @@ class GroupTemplateView(CanEditMixin, DetailFormView): | ||||
|  | ||||
|  | ||||
| class GroupDeleteView(CanEditMixin, DeleteView): | ||||
|     """ | ||||
|     Delete a Group | ||||
|     """ | ||||
|     """Delete a Group.""" | ||||
|  | ||||
|     model = RealGroup | ||||
|     pk_url_kwarg = "group_id" | ||||
|   | ||||
| @@ -74,9 +74,7 @@ from trombi.views import UserTrombiForm | ||||
|  | ||||
| @method_decorator(check_honeypot, name="post") | ||||
| class SithLoginView(views.LoginView): | ||||
|     """ | ||||
|     The login View | ||||
|     """ | ||||
|     """The login View.""" | ||||
|  | ||||
|     template_name = "core/login.jinja" | ||||
|     authentication_form = LoginForm | ||||
| @@ -85,33 +83,25 @@ class SithLoginView(views.LoginView): | ||||
|  | ||||
|  | ||||
| class SithPasswordChangeView(views.PasswordChangeView): | ||||
|     """ | ||||
|     Allows a user to change its password | ||||
|     """ | ||||
|     """Allows a user to change its password.""" | ||||
|  | ||||
|     template_name = "core/password_change.jinja" | ||||
|     success_url = reverse_lazy("core:password_change_done") | ||||
|  | ||||
|  | ||||
| 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" | ||||
|  | ||||
|  | ||||
| def logout(request): | ||||
|     """ | ||||
|     The logout view | ||||
|     """ | ||||
|     """The logout view.""" | ||||
|     return views.logout_then_login(request) | ||||
|  | ||||
|  | ||||
| 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: | ||||
|         raise PermissionDenied | ||||
|     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") | ||||
| 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" | ||||
|     email_template_name = "core/password_reset_email.jinja" | ||||
| @@ -141,26 +129,20 @@ class SithPasswordResetView(views.PasswordResetView): | ||||
|  | ||||
|  | ||||
| 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" | ||||
|  | ||||
|  | ||||
| class SithPasswordResetConfirmView(views.PasswordResetConfirmView): | ||||
|     """ | ||||
|     Provide a reset password form | ||||
|     """ | ||||
|     """Provide a reset password form.""" | ||||
|  | ||||
|     template_name = "core/password_reset_confirm.jinja" | ||||
|     success_url = reverse_lazy("core:password_reset_complete") | ||||
|  | ||||
|  | ||||
| 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" | ||||
|  | ||||
| @@ -302,9 +284,7 @@ class UserTabsMixin(TabedViewMixin): | ||||
|  | ||||
|  | ||||
| class UserView(UserTabsMixin, CanViewMixin, DetailView): | ||||
|     """ | ||||
|     Display a user's profile | ||||
|     """ | ||||
|     """Display a user's profile.""" | ||||
|  | ||||
|     model = User | ||||
|     pk_url_kwarg = "user_id" | ||||
| @@ -321,9 +301,7 @@ class UserView(UserTabsMixin, CanViewMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView): | ||||
|     """ | ||||
|     Display a user's pictures | ||||
|     """ | ||||
|     """Display a user's pictures.""" | ||||
|  | ||||
|     model = User | ||||
|     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): | ||||
|     """ | ||||
|     Display a user's godfathers | ||||
|     """ | ||||
|     """Display a user's godfathers.""" | ||||
|  | ||||
|     model = User | ||||
|     pk_url_kwarg = "user_id" | ||||
| @@ -394,9 +370,7 @@ class UserGodfathersView(UserTabsMixin, CanViewMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class UserGodfathersTreeView(UserTabsMixin, CanViewMixin, DetailView): | ||||
|     """ | ||||
|     Display a user's family tree | ||||
|     """ | ||||
|     """Display a user's family tree.""" | ||||
|  | ||||
|     model = User | ||||
|     pk_url_kwarg = "user_id" | ||||
| @@ -415,9 +389,7 @@ class UserGodfathersTreeView(UserTabsMixin, CanViewMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class UserGodfathersTreePictureView(CanViewMixin, DetailView): | ||||
|     """ | ||||
|     Display a user's tree as a picture | ||||
|     """ | ||||
|     """Display a user's tree as a picture.""" | ||||
|  | ||||
|     model = User | ||||
|     pk_url_kwarg = "user_id" | ||||
| @@ -489,9 +461,7 @@ class UserGodfathersTreePictureView(CanViewMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class UserStatsView(UserTabsMixin, CanViewMixin, DetailView): | ||||
|     """ | ||||
|     Display a user's stats | ||||
|     """ | ||||
|     """Display a user's stats.""" | ||||
|  | ||||
|     model = User | ||||
|     pk_url_kwarg = "user_id" | ||||
| @@ -591,9 +561,7 @@ class UserStatsView(UserTabsMixin, CanViewMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class UserMiniView(CanViewMixin, DetailView): | ||||
|     """ | ||||
|     Display a user's profile | ||||
|     """ | ||||
|     """Display a user's profile.""" | ||||
|  | ||||
|     model = User | ||||
|     pk_url_kwarg = "user_id" | ||||
| @@ -602,18 +570,14 @@ class UserMiniView(CanViewMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class UserListView(ListView, CanEditPropMixin): | ||||
|     """ | ||||
|     Displays the user list | ||||
|     """ | ||||
|     """Displays the user list.""" | ||||
|  | ||||
|     model = User | ||||
|     template_name = "core/user_list.jinja" | ||||
|  | ||||
|  | ||||
| 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 | ||||
|     pk_url_kwarg = "user_id" | ||||
| @@ -650,9 +614,7 @@ class UserUploadProfilePictView(CanEditMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView): | ||||
|     """ | ||||
|     Edit a user's profile | ||||
|     """ | ||||
|     """Edit a user's profile.""" | ||||
|  | ||||
|     model = User | ||||
|     pk_url_kwarg = "user_id" | ||||
| @@ -663,9 +625,7 @@ class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView): | ||||
|     board_only = [] | ||||
|  | ||||
|     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: | ||||
|             if getattr(self.form.instance, i) and not ( | ||||
|                 request.user.is_board_member or request.user.is_root | ||||
| @@ -703,9 +663,7 @@ class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView): | ||||
|  | ||||
|  | ||||
| class UserClubView(UserTabsMixin, CanViewMixin, DetailView): | ||||
|     """ | ||||
|     Display the user's club(s) | ||||
|     """ | ||||
|     """Display the user's club(s).""" | ||||
|  | ||||
|     model = User | ||||
|     context_object_name = "profile" | ||||
| @@ -715,9 +673,7 @@ class UserClubView(UserTabsMixin, CanViewMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView): | ||||
|     """ | ||||
|     Edit a user's preferences | ||||
|     """ | ||||
|     """Edit a user's preferences.""" | ||||
|  | ||||
|     model = User | ||||
|     pk_url_kwarg = "user_id" | ||||
| @@ -752,9 +708,7 @@ class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView): | ||||
|  | ||||
|  | ||||
| class UserUpdateGroupView(UserTabsMixin, CanEditPropMixin, UpdateView): | ||||
|     """ | ||||
|     Edit a user's groups | ||||
|     """ | ||||
|     """Edit a user's groups.""" | ||||
|  | ||||
|     model = User | ||||
|     pk_url_kwarg = "user_id" | ||||
| @@ -767,9 +721,7 @@ class UserUpdateGroupView(UserTabsMixin, CanEditPropMixin, UpdateView): | ||||
|  | ||||
|  | ||||
| class UserToolsView(QuickNotifMixin, UserTabsMixin, UserIsLoggedMixin, TemplateView): | ||||
|     """ | ||||
|     Displays the logged user's tools | ||||
|     """ | ||||
|     """Displays the logged user's tools.""" | ||||
|  | ||||
|     template_name = "core/user_tools.jinja" | ||||
|     current_tab = "tools" | ||||
| @@ -786,9 +738,7 @@ class UserToolsView(QuickNotifMixin, UserTabsMixin, UserIsLoggedMixin, TemplateV | ||||
|  | ||||
|  | ||||
| class UserAccountBase(UserTabsMixin, DetailView): | ||||
|     """ | ||||
|     Base class for UserAccount | ||||
|     """ | ||||
|     """Base class for UserAccount.""" | ||||
|  | ||||
|     model = User | ||||
|     pk_url_kwarg = "user_id" | ||||
| @@ -809,9 +759,7 @@ class UserAccountBase(UserTabsMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class UserAccountView(UserAccountBase): | ||||
|     """ | ||||
|     Display a user's account | ||||
|     """ | ||||
|     """Display a user's account.""" | ||||
|  | ||||
|     template_name = "core/user_account.jinja" | ||||
|  | ||||
| @@ -858,9 +806,7 @@ class UserAccountView(UserAccountBase): | ||||
|  | ||||
|  | ||||
| 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" | ||||
|  | ||||
|   | ||||
| @@ -30,9 +30,8 @@ class BillingInfoForm(forms.ModelForm): | ||||
|  | ||||
|  | ||||
| class StudentCardForm(forms.ModelForm): | ||||
|     """ | ||||
|     Form for adding student cards | ||||
|     Only used for user profile since CounterClick is to complicated | ||||
|     """Form for adding student cards | ||||
|     Only used for user profile since CounterClick is to complicated. | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
| @@ -48,8 +47,7 @@ class StudentCardForm(forms.ModelForm): | ||||
|  | ||||
|  | ||||
| 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. | ||||
|  | ||||
|     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): | ||||
|     """ | ||||
|     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 | ||||
|     """Customer data of a 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) | ||||
| @@ -63,10 +64,9 @@ class Customer(models.Model): | ||||
|         return "%s - %s" % (self.user.username, self.account_id) | ||||
|  | ||||
|     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 | ||||
|         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): | ||||
|             raise ValidationError(_("Not enough money")) | ||||
| @@ -84,9 +84,8 @@ class Customer(models.Model): | ||||
|  | ||||
|     @property | ||||
|     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) | ||||
|         method as the present method returns an information | ||||
|         about a customer whereas the other tells something | ||||
| @@ -100,8 +99,7 @@ class Customer(models.Model): | ||||
|  | ||||
|     @classmethod | ||||
|     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. | ||||
|  | ||||
|         If the user has an account, return it as is. | ||||
| @@ -158,9 +156,8 @@ class Customer(models.Model): | ||||
|  | ||||
|  | ||||
| class BillingInfo(models.Model): | ||||
|     """ | ||||
|     Represent the billing information of a user, which are required | ||||
|     by the 3D-Secure v2 system used by the etransaction module | ||||
|     """Represent the billing information of a user, which are required | ||||
|     by the 3D-Secure v2 system used by the etransaction module. | ||||
|     """ | ||||
|  | ||||
|     customer = models.OneToOneField( | ||||
| @@ -182,10 +179,9 @@ class BillingInfo(models.Model): | ||||
|         return f"{self.first_name} {self.last_name}" | ||||
|  | ||||
|     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. | ||||
|         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 = { | ||||
|             "Address": { | ||||
| @@ -204,9 +200,9 @@ class BillingInfo(models.Model): | ||||
|  | ||||
|  | ||||
| class ProductType(models.Model): | ||||
|     """ | ||||
|     This describes a product type | ||||
|     Useful only for categorizing, changes are made at the product level for now | ||||
|     """A product type. | ||||
|  | ||||
|     Useful only for categorizing. | ||||
|     """ | ||||
|  | ||||
|     name = models.CharField(_("name"), max_length=30) | ||||
| @@ -229,9 +225,7 @@ class ProductType(models.Model): | ||||
|         return reverse("counter:producttype_list") | ||||
|  | ||||
|     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: | ||||
|             return False | ||||
|         if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): | ||||
| @@ -240,9 +234,7 @@ class ProductType(models.Model): | ||||
|  | ||||
|  | ||||
| class Product(models.Model): | ||||
|     """ | ||||
|     This describes a product, with all its related informations | ||||
|     """ | ||||
|     """A product, with all its related information.""" | ||||
|  | ||||
|     name = models.CharField(_("name"), max_length=64) | ||||
|     description = models.TextField(_("description"), blank=True) | ||||
| @@ -297,9 +289,7 @@ class Product(models.Model): | ||||
|         return settings.SITH_ECOCUP_DECO == self.id | ||||
|  | ||||
|     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: | ||||
|             return False | ||||
|         if user.is_in_group( | ||||
| @@ -309,8 +299,7 @@ class Product(models.Model): | ||||
|         return False | ||||
|  | ||||
|     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 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 | ||||
|         (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(): | ||||
|             return True | ||||
| @@ -335,15 +325,16 @@ class Product(models.Model): | ||||
|  | ||||
| class CounterQuerySet(models.QuerySet): | ||||
|     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 | ||||
|         is a barman of this counter, else False. | ||||
|  | ||||
|         :param user: the user we want to check if he is a barman | ||||
|  | ||||
|         Example:: | ||||
|         Args: | ||||
|             user: the user we want to check if he is a barman | ||||
|  | ||||
|         Examples: | ||||
|             ```python | ||||
|             sli = User.objects.get(username="sli") | ||||
|             counters = ( | ||||
|                 Counter.objects | ||||
| @@ -353,6 +344,7 @@ class CounterQuerySet(models.QuerySet): | ||||
|             print("Sli est barman dans les comptoirs suivants :") | ||||
|             for counter in counters: | ||||
|                 print(f"- {counter.name}") | ||||
|             ``` | ||||
|         """ | ||||
|         subquery = user.counters.filter(pk=OuterRef("pk")) | ||||
|         # noinspection PyTypeChecker | ||||
| @@ -417,23 +409,21 @@ class Counter(models.Model): | ||||
|         return user.is_board_member or user in self.sellers.all() | ||||
|  | ||||
|     def gen_token(self): | ||||
|         """Generate a new token for this counter""" | ||||
|         """Generate a new token for this counter.""" | ||||
|         self.token = "".join( | ||||
|             random.choice(string.ascii_letters + string.digits) for x in range(30) | ||||
|         ) | ||||
|         self.save() | ||||
|  | ||||
|     def add_barman(self, user): | ||||
|         """ | ||||
|         Logs a barman in to the given counter | ||||
|         A user is stored as a tuple with its login time | ||||
|         """Logs a barman in to the given counter. | ||||
|  | ||||
|         A user is stored as a tuple with its login time. | ||||
|         """ | ||||
|         Permanency(user=user, counter=self, start=timezone.now(), end=None).save() | ||||
|  | ||||
|     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() | ||||
|         for p in perm: | ||||
|             p.end = p.activity | ||||
| @@ -444,8 +434,7 @@ class Counter(models.Model): | ||||
|         return self.get_barmen_list() | ||||
|  | ||||
|     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 | ||||
|         """ | ||||
| @@ -462,16 +451,12 @@ class Counter(models.Model): | ||||
|         return bl | ||||
|  | ||||
|     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() | ||||
|         return bl[random.randrange(0, len(bl))] | ||||
|  | ||||
|     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(): | ||||
|             p.save()  # Update activity | ||||
|  | ||||
| @@ -479,25 +464,18 @@ class Counter(models.Model): | ||||
|         return len(self.barmen_list) > 0 | ||||
|  | ||||
|     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 ( | ||||
|             (timezone.now() - self.permanencies.order_by("-activity").first().activity) | ||||
|             > timedelta(minutes=settings.SITH_COUNTER_MINUTE_INACTIVE) | ||||
|         ) | ||||
|  | ||||
|     def barman_list(self): | ||||
|         """ | ||||
|         Returns the barman id list | ||||
|         """ | ||||
|         """Returns the barman id list.""" | ||||
|         return [b.id for b in self.get_barmen_list()] | ||||
|  | ||||
|     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": | ||||
|             return False | ||||
|         if self.id in SITH_COUNTER_OFFICES: | ||||
| @@ -511,8 +489,7 @@ class Counter(models.Model): | ||||
|         return is_ae_member | ||||
|  | ||||
|     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. | ||||
|  | ||||
|         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: | ||||
|         """ | ||||
|         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. | ||||
|  | ||||
|         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: | ||||
|             since = get_start_of_semester() | ||||
| @@ -573,12 +551,15 @@ class Counter(models.Model): | ||||
|         ) | ||||
|  | ||||
|     def get_total_sales(self, since: datetime | date | None = None) -> CurrencyField: | ||||
|         """ | ||||
|         Compute and return the total turnover of this counter | ||||
|         since the date specified in parameter (by default, since the start of the current | ||||
|         semester) | ||||
|         :param since: timestamp from which to perform the calculation | ||||
|         :return: Total revenue earned at this counter | ||||
|         """Compute and return the total turnover of this counter since the given date. | ||||
|  | ||||
|         By default, the date is the start of the current semester. | ||||
|  | ||||
|         Args: | ||||
|             since: timestamp from which to perform the calculation | ||||
|  | ||||
|         Returns: | ||||
|             Total revenue earned at this counter. | ||||
|         """ | ||||
|         if since is None: | ||||
|             since = get_start_of_semester() | ||||
| @@ -591,9 +572,7 @@ class Counter(models.Model): | ||||
|  | ||||
|  | ||||
| class Refilling(models.Model): | ||||
|     """ | ||||
|     Handle the refilling | ||||
|     """ | ||||
|     """Handle the refilling.""" | ||||
|  | ||||
|     counter = models.ForeignKey( | ||||
|         Counter, related_name="refillings", blank=False, on_delete=models.CASCADE | ||||
| @@ -665,9 +644,7 @@ class Refilling(models.Model): | ||||
|  | ||||
|  | ||||
| class Selling(models.Model): | ||||
|     """ | ||||
|     Handle the sellings | ||||
|     """ | ||||
|     """Handle the sellings.""" | ||||
|  | ||||
|     label = models.CharField(_("label"), max_length=64) | ||||
|     product = models.ForeignKey( | ||||
| @@ -724,9 +701,7 @@ class Selling(models.Model): | ||||
|         ) | ||||
|  | ||||
|     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: | ||||
|             self.date = timezone.now() | ||||
|         self.full_clean() | ||||
| @@ -864,8 +839,10 @@ class Selling(models.Model): | ||||
|  | ||||
|  | ||||
| class Permanency(models.Model): | ||||
|     """ | ||||
|     This class aims at storing a traceability of who was barman where and when | ||||
|     """A permanency of a barman, on a counter. | ||||
|  | ||||
|     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( | ||||
| @@ -971,9 +948,7 @@ class CashRegisterSummary(models.Model): | ||||
|             return object.__getattribute__(self, name) | ||||
|  | ||||
|     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: | ||||
|             return False | ||||
|         if user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID): | ||||
| @@ -1010,9 +985,7 @@ class CashRegisterSummaryItem(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, | ||||
| @@ -1041,9 +1014,7 @@ class Eticket(models.Model): | ||||
|         return reverse("counter:eticket_list") | ||||
|  | ||||
|     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: | ||||
|             return False | ||||
|         return user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID) | ||||
| @@ -1058,11 +1029,11 @@ class Eticket(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 | ||||
|     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 | ||||
|   | ||||
| @@ -140,10 +140,7 @@ class CounterTest(TestCase): | ||||
|         assert response.status_code == 200 | ||||
|  | ||||
|     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]) | ||||
|         counters = Counter.objects.annotate_has_barman(self.sli) | ||||
|         for counter in counters: | ||||
| @@ -265,15 +262,11 @@ class CounterStatsTest(TestCase): | ||||
|         assert response.status_code == 403 | ||||
|  | ||||
|     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 | ||||
|  | ||||
|     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] | ||||
|         perm_times = [ | ||||
|             timedelta(days=16, hours=2, minutes=35, seconds=54), | ||||
| @@ -292,9 +285,7 @@ class CounterStatsTest(TestCase): | ||||
|         ] | ||||
|  | ||||
|     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] | ||||
|         sale_amounts = [2000, 1000, 100, 2] | ||||
|         assert list(self.counter.get_top_customers()) == [ | ||||
| @@ -588,9 +579,8 @@ class BarmanConnectionTest(TestCase): | ||||
|  | ||||
|  | ||||
| class StudentCardTest(TestCase): | ||||
|     """ | ||||
|     Tests for adding and deleting Stundent Cards | ||||
|     Test that an user can be found with it's student card | ||||
|     """Tests for adding and deleting Stundent Cards | ||||
|     Test that an user can be found with it's student card. | ||||
|     """ | ||||
|  | ||||
|     @classmethod | ||||
|   | ||||
							
								
								
									
										197
									
								
								counter/views.py
									
									
									
									
									
								
							
							
						
						
									
										197
									
								
								counter/views.py
									
									
									
									
									
								
							| @@ -75,9 +75,7 @@ from counter.models import ( | ||||
|  | ||||
|  | ||||
| class CounterAdminMixin(View): | ||||
|     """ | ||||
|     This view is made to protect counter admin section | ||||
|     """ | ||||
|     """Protect counter admin section.""" | ||||
|  | ||||
|     edit_group = [settings.SITH_GROUP_COUNTER_ADMIN_ID] | ||||
|     edit_club = [] | ||||
| @@ -105,9 +103,7 @@ class CounterAdminMixin(View): | ||||
|  | ||||
|  | ||||
| class StudentCardDeleteView(DeleteView, CanEditMixin): | ||||
|     """ | ||||
|     View used to delete a card from a user | ||||
|     """ | ||||
|     """View used to delete a card from a user.""" | ||||
|  | ||||
|     model = StudentCard | ||||
|     template_name = "core/delete_confirm.jinja" | ||||
| @@ -210,9 +206,7 @@ class CounterTabsMixin(TabedViewMixin): | ||||
| class CounterMain( | ||||
|     CounterTabsMixin, CanViewMixin, DetailView, ProcessFormView, FormMixin | ||||
| ): | ||||
|     """ | ||||
|     The public (barman) view | ||||
|     """ | ||||
|     """The public (barman) view.""" | ||||
|  | ||||
|     model = Counter | ||||
|     template_name = "counter/counter_main.jinja" | ||||
| @@ -239,9 +233,7 @@ class CounterMain( | ||||
|         return super().post(request, *args, **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": | ||||
|             self.object = self.get_object() | ||||
|         self.object.update_activity() | ||||
| @@ -275,9 +267,7 @@ class CounterMain( | ||||
|         return kwargs | ||||
|  | ||||
|     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"] | ||||
|         return super().form_valid(form) | ||||
|  | ||||
| @@ -286,10 +276,9 @@ class CounterMain( | ||||
|  | ||||
|  | ||||
| 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 | ||||
|     Everything is made by hand in the post method | ||||
|     Everything is made by hand in the post method. | ||||
|     """ | ||||
|  | ||||
|     model = Counter | ||||
| @@ -347,7 +336,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | ||||
|         return super().dispatch(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 | ||||
|             request.session["basket"] = {} | ||||
|             request.session["basket_total"] = 0 | ||||
| @@ -364,7 +353,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | ||||
|         return ret | ||||
|  | ||||
|     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.refill_form = None | ||||
|         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 | ||||
|  | ||||
|     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 | ||||
|         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 = str(pid) | ||||
| @@ -543,9 +531,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | ||||
|         return True | ||||
|  | ||||
|     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 = str(uid) | ||||
|         if not StudentCard.is_valid(uid): | ||||
| @@ -564,7 +550,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | ||||
|         return True | ||||
|  | ||||
|     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] | ||||
|         product = self.get_product(pid) | ||||
|         if pid in request.session["basket"]: | ||||
| @@ -581,11 +567,11 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | ||||
|         request.session.modified = True | ||||
|  | ||||
|     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 : | ||||
|             - <str>, where the string is the code of the product | ||||
|             - <int>X<str>, where the integer is the quantity and str the code | ||||
|             - `<str>`, where the string is the code of the product | ||||
|             - `<int>X<str>`, where the integer is the quantity and str the code. | ||||
|         """ | ||||
|         string = parse_qs(request.body.decode()).get("code", [""])[0].upper() | ||||
|         if string == "FIN": | ||||
| @@ -605,7 +591,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | ||||
|         return self.render_to_response(context) | ||||
|  | ||||
|     def finish(self, request): | ||||
|         """Finish the click session, and validate the basket""" | ||||
|         """Finish the click session, and validate the basket.""" | ||||
|         with transaction.atomic(): | ||||
|             request.session["last_basket"] = [] | ||||
|             if self.sum_basket(request) > self.customer.amount: | ||||
| @@ -657,7 +643,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | ||||
|             ) | ||||
|  | ||||
|     def cancel(self, request): | ||||
|         """Cancel the click session""" | ||||
|         """Cancel the click session.""" | ||||
|         kwargs = {"counter_id": self.object.id} | ||||
|         request.session.pop("basket", None) | ||||
|         return HttpResponseRedirect( | ||||
| @@ -665,7 +651,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | ||||
|         ) | ||||
|  | ||||
|     def refill(self, request): | ||||
|         """Refill the customer's account""" | ||||
|         """Refill the customer's account.""" | ||||
|         if not self.object.can_refill(): | ||||
|             raise PermissionDenied | ||||
|         form = RefillForm(request.POST) | ||||
| @@ -678,7 +664,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | ||||
|             self.refill_form = form | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         """Add customer to the context""" | ||||
|         """Add customer to the context.""" | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
|         products = self.object.products.select_related("product_type") | ||||
|         if self.customer_is_barman(): | ||||
| @@ -701,8 +687,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class CounterLogin(RedirectView): | ||||
|     """ | ||||
|     Handle the login of a barman | ||||
|     """Handle the login of a barman. | ||||
|  | ||||
|     Logged barmen are stored in the Permanency model | ||||
|     """ | ||||
| @@ -710,9 +695,7 @@ class CounterLogin(RedirectView): | ||||
|     permanent = False | ||||
|  | ||||
|     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 = Counter.objects.filter(id=kwargs["counter_id"]).first() | ||||
|         form = LoginForm(request, data=request.POST) | ||||
| @@ -745,9 +728,7 @@ class CounterLogout(RedirectView): | ||||
|     permanent = False | ||||
|  | ||||
|     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() | ||||
|         user = User.objects.filter(id=request.POST["user_id"]).first() | ||||
|         self.counter.del_barman(user) | ||||
| @@ -803,9 +784,7 @@ class CounterAdminTabsMixin(TabedViewMixin): | ||||
|  | ||||
|  | ||||
| class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView): | ||||
|     """ | ||||
|     A list view for the admins | ||||
|     """ | ||||
|     """A list view for the admins.""" | ||||
|  | ||||
|     model = Counter | ||||
|     template_name = "counter/counter_list.jinja" | ||||
| @@ -813,9 +792,7 @@ class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView): | ||||
|  | ||||
|  | ||||
| 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 | ||||
|     form_class = CounterEditForm | ||||
| @@ -833,9 +810,7 @@ class CounterEditView(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 | ||||
|     form_class = modelform_factory(Counter, fields=["name", "club", "type"]) | ||||
| @@ -845,9 +820,7 @@ class CounterEditPropView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): | ||||
|  | ||||
|  | ||||
| class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): | ||||
|     """ | ||||
|     Create a counter (for the admins) | ||||
|     """ | ||||
|     """Create a counter (for the admins).""" | ||||
|  | ||||
|     model = Counter | ||||
|     form_class = modelform_factory( | ||||
| @@ -860,9 +833,7 @@ class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): | ||||
|  | ||||
|  | ||||
| class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView): | ||||
|     """ | ||||
|     Delete a counter (for the admins) | ||||
|     """ | ||||
|     """Delete a counter (for the admins).""" | ||||
|  | ||||
|     model = Counter | ||||
|     pk_url_kwarg = "counter_id" | ||||
| @@ -875,9 +846,7 @@ class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView): | ||||
|  | ||||
|  | ||||
| class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): | ||||
|     """ | ||||
|     A list view for the admins | ||||
|     """ | ||||
|     """A list view for the admins.""" | ||||
|  | ||||
|     model = ProductType | ||||
|     template_name = "counter/producttype_list.jinja" | ||||
| @@ -885,9 +854,7 @@ class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): | ||||
|  | ||||
|  | ||||
| class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): | ||||
|     """ | ||||
|     A create view for the admins | ||||
|     """ | ||||
|     """A create view for the admins.""" | ||||
|  | ||||
|     model = ProductType | ||||
|     fields = ["name", "description", "comment", "icon", "priority"] | ||||
| @@ -896,9 +863,7 @@ class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView | ||||
|  | ||||
|  | ||||
| class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): | ||||
|     """ | ||||
|     An edit view for the admins | ||||
|     """ | ||||
|     """An edit view for the admins.""" | ||||
|  | ||||
|     model = ProductType | ||||
|     template_name = "core/edit.jinja" | ||||
| @@ -908,9 +873,7 @@ class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): | ||||
|  | ||||
|  | ||||
| class ProductArchivedListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): | ||||
|     """ | ||||
|     A list view for the admins | ||||
|     """ | ||||
|     """A list view for the admins.""" | ||||
|  | ||||
|     model = Product | ||||
|     template_name = "counter/product_list.jinja" | ||||
| @@ -920,9 +883,7 @@ class ProductArchivedListView(CounterAdminTabsMixin, CounterAdminMixin, ListView | ||||
|  | ||||
|  | ||||
| class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): | ||||
|     """ | ||||
|     A list view for the admins | ||||
|     """ | ||||
|     """A list view for the admins.""" | ||||
|  | ||||
|     model = Product | ||||
|     template_name = "counter/product_list.jinja" | ||||
| @@ -932,9 +893,7 @@ class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): | ||||
|  | ||||
|  | ||||
| class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): | ||||
|     """ | ||||
|     A create view for the admins | ||||
|     """ | ||||
|     """A create view for the admins.""" | ||||
|  | ||||
|     model = Product | ||||
|     form_class = ProductEditForm | ||||
| @@ -943,9 +902,7 @@ class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): | ||||
|  | ||||
|  | ||||
| class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): | ||||
|     """ | ||||
|     An edit view for the admins | ||||
|     """ | ||||
|     """An edit view for the admins.""" | ||||
|  | ||||
|     model = Product | ||||
|     form_class = ProductEditForm | ||||
| @@ -955,18 +912,14 @@ class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): | ||||
|  | ||||
|  | ||||
| class RefillingDeleteView(DeleteView): | ||||
|     """ | ||||
|     Delete a refilling (for the admins) | ||||
|     """ | ||||
|     """Delete a refilling (for the admins).""" | ||||
|  | ||||
|     model = Refilling | ||||
|     pk_url_kwarg = "refilling_id" | ||||
|     template_name = "core/delete_confirm.jinja" | ||||
|  | ||||
|     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() | ||||
|         if ( | ||||
|             timezone.now() - self.object.date | ||||
| @@ -990,18 +943,14 @@ class RefillingDeleteView(DeleteView): | ||||
|  | ||||
|  | ||||
| class SellingDeleteView(DeleteView): | ||||
|     """ | ||||
|     Delete a selling (for the admins) | ||||
|     """ | ||||
|     """Delete a selling (for the admins).""" | ||||
|  | ||||
|     model = Selling | ||||
|     pk_url_kwarg = "selling_id" | ||||
|     template_name = "core/delete_confirm.jinja" | ||||
|  | ||||
|     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() | ||||
|         if ( | ||||
|             timezone.now() - self.object.date | ||||
| @@ -1028,9 +977,7 @@ class SellingDeleteView(DeleteView): | ||||
|  | ||||
|  | ||||
| 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) | ||||
|     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): | ||||
|     """ | ||||
|     Provide the last operations to allow barmen to delete them | ||||
|     """ | ||||
|     """Provide the last operations to allow barmen to delete them.""" | ||||
|  | ||||
|     model = Counter | ||||
|     pk_url_kwarg = "counter_id" | ||||
| @@ -1248,9 +1193,7 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView): | ||||
|     current_tab = "last_ops" | ||||
|  | ||||
|     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() | ||||
|         if ( | ||||
|             self.object.get_barmen_list() | ||||
| @@ -1267,7 +1210,7 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView): | ||||
|         ) | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         """Add form to the context""" | ||||
|         """Add form to the context.""" | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
|         threshold = timezone.now() - timedelta( | ||||
|             minutes=settings.SITH_LAST_OPERATIONS_LIMIT | ||||
| @@ -1282,9 +1225,7 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView): | ||||
|     """ | ||||
|     Provide the cash summary form | ||||
|     """ | ||||
|     """Provide the cash summary form.""" | ||||
|  | ||||
|     model = Counter | ||||
|     pk_url_kwarg = "counter_id" | ||||
| @@ -1292,9 +1233,7 @@ class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView): | ||||
|     current_tab = "cash_summary" | ||||
|  | ||||
|     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() | ||||
|         if ( | ||||
|             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}) | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         """Add form to the context""" | ||||
|         """Add form to the context.""" | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
|         kwargs["form"] = self.form | ||||
|         return kwargs | ||||
|  | ||||
|  | ||||
| class CounterActivityView(DetailView): | ||||
|     """ | ||||
|     Show the bar activity | ||||
|     """ | ||||
|     """Show the bar activity.""" | ||||
|  | ||||
|     model = Counter | ||||
|     pk_url_kwarg = "counter_id" | ||||
| @@ -1344,16 +1281,14 @@ class CounterActivityView(DetailView): | ||||
|  | ||||
|  | ||||
| class CounterStatView(DetailView, CounterAdminMixin): | ||||
|     """ | ||||
|     Show the bar stats | ||||
|     """ | ||||
|     """Show the bar stats.""" | ||||
|  | ||||
|     model = Counter | ||||
|     pk_url_kwarg = "counter_id" | ||||
|     template_name = "counter/stats.jinja" | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         """Add stats to the context""" | ||||
|         """Add stats to the context.""" | ||||
|         counter: Counter = self.object | ||||
|         semester_start = get_start_of_semester() | ||||
|         office_hours = counter.get_top_barmen() | ||||
| @@ -1386,7 +1321,7 @@ class CounterStatView(DetailView, CounterAdminMixin): | ||||
|  | ||||
|  | ||||
| class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): | ||||
|     """Edit cash summaries""" | ||||
|     """Edit cash summaries.""" | ||||
|  | ||||
|     model = CashRegisterSummary | ||||
|     template_name = "counter/cash_register_summary.jinja" | ||||
| @@ -1400,7 +1335,7 @@ class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): | ||||
|  | ||||
|  | ||||
| class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): | ||||
|     """Display a list of cash summaries""" | ||||
|     """Display a list of cash summaries.""" | ||||
|  | ||||
|     model = CashRegisterSummary | ||||
|     template_name = "counter/cash_summary_list.jinja" | ||||
| @@ -1410,7 +1345,7 @@ class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): | ||||
|     paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         """Add sums to the context""" | ||||
|         """Add sums to the context.""" | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
|         form = CashSummaryFormBase(self.request.GET) | ||||
|         kwargs["form"] = form | ||||
| @@ -1461,7 +1396,7 @@ class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView): | ||||
|     current_tab = "invoices_call" | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         """Add sums to the context""" | ||||
|         """Add sums to the context.""" | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
|         kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC") | ||||
|         if "month" in self.request.GET: | ||||
| @@ -1522,9 +1457,7 @@ class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView): | ||||
|  | ||||
|  | ||||
| class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): | ||||
|     """ | ||||
|     A list view for the admins | ||||
|     """ | ||||
|     """A list view for the admins.""" | ||||
|  | ||||
|     model = Eticket | ||||
|     template_name = "counter/eticket_list.jinja" | ||||
| @@ -1533,9 +1466,7 @@ class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): | ||||
|  | ||||
|  | ||||
| class EticketCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): | ||||
|     """ | ||||
|     Create an eticket | ||||
|     """ | ||||
|     """Create an eticket.""" | ||||
|  | ||||
|     model = Eticket | ||||
|     template_name = "core/create.jinja" | ||||
| @@ -1544,9 +1475,7 @@ class EticketCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): | ||||
|  | ||||
|  | ||||
| class EticketEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): | ||||
|     """ | ||||
|     Edit an eticket | ||||
|     """ | ||||
|     """Edit an eticket.""" | ||||
|  | ||||
|     model = Eticket | ||||
|     template_name = "core/edit.jinja" | ||||
| @@ -1556,9 +1485,7 @@ class EticketEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): | ||||
|  | ||||
|  | ||||
| class EticketPDFView(CanViewMixin, DetailView): | ||||
|     """ | ||||
|     Display the PDF of an eticket | ||||
|     """ | ||||
|     """Display the PDF of an eticket.""" | ||||
|  | ||||
|     model = Selling | ||||
|     pk_url_kwarg = "selling_id" | ||||
| @@ -1647,9 +1574,7 @@ class EticketPDFView(CanViewMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): | ||||
|     """ | ||||
|     List of refillings on a counter | ||||
|     """ | ||||
|     """List of refillings on a counter.""" | ||||
|  | ||||
|     model = Refilling | ||||
|     template_name = "counter/refilling_list.jinja" | ||||
| @@ -1668,9 +1593,7 @@ class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListVie | ||||
|  | ||||
|  | ||||
| class StudentCardFormView(FormView): | ||||
|     """ | ||||
|     Add a new student card | ||||
|     """ | ||||
|     """Add a new student card.""" | ||||
|  | ||||
|     form_class = StudentCardForm | ||||
|     template_name = "core/create.jinja" | ||||
|   | ||||
| @@ -21,11 +21,10 @@ | ||||
|  | ||||
|  | ||||
| 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 | ||||
|     either ``/eboutic/pay/success/`` or ``/eboutic/pay/failure/`` | ||||
|     either `/eboutic/pay/success/` or `/eboutic/pay/failure/` | ||||
|     but nothing else. | ||||
|     """ | ||||
|  | ||||
|   | ||||
| @@ -34,9 +34,8 @@ from eboutic.models import get_eboutic_products | ||||
|  | ||||
|  | ||||
| class BasketForm: | ||||
|     """ | ||||
|     Class intended to perform checks on the request sended to the server when | ||||
|     the user submits his basket from /eboutic/ | ||||
|     """Class intended to perform checks on the request sended to the server when | ||||
|     the user submits his basket from /eboutic/. | ||||
|  | ||||
|     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 | ||||
| @@ -45,6 +44,7 @@ class BasketForm: | ||||
|     However, it still tries to share some similarities with a standard django Form. | ||||
|  | ||||
|     Example: | ||||
|     ------- | ||||
|         :: | ||||
|  | ||||
|             def my_view(request): | ||||
| @@ -62,6 +62,7 @@ class BasketForm: | ||||
|     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 | ||||
|     implicitly called. | ||||
|  | ||||
|     """ | ||||
|  | ||||
|     # check the json is an array containing non-nested objects. | ||||
| @@ -85,8 +86,7 @@ class BasketForm: | ||||
|         self.correct_cookie = [] | ||||
|  | ||||
|     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. | ||||
|  | ||||
|         The form shall be considered as valid if it meets all the following conditions : | ||||
| @@ -170,9 +170,9 @@ class BasketForm: | ||||
|         # the form is invalid | ||||
|  | ||||
|     def is_valid(self) -> bool: | ||||
|         """ | ||||
|         return True if the form is correct else False. | ||||
|         If the `clean()` method has not been called beforehand, call it | ||||
|         """Return True if the form is correct else False. | ||||
|  | ||||
|         If the `clean()` method has not been called beforehand, call it. | ||||
|         """ | ||||
|         if self.error_messages == set() and self.correct_cookie == []: | ||||
|             self.clean() | ||||
|   | ||||
| @@ -12,10 +12,10 @@ | ||||
| # OR WITHIN THE LOCAL FILE "LICENSE" | ||||
| # | ||||
| # | ||||
| from __future__ import annotations | ||||
|  | ||||
| import hmac | ||||
| import typing | ||||
| from datetime import datetime | ||||
| from typing import List | ||||
|  | ||||
| from dict2xml import dict2xml | ||||
| from django.conf import settings | ||||
| @@ -29,7 +29,7 @@ from core.models import User | ||||
| 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 = ( | ||||
|         Counter.objects.get(type="EBOUTIC") | ||||
|         .products.filter(product_type__isnull=False) | ||||
| @@ -43,9 +43,7 @@ def get_eboutic_products(user: User) -> List[Product]: | ||||
|  | ||||
|  | ||||
| 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, | ||||
| @@ -60,8 +58,7 @@ class Basket(models.Model): | ||||
|         return f"{self.user}'s basket ({self.items.all().count()} items)" | ||||
|  | ||||
|     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. | ||||
|  | ||||
|         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() | ||||
|  | ||||
|     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. | ||||
|  | ||||
|         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() | ||||
|  | ||||
|     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() | ||||
|  | ||||
|     @cached_property | ||||
| @@ -116,11 +110,8 @@ class Basket(models.Model): | ||||
|         return float(total) if total is not None else 0 | ||||
|  | ||||
|     @classmethod | ||||
|     def from_session(cls, session) -> typing.Union["Basket", None]: | ||||
|         """ | ||||
|         Given an HttpRequest django object, return the basket used in the current session | ||||
|         if it exists else None | ||||
|         """ | ||||
|     def from_session(cls, session) -> Basket | None: | ||||
|         """The basket stored in the session object, if it exists.""" | ||||
|         if "basket_id" in session: | ||||
|             try: | ||||
|                 return cls.objects.get(id=session["basket_id"]) | ||||
| @@ -129,23 +120,22 @@ class Basket(models.Model): | ||||
|         return None | ||||
|  | ||||
|     def generate_sales(self, counter, seller: User, payment_method: str): | ||||
|         """ | ||||
|         Generate a list of sold items corresponding to the items | ||||
|         of this basket WITHOUT saving them NOR deleting the basket | ||||
|         """Generate a list of sold items corresponding to the items | ||||
|         of this basket WITHOUT saving them NOR deleting the basket. | ||||
|  | ||||
|         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") | ||||
|                 sales = basket.generate_sales(counter, "SITH_ACCOUNT") | ||||
|                 # here the basket is in the same state as before the method call | ||||
|  | ||||
|                 with transaction.atomic(): | ||||
|                     for sale in sales: | ||||
|                         sale.save() | ||||
|                     basket.delete() | ||||
|                     # all the basket items are deleted by the on_delete=CASCADE relation | ||||
|                     # thus only the sales remain | ||||
|             with transaction.atomic(): | ||||
|                 for sale in sales: | ||||
|                     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 | ||||
|         # only one with a join because the AbstractBaseItem model has been | ||||
| @@ -212,9 +202,7 @@ class Basket(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, | ||||
| @@ -297,11 +285,12 @@ class BasketItem(AbstractBaseItem): | ||||
|  | ||||
|     @classmethod | ||||
|     def from_product(cls, product: Product, quantity: int): | ||||
|         """ | ||||
|         Create a BasketItem with the same characteristics as the | ||||
|         product passed in parameters, with the specified quantity | ||||
|         WARNING : the basket field is not filled, so you must set | ||||
|         it yourself before saving the model | ||||
|         """Create a BasketItem with the same characteristics as the | ||||
|         product passed in parameters, with the specified quantity. | ||||
|  | ||||
|         Warnings: | ||||
|             the basket field is not filled, so you must set | ||||
|             it yourself before saving the model. | ||||
|         """ | ||||
|         return cls( | ||||
|             product_id=product.id, | ||||
|   | ||||
| @@ -52,9 +52,9 @@ class EbouticTest(TestCase): | ||||
|         cls.public = User.objects.get(username="public") | ||||
|  | ||||
|     def get_busy_basket(self, user) -> Basket: | ||||
|         """ | ||||
|         Create and return a basket with 3 barbar and 1 cotis in it. | ||||
|         Edit the client session to store the basket id 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. | ||||
|         """ | ||||
|         session = self.client.session | ||||
|         basket = Basket.objects.create(user=user) | ||||
|   | ||||
| @@ -43,8 +43,8 @@ from eboutic.models import Basket, Invoice, InvoiceItem, get_eboutic_products | ||||
| @login_required | ||||
| @require_GET | ||||
| 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. | ||||
|     The latter represents the page from which a user can see | ||||
|     the catalogue of products that he can buy and fill | ||||
|   | ||||
| @@ -7,9 +7,7 @@ from core.models import Group, User | ||||
|  | ||||
|  | ||||
| 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) | ||||
|     description = models.TextField(_("description"), null=True, blank=True) | ||||
| @@ -105,9 +103,7 @@ class Election(models.Model): | ||||
|  | ||||
|  | ||||
| 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, | ||||
| @@ -151,9 +147,7 @@ class Role(OrderedModel): | ||||
|  | ||||
|  | ||||
| class ElectionList(models.Model): | ||||
|     """ | ||||
|     To allow per list vote | ||||
|     """ | ||||
|     """To allow per list vote.""" | ||||
|  | ||||
|     title = models.CharField(_("title"), max_length=255) | ||||
|     election = models.ForeignKey( | ||||
| @@ -176,9 +170,7 @@ class ElectionList(models.Model): | ||||
|  | ||||
|  | ||||
| class Candidature(models.Model): | ||||
|     """ | ||||
|     This class is a component of responsability | ||||
|     """ | ||||
|     """This class is a component of responsability.""" | ||||
|  | ||||
|     role = models.ForeignKey( | ||||
|         Role, | ||||
| @@ -214,9 +206,7 @@ class Candidature(models.Model): | ||||
|  | ||||
|  | ||||
| class Vote(models.Model): | ||||
|     """ | ||||
|     This class allows to vote for candidates | ||||
|     """ | ||||
|     """This class allows to vote for candidates.""" | ||||
|  | ||||
|     role = models.ForeignKey( | ||||
|         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): | ||||
|     """ | ||||
|     Used to replace ModelMultipleChoiceField but with | ||||
|     automatic backend verification | ||||
|     """ | ||||
|     """A `ModelMultipleChoiceField`, with a max limit of selectable inputs.""" | ||||
|  | ||||
|     def __init__(self, queryset, max_choice, **kwargs): | ||||
|         self.max_choice = max_choice | ||||
| @@ -45,7 +42,7 @@ class LimitedCheckboxField(forms.ModelMultipleChoiceField): | ||||
|  | ||||
|  | ||||
| class CandidateForm(forms.ModelForm): | ||||
|     """Form to candidate""" | ||||
|     """Form to candidate.""" | ||||
|  | ||||
|     class Meta: | ||||
|         model = Candidature | ||||
| @@ -91,7 +88,7 @@ class VoteForm(forms.Form): | ||||
|  | ||||
|  | ||||
| class RoleForm(forms.ModelForm): | ||||
|     """Form for creating a role""" | ||||
|     """Form for creating a role.""" | ||||
|  | ||||
|     class Meta: | ||||
|         model = Role | ||||
| @@ -175,9 +172,7 @@ class ElectionForm(forms.ModelForm): | ||||
|  | ||||
|  | ||||
| class ElectionsListView(CanViewMixin, ListView): | ||||
|     """ | ||||
|     A list of all non archived elections visible | ||||
|     """ | ||||
|     """A list of all non archived elections visible.""" | ||||
|  | ||||
|     model = Election | ||||
|     ordering = ["-id"] | ||||
| @@ -189,9 +184,7 @@ class ElectionsListView(CanViewMixin, ListView): | ||||
|  | ||||
|  | ||||
| class ElectionListArchivedView(CanViewMixin, ListView): | ||||
|     """ | ||||
|     A list of all archived elections visible | ||||
|     """ | ||||
|     """A list of all archived elections visible.""" | ||||
|  | ||||
|     model = Election | ||||
|     ordering = ["-id"] | ||||
| @@ -203,9 +196,7 @@ class ElectionListArchivedView(CanViewMixin, ListView): | ||||
|  | ||||
|  | ||||
| class ElectionDetailView(CanViewMixin, DetailView): | ||||
|     """ | ||||
|     Details an election responsability by responsability | ||||
|     """ | ||||
|     """Details an election responsability by responsability.""" | ||||
|  | ||||
|     model = Election | ||||
|     template_name = "election/election_detail.jinja" | ||||
| @@ -232,7 +223,7 @@ class ElectionDetailView(CanViewMixin, DetailView): | ||||
|         return response | ||||
|  | ||||
|     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["election_form"] = VoteForm(self.object, self.request.user) | ||||
|         kwargs["election_results"] = self.object.results | ||||
| @@ -243,9 +234,7 @@ class ElectionDetailView(CanViewMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class VoteFormView(CanCreateMixin, FormView): | ||||
|     """ | ||||
|     Alows users to vote | ||||
|     """ | ||||
|     """Alows users to vote.""" | ||||
|  | ||||
|     form_class = VoteForm | ||||
|     template_name = "election/election_detail.jinja" | ||||
| @@ -278,9 +267,7 @@ class VoteFormView(CanCreateMixin, FormView): | ||||
|         return kwargs | ||||
|  | ||||
|     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() | ||||
|         res = super(FormView, self).form_valid(form) | ||||
|         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}) | ||||
|  | ||||
|     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["object"] = self.election | ||||
|         kwargs["election"] = self.election | ||||
| @@ -305,9 +292,7 @@ class VoteFormView(CanCreateMixin, FormView): | ||||
|  | ||||
|  | ||||
| class CandidatureCreateView(CanCreateMixin, CreateView): | ||||
|     """ | ||||
|     View dedicated to a cundidature creation | ||||
|     """ | ||||
|     """View dedicated to a cundidature creation.""" | ||||
|  | ||||
|     form_class = CandidateForm | ||||
|     model = Candidature | ||||
| @@ -330,9 +315,7 @@ class CandidatureCreateView(CanCreateMixin, CreateView): | ||||
|         return kwargs | ||||
|  | ||||
|     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.election = Election.objects.get(id=self.election.id) | ||||
|         if (obj.election.can_candidate(obj.user)) and ( | ||||
| @@ -361,10 +344,7 @@ class ElectionCreateView(CanCreateMixin, CreateView): | ||||
|         return super().dispatch(request, *args, **kwargs) | ||||
|  | ||||
|     def form_valid(self, form): | ||||
|         """ | ||||
|         Allow every users that had passed the dispatch | ||||
|         to create an election | ||||
|         """ | ||||
|         """Allow every user that had passed the dispatch to create an election.""" | ||||
|         return super(CreateView, self).form_valid(form) | ||||
|  | ||||
|     def get_success_url(self, **kwargs): | ||||
| @@ -388,9 +368,7 @@ class RoleCreateView(CanCreateMixin, CreateView): | ||||
|         return init | ||||
|  | ||||
|     def form_valid(self, form): | ||||
|         """ | ||||
|         Verify that the user can edit properly | ||||
|         """ | ||||
|         """Verify that the user can edit properly.""" | ||||
|         obj: Role = form.instance | ||||
|         user: User = self.request.user | ||||
|         if obj.election: | ||||
| @@ -432,9 +410,7 @@ class ElectionListCreateView(CanCreateMixin, CreateView): | ||||
|         return kwargs | ||||
|  | ||||
|     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 | ||||
|         user: User = self.request.user | ||||
|         if obj.election: | ||||
|   | ||||
| @@ -48,8 +48,7 @@ def get_default_view_group(): | ||||
|  | ||||
|  | ||||
| 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 | ||||
|     edit_groups allows to put any group as a forum admin | ||||
| @@ -157,7 +156,7 @@ class Forum(models.Model): | ||||
|             c.apply_rights_recursively() | ||||
|  | ||||
|     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: | ||||
|             self.owner_club = self.parent.owner_club | ||||
|             self.edit_groups.set(self.parent.edit_groups.all()) | ||||
| @@ -187,7 +186,7 @@ class Forum(models.Model): | ||||
|         return False | ||||
|  | ||||
|     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 = [] | ||||
|         cur = self | ||||
|         while cur.parent is not None: | ||||
| @@ -299,9 +298,7 @@ class ForumTopic(models.Model): | ||||
|  | ||||
|  | ||||
| class ForumMessage(models.Model): | ||||
|     """ | ||||
|     "A ForumMessage object represents a message in the forum" -- Cpt. Obvious | ||||
|     """ | ||||
|     """A message in the forum (thx Cpt. Obvious.).""" | ||||
|  | ||||
|     topic = models.ForeignKey( | ||||
|         ForumTopic, related_name="messages", on_delete=models.CASCADE | ||||
| @@ -425,7 +422,8 @@ class ForumMessageMeta(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". | ||||
|     However, this can be extended with lot of user preferences dedicated to a | ||||
|     user, such as the favourite topics, the signature, and so on... | ||||
|   | ||||
| @@ -118,11 +118,11 @@ class Command(BaseCommand): | ||||
|             self.make_important_citizen(u) | ||||
|  | ||||
|     def make_clubs(self): | ||||
|         """ | ||||
|         Create all the clubs (:class:`club.models.Club`) | ||||
|         and store them in `self.clubs` for fast access later. | ||||
|         """Create all the clubs (:class:`club.models.Club`). | ||||
|  | ||||
|         After creation, the clubs are stored in `self.clubs` for fast access later. | ||||
|         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 = [] | ||||
|         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()) | ||||
|  | ||||
|     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. | ||||
|         """ | ||||
| @@ -167,8 +166,7 @@ class Command(BaseCommand): | ||||
|         Subscription.objects.bulk_create(subs) | ||||
|  | ||||
|     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. | ||||
|  | ||||
| @@ -194,8 +192,7 @@ class Command(BaseCommand): | ||||
|         User.godfathers.through.objects.bulk_create(godfathers) | ||||
|  | ||||
|     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 | ||||
|         must have been called beforehand. | ||||
| @@ -265,8 +262,7 @@ class Command(BaseCommand): | ||||
|         Membership.objects.bulk_create(memberships) | ||||
|  | ||||
|     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. | ||||
|         """ | ||||
| @@ -301,11 +297,10 @@ class Command(BaseCommand): | ||||
|         self.picts = list(Picture.objects.filter(name__startswith="galaxy-").all()) | ||||
|  | ||||
|     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. | ||||
|  | ||||
|         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 = [] | ||||
|  | ||||
| @@ -363,9 +358,9 @@ class Command(BaseCommand): | ||||
|         PeoplePictureRelation.objects.bulk_create(self.pictures_tags) | ||||
|  | ||||
|     def make_important_citizen(self, uid: int): | ||||
|         """ | ||||
|         Make the user whose uid is given in parameter a more important citizen, | ||||
|         thus triggering many more connections to others (lanes) | ||||
|         """Make the user whose uid is given in parameter a more important citizen. | ||||
|  | ||||
|         This will trigger many more connections to others (lanes) | ||||
|         and dragging him towards the center of the Galaxy. | ||||
|  | ||||
|         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 | ||||
|         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] | ||||
|         u2 = self.users[uid - 100] | ||||
|   | ||||
| @@ -26,7 +26,7 @@ from __future__ import annotations | ||||
| import logging | ||||
| import math | ||||
| import time | ||||
| from typing import List, NamedTuple, Optional, TypedDict, Union | ||||
| from typing import NamedTuple, TypedDict | ||||
|  | ||||
| from django.db import models | ||||
| from django.db.models import Case, Count, F, Q, Value, When | ||||
| @@ -40,9 +40,9 @@ from sas.models import Picture | ||||
|  | ||||
|  | ||||
| class GalaxyStar(models.Model): | ||||
|     """ | ||||
|     Define a star (vertex -> user) in the galaxy graph, | ||||
|     storing a reference to its owner citizen. | ||||
|     """Define a star (vertex -> user) in the galaxy graph. | ||||
|  | ||||
|     Store a reference to its owner citizen. | ||||
|  | ||||
|     Stars are linked to each others through the :class:`GalaxyLane` model. | ||||
|  | ||||
| @@ -74,13 +74,14 @@ class GalaxyStar(models.Model): | ||||
|  | ||||
|  | ||||
| @property | ||||
| def current_star(self) -> Optional[GalaxyStar]: | ||||
|     """ | ||||
|     The star of this user in the :class:`Galaxy`. | ||||
| def current_star(self) -> GalaxyStar | None: | ||||
|     """The star of this user in the :class:`Galaxy`. | ||||
|  | ||||
|     Only take into account the most recent active galaxy. | ||||
|  | ||||
|     :return: The star of this user if there is an active Galaxy | ||||
|              and this user is a citizen of it, else ``None`` | ||||
|     Returns: | ||||
|         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() | ||||
|  | ||||
| @@ -90,10 +91,9 @@ User.current_star = current_star | ||||
|  | ||||
|  | ||||
| class GalaxyLane(models.Model): | ||||
|     """ | ||||
|     Define a lane (edge -> link between galaxy citizen) | ||||
|     in the galaxy map, storing a reference to both its | ||||
|     ends and the distance it covers. | ||||
|     """Define a lane (edge -> link between galaxy citizen) in the galaxy map. | ||||
|  | ||||
|     Store a reference to both its ends and the distance it covers. | ||||
|     Score details between citizen owning the stars is also stored here. | ||||
|     """ | ||||
|  | ||||
| @@ -138,8 +138,8 @@ class StarDict(TypedDict): | ||||
|  | ||||
|  | ||||
| class GalaxyDict(TypedDict): | ||||
|     nodes: List[StarDict] | ||||
|     links: List | ||||
|     nodes: list[StarDict] | ||||
|     links: list | ||||
|  | ||||
|  | ||||
| class RelationScore(NamedTuple): | ||||
| @@ -149,8 +149,8 @@ class RelationScore(NamedTuple): | ||||
|  | ||||
|  | ||||
| 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 | ||||
|     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 | ||||
| @@ -204,8 +204,8 @@ class Galaxy(models.Model): | ||||
|  | ||||
|     @classmethod | ||||
|     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 | ||||
|         higher scores towards the center of the galaxy. | ||||
|  | ||||
| @@ -231,10 +231,7 @@ class Galaxy(models.Model): | ||||
|  | ||||
|     @classmethod | ||||
|     def query_user_score(cls, user: User) -> int: | ||||
|         """ | ||||
|         Perform the db query to get the  individual score | ||||
|         of the given user in the galaxy. | ||||
|         """ | ||||
|         """Get the individual score of the given user in the galaxy.""" | ||||
|         score_query = ( | ||||
|             User.objects.filter(id=user.id) | ||||
|             .annotate( | ||||
| @@ -262,9 +259,9 @@ class Galaxy(models.Model): | ||||
|  | ||||
|     @classmethod | ||||
|     def compute_users_score(cls, user1: User, user2: User) -> RelationScore: | ||||
|         """ | ||||
|         Compute the relationship scores of the two given users | ||||
|         in the following fields : | ||||
|         """Compute the relationship scores of the two given users. | ||||
|  | ||||
|         The computation is done with the following fields : | ||||
|  | ||||
|         - family: if they have some godfather/godchild relation | ||||
|         - pictures: in how many pictures are both tagged | ||||
| @@ -277,11 +274,12 @@ class Galaxy(models.Model): | ||||
|  | ||||
|     @classmethod | ||||
|     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. | ||||
|  | ||||
|         :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( | ||||
|             Q(id=user1.id, godfathers=user2) | Q(id=user2.id, godfathers=user1) | ||||
| @@ -294,14 +292,14 @@ class Galaxy(models.Model): | ||||
|  | ||||
|     @classmethod | ||||
|     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 | ||||
|         of :class:`Picture` in which they have been both identified. | ||||
|         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.objects.filter(people__user__in=(user1,)) | ||||
| @@ -316,8 +314,7 @@ class Galaxy(models.Model): | ||||
|  | ||||
|     @classmethod | ||||
|     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 | ||||
|         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 | ||||
|         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( | ||||
|             members__in=user2.memberships.all() | ||||
| @@ -380,13 +378,13 @@ class Galaxy(models.Model): | ||||
|     ################### | ||||
|  | ||||
|     @classmethod | ||||
|     def scale_distance(cls, value: Union[int, float]) -> int: | ||||
|         """ | ||||
|         Given a numeric value, return a scaled value which can | ||||
|     def scale_distance(cls, value: int | float) -> int: | ||||
|         """Given a numeric value, return a scaled value which can | ||||
|         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 | ||||
|         if value == 0: | ||||
| @@ -409,8 +407,8 @@ class Galaxy(models.Model): | ||||
|         return int(value) | ||||
|  | ||||
|     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. | ||||
|         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. | ||||
| @@ -566,9 +564,7 @@ class Galaxy(models.Model): | ||||
|         ) | ||||
|  | ||||
|     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( | ||||
|             "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") | ||||
|  | ||||
|     def test_user_self_score(self): | ||||
|         """ | ||||
|         Test that individual user scores are correct | ||||
|         """ | ||||
|         """Test that individual user scores are correct.""" | ||||
|         with self.assertNumQueries(8): | ||||
|             assert Galaxy.compute_user_score(self.root) == 9 | ||||
|             assert Galaxy.compute_user_score(self.skia) == 10 | ||||
| @@ -60,9 +58,8 @@ class GalaxyTestModel(TestCase): | ||||
|             assert Galaxy.compute_user_score(self.com) == 1 | ||||
|  | ||||
|     def test_users_score(self): | ||||
|         """ | ||||
|         Test on the default dataset generated by the `populate` command | ||||
|         that the relation scores are correct | ||||
|         """Test on the default dataset generated by the `populate` command | ||||
|         that the relation scores are correct. | ||||
|         """ | ||||
|         expected_scores = { | ||||
|             "krophil": { | ||||
| @@ -138,8 +135,7 @@ class GalaxyTestModel(TestCase): | ||||
|         self.assertDictEqual(expected_scores, computed_scores) | ||||
|  | ||||
|     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. | ||||
|         """ | ||||
|         galaxy = Galaxy.objects.create() | ||||
| @@ -151,18 +147,14 @@ class GalaxyTestModel(TestCase): | ||||
| class GalaxyTestView(TestCase): | ||||
|     @classmethod | ||||
|     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") | ||||
|         galaxy = Galaxy.objects.create() | ||||
|         galaxy.rule(26)  # We want a fast test | ||||
|         cls.root = User.objects.get(username="root") | ||||
|  | ||||
|     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) | ||||
|         user = User.objects.get(last_name="n°500") | ||||
|         response = self.client.get(reverse("galaxy:user", args=[user.id])) | ||||
| @@ -173,20 +165,19 @@ class GalaxyTestView(TestCase): | ||||
|         ) | ||||
|  | ||||
|     def test_page_not_citizen(self): | ||||
|         """ | ||||
|         Test that trying to access the galaxy page of a user who is not | ||||
|         citizens return a 404 | ||||
|         """ | ||||
|         """Test that trying to access the galaxy page of non-citizen users return a 404.""" | ||||
|         self.client.force_login(self.root) | ||||
|         user = User.objects.get(last_name="n°1") | ||||
|         response = self.client.get(reverse("galaxy:user", args=[user.id])) | ||||
|         assert response.status_code == 404 | ||||
|  | ||||
|     def test_full_galaxy_state(self): | ||||
|         """ | ||||
|         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 | ||||
|         right data. | ||||
|         """Test with a more complete galaxy. | ||||
|  | ||||
|         This time, the test is done 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 right data. | ||||
|         """ | ||||
|         self.client.force_login(self.root) | ||||
|         response = self.client.get(reverse("galaxy:data")) | ||||
|   | ||||
| @@ -44,9 +44,7 @@ class Launderette(models.Model): | ||||
|         return reverse("launderette:launderette_list") | ||||
|  | ||||
|     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: | ||||
|             return False | ||||
|         launderette_club = Club.objects.filter( | ||||
| @@ -108,9 +106,7 @@ class Machine(models.Model): | ||||
|         ) | ||||
|  | ||||
|     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: | ||||
|             return False | ||||
|         launderette_club = Club.objects.filter( | ||||
| @@ -161,9 +157,7 @@ class Token(models.Model): | ||||
|             super().save(*args, **kwargs) | ||||
|  | ||||
|     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: | ||||
|             return False | ||||
|         launderette_club = Club.objects.filter( | ||||
|   | ||||
| @@ -38,26 +38,26 @@ from launderette.models import Launderette, Machine, Slot, Token | ||||
|  | ||||
|  | ||||
| class LaunderetteMainView(TemplateView): | ||||
|     """Main presentation view""" | ||||
|     """Main presentation view.""" | ||||
|  | ||||
|     template_name = "launderette/launderette_main.jinja" | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         """Add page to the context""" | ||||
|         """Add page to the context.""" | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
|         kwargs["page"] = Page.objects.filter(name="launderette").first() | ||||
|         return kwargs | ||||
|  | ||||
|  | ||||
| class LaunderetteBookMainView(CanViewMixin, ListView): | ||||
|     """Choose which launderette to book""" | ||||
|     """Choose which launderette to book.""" | ||||
|  | ||||
|     model = Launderette | ||||
|     template_name = "launderette/launderette_book_choose.jinja" | ||||
|  | ||||
|  | ||||
| class LaunderetteBookView(CanViewMixin, DetailView): | ||||
|     """Display the launderette schedule""" | ||||
|     """Display the launderette schedule.""" | ||||
|  | ||||
|     model = Launderette | ||||
|     pk_url_kwarg = "launderette_id" | ||||
| @@ -133,7 +133,7 @@ class LaunderetteBookView(CanViewMixin, DetailView): | ||||
|             currentDate += delta | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         """Add page to the context""" | ||||
|         """Add page to the context.""" | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
|         kwargs["planning"] = OrderedDict() | ||||
|         kwargs["slot_type"] = self.slot_type | ||||
| @@ -166,7 +166,7 @@ class LaunderetteBookView(CanViewMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class SlotDeleteView(CanEditPropMixin, DeleteView): | ||||
|     """Delete a slot""" | ||||
|     """Delete a slot.""" | ||||
|  | ||||
|     model = Slot | ||||
|     pk_url_kwarg = "slot_id" | ||||
| @@ -180,14 +180,14 @@ class SlotDeleteView(CanEditPropMixin, DeleteView): | ||||
|  | ||||
|  | ||||
| class LaunderetteListView(CanEditPropMixin, ListView): | ||||
|     """Choose which launderette to administer""" | ||||
|     """Choose which launderette to administer.""" | ||||
|  | ||||
|     model = Launderette | ||||
|     template_name = "launderette/launderette_list.jinja" | ||||
|  | ||||
|  | ||||
| class LaunderetteEditView(CanEditPropMixin, UpdateView): | ||||
|     """Edit a launderette""" | ||||
|     """Edit a launderette.""" | ||||
|  | ||||
|     model = Launderette | ||||
|     pk_url_kwarg = "launderette_id" | ||||
| @@ -196,7 +196,7 @@ class LaunderetteEditView(CanEditPropMixin, UpdateView): | ||||
|  | ||||
|  | ||||
| class LaunderetteCreateView(CanCreateMixin, CreateView): | ||||
|     """Create a new launderette""" | ||||
|     """Create a new launderette.""" | ||||
|  | ||||
|     model = Launderette | ||||
|     fields = ["name"] | ||||
| @@ -275,7 +275,7 @@ class ManageTokenForm(forms.Form): | ||||
|  | ||||
|  | ||||
| class LaunderetteAdminView(CanEditPropMixin, BaseFormView, DetailView): | ||||
|     """The admin page of the launderette""" | ||||
|     """The admin page of the launderette.""" | ||||
|  | ||||
|     model = Launderette | ||||
|     pk_url_kwarg = "launderette_id" | ||||
| @@ -297,9 +297,7 @@ class LaunderetteAdminView(CanEditPropMixin, BaseFormView, DetailView): | ||||
|             return self.form_invalid(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) | ||||
|         if form.is_valid(): | ||||
|             return super().form_valid(form) | ||||
| @@ -307,9 +305,7 @@ class LaunderetteAdminView(CanEditPropMixin, BaseFormView, DetailView): | ||||
|             return super().form_invalid(form) | ||||
|  | ||||
|     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) | ||||
|         if self.request.method == "GET": | ||||
|             kwargs["form"] = self.get_form() | ||||
| @@ -331,7 +327,7 @@ class GetLaunderetteUserForm(GetUserForm): | ||||
|  | ||||
|  | ||||
| class LaunderetteMainClickView(CanEditMixin, BaseFormView, DetailView): | ||||
|     """The click page of the launderette""" | ||||
|     """The click page of the launderette.""" | ||||
|  | ||||
|     model = Launderette | ||||
|     pk_url_kwarg = "launderette_id" | ||||
| @@ -347,16 +343,12 @@ class LaunderetteMainClickView(CanEditMixin, BaseFormView, DetailView): | ||||
|         return super().post(request, *args, **kwargs) | ||||
|  | ||||
|     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"] | ||||
|         return super().form_valid(form) | ||||
|  | ||||
|     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["counter"] = self.object.counter | ||||
|         kwargs["form"] = self.get_form() | ||||
| @@ -417,7 +409,7 @@ class ClickTokenForm(forms.BaseForm): | ||||
|  | ||||
|  | ||||
| class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView): | ||||
|     """The click page of the launderette""" | ||||
|     """The click page of the launderette.""" | ||||
|  | ||||
|     model = Launderette | ||||
|     pk_url_kwarg = "launderette_id" | ||||
| @@ -465,14 +457,14 @@ class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView): | ||||
|         return type("ClickForm", (ClickTokenForm,), 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.subscriber = self.customer.user | ||||
|         self.operator = request.user | ||||
|         return super().get(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.customer = Customer.objects.filter(user__id=self.kwargs["user_id"]).first() | ||||
|         self.subscriber = self.customer.user | ||||
| @@ -480,16 +472,12 @@ class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView): | ||||
|         return super().post(request, *args, **kwargs) | ||||
|  | ||||
|     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) | ||||
|         return super().form_valid(form) | ||||
|  | ||||
|     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) | ||||
|         if "form" not in kwargs.keys(): | ||||
|             kwargs["form"] = self.get_form() | ||||
| @@ -505,7 +493,7 @@ class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView): | ||||
|  | ||||
|  | ||||
| class MachineEditView(CanEditPropMixin, UpdateView): | ||||
|     """Edit a machine""" | ||||
|     """Edit a machine.""" | ||||
|  | ||||
|     model = Machine | ||||
|     pk_url_kwarg = "machine_id" | ||||
| @@ -514,7 +502,7 @@ class MachineEditView(CanEditPropMixin, UpdateView): | ||||
|  | ||||
|  | ||||
| class MachineDeleteView(CanEditPropMixin, DeleteView): | ||||
|     """Edit a machine""" | ||||
|     """Edit a machine.""" | ||||
|  | ||||
|     model = Machine | ||||
|     pk_url_kwarg = "machine_id" | ||||
| @@ -523,7 +511,7 @@ class MachineDeleteView(CanEditPropMixin, DeleteView): | ||||
|  | ||||
|  | ||||
| class MachineCreateView(CanCreateMixin, CreateView): | ||||
|     """Create a new machine""" | ||||
|     """Create a new machine.""" | ||||
|  | ||||
|     model = Machine | ||||
|     fields = ["name", "launderette", "type"] | ||||
|   | ||||
| @@ -155,9 +155,7 @@ class SearchFormListView(FormerSubscriberMixin, SingleObjectMixin, ListView): | ||||
|  | ||||
|  | ||||
| class SearchFormView(FormerSubscriberMixin, FormView): | ||||
|     """ | ||||
|     Allows users to search inside the user list | ||||
|     """ | ||||
|     """Allows users to search inside the user list.""" | ||||
|  | ||||
|     form_class = SearchForm | ||||
|  | ||||
| @@ -200,9 +198,7 @@ class SearchQuickFormView(SearchFormView): | ||||
|  | ||||
|  | ||||
| class SearchClearFormView(FormerSubscriberMixin, View): | ||||
|     """ | ||||
|     Clear SearchFormView and redirect to it | ||||
|     """ | ||||
|     """Clear SearchFormView and redirect to it.""" | ||||
|  | ||||
|     def dispatch(self, request, *args, **kwargs): | ||||
|         super().dispatch(request, *args, **kwargs) | ||||
|   | ||||
| @@ -30,9 +30,7 @@ from pedagogy.models import UV, UVComment, UVCommentReport | ||||
|  | ||||
|  | ||||
| class UVForm(forms.ModelForm): | ||||
|     """ | ||||
|     Form handeling creation and edit of an UV | ||||
|     """ | ||||
|     """Form handeling creation and edit of an UV.""" | ||||
|  | ||||
|     class Meta: | ||||
|         model = UV | ||||
| @@ -85,9 +83,7 @@ class StarList(forms.NumberInput): | ||||
|  | ||||
|  | ||||
| class UVCommentForm(forms.ModelForm): | ||||
|     """ | ||||
|     Form handeling creation and edit of an UVComment | ||||
|     """ | ||||
|     """Form handeling creation and edit of an UVComment.""" | ||||
|  | ||||
|     class Meta: | ||||
|         model = UVComment | ||||
| @@ -137,9 +133,7 @@ class UVCommentForm(forms.ModelForm): | ||||
|  | ||||
|  | ||||
| class UVCommentReportForm(forms.ModelForm): | ||||
|     """ | ||||
|     Form handeling creation and edit of an UVReport | ||||
|     """ | ||||
|     """Form handeling creation and edit of an UVReport.""" | ||||
|  | ||||
|     class Meta: | ||||
|         model = UVCommentReport | ||||
| @@ -159,9 +153,7 @@ class UVCommentReportForm(forms.ModelForm): | ||||
|  | ||||
|  | ||||
| class UVCommentModerationForm(forms.Form): | ||||
|     """ | ||||
|     Form handeling bulk comment deletion | ||||
|     """ | ||||
|     """Form handeling bulk comment deletion.""" | ||||
|  | ||||
|     accepted_reports = forms.ModelMultipleChoiceField( | ||||
|         UVCommentReport.objects.all(), | ||||
|   | ||||
| @@ -36,9 +36,7 @@ from core.models import User | ||||
|  | ||||
|  | ||||
| class UV(models.Model): | ||||
|     """ | ||||
|     Contains infos about an UV (course) | ||||
|     """ | ||||
|     """Contains infos about an UV (course).""" | ||||
|  | ||||
|     code = models.CharField( | ||||
|         _("code"), | ||||
| @@ -148,15 +146,11 @@ class UV(models.Model): | ||||
|         return reverse("pedagogy:uv_detail", kwargs={"uv_id": self.id}) | ||||
|  | ||||
|     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) | ||||
|  | ||||
|     def can_be_viewed_by(self, user): | ||||
|         """ | ||||
|         Only visible by subscribers | ||||
|         """ | ||||
|         """Only visible by subscribers.""" | ||||
|         return user.is_subscribed | ||||
|  | ||||
|     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()) | ||||
|  | ||||
|     def has_user_already_commented(self, user): | ||||
|         """ | ||||
|         Help prevent multiples comments from the same user | ||||
|         This function checks that no other comment has been posted by a specified user | ||||
|     def has_user_already_commented(self, user: User) -> bool: | ||||
|         """Help prevent multiples comments from the same user. | ||||
|  | ||||
|         :param user: core.models.User | ||||
|         :return: if the user has already posted a comment on this UV | ||||
|         :rtype: bool | ||||
|         This function checks that no other comment has been posted by a specified user. | ||||
|  | ||||
|         Returns: | ||||
|             True if the user has already posted a comment on this UV, else False. | ||||
|         """ | ||||
|         return self.comments.filter(author=user).exists() | ||||
|  | ||||
| @@ -199,9 +192,7 @@ class UV(models.Model): | ||||
|  | ||||
|  | ||||
| class UVComment(models.Model): | ||||
|     """ | ||||
|     A comment about an UV | ||||
|     """ | ||||
|     """A comment about an UV.""" | ||||
|  | ||||
|     author = models.ForeignKey( | ||||
|         User, | ||||
| @@ -261,28 +252,30 @@ class UVComment(models.Model): | ||||
|         super().save(*args, **kwargs) | ||||
|  | ||||
|     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) | ||||
|  | ||||
|     @cached_property | ||||
|     def is_reported(self): | ||||
|         """ | ||||
|         Return True if someone reported this UV | ||||
|         """ | ||||
|         """Return True if someone reported this UV.""" | ||||
|         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): | ||||
|     """ | ||||
|     Results got to an UV | ||||
|     """Results got to an UV. | ||||
|  | ||||
|     Views will be implemented after the first release | ||||
|     Will list every UV done by an user | ||||
|     Linked to user | ||||
|               uv | ||||
|     Contains a grade settings.SITH_PEDAGOGY_UV_RESULT_GRADE | ||||
|              a semester (P/A)20xx | ||||
|              a semester (P/A)20xx. | ||||
|     """ | ||||
|  | ||||
|     uv = models.ForeignKey( | ||||
| @@ -308,9 +301,7 @@ class UVResult(models.Model): | ||||
|  | ||||
|  | ||||
| class UVCommentReport(models.Model): | ||||
|     """ | ||||
|     Report an inapropriate comment | ||||
|     """ | ||||
|     """Report an inapropriate comment.""" | ||||
|  | ||||
|     comment = models.ForeignKey( | ||||
|         UVComment, | ||||
| @@ -334,9 +325,7 @@ class UVCommentReport(models.Model): | ||||
|         return self.comment.uv | ||||
|  | ||||
|     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) | ||||
|  | ||||
|  | ||||
| @@ -344,9 +333,9 @@ class UVCommentReport(models.Model): | ||||
|  | ||||
|  | ||||
| class UVSerializer(serializers.ModelSerializer): | ||||
|     """ | ||||
|     Custom seralizer for UVs | ||||
|     Allow adding more informations like absolute_url | ||||
|     """Custom seralizer for UVs. | ||||
|  | ||||
|     Allow adding more informations like absolute_url. | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|   | ||||
| @@ -29,9 +29,7 @@ from pedagogy.models import UV | ||||
|  | ||||
|  | ||||
| class IndexSignalProcessor(signals.BaseSignalProcessor): | ||||
|     """ | ||||
|     Auto update index on CRUD operations | ||||
|     """ | ||||
|     """Auto update index on CRUD operations.""" | ||||
|  | ||||
|     def setup(self): | ||||
|         # Listen only to the ``UV`` model. | ||||
| @@ -45,9 +43,7 @@ class IndexSignalProcessor(signals.BaseSignalProcessor): | ||||
|  | ||||
|  | ||||
| class UVIndex(indexes.SearchIndex, indexes.Indexable): | ||||
|     """ | ||||
|     Indexer class for UVs | ||||
|     """ | ||||
|     """Indexer class for UVs.""" | ||||
|  | ||||
|     text = BigCharFieldIndex(document=True, 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): | ||||
|     """ | ||||
|     Factory to help UV creation/update in post requests | ||||
|     """ | ||||
|     """Factory to help UV creation/update in post requests.""" | ||||
|     if exclude_list is None: | ||||
|         exclude_list = [] | ||||
|     uv = { | ||||
| @@ -79,9 +77,7 @@ def create_uv_template(user_id, code="IFC1", exclude_list=None): | ||||
|  | ||||
|  | ||||
| class UVCreation(TestCase): | ||||
|     """ | ||||
|     Test uv creation | ||||
|     """ | ||||
|     """Test uv creation.""" | ||||
|  | ||||
|     @classmethod | ||||
|     def setUpTestData(cls): | ||||
| @@ -291,9 +287,7 @@ class UVUpdateTest(TestCase): | ||||
|  | ||||
|  | ||||
| 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: | ||||
|         exclude_list = [] | ||||
|     comment = { | ||||
| @@ -312,9 +306,9 @@ def create_uv_comment_template(user_id, uv_code="PA00", exclude_list=None): | ||||
|  | ||||
|  | ||||
| class UVCommentCreationAndDisplay(TestCase): | ||||
|     """ | ||||
|     Test UVComment creation and it's display | ||||
|     Display and creation are the same view | ||||
|     """Test UVComment creation and its display. | ||||
|  | ||||
|     Display and creation are the same view. | ||||
|     """ | ||||
|  | ||||
|     @classmethod | ||||
| @@ -575,10 +569,7 @@ class UVCommentUpdateTest(TestCase): | ||||
|  | ||||
|  | ||||
| class UVSearchTest(TestCase): | ||||
|     """ | ||||
|     Test UV guide rights for view and API | ||||
|     Test that the API is working well | ||||
|     """ | ||||
|     """Test UV guide rights for view and API.""" | ||||
|  | ||||
|     @classmethod | ||||
|     def setUpTestData(cls): | ||||
| @@ -751,10 +742,7 @@ class UVSearchTest(TestCase): | ||||
|  | ||||
|  | ||||
| class UVModerationFormTest(TestCase): | ||||
|     """ | ||||
|     Test moderation view | ||||
|     Assert access rights and if the form works well | ||||
|     """ | ||||
|     """Assert access rights and if the form works well.""" | ||||
|  | ||||
|     @classmethod | ||||
|     def setUpTestData(cls): | ||||
| @@ -967,9 +955,9 @@ class UVModerationFormTest(TestCase): | ||||
|  | ||||
|  | ||||
| class UVCommentReportCreateTest(TestCase): | ||||
|     """ | ||||
|     Test report creation view view | ||||
|     Assert access rights and if you can create with it | ||||
|     """Test report creation view. | ||||
|  | ||||
|     Assert access rights and if you can create with it. | ||||
|     """ | ||||
|  | ||||
|     def setUp(self): | ||||
|   | ||||
| @@ -57,21 +57,15 @@ from pedagogy.models import UV, UVComment, UVCommentReport, UVSerializer | ||||
|  | ||||
|  | ||||
| class CanCreateUVFunctionMixin(View): | ||||
|     """ | ||||
|     Add the function can_create_uv(user) into the template | ||||
|     """ | ||||
|     """Add the function can_create_uv(user) into the template.""" | ||||
|  | ||||
|     @staticmethod | ||||
|     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()) | ||||
|  | ||||
|     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["can_create_uv"] = self.can_create_uv | ||||
|         return kwargs | ||||
| @@ -81,9 +75,9 @@ class CanCreateUVFunctionMixin(View): | ||||
|  | ||||
|  | ||||
| class UVDetailFormView(CanViewMixin, CanCreateUVFunctionMixin, DetailFormView): | ||||
|     """ | ||||
|     Dispaly every comment of an UV and detailed infos about it | ||||
|     Allow to comment the UV | ||||
|     """Display every comment of an UV and detailed infos about it. | ||||
|  | ||||
|     Allow to comment the UV. | ||||
|     """ | ||||
|  | ||||
|     model = UV | ||||
| @@ -109,9 +103,7 @@ class UVDetailFormView(CanViewMixin, CanCreateUVFunctionMixin, DetailFormView): | ||||
|  | ||||
|  | ||||
| class UVCommentUpdateView(CanEditPropMixin, UpdateView): | ||||
|     """ | ||||
|     Allow edit of a given comment | ||||
|     """ | ||||
|     """Allow edit of a given comment.""" | ||||
|  | ||||
|     model = UVComment | ||||
|     form_class = UVCommentForm | ||||
| @@ -132,9 +124,7 @@ class UVCommentUpdateView(CanEditPropMixin, UpdateView): | ||||
|  | ||||
|  | ||||
| class UVCommentDeleteView(CanEditPropMixin, DeleteView): | ||||
|     """ | ||||
|     Allow delete of a given comment | ||||
|     """ | ||||
|     """Allow delete of a given comment.""" | ||||
|  | ||||
|     model = UVComment | ||||
|     pk_url_kwarg = "comment_id" | ||||
| @@ -145,9 +135,7 @@ class UVCommentDeleteView(CanEditPropMixin, DeleteView): | ||||
|  | ||||
|  | ||||
| class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView): | ||||
|     """ | ||||
|     UV guide main page | ||||
|     """ | ||||
|     """UV guide main page.""" | ||||
|  | ||||
|     # This is very basic and is prone to changment | ||||
|  | ||||
| @@ -208,9 +196,7 @@ class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView): | ||||
|  | ||||
|  | ||||
| class UVCommentReportCreateView(CanCreateMixin, CreateView): | ||||
|     """ | ||||
|     Create a new report for an inapropriate comment | ||||
|     """ | ||||
|     """Create a new report for an inapropriate comment.""" | ||||
|  | ||||
|     model = UVCommentReport | ||||
|     form_class = UVCommentReportForm | ||||
| @@ -253,9 +239,7 @@ class UVCommentReportCreateView(CanCreateMixin, CreateView): | ||||
|  | ||||
|  | ||||
| class UVModerationFormView(FormView): | ||||
|     """ | ||||
|     Moderation interface (Privileged) | ||||
|     """ | ||||
|     """Moderation interface (Privileged).""" | ||||
|  | ||||
|     form_class = UVCommentModerationForm | ||||
|     template_name = "pedagogy/moderation.jinja" | ||||
| @@ -286,9 +270,7 @@ class UVModerationFormView(FormView): | ||||
|  | ||||
|  | ||||
| class UVCreateView(CanCreateMixin, CreateView): | ||||
|     """ | ||||
|     Add a new UV (Privileged) | ||||
|     """ | ||||
|     """Add a new UV (Privileged).""" | ||||
|  | ||||
|     model = UV | ||||
|     form_class = UVForm | ||||
| @@ -304,9 +286,7 @@ class UVCreateView(CanCreateMixin, CreateView): | ||||
|  | ||||
|  | ||||
| class UVDeleteView(CanEditPropMixin, DeleteView): | ||||
|     """ | ||||
|     Allow to delete an UV (Privileged) | ||||
|     """ | ||||
|     """Allow to delete an UV (Privileged).""" | ||||
|  | ||||
|     model = UV | ||||
|     pk_url_kwarg = "uv_id" | ||||
| @@ -317,9 +297,7 @@ class UVDeleteView(CanEditPropMixin, DeleteView): | ||||
|  | ||||
|  | ||||
| class UVUpdateView(CanEditPropMixin, UpdateView): | ||||
|     """ | ||||
|     Allow to edit an UV (Privilegied) | ||||
|     """ | ||||
|     """Allow to edit an UV (Privilegied).""" | ||||
|  | ||||
|     model = UV | ||||
|     form_class = UVForm | ||||
|   | ||||
| @@ -100,6 +100,9 @@ ignore = [ | ||||
| 	"DJ001",  # null=True in CharField/TextField. this one would require a migration | ||||
| ] | ||||
|  | ||||
| [tool.ruff.lint.pydocstyle] | ||||
| convention = "google" | ||||
|  | ||||
| [tool.pytest.ini_options] | ||||
| DJANGO_SETTINGS_MODULE = "sith.settings" | ||||
| python_files = ["tests.py", "test_*.py", "*_tests.py"] | ||||
|   | ||||
| @@ -29,9 +29,7 @@ from rootplace.views import delete_all_forum_user_messages | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     """ | ||||
|     Delete all forum messages from a user | ||||
|     """ | ||||
|     """Delete all forum messages from a user.""" | ||||
|  | ||||
|     help = "Delete all user's forum message" | ||||
|  | ||||
|   | ||||
| @@ -38,8 +38,8 @@ from forum.models import ForumMessageMeta | ||||
|  | ||||
|  | ||||
| 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 | ||||
|     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: | ||||
|     """ | ||||
|     Merge u2 into u1 | ||||
|     """Merge u2 into u1. | ||||
|  | ||||
|     This means that u1 shall receive everything that belonged to u2 : | ||||
|  | ||||
|         - pictures | ||||
| @@ -134,11 +134,12 @@ def merge_users(u1: User, u2: User) -> User: | ||||
|  | ||||
|  | ||||
| def delete_all_forum_user_messages(user, moderator, *, verbose=False): | ||||
|     """ | ||||
|     Create a ForumMessageMeta that says a forum | ||||
|         message is deleted on every forum message of an user | ||||
|     user: the user to delete messages from | ||||
|     moderator: the one marked as the moderator | ||||
|     """Soft delete all messages of a user. | ||||
|  | ||||
|     Args: | ||||
|         user: the user to delete messages from | ||||
|         moderator: the one marked as the moderator. | ||||
|         verbose: it True, print the deleted messages | ||||
|     """ | ||||
|     for message in user.forum_messages.all(): | ||||
|         if message.is_deleted(): | ||||
| @@ -184,10 +185,10 @@ class MergeUsersView(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 | ||||
|     GUI frontend to the dedicated command | ||||
|     GUI frontend to the dedicated command. | ||||
|     """ | ||||
|  | ||||
|     template_name = "rootplace/delete_user_messages.jinja" | ||||
| @@ -209,9 +210,7 @@ class DeleteAllForumUserMessagesView(FormView): | ||||
|  | ||||
|  | ||||
| class OperationLogListView(ListView, CanEditPropMixin): | ||||
|     """ | ||||
|     List all logs | ||||
|     """ | ||||
|     """List all logs.""" | ||||
|  | ||||
|     model = OperationLog | ||||
|     template_name = "rootplace/logs.jinja" | ||||
|   | ||||
| @@ -221,10 +221,7 @@ def sas_notification_callback(notif): | ||||
|  | ||||
|  | ||||
| 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, | ||||
|   | ||||
| @@ -22,8 +22,7 @@ | ||||
| # | ||||
| # | ||||
|  | ||||
| """ | ||||
| Django settings for sith project. | ||||
| """Django settings for sith project. | ||||
|  | ||||
| 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 django.conf import settings | ||||
| 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``. | ||||
|  | ||||
|   | ||||
| @@ -31,9 +31,7 @@ from counter.models import Counter, ProductType | ||||
|  | ||||
|  | ||||
| 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) | ||||
|     counter = models.OneToOneField( | ||||
| @@ -54,9 +52,7 @@ class Stock(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) | ||||
|     unit_quantity = models.IntegerField( | ||||
| @@ -95,9 +91,7 @@ class StockItem(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")) | ||||
|     name = models.CharField(_("name"), max_length=64) | ||||
| @@ -118,7 +112,7 @@ class ShoppingList(models.Model): | ||||
|  | ||||
|  | ||||
| class ShoppingListItem(models.Model): | ||||
|     """""" | ||||
|     """An Item on a shopping list.""" | ||||
|  | ||||
|     shopping_lists = models.ManyToManyField( | ||||
|         ShoppingList, | ||||
|   | ||||
| @@ -41,9 +41,7 @@ from stock.models import ShoppingList, ShoppingListItem, Stock, StockItem | ||||
|  | ||||
|  | ||||
| class StockItemList(CounterAdminTabsMixin, CanCreateMixin, ListView): | ||||
|     """ | ||||
|     The stockitems list view for the counter owner | ||||
|     """ | ||||
|     """The stockitems list view for the counter owner.""" | ||||
|  | ||||
|     model = Stock | ||||
|     template_name = "stock/stock_item_list.jinja" | ||||
| @@ -58,9 +56,7 @@ class StockItemList(CounterAdminTabsMixin, CanCreateMixin, ListView): | ||||
|  | ||||
|  | ||||
| class StockListView(CounterAdminTabsMixin, CanViewMixin, ListView): | ||||
|     """ | ||||
|     A list view for the admins | ||||
|     """ | ||||
|     """A list view for the admins.""" | ||||
|  | ||||
|     model = Stock | ||||
|     template_name = "stock/stock_list.jinja" | ||||
| @@ -68,9 +64,7 @@ class StockListView(CounterAdminTabsMixin, CanViewMixin, ListView): | ||||
|  | ||||
|  | ||||
| class StockEditForm(forms.ModelForm): | ||||
|     """ | ||||
|     A form to change stock's characteristics | ||||
|     """ | ||||
|     """A form to change stock's characteristics.""" | ||||
|  | ||||
|     class Meta: | ||||
|         model = Stock | ||||
| @@ -84,9 +78,7 @@ class StockEditForm(forms.ModelForm): | ||||
|  | ||||
|  | ||||
| class StockEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView): | ||||
|     """ | ||||
|     An edit view for the stock | ||||
|     """ | ||||
|     """An edit view for the stock.""" | ||||
|  | ||||
|     model = Stock | ||||
|     form_class = modelform_factory(Stock, fields=["name", "counter"]) | ||||
| @@ -96,9 +88,7 @@ class StockEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView): | ||||
|  | ||||
|  | ||||
| class StockItemEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView): | ||||
|     """ | ||||
|     An edit view for a stock item | ||||
|     """ | ||||
|     """An edit view for a stock item.""" | ||||
|  | ||||
|     model = StockItem | ||||
|     form_class = modelform_factory( | ||||
| @@ -118,9 +108,7 @@ class StockItemEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView): | ||||
|  | ||||
|  | ||||
| class StockCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView): | ||||
|     """ | ||||
|     A create view for a new Stock | ||||
|     """ | ||||
|     """A create view for a new Stock.""" | ||||
|  | ||||
|     model = Stock | ||||
|     form_class = modelform_factory(Stock, fields=["name", "counter"]) | ||||
| @@ -137,9 +125,7 @@ class StockCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView): | ||||
|  | ||||
|  | ||||
| class StockItemCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView): | ||||
|     """ | ||||
|     A create view for a new StockItem | ||||
|     """ | ||||
|     """A create view for a new StockItem.""" | ||||
|  | ||||
|     model = StockItem | ||||
|     form_class = modelform_factory( | ||||
| @@ -170,9 +156,7 @@ class StockItemCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView): | ||||
|  | ||||
|  | ||||
| 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 | ||||
|     template_name = "stock/stock_shopping_list.jinja" | ||||
| @@ -225,9 +209,7 @@ class StockItemQuantityForm(forms.BaseForm): | ||||
| class StockItemQuantityBaseFormView( | ||||
|     CounterAdminTabsMixin, CanEditMixin, DetailView, BaseFormView | ||||
| ): | ||||
|     """ | ||||
|     docstring for StockItemOutList | ||||
|     """ | ||||
|     """docstring for StockItemOutList.""" | ||||
|  | ||||
|     model = StockItem | ||||
|     template_name = "stock/shopping_list_quantity.jinja" | ||||
| @@ -266,16 +248,12 @@ class StockItemQuantityBaseFormView( | ||||
|         return type("StockItemQuantityForm", (StockItemQuantityForm,), kwargs) | ||||
|  | ||||
|     def get(self, request, *args, **kwargs): | ||||
|         """ | ||||
|         Simple get view | ||||
|         """ | ||||
|         """Simple get view.""" | ||||
|         self.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first() | ||||
|         return super().get(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.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first() | ||||
|         return super().post(request, *args, **kwargs) | ||||
| @@ -297,7 +275,7 @@ class StockItemQuantityBaseFormView( | ||||
|  | ||||
|  | ||||
| class StockShoppingListItemListView(CounterAdminTabsMixin, CanViewMixin, ListView): | ||||
|     """docstring for StockShoppingListItemListView""" | ||||
|     """docstring for StockShoppingListItemListView.""" | ||||
|  | ||||
|     model = ShoppingList | ||||
|     template_name = "stock/shopping_list_items.jinja" | ||||
| @@ -314,9 +292,7 @@ class StockShoppingListItemListView(CounterAdminTabsMixin, CanViewMixin, ListVie | ||||
|  | ||||
|  | ||||
| class StockShoppingListDeleteView(CounterAdminTabsMixin, CanEditMixin, DeleteView): | ||||
|     """ | ||||
|     Delete a ShoppingList (for the resonsible account) | ||||
|     """ | ||||
|     """Delete a ShoppingList (for the resonsible account).""" | ||||
|  | ||||
|     model = ShoppingList | ||||
|     pk_url_kwarg = "shoppinglist_id" | ||||
| @@ -330,9 +306,7 @@ class StockShoppingListDeleteView(CounterAdminTabsMixin, CanEditMixin, DeleteVie | ||||
|  | ||||
|  | ||||
| class StockShopppingListSetDone(CanEditMixin, DetailView): | ||||
|     """ | ||||
|     Set a ShoppingList as done | ||||
|     """ | ||||
|     """Set a ShoppingList as done.""" | ||||
|  | ||||
|     model = ShoppingList | ||||
|     pk_url_kwarg = "shoppinglist_id" | ||||
| @@ -361,9 +335,7 @@ class StockShopppingListSetDone(CanEditMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class StockShopppingListSetTodo(CanEditMixin, DetailView): | ||||
|     """ | ||||
|     Set a ShoppingList as done | ||||
|     """ | ||||
|     """Set a ShoppingList as done.""" | ||||
|  | ||||
|     model = ShoppingList | ||||
|     pk_url_kwarg = "shoppinglist_id" | ||||
| @@ -415,9 +387,7 @@ class StockUpdateAfterShopppingForm(forms.BaseForm): | ||||
| class StockUpdateAfterShopppingBaseFormView( | ||||
|     CounterAdminTabsMixin, CanEditMixin, DetailView, BaseFormView | ||||
| ): | ||||
|     """ | ||||
|     docstring for StockUpdateAfterShopppingBaseFormView | ||||
|     """ | ||||
|     """docstring for StockUpdateAfterShopppingBaseFormView.""" | ||||
|  | ||||
|     model = ShoppingList | ||||
|     template_name = "stock/update_after_shopping.jinja" | ||||
| @@ -453,9 +423,7 @@ class StockUpdateAfterShopppingBaseFormView( | ||||
|         return super().get(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.shoppinglist = ShoppingList.objects.filter( | ||||
|             id=self.kwargs["shoppinglist_id"] | ||||
| @@ -463,9 +431,7 @@ class StockUpdateAfterShopppingBaseFormView( | ||||
|         return super().post(request, *args, **kwargs) | ||||
|  | ||||
|     def form_valid(self, form): | ||||
|         """ | ||||
|         We handle here the redirection | ||||
|         """ | ||||
|         """We handle here the redirection.""" | ||||
|         return super().form_valid(form) | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
| @@ -484,9 +450,7 @@ class StockUpdateAfterShopppingBaseFormView( | ||||
|  | ||||
|  | ||||
| class StockTakeItemsForm(forms.BaseForm): | ||||
|     """ | ||||
|     docstring for StockTakeItemsFormView | ||||
|     """ | ||||
|     """docstring for StockTakeItemsFormView.""" | ||||
|  | ||||
|     def clean(self): | ||||
|         with transaction.atomic(): | ||||
| @@ -502,9 +466,7 @@ class StockTakeItemsForm(forms.BaseForm): | ||||
| class StockTakeItemsBaseFormView( | ||||
|     CounterTabsMixin, CanEditMixin, DetailView, BaseFormView | ||||
| ): | ||||
|     """ | ||||
|     docstring for StockTakeItemsBaseFormView | ||||
|     """ | ||||
|     """docstring for StockTakeItemsBaseFormView.""" | ||||
|  | ||||
|     model = StockItem | ||||
|     template_name = "stock/stock_take_items.jinja" | ||||
| @@ -535,16 +497,11 @@ class StockTakeItemsBaseFormView( | ||||
|         return type("StockTakeItemsForm", (StockTakeItemsForm,), kwargs) | ||||
|  | ||||
|     def get(self, request, *args, **kwargs): | ||||
|         """ | ||||
|         Simple get view | ||||
|         """ | ||||
|         self.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first() | ||||
|         return super().get(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.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first() | ||||
|         if self.stock.counter.type == "BAR" and not ( | ||||
|   | ||||
| @@ -108,14 +108,15 @@ class Subscription(models.Model): | ||||
|  | ||||
|     @staticmethod | ||||
|     def compute_start(d: date = None, duration: int = 1, user: User = None) -> date: | ||||
|         """ | ||||
|         This function computes the start date of the subscription with respect to the given date (default is today), | ||||
|         """Computes the start date of the subscription. | ||||
|  | ||||
|         The computation is done with respect to the given date (default is today) | ||||
|         and the start date given in settings.SITH_SEMESTER_START_AUTUMN. | ||||
|         It takes the nearest past start date. | ||||
|         Exemples: with SITH_SEMESTER_START_AUTUMN = (8, 15) | ||||
|             Today      -> Start date | ||||
|             2015-03-17 -> 2015-02-15 | ||||
|             2015-01-11 -> 2014-08-15 | ||||
|             2015-01-11 -> 2014-08-15. | ||||
|         """ | ||||
|         if not d: | ||||
|             d = date.today() | ||||
| @@ -129,14 +130,21 @@ class Subscription(models.Model): | ||||
|  | ||||
|     @staticmethod | ||||
|     def compute_end(duration: int, start: date = None, user: User = None) -> date: | ||||
|         """ | ||||
|         This function compute the end date of the subscription given a start date and a duration in number of semester | ||||
|         Exemple: | ||||
|         """Compute the end date of the subscription. | ||||
|  | ||||
|         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 | ||||
|             2015-09-18 - 1 -> 2016-03-18 | ||||
|             2015-09-18 - 2 -> 2016-09-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: | ||||
|             start = Subscription.compute_start(duration=duration, user=user) | ||||
|   | ||||
| @@ -45,8 +45,8 @@ class AvailableTrombiManager(models.Manager): | ||||
|  | ||||
|  | ||||
| class Trombi(models.Model): | ||||
|     """ | ||||
|     This is the main class, the Trombi itself. | ||||
|     """Main class of the trombi, the Trombi itself. | ||||
|  | ||||
|     It contains the deadlines for the users, and the link to the club that makes | ||||
|     its Trombi. | ||||
|     """ | ||||
| @@ -103,10 +103,10 @@ class Trombi(models.Model): | ||||
|  | ||||
|  | ||||
| class TrombiUser(models.Model): | ||||
|     """ | ||||
|     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 | ||||
|     Trombi into the core. | ||||
|     """Bound between a `User` and a `Trombi`. | ||||
|  | ||||
|     This class is here to avoid cross-references between the core, club, | ||||
|     and trombi modules. | ||||
|     It also adds the pictures to the profile without needing all the security | ||||
|     like the other SithFiles. | ||||
|     """ | ||||
| @@ -172,10 +172,7 @@ class TrombiUser(models.Model): | ||||
|  | ||||
|  | ||||
| class TrombiComment(models.Model): | ||||
|     """ | ||||
|     This represent a comment given by someone to someone else in the same Trombi | ||||
|     instance. | ||||
|     """ | ||||
|     """A comment given by someone to someone else in the same Trombi instance.""" | ||||
|  | ||||
|     author = models.ForeignKey( | ||||
|         TrombiUser, | ||||
| @@ -202,9 +199,7 @@ class TrombiComment(models.Model): | ||||
|  | ||||
|  | ||||
| class TrombiClubMembership(models.Model): | ||||
|     """ | ||||
|     This represent a membership to a club | ||||
|     """ | ||||
|     """A membership in a club.""" | ||||
|  | ||||
|     user = models.ForeignKey( | ||||
|         TrombiUser, | ||||
|   | ||||
| @@ -94,9 +94,7 @@ class TrombiTabsMixin(TabedViewMixin): | ||||
|  | ||||
|  | ||||
| class UserIsInATrombiMixin(View): | ||||
|     """ | ||||
|     This view check if the requested user has a trombi_user attribute | ||||
|     """ | ||||
|     """Check if the requested user has a trombi_user attribute.""" | ||||
|  | ||||
|     def dispatch(self, request, *args, **kwargs): | ||||
|         if not hasattr(self.request.user, "trombi_user"): | ||||
| @@ -118,18 +116,14 @@ class TrombiForm(forms.ModelForm): | ||||
|  | ||||
|  | ||||
| class TrombiCreateView(CanCreateMixin, CreateView): | ||||
|     """ | ||||
|     Create a trombi for a club | ||||
|     """ | ||||
|     """Create a trombi for a club.""" | ||||
|  | ||||
|     model = Trombi | ||||
|     form_class = TrombiForm | ||||
|     template_name = "core/create.jinja" | ||||
|  | ||||
|     def post(self, request, *args, **kwargs): | ||||
|         """ | ||||
|         Affect club | ||||
|         """ | ||||
|         """Affect club.""" | ||||
|         form = self.get_form() | ||||
|         if form.is_valid(): | ||||
|             club = get_object_or_404(Club, id=self.kwargs["club_id"]) | ||||
| @@ -304,9 +298,7 @@ class UserTrombiForm(forms.Form): | ||||
| class UserTrombiToolsView( | ||||
|     QuickNotifMixin, TrombiTabsMixin, UserIsLoggedMixin, TemplateView | ||||
| ): | ||||
|     """ | ||||
|     Display a user's trombi tools | ||||
|     """ | ||||
|     """Display a user's trombi tools.""" | ||||
|  | ||||
|     template_name = "trombi/user_tools.jinja" | ||||
|     current_tab = "tools" | ||||
| @@ -466,9 +458,7 @@ class UserTrombiProfileView(TrombiTabsMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class TrombiCommentFormView(LoginRequiredMixin, View): | ||||
|     """ | ||||
|     Create/edit a trombi comment | ||||
|     """ | ||||
|     """Create/edit a trombi comment.""" | ||||
|  | ||||
|     model = TrombiComment | ||||
|     fields = ["content"] | ||||
|   | ||||
		Reference in New Issue
	
	Block a user