use google convention for docstrings

This commit is contained in:
thomas girod 2024-07-12 09:34:16 +02:00
parent 07b625d4aa
commit 8c69a94488
72 changed files with 970 additions and 1694 deletions

View File

@ -29,9 +29,7 @@ from core.models import SithFile, User
class CurrencyField(models.DecimalField): class CurrencyField(models.DecimalField):
""" """Custom database field used for currency."""
This is a custom database field used for currency
"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs["max_digits"] = 12 kwargs["max_digits"] = 12
@ -71,30 +69,22 @@ class Company(models.Model):
return self.name return self.name
def is_owned_by(self, user): def is_owned_by(self, user):
""" """Check if that object can be edited by the given user."""
Method to see if that object can be edited by the given user
"""
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True return True
return False return False
def can_be_edited_by(self, user): def can_be_edited_by(self, user):
""" """Check if that object can be edited by the given user."""
Method to see if that object can be edited by the given user return user.memberships.filter(
""" end_date=None, club__role=settings.SITH_CLUB_ROLES_ID["Treasurer"]
for club in user.memberships.filter(end_date=None).all(): ).exists()
if club and club.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]:
return True
return False
def can_be_viewed_by(self, user): def can_be_viewed_by(self, user):
""" """Check if that object can be viewed by the given user."""
Method to see if that object can be viewed by the given user return user.memberships.filter(
""" end_date=None, club__role_gte=settings.SITH_CLUB_ROLES_ID["Treasurer"]
for club in user.memberships.filter(end_date=None).all(): ).exists()
if club and club.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
return True
return False
class BankAccount(models.Model): class BankAccount(models.Model):
@ -119,9 +109,7 @@ class BankAccount(models.Model):
return reverse("accounting:bank_details", kwargs={"b_account_id": self.id}) return reverse("accounting:bank_details", kwargs={"b_account_id": self.id})
def is_owned_by(self, user): def is_owned_by(self, user):
""" """Check if that object can be edited by the given user."""
Method to see if that object can be edited by the given user
"""
if user.is_anonymous: if user.is_anonymous:
return False return False
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
@ -158,9 +146,7 @@ class ClubAccount(models.Model):
return reverse("accounting:club_details", kwargs={"c_account_id": self.id}) return reverse("accounting:club_details", kwargs={"c_account_id": self.id})
def is_owned_by(self, user): def is_owned_by(self, user):
""" """Check if that object can be edited by the given user."""
Method to see if that object can be edited by the given user
"""
if user.is_anonymous: if user.is_anonymous:
return False return False
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
@ -168,18 +154,14 @@ class ClubAccount(models.Model):
return False return False
def can_be_edited_by(self, user): def can_be_edited_by(self, user):
""" """Check if that object can be edited by the given user."""
Method to see if that object can be edited by the given user
"""
m = self.club.get_membership_for(user) m = self.club.get_membership_for(user)
if m and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]: if m and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]:
return True return True
return False return False
def can_be_viewed_by(self, user): def can_be_viewed_by(self, user):
""" """Check if that object can be viewed by the given user."""
Method to see if that object can be viewed by the given user
"""
m = self.club.get_membership_for(user) m = self.club.get_membership_for(user)
if m and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]: if m and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
return True return True
@ -202,9 +184,7 @@ class ClubAccount(models.Model):
class GeneralJournal(models.Model): class GeneralJournal(models.Model):
""" """Class storing all the operations for a period of time."""
Class storing all the operations for a period of time
"""
start_date = models.DateField(_("start date")) start_date = models.DateField(_("start date"))
end_date = models.DateField(_("end date"), null=True, blank=True, default=None) end_date = models.DateField(_("end date"), null=True, blank=True, default=None)
@ -231,9 +211,7 @@ class GeneralJournal(models.Model):
return reverse("accounting:journal_details", kwargs={"j_id": self.id}) return reverse("accounting:journal_details", kwargs={"j_id": self.id})
def is_owned_by(self, user): def is_owned_by(self, user):
""" """Check if that object can be edited by the given user."""
Method to see if that object can be edited by the given user
"""
if user.is_anonymous: if user.is_anonymous:
return False return False
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
@ -243,9 +221,7 @@ class GeneralJournal(models.Model):
return False return False
def can_be_edited_by(self, user): def can_be_edited_by(self, user):
""" """Check if that object can be edited by the given user."""
Method to see if that object can be edited by the given user
"""
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True return True
if self.club_account.can_be_edited_by(user): if self.club_account.can_be_edited_by(user):
@ -271,9 +247,7 @@ class GeneralJournal(models.Model):
class Operation(models.Model): class Operation(models.Model):
""" """An operation is a line in the journal, a debit or a credit."""
An operation is a line in the journal, a debit or a credit
"""
number = models.IntegerField(_("number")) number = models.IntegerField(_("number"))
journal = models.ForeignKey( journal = models.ForeignKey(
@ -422,9 +396,7 @@ class Operation(models.Model):
return tar return tar
def is_owned_by(self, user): def is_owned_by(self, user):
""" """Check if that object can be edited by the given user."""
Method to see if that object can be edited by the given user
"""
if user.is_anonymous: if user.is_anonymous:
return False return False
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
@ -437,9 +409,7 @@ class Operation(models.Model):
return False return False
def can_be_edited_by(self, user): def can_be_edited_by(self, user):
""" """Check if that object can be edited by the given user."""
Method to see if that object can be edited by the given user
"""
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True return True
if self.journal.closed: if self.journal.closed:
@ -451,10 +421,9 @@ class Operation(models.Model):
class AccountingType(models.Model): class AccountingType(models.Model):
""" """Accounting types.
Class describing the accounting types.
Thoses are numbers used in accounting to classify operations Those are numbers used in accounting to classify operations
""" """
code = models.CharField( code = models.CharField(
@ -488,9 +457,7 @@ class AccountingType(models.Model):
return reverse("accounting:type_list") return reverse("accounting:type_list")
def is_owned_by(self, user): def is_owned_by(self, user):
""" """Check if that object can be edited by the given user."""
Method to see if that object can be edited by the given user
"""
if user.is_anonymous: if user.is_anonymous:
return False return False
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
@ -499,9 +466,7 @@ class AccountingType(models.Model):
class SimplifiedAccountingType(models.Model): class SimplifiedAccountingType(models.Model):
""" """Simplified version of `AccountingType`."""
Class describing the simplified accounting types.
"""
label = models.CharField(_("label"), max_length=128) label = models.CharField(_("label"), max_length=128)
accounting_type = models.ForeignKey( accounting_type = models.ForeignKey(
@ -533,7 +498,7 @@ class SimplifiedAccountingType(models.Model):
class Label(models.Model): class Label(models.Model):
"""Label allow a club to sort its operations""" """Label allow a club to sort its operations."""
name = models.CharField(_("label"), max_length=64) name = models.CharField(_("label"), max_length=64)
club_account = models.ForeignKey( club_account = models.ForeignKey(

View File

@ -53,9 +53,7 @@ from counter.models import Counter, Product, Selling
class BankAccountListView(CanViewMixin, ListView): class BankAccountListView(CanViewMixin, ListView):
""" """A list view for the admins."""
A list view for the admins
"""
model = BankAccount model = BankAccount
template_name = "accounting/bank_account_list.jinja" template_name = "accounting/bank_account_list.jinja"
@ -66,18 +64,14 @@ class BankAccountListView(CanViewMixin, ListView):
class SimplifiedAccountingTypeListView(CanViewMixin, ListView): class SimplifiedAccountingTypeListView(CanViewMixin, ListView):
""" """A list view for the admins."""
A list view for the admins
"""
model = SimplifiedAccountingType model = SimplifiedAccountingType
template_name = "accounting/simplifiedaccountingtype_list.jinja" template_name = "accounting/simplifiedaccountingtype_list.jinja"
class SimplifiedAccountingTypeEditView(CanViewMixin, UpdateView): class SimplifiedAccountingTypeEditView(CanViewMixin, UpdateView):
""" """An edit view for the admins."""
An edit view for the admins
"""
model = SimplifiedAccountingType model = SimplifiedAccountingType
pk_url_kwarg = "type_id" pk_url_kwarg = "type_id"
@ -86,9 +80,7 @@ class SimplifiedAccountingTypeEditView(CanViewMixin, UpdateView):
class SimplifiedAccountingTypeCreateView(CanCreateMixin, CreateView): class SimplifiedAccountingTypeCreateView(CanCreateMixin, CreateView):
""" """Create an accounting type (for the admins)."""
Create an accounting type (for the admins)
"""
model = SimplifiedAccountingType model = SimplifiedAccountingType
fields = ["label", "accounting_type"] fields = ["label", "accounting_type"]
@ -99,18 +91,14 @@ class SimplifiedAccountingTypeCreateView(CanCreateMixin, CreateView):
class AccountingTypeListView(CanViewMixin, ListView): class AccountingTypeListView(CanViewMixin, ListView):
""" """A list view for the admins."""
A list view for the admins
"""
model = AccountingType model = AccountingType
template_name = "accounting/accountingtype_list.jinja" template_name = "accounting/accountingtype_list.jinja"
class AccountingTypeEditView(CanViewMixin, UpdateView): class AccountingTypeEditView(CanViewMixin, UpdateView):
""" """An edit view for the admins."""
An edit view for the admins
"""
model = AccountingType model = AccountingType
pk_url_kwarg = "type_id" pk_url_kwarg = "type_id"
@ -119,9 +107,7 @@ class AccountingTypeEditView(CanViewMixin, UpdateView):
class AccountingTypeCreateView(CanCreateMixin, CreateView): class AccountingTypeCreateView(CanCreateMixin, CreateView):
""" """Create an accounting type (for the admins)."""
Create an accounting type (for the admins)
"""
model = AccountingType model = AccountingType
fields = ["code", "label", "movement_type"] fields = ["code", "label", "movement_type"]
@ -132,9 +118,7 @@ class AccountingTypeCreateView(CanCreateMixin, CreateView):
class BankAccountEditView(CanViewMixin, UpdateView): class BankAccountEditView(CanViewMixin, UpdateView):
""" """An edit view for the admins."""
An edit view for the admins
"""
model = BankAccount model = BankAccount
pk_url_kwarg = "b_account_id" pk_url_kwarg = "b_account_id"
@ -143,9 +127,7 @@ class BankAccountEditView(CanViewMixin, UpdateView):
class BankAccountDetailView(CanViewMixin, DetailView): class BankAccountDetailView(CanViewMixin, DetailView):
""" """A detail view, listing every club account."""
A detail view, listing every club account
"""
model = BankAccount model = BankAccount
pk_url_kwarg = "b_account_id" pk_url_kwarg = "b_account_id"
@ -153,9 +135,7 @@ class BankAccountDetailView(CanViewMixin, DetailView):
class BankAccountCreateView(CanCreateMixin, CreateView): class BankAccountCreateView(CanCreateMixin, CreateView):
""" """Create a bank account (for the admins)."""
Create a bank account (for the admins)
"""
model = BankAccount model = BankAccount
fields = ["name", "club", "iban", "number"] fields = ["name", "club", "iban", "number"]
@ -165,9 +145,7 @@ class BankAccountCreateView(CanCreateMixin, CreateView):
class BankAccountDeleteView( class BankAccountDeleteView(
CanEditPropMixin, DeleteView CanEditPropMixin, DeleteView
): # TODO change Delete to Close ): # TODO change Delete to Close
""" """Delete a bank account (for the admins)."""
Delete a bank account (for the admins)
"""
model = BankAccount model = BankAccount
pk_url_kwarg = "b_account_id" pk_url_kwarg = "b_account_id"
@ -179,9 +157,7 @@ class BankAccountDeleteView(
class ClubAccountEditView(CanViewMixin, UpdateView): class ClubAccountEditView(CanViewMixin, UpdateView):
""" """An edit view for the admins."""
An edit view for the admins
"""
model = ClubAccount model = ClubAccount
pk_url_kwarg = "c_account_id" pk_url_kwarg = "c_account_id"
@ -190,9 +166,7 @@ class ClubAccountEditView(CanViewMixin, UpdateView):
class ClubAccountDetailView(CanViewMixin, DetailView): class ClubAccountDetailView(CanViewMixin, DetailView):
""" """A detail view, listing every journal."""
A detail view, listing every journal
"""
model = ClubAccount model = ClubAccount
pk_url_kwarg = "c_account_id" pk_url_kwarg = "c_account_id"
@ -200,9 +174,7 @@ class ClubAccountDetailView(CanViewMixin, DetailView):
class ClubAccountCreateView(CanCreateMixin, CreateView): class ClubAccountCreateView(CanCreateMixin, CreateView):
""" """Create a club account (for the admins)."""
Create a club account (for the admins)
"""
model = ClubAccount model = ClubAccount
fields = ["name", "club", "bank_account"] fields = ["name", "club", "bank_account"]
@ -220,9 +192,7 @@ class ClubAccountCreateView(CanCreateMixin, CreateView):
class ClubAccountDeleteView( class ClubAccountDeleteView(
CanEditPropMixin, DeleteView CanEditPropMixin, DeleteView
): # TODO change Delete to Close ): # TODO change Delete to Close
""" """Delete a club account (for the admins)."""
Delete a club account (for the admins)
"""
model = ClubAccount model = ClubAccount
pk_url_kwarg = "c_account_id" pk_url_kwarg = "c_account_id"
@ -282,9 +252,7 @@ class JournalTabsMixin(TabedViewMixin):
class JournalCreateView(CanCreateMixin, CreateView): class JournalCreateView(CanCreateMixin, CreateView):
""" """Create a general journal."""
Create a general journal
"""
model = GeneralJournal model = GeneralJournal
form_class = modelform_factory( form_class = modelform_factory(
@ -304,9 +272,7 @@ class JournalCreateView(CanCreateMixin, CreateView):
class JournalDetailView(JournalTabsMixin, CanViewMixin, DetailView): class JournalDetailView(JournalTabsMixin, CanViewMixin, DetailView):
""" """A detail view, listing every operation."""
A detail view, listing every operation
"""
model = GeneralJournal model = GeneralJournal
pk_url_kwarg = "j_id" pk_url_kwarg = "j_id"
@ -315,9 +281,7 @@ class JournalDetailView(JournalTabsMixin, CanViewMixin, DetailView):
class JournalEditView(CanEditMixin, UpdateView): class JournalEditView(CanEditMixin, UpdateView):
""" """Update a general journal."""
Update a general journal
"""
model = GeneralJournal model = GeneralJournal
pk_url_kwarg = "j_id" pk_url_kwarg = "j_id"
@ -326,9 +290,7 @@ class JournalEditView(CanEditMixin, UpdateView):
class JournalDeleteView(CanEditPropMixin, DeleteView): class JournalDeleteView(CanEditPropMixin, DeleteView):
""" """Delete a club account (for the admins)."""
Delete a club account (for the admins)
"""
model = GeneralJournal model = GeneralJournal
pk_url_kwarg = "j_id" pk_url_kwarg = "j_id"
@ -467,9 +429,7 @@ class OperationForm(forms.ModelForm):
class OperationCreateView(CanCreateMixin, CreateView): class OperationCreateView(CanCreateMixin, CreateView):
""" """Create an operation."""
Create an operation
"""
model = Operation model = Operation
form_class = OperationForm form_class = OperationForm
@ -487,7 +447,7 @@ class OperationCreateView(CanCreateMixin, CreateView):
return ret return ret
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add journal to the context""" """Add journal to the context."""
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
if self.journal: if self.journal:
kwargs["object"] = self.journal kwargs["object"] = self.journal
@ -495,9 +455,7 @@ class OperationCreateView(CanCreateMixin, CreateView):
class OperationEditView(CanEditMixin, UpdateView): class OperationEditView(CanEditMixin, UpdateView):
""" """An edit view, working as detail for the moment."""
An edit view, working as detail for the moment
"""
model = Operation model = Operation
pk_url_kwarg = "op_id" pk_url_kwarg = "op_id"
@ -505,16 +463,14 @@ class OperationEditView(CanEditMixin, UpdateView):
template_name = "accounting/operation_edit.jinja" template_name = "accounting/operation_edit.jinja"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add journal to the context""" """Add journal to the context."""
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["object"] = self.object.journal kwargs["object"] = self.object.journal
return kwargs return kwargs
class OperationPDFView(CanViewMixin, DetailView): class OperationPDFView(CanViewMixin, DetailView):
""" """Display the PDF of a given operation."""
Display the PDF of a given operation
"""
model = Operation model = Operation
pk_url_kwarg = "op_id" pk_url_kwarg = "op_id"
@ -666,9 +622,7 @@ class OperationPDFView(CanViewMixin, DetailView):
class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView): class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
""" """Display a statement sorted by labels."""
Display a statement sorted by labels
"""
model = GeneralJournal model = GeneralJournal
pk_url_kwarg = "j_id" pk_url_kwarg = "j_id"
@ -726,16 +680,14 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
return statement return statement
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add infos to the context""" """Add infos to the context."""
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["statement"] = self.big_statement() kwargs["statement"] = self.big_statement()
return kwargs return kwargs
class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView): class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
""" """Calculate a dictionary with operation target and sum of operations."""
Calculate a dictionary with operation target and sum of operations
"""
model = GeneralJournal model = GeneralJournal
pk_url_kwarg = "j_id" pk_url_kwarg = "j_id"
@ -765,7 +717,7 @@ class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
return sum(self.statement(movement_type).values()) return sum(self.statement(movement_type).values())
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add journal to the context""" """Add journal to the context."""
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["credit_statement"] = self.statement("CREDIT") kwargs["credit_statement"] = self.statement("CREDIT")
kwargs["debit_statement"] = self.statement("DEBIT") kwargs["debit_statement"] = self.statement("DEBIT")
@ -775,9 +727,7 @@ class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView): class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView):
""" """Calculate a dictionary with operation type and sum of operations."""
Calculate a dictionary with operation type and sum of operations
"""
model = GeneralJournal model = GeneralJournal
pk_url_kwarg = "j_id" pk_url_kwarg = "j_id"
@ -795,7 +745,7 @@ class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView)
return statement return statement
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add journal to the context""" """Add journal to the context."""
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["statement"] = self.statement() kwargs["statement"] = self.statement()
return kwargs return kwargs
@ -810,9 +760,7 @@ class CompanyListView(CanViewMixin, ListView):
class CompanyCreateView(CanCreateMixin, CreateView): class CompanyCreateView(CanCreateMixin, CreateView):
""" """Create a company."""
Create a company
"""
model = Company model = Company
fields = ["name"] fields = ["name"]
@ -821,9 +769,7 @@ class CompanyCreateView(CanCreateMixin, CreateView):
class CompanyEditView(CanCreateMixin, UpdateView): class CompanyEditView(CanCreateMixin, UpdateView):
""" """Edit a company."""
Edit a company
"""
model = Company model = Company
pk_url_kwarg = "co_id" pk_url_kwarg = "co_id"
@ -882,9 +828,7 @@ class CloseCustomerAccountForm(forms.Form):
class RefoundAccountView(FormView): class RefoundAccountView(FormView):
""" """Create a selling with the same amount than the current user money."""
Create a selling with the same amount than the current user money
"""
template_name = "accounting/refound_account.jinja" template_name = "accounting/refound_account.jinja"
form_class = CloseCustomerAccountForm form_class = CloseCustomerAccountForm

View File

@ -23,9 +23,9 @@ from core.views import can_edit, can_view
def check_if(obj, user, test): def check_if(obj, user, test):
""" """Detect if it's a single object or a queryset.
Detect if it's a single object or a queryset
aply a given test on individual object and return global permission Apply a given test on individual object and return global permission.
""" """
if isinstance(obj, QuerySet): if isinstance(obj, QuerySet):
for o in obj: for o in obj:
@ -39,9 +39,7 @@ def check_if(obj, user, test):
class ManageModelMixin: class ManageModelMixin:
@action(detail=True) @action(detail=True)
def id(self, request, pk=None): def id(self, request, pk=None):
""" """Get by id (api/v1/router/{pk}/id/)."""
Get by id (api/v1/router/{pk}/id/)
"""
self.queryset = get_object_or_404(self.queryset.filter(id=pk)) self.queryset = get_object_or_404(self.queryset.filter(id=pk))
serializer = self.get_serializer(self.queryset) serializer = self.get_serializer(self.queryset)
return Response(serializer.data) return Response(serializer.data)

View File

@ -23,9 +23,7 @@ from core.templatetags.renderer import markdown
@api_view(["POST"]) @api_view(["POST"])
@renderer_classes((StaticHTMLRenderer,)) @renderer_classes((StaticHTMLRenderer,))
def RenderMarkdown(request): def RenderMarkdown(request):
""" """Render Markdown."""
Render Markdown
"""
try: try:
data = markdown(request.POST["text"]) data = markdown(request.POST["text"])
except: except:

View File

@ -31,9 +31,7 @@ class ClubSerializer(serializers.ModelSerializer):
class ClubViewSet(RightModelViewSet): class ClubViewSet(RightModelViewSet):
""" """Manage Clubs (api/v1/club/)."""
Manage Clubs (api/v1/club/)
"""
serializer_class = ClubSerializer serializer_class = ClubSerializer
queryset = Club.objects.all() queryset = Club.objects.all()

View File

@ -33,18 +33,14 @@ class CounterSerializer(serializers.ModelSerializer):
class CounterViewSet(RightModelViewSet): class CounterViewSet(RightModelViewSet):
""" """Manage Counters (api/v1/counter/)."""
Manage Counters (api/v1/counter/)
"""
serializer_class = CounterSerializer serializer_class = CounterSerializer
queryset = Counter.objects.all() queryset = Counter.objects.all()
@action(detail=False) @action(detail=False)
def bar(self, request): def bar(self, request):
""" """Return all bars (api/v1/counter/bar/)."""
Return all bars (api/v1/counter/bar/)
"""
self.queryset = self.queryset.filter(type="BAR") self.queryset = self.queryset.filter(type="BAR")
serializer = self.get_serializer(self.queryset, many=True) serializer = self.get_serializer(self.queryset, many=True)
return Response(serializer.data) return Response(serializer.data)

View File

@ -25,9 +25,7 @@ class GroupSerializer(serializers.ModelSerializer):
class GroupViewSet(RightModelViewSet): class GroupViewSet(RightModelViewSet):
""" """Manage Groups (api/v1/group/)."""
Manage Groups (api/v1/group/)
"""
serializer_class = GroupSerializer serializer_class = GroupSerializer
queryset = RealGroup.objects.all() queryset = RealGroup.objects.all()

View File

@ -60,54 +60,42 @@ class LaunderetteTokenSerializer(serializers.ModelSerializer):
class LaunderettePlaceViewSet(RightModelViewSet): class LaunderettePlaceViewSet(RightModelViewSet):
""" """Manage Launderette (api/v1/launderette/place/)."""
Manage Launderette (api/v1/launderette/place/)
"""
serializer_class = LaunderettePlaceSerializer serializer_class = LaunderettePlaceSerializer
queryset = Launderette.objects.all() queryset = Launderette.objects.all()
class LaunderetteMachineViewSet(RightModelViewSet): class LaunderetteMachineViewSet(RightModelViewSet):
""" """Manage Washing Machines (api/v1/launderette/machine/)."""
Manage Washing Machines (api/v1/launderette/machine/)
"""
serializer_class = LaunderetteMachineSerializer serializer_class = LaunderetteMachineSerializer
queryset = Machine.objects.all() queryset = Machine.objects.all()
class LaunderetteTokenViewSet(RightModelViewSet): class LaunderetteTokenViewSet(RightModelViewSet):
""" """Manage Launderette's tokens (api/v1/launderette/token/)."""
Manage Launderette's tokens (api/v1/launderette/token/)
"""
serializer_class = LaunderetteTokenSerializer serializer_class = LaunderetteTokenSerializer
queryset = Token.objects.all() queryset = Token.objects.all()
@action(detail=False) @action(detail=False)
def washing(self, request): def washing(self, request):
""" """Return all washing tokens (api/v1/launderette/token/washing)."""
Return all washing tokens (api/v1/launderette/token/washing)
"""
self.queryset = self.queryset.filter(type="WASHING") self.queryset = self.queryset.filter(type="WASHING")
serializer = self.get_serializer(self.queryset, many=True) serializer = self.get_serializer(self.queryset, many=True)
return Response(serializer.data) return Response(serializer.data)
@action(detail=False) @action(detail=False)
def drying(self, request): def drying(self, request):
""" """Return all drying tokens (api/v1/launderette/token/drying)."""
Return all drying tokens (api/v1/launderette/token/drying)
"""
self.queryset = self.queryset.filter(type="DRYING") self.queryset = self.queryset.filter(type="DRYING")
serializer = self.get_serializer(self.queryset, many=True) serializer = self.get_serializer(self.queryset, many=True)
return Response(serializer.data) return Response(serializer.data)
@action(detail=False) @action(detail=False)
def avaliable(self, request): def avaliable(self, request):
""" """Return all avaliable tokens (api/v1/launderette/token/avaliable)."""
Return all avaliable tokens (api/v1/launderette/token/avaliable)
"""
self.queryset = self.queryset.filter( self.queryset = self.queryset.filter(
borrow_date__isnull=True, user__isnull=True borrow_date__isnull=True, user__isnull=True
) )
@ -116,9 +104,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
@action(detail=False) @action(detail=False)
def unavaliable(self, request): def unavaliable(self, request):
""" """Return all unavaliable tokens (api/v1/launderette/token/unavaliable)."""
Return all unavaliable tokens (api/v1/launderette/token/unavaliable)
"""
self.queryset = self.queryset.filter( self.queryset = self.queryset.filter(
borrow_date__isnull=False, user__isnull=False borrow_date__isnull=False, user__isnull=False
) )

View File

@ -39,9 +39,9 @@ class UserSerializer(serializers.ModelSerializer):
class UserViewSet(RightModelViewSet): class UserViewSet(RightModelViewSet):
""" """Manage Users (api/v1/user/).
Manage Users (api/v1/user/)
Only show active users Only show active users.
""" """
serializer_class = UserSerializer serializer_class = UserSerializer
@ -49,9 +49,7 @@ class UserViewSet(RightModelViewSet):
@action(detail=False) @action(detail=False)
def birthday(self, request): def birthday(self, request):
""" """Return all users born today (api/v1/user/birstdays)."""
Return all users born today (api/v1/user/birstdays)
"""
date = datetime.datetime.today() date = datetime.datetime.today()
self.queryset = self.queryset.filter(date_of_birth=date) self.queryset = self.queryset.filter(date_of_birth=date)
serializer = self.get_serializer(self.queryset, many=True) serializer = self.get_serializer(self.queryset, many=True)

View File

@ -28,10 +28,10 @@ def uv_endpoint(request):
return Response(make_clean_uv(short_uv, full_uv)) return Response(make_clean_uv(short_uv, full_uv))
def find_uv(lang, year, code): def find_uv(lang: str, year: int | str, code: str) -> tuple[dict | None, dict | None]:
""" """Uses the UTBM API to find an UV.
Uses the UTBM API to find an UV.
short_uv is the UV entry in the UV list. It is returned as it contains Short_uv is the UV entry in the UV list. It is returned as it contains
information which are not in full_uv. information which are not in full_uv.
full_uv is the detailed representation of an UV. full_uv is the detailed representation of an UV.
""" """
@ -44,7 +44,7 @@ def find_uv(lang, year, code):
# find the first UV which matches the code # find the first UV which matches the code
short_uv = next(uv for uv in uvs if uv["code"] == code) short_uv = next(uv for uv in uvs if uv["code"] == code)
except StopIteration: except StopIteration:
return (None, None) return None, None
# get detailed information about the UV # get detailed information about the UV
uv_url = settings.SITH_PEDAGOGY_UTBM_API + "/uv/{}/{}/{}/{}".format( uv_url = settings.SITH_PEDAGOGY_UTBM_API + "/uv/{}/{}/{}/{}".format(
@ -53,13 +53,11 @@ def find_uv(lang, year, code):
response = urllib.request.urlopen(uv_url) response = urllib.request.urlopen(uv_url)
full_uv = json.loads(response.read().decode("utf-8")) full_uv = json.loads(response.read().decode("utf-8"))
return (short_uv, full_uv) return short_uv, full_uv
def make_clean_uv(short_uv, full_uv): def make_clean_uv(short_uv: dict, full_uv: dict):
""" """Cleans the data up so that it corresponds to our data representation."""
Cleans the data up so that it corresponds to our data representation.
"""
res = {} res = {}
res["credit_type"] = short_uv["codeCategorie"] res["credit_type"] = short_uv["codeCategorie"]

View File

@ -44,9 +44,7 @@ class ClubEditForm(forms.ModelForm):
class MailingForm(forms.Form): class MailingForm(forms.Form):
""" """Form handling mailing lists right."""
Form handling mailing lists right
"""
ACTION_NEW_MAILING = 1 ACTION_NEW_MAILING = 1
ACTION_NEW_SUBSCRIPTION = 2 ACTION_NEW_SUBSCRIPTION = 2
@ -105,16 +103,12 @@ class MailingForm(forms.Form):
) )
def check_required(self, cleaned_data, field): def check_required(self, cleaned_data, field):
""" """If the given field doesn't exist or has no value, add a required error on it."""
If the given field doesn't exist or has no value, add a required error on it
"""
if not cleaned_data.get(field, None): if not cleaned_data.get(field, None):
self.add_error(field, _("This field is required")) self.add_error(field, _("This field is required"))
def clean_subscription_users(self): def clean_subscription_users(self):
""" """Convert given users into real users and check their validity."""
Convert given users into real users and check their validity
"""
cleaned_data = super().clean() cleaned_data = super().clean()
users = [] users = []
for user in cleaned_data["subscription_users"]: for user in cleaned_data["subscription_users"]:
@ -177,9 +171,7 @@ class SellingsForm(forms.Form):
class ClubMemberForm(forms.Form): class ClubMemberForm(forms.Form):
""" """Form handling the members of a club."""
Form handling the members of a club
"""
error_css_class = "error" error_css_class = "error"
required_css_class = "required" required_css_class = "required"
@ -236,9 +228,9 @@ class ClubMemberForm(forms.Form):
self.fields.pop("start_date") self.fields.pop("start_date")
def clean_users(self): def clean_users(self):
""" """Check that the user is not trying to add an user already in the club.
Check that the user is not trying to add an user already in the club
Also check that the user is valid and has a valid subscription Also check that the user is valid and has a valid subscription.
""" """
cleaned_data = super().clean() cleaned_data = super().clean()
users = [] users = []
@ -260,9 +252,7 @@ class ClubMemberForm(forms.Form):
return users return users
def clean(self): def clean(self):
""" """Check user rights for adding an user."""
Check user rights for adding an user
"""
cleaned_data = super().clean() cleaned_data = super().clean()
if "start_date" in cleaned_data and not cleaned_data["start_date"]: if "start_date" in cleaned_data and not cleaned_data["start_date"]:

View File

@ -21,7 +21,7 @@
# Place - Suite 330, Boston, MA 02111-1307, USA. # Place - Suite 330, Boston, MA 02111-1307, USA.
# #
# #
from typing import Optional from __future__ import annotations
from django.conf import settings from django.conf import settings
from django.core import validators from django.core import validators
@ -46,9 +46,7 @@ def get_default_owner_group():
class Club(models.Model): class Club(models.Model):
""" """The Club class, made as a tree to allow nice tidy organization."""
The Club class, made as a tree to allow nice tidy organization
"""
id = models.AutoField(primary_key=True, db_index=True) id = models.AutoField(primary_key=True, db_index=True)
name = models.CharField(_("name"), max_length=64) name = models.CharField(_("name"), max_length=64)
@ -141,7 +139,7 @@ class Club(models.Model):
).first() ).first()
def check_loop(self): def check_loop(self):
"""Raise a validation error when a loop is found within the parent list""" """Raise a validation error when a loop is found within the parent list."""
objs = [] objs = []
cur = self cur = self
while cur.parent is not None: while cur.parent is not None:
@ -223,9 +221,7 @@ class Club(models.Model):
return self.name return self.name
def is_owned_by(self, user): def is_owned_by(self, user):
""" """Method to see if that object can be super edited by the given user."""
Method to see if that object can be super edited by the given user
"""
if user.is_anonymous: if user.is_anonymous:
return False return False
return user.is_board_member return user.is_board_member
@ -234,24 +230,21 @@ class Club(models.Model):
return "https://%s%s" % (settings.SITH_URL, self.logo.url) return "https://%s%s" % (settings.SITH_URL, self.logo.url)
def can_be_edited_by(self, user): def can_be_edited_by(self, user):
""" """Method to see if that object can be edited by the given user."""
Method to see if that object can be edited by the given user
"""
return self.has_rights_in_club(user) return self.has_rights_in_club(user)
def can_be_viewed_by(self, user): def can_be_viewed_by(self, user):
""" """Method to see if that object can be seen by the given user."""
Method to see if that object can be seen by the given user
"""
sub = User.objects.filter(pk=user.pk).first() sub = User.objects.filter(pk=user.pk).first()
if sub is None: if sub is None:
return False return False
return sub.was_subscribed return sub.was_subscribed
def get_membership_for(self, user: User) -> Optional["Membership"]: def get_membership_for(self, user: User) -> Membership | None:
""" """Return the current membership the given user.
Return the current membership the given user.
The result is cached. Note:
The result is cached.
""" """
if user.is_anonymous: if user.is_anonymous:
return None return None
@ -273,15 +266,12 @@ class Club(models.Model):
class MembershipQuerySet(models.QuerySet): class MembershipQuerySet(models.QuerySet):
def ongoing(self) -> "MembershipQuerySet": def ongoing(self) -> "MembershipQuerySet":
""" """Filter all memberships which are not finished yet."""
Filter all memberships which are not finished yet
"""
# noinspection PyTypeChecker # noinspection PyTypeChecker
return self.filter(Q(end_date=None) | Q(end_date__gte=timezone.now())) return self.filter(Q(end_date=None) | Q(end_date__gte=timezone.now()))
def board(self) -> "MembershipQuerySet": def board(self) -> "MembershipQuerySet":
""" """Filter all memberships where the user is/was in the board.
Filter all memberships where the user is/was in the board.
Be aware that users who were in the board in the past Be aware that users who were in the board in the past
are included, even if there are no more members. are included, even if there are no more members.
@ -293,9 +283,9 @@ class MembershipQuerySet(models.QuerySet):
return self.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE) return self.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE)
def update(self, **kwargs): def update(self, **kwargs):
""" """Refresh the cache for the elements of the queryset.
Work just like the default Django's update() method,
but add a cache refresh for the elements of the queryset. Besides that, does the same job as a regular update method.
Be aware that this adds a db query to retrieve the updated objects Be aware that this adds a db query to retrieve the updated objects
""" """
@ -315,8 +305,7 @@ class MembershipQuerySet(models.QuerySet):
) )
def delete(self): def delete(self):
""" """Work just like the default Django's delete() method,
Work just like the default Django's delete() method,
but add a cache invalidation for the elements of the queryset but add a cache invalidation for the elements of the queryset
before the deletion. before the deletion.
@ -332,8 +321,7 @@ class MembershipQuerySet(models.QuerySet):
class Membership(models.Model): class Membership(models.Model):
""" """The Membership class makes the connection between User and Clubs.
The Membership class makes the connection between User and Clubs
Both Users and Clubs can have many Membership objects: Both Users and Clubs can have many Membership objects:
- a user can be a member of many clubs at a time - a user can be a member of many clubs at a time
@ -390,17 +378,13 @@ class Membership(models.Model):
return reverse("club:club_members", kwargs={"club_id": self.club_id}) return reverse("club:club_members", kwargs={"club_id": self.club_id})
def is_owned_by(self, user): def is_owned_by(self, user):
""" """Method to see if that object can be super edited by the given user."""
Method to see if that object can be super edited by the given user
"""
if user.is_anonymous: if user.is_anonymous:
return False return False
return user.is_board_member return user.is_board_member
def can_be_edited_by(self, user: User) -> bool: def can_be_edited_by(self, user: User) -> bool:
""" """Check if that object can be edited by the given user."""
Check if that object can be edited by the given user
"""
if user.is_root or user.is_board_member: if user.is_root or user.is_board_member:
return True return True
membership = self.club.get_membership_for(user) membership = self.club.get_membership_for(user)
@ -414,9 +398,10 @@ class Membership(models.Model):
class Mailing(models.Model): class Mailing(models.Model):
""" """A Mailing list for a club.
This class correspond to a mailing list
Remember that mailing lists should be validated by UTBM Warning:
Remember that mailing lists should be validated by UTBM.
""" """
club = models.ForeignKey( club = models.ForeignKey(
@ -508,9 +493,7 @@ class Mailing(models.Model):
class MailingSubscription(models.Model): class MailingSubscription(models.Model):
""" """Link between user and mailing list."""
This class makes the link between user and mailing list
"""
mailing = models.ForeignKey( mailing = models.ForeignKey(
Mailing, Mailing,

View File

@ -29,8 +29,8 @@ from sith.settings import SITH_BAR_MANAGER, SITH_MAIN_CLUB_ID
class ClubTest(TestCase): class ClubTest(TestCase):
""" """Set up data for test cases related to clubs and membership.
Set up data for test cases related to clubs and membership
The generated dataset is the one created by the populate command, The generated dataset is the one created by the populate command,
plus the following modifications : plus the following modifications :
@ -94,8 +94,7 @@ class ClubTest(TestCase):
class MembershipQuerySetTest(ClubTest): class MembershipQuerySetTest(ClubTest):
def test_ongoing(self): def test_ongoing(self):
""" """Test that the ongoing queryset method returns the memberships that
Test that the ongoing queryset method returns the memberships that
are not ended. are not ended.
""" """
current_members = list(self.club.members.ongoing().order_by("id")) current_members = list(self.club.members.ongoing().order_by("id"))
@ -108,9 +107,8 @@ class MembershipQuerySetTest(ClubTest):
assert current_members == expected assert current_members == expected
def test_board(self): def test_board(self):
""" """Test that the board queryset method returns the memberships
Test that the board queryset method returns the memberships of user in the club board.
of user in the club board
""" """
board_members = list(self.club.members.board().order_by("id")) board_members = list(self.club.members.board().order_by("id"))
expected = [ expected = [
@ -123,9 +121,8 @@ class MembershipQuerySetTest(ClubTest):
assert board_members == expected assert board_members == expected
def test_ongoing_board(self): def test_ongoing_board(self):
""" """Test that combining ongoing and board returns users
Test that combining ongoing and board returns users who are currently board members of the club.
who are currently board members of the club
""" """
members = list(self.club.members.ongoing().board().order_by("id")) members = list(self.club.members.ongoing().board().order_by("id"))
expected = [ expected = [
@ -136,9 +133,7 @@ class MembershipQuerySetTest(ClubTest):
assert members == expected assert members == expected
def test_update_invalidate_cache(self): def test_update_invalidate_cache(self):
""" """Test that the `update` queryset method properly invalidate cache."""
Test that the `update` queryset method properly invalidate cache
"""
mem_skia = self.skia.memberships.get(club=self.club) mem_skia = self.skia.memberships.get(club=self.club)
cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia) cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia)
self.skia.memberships.update(end_date=localtime(now()).date()) self.skia.memberships.update(end_date=localtime(now()).date())
@ -157,10 +152,7 @@ class MembershipQuerySetTest(ClubTest):
assert new_mem.role == 5 assert new_mem.role == 5
def test_delete_invalidate_cache(self): def test_delete_invalidate_cache(self):
""" """Test that the `delete` queryset properly invalidate cache."""
Test that the `delete` queryset properly invalidate cache
"""
mem_skia = self.skia.memberships.get(club=self.club) mem_skia = self.skia.memberships.get(club=self.club)
mem_comptable = self.comptable.memberships.get(club=self.club) mem_comptable = self.comptable.memberships.get(club=self.club)
cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia) cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia)
@ -180,9 +172,7 @@ class MembershipQuerySetTest(ClubTest):
class ClubModelTest(ClubTest): class ClubModelTest(ClubTest):
def assert_membership_started_today(self, user: User, role: int): def assert_membership_started_today(self, user: User, role: int):
""" """Assert that the given membership is active and started today."""
Assert that the given membership is active and started today
"""
membership = user.memberships.ongoing().filter(club=self.club).first() membership = user.memberships.ongoing().filter(club=self.club).first()
assert membership is not None assert membership is not None
assert localtime(now()).date() == membership.start_date assert localtime(now()).date() == membership.start_date
@ -195,17 +185,14 @@ class ClubModelTest(ClubTest):
assert user.is_in_group(name=board_group) assert user.is_in_group(name=board_group)
def assert_membership_ended_today(self, user: User): def assert_membership_ended_today(self, user: User):
""" """Assert that the given user have a membership which ended today."""
Assert that the given user have a membership which ended today
"""
today = localtime(now()).date() today = localtime(now()).date()
assert user.memberships.filter(club=self.club, end_date=today).exists() assert user.memberships.filter(club=self.club, end_date=today).exists()
assert self.club.get_membership_for(user) is None assert self.club.get_membership_for(user) is None
def test_access_unauthorized(self): def test_access_unauthorized(self):
""" """Test that users who never subscribed and anonymous users
Test that users who never subscribed and anonymous users cannot see the page.
cannot see the page
""" """
response = self.client.post(self.members_url) response = self.client.post(self.members_url)
assert response.status_code == 403 assert response.status_code == 403
@ -215,8 +202,7 @@ class ClubModelTest(ClubTest):
assert response.status_code == 403 assert response.status_code == 403
def test_display(self): def test_display(self):
""" """Test that a GET request return a page where the requested
Test that a GET request return a page where the requested
information are displayed. information are displayed.
""" """
self.client.force_login(self.skia) self.client.force_login(self.skia)
@ -251,9 +237,7 @@ class ClubModelTest(ClubTest):
self.assertInHTML(expected_html, response.content.decode()) self.assertInHTML(expected_html, response.content.decode())
def test_root_add_one_club_member(self): def test_root_add_one_club_member(self):
""" """Test that root users can add members to clubs, one at a time."""
Test that root users can add members to clubs, one at a time
"""
self.client.force_login(self.root) self.client.force_login(self.root)
response = self.client.post( response = self.client.post(
self.members_url, self.members_url,
@ -264,9 +248,7 @@ class ClubModelTest(ClubTest):
self.assert_membership_started_today(self.subscriber, role=3) self.assert_membership_started_today(self.subscriber, role=3)
def test_root_add_multiple_club_member(self): def test_root_add_multiple_club_member(self):
""" """Test that root users can add multiple members at once to clubs."""
Test that root users can add multiple members at once to clubs
"""
self.client.force_login(self.root) self.client.force_login(self.root)
response = self.client.post( response = self.client.post(
self.members_url, self.members_url,
@ -281,8 +263,7 @@ class ClubModelTest(ClubTest):
self.assert_membership_started_today(self.krophil, role=3) self.assert_membership_started_today(self.krophil, role=3)
def test_add_unauthorized_members(self): def test_add_unauthorized_members(self):
""" """Test that users who are not currently subscribed
Test that users who are not currently subscribed
cannot be members of clubs. cannot be members of clubs.
""" """
self.client.force_login(self.root) self.client.force_login(self.root)
@ -302,9 +283,8 @@ class ClubModelTest(ClubTest):
assert '<ul class="errorlist"><li>' in response.content.decode() assert '<ul class="errorlist"><li>' in response.content.decode()
def test_add_members_already_members(self): def test_add_members_already_members(self):
""" """Test that users who are already members of a club
Test that users who are already members of a club cannot be added again to this club.
cannot be added again to this club
""" """
self.client.force_login(self.root) self.client.force_login(self.root)
current_membership = self.skia.memberships.ongoing().get(club=self.club) current_membership = self.skia.memberships.ongoing().get(club=self.club)
@ -320,8 +300,7 @@ class ClubModelTest(ClubTest):
assert self.club.get_membership_for(self.skia) == new_membership assert self.club.get_membership_for(self.skia) == new_membership
def test_add_not_existing_users(self): def test_add_not_existing_users(self):
""" """Test that not existing users cannot be added in clubs.
Test that not existing users cannot be added in clubs.
If one user in the request is invalid, no membership creation at all If one user in the request is invalid, no membership creation at all
can take place. can take place.
""" """
@ -349,9 +328,7 @@ class ClubModelTest(ClubTest):
assert self.club.members.count() == nb_memberships assert self.club.members.count() == nb_memberships
def test_president_add_members(self): def test_president_add_members(self):
""" """Test that the president of the club can add members."""
Test that the president of the club can add members
"""
president = self.club.members.get(role=10).user president = self.club.members.get(role=10).user
nb_club_membership = self.club.members.count() nb_club_membership = self.club.members.count()
nb_subscriber_memberships = self.subscriber.memberships.count() nb_subscriber_memberships = self.subscriber.memberships.count()
@ -368,8 +345,7 @@ class ClubModelTest(ClubTest):
self.assert_membership_started_today(self.subscriber, role=9) self.assert_membership_started_today(self.subscriber, role=9)
def test_add_member_greater_role(self): def test_add_member_greater_role(self):
""" """Test that a member of the club member cannot create
Test that a member of the club member cannot create
a membership with a greater role than its own. a membership with a greater role than its own.
""" """
self.client.force_login(self.skia) self.client.force_login(self.skia)
@ -388,9 +364,7 @@ class ClubModelTest(ClubTest):
assert not self.subscriber.memberships.filter(club=self.club).exists() assert not self.subscriber.memberships.filter(club=self.club).exists()
def test_add_member_without_role(self): def test_add_member_without_role(self):
""" """Test that trying to add members without specifying their role fails."""
Test that trying to add members without specifying their role fails
"""
self.client.force_login(self.root) self.client.force_login(self.root)
response = self.client.post( response = self.client.post(
self.members_url, self.members_url,
@ -402,9 +376,7 @@ class ClubModelTest(ClubTest):
) )
def test_end_membership_self(self): def test_end_membership_self(self):
""" """Test that a member can end its own membership."""
Test that a member can end its own membership
"""
self.client.force_login(self.skia) self.client.force_login(self.skia)
self.client.post( self.client.post(
self.members_url, self.members_url,
@ -414,9 +386,8 @@ class ClubModelTest(ClubTest):
self.assert_membership_ended_today(self.skia) self.assert_membership_ended_today(self.skia)
def test_end_membership_lower_role(self): def test_end_membership_lower_role(self):
""" """Test that board members of the club can end memberships
Test that board members of the club can end memberships of users with lower roles.
of users with lower roles
""" """
# remainder : skia has role 3, comptable has role 10, richard has role 1 # remainder : skia has role 3, comptable has role 10, richard has role 1
self.client.force_login(self.skia) self.client.force_login(self.skia)
@ -429,9 +400,8 @@ class ClubModelTest(ClubTest):
self.assert_membership_ended_today(self.richard) self.assert_membership_ended_today(self.richard)
def test_end_membership_higher_role(self): def test_end_membership_higher_role(self):
""" """Test that board members of the club cannot end memberships
Test that board members of the club cannot end memberships of users with higher roles.
of users with higher roles
""" """
membership = self.comptable.memberships.filter(club=self.club).first() membership = self.comptable.memberships.filter(club=self.club).first()
self.client.force_login(self.skia) self.client.force_login(self.skia)
@ -448,9 +418,8 @@ class ClubModelTest(ClubTest):
assert membership.end_date is None assert membership.end_date is None
def test_end_membership_as_main_club_board(self): def test_end_membership_as_main_club_board(self):
""" """Test that board members of the main club can end the membership
Test that board members of the main club can end the membership of anyone.
of anyone
""" """
# make subscriber a board member # make subscriber a board member
self.subscriber.memberships.all().delete() self.subscriber.memberships.all().delete()
@ -467,9 +436,7 @@ class ClubModelTest(ClubTest):
assert self.club.members.ongoing().count() == nb_memberships - 1 assert self.club.members.ongoing().count() == nb_memberships - 1
def test_end_membership_as_root(self): def test_end_membership_as_root(self):
""" """Test that root users can end the membership of anyone."""
Test that root users can end the membership of anyone
"""
nb_memberships = self.club.members.count() nb_memberships = self.club.members.count()
self.client.force_login(self.root) self.client.force_login(self.root)
response = self.client.post( response = self.client.post(
@ -482,9 +449,7 @@ class ClubModelTest(ClubTest):
assert self.club.members.count() == nb_memberships assert self.club.members.count() == nb_memberships
def test_end_membership_as_foreigner(self): def test_end_membership_as_foreigner(self):
""" """Test that users who are not in this club cannot end its memberships."""
Test that users who are not in this club cannot end its memberships
"""
nb_memberships = self.club.members.count() nb_memberships = self.club.members.count()
membership = self.richard.memberships.filter(club=self.club).first() membership = self.richard.memberships.filter(club=self.club).first()
self.client.force_login(self.subscriber) self.client.force_login(self.subscriber)
@ -498,9 +463,8 @@ class ClubModelTest(ClubTest):
assert membership == new_mem assert membership == new_mem
def test_delete_remove_from_meta_group(self): def test_delete_remove_from_meta_group(self):
""" """Test that when a club is deleted, all its members are removed from the
Test that when a club is deleted, all its members are removed from the associated metagroup.
associated metagroup
""" """
memberships = self.club.members.select_related("user") memberships = self.club.members.select_related("user")
users = [membership.user for membership in memberships] users = [membership.user for membership in memberships]
@ -511,9 +475,7 @@ class ClubModelTest(ClubTest):
assert not user.is_in_group(name=meta_group) assert not user.is_in_group(name=meta_group)
def test_add_to_meta_group(self): def test_add_to_meta_group(self):
""" """Test that when a membership begins, the user is added to the meta group."""
Test that when a membership begins, the user is added to the meta group
"""
group_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX group_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX
board_members = self.club.unix_name + settings.SITH_BOARD_SUFFIX board_members = self.club.unix_name + settings.SITH_BOARD_SUFFIX
assert not self.subscriber.is_in_group(name=group_members) assert not self.subscriber.is_in_group(name=group_members)
@ -523,9 +485,7 @@ class ClubModelTest(ClubTest):
assert self.subscriber.is_in_group(name=board_members) assert self.subscriber.is_in_group(name=board_members)
def test_remove_from_meta_group(self): def test_remove_from_meta_group(self):
""" """Test that when a membership ends, the user is removed from meta group."""
Test that when a membership ends, the user is removed from meta group
"""
group_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX group_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX
board_members = self.club.unix_name + settings.SITH_BOARD_SUFFIX board_members = self.club.unix_name + settings.SITH_BOARD_SUFFIX
assert self.comptable.is_in_group(name=group_members) assert self.comptable.is_in_group(name=group_members)
@ -535,9 +495,7 @@ class ClubModelTest(ClubTest):
assert not self.comptable.is_in_group(name=board_members) assert not self.comptable.is_in_group(name=board_members)
def test_club_owner(self): def test_club_owner(self):
""" """Test that a club is owned only by board members of the main club."""
Test that a club is owned only by board members of the main club
"""
anonymous = AnonymousUser() anonymous = AnonymousUser()
assert not self.club.is_owned_by(anonymous) assert not self.club.is_owned_by(anonymous)
assert not self.club.is_owned_by(self.subscriber) assert not self.club.is_owned_by(self.subscriber)
@ -549,7 +507,7 @@ class ClubModelTest(ClubTest):
class MailingFormTest(TestCase): class MailingFormTest(TestCase):
"""Perform validation tests for MailingForm""" """Perform validation tests for MailingForm."""
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -865,9 +823,7 @@ class MailingFormTest(TestCase):
class ClubSellingViewTest(TestCase): class ClubSellingViewTest(TestCase):
""" """Perform basics tests to ensure that the page is available."""
Perform basics tests to ensure that the page is available
"""
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -875,9 +831,7 @@ class ClubSellingViewTest(TestCase):
cls.skia = User.objects.get(username="skia") cls.skia = User.objects.get(username="skia")
def test_page_not_internal_error(self): def test_page_not_internal_error(self):
""" """Test that the page does not return and internal error."""
Test that the page does not return and internal error
"""
self.client.force_login(self.skia) self.client.force_login(self.skia)
response = self.client.get( response = self.client.get(
reverse("club:club_sellings", kwargs={"club_id": self.ae.id}) reverse("club:club_sellings", kwargs={"club_id": self.ae.id})

View File

@ -175,18 +175,14 @@ class ClubTabsMixin(TabedViewMixin):
class ClubListView(ListView): class ClubListView(ListView):
""" """List the Clubs."""
List the Clubs
"""
model = Club model = Club
template_name = "club/club_list.jinja" template_name = "club/club_list.jinja"
class ClubView(ClubTabsMixin, DetailView): class ClubView(ClubTabsMixin, DetailView):
""" """Front page of a Club."""
Front page of a Club
"""
model = Club model = Club
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
@ -201,9 +197,7 @@ class ClubView(ClubTabsMixin, DetailView):
class ClubRevView(ClubView): class ClubRevView(ClubView):
""" """Display a specific page revision."""
Display a specific page revision
"""
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
obj = self.get_object() obj = self.get_object()
@ -235,9 +229,7 @@ class ClubPageEditView(ClubTabsMixin, PageEditViewBase):
class ClubPageHistView(ClubTabsMixin, CanViewMixin, DetailView): class ClubPageHistView(ClubTabsMixin, CanViewMixin, DetailView):
""" """Modification hostory of the page."""
Modification hostory of the page
"""
model = Club model = Club
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
@ -246,9 +238,7 @@ class ClubPageHistView(ClubTabsMixin, CanViewMixin, DetailView):
class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView): class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView):
""" """Tools page of a Club."""
Tools page of a Club
"""
model = Club model = Club
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
@ -257,9 +247,7 @@ class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView):
class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView): class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
""" """View of a club's members."""
View of a club's members
"""
model = Club model = Club
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
@ -280,9 +268,7 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
return kwargs return kwargs
def form_valid(self, form): def form_valid(self, form):
""" """Check user rights."""
Check user rights
"""
resp = super().form_valid(form) resp = super().form_valid(form)
data = form.clean() data = form.clean()
@ -307,9 +293,7 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView): class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView):
""" """Old members of a club."""
Old members of a club
"""
model = Club model = Club
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
@ -318,9 +302,7 @@ class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView):
class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView): class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
""" """Sellings of a club."""
Sellings of a club
"""
model = Club model = Club
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
@ -396,12 +378,10 @@ class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
class ClubSellingCSVView(ClubSellingView): class ClubSellingCSVView(ClubSellingView):
""" """Generate sellings in csv for a given period."""
Generate sellings in csv for a given period
"""
class StreamWriter: class StreamWriter:
"""Implements a file-like interface for streaming the CSV""" """Implements a file-like interface for streaming the CSV."""
def write(self, value): def write(self, value):
"""Write the value by returning it, instead of storing in a buffer.""" """Write the value by returning it, instead of storing in a buffer."""
@ -475,9 +455,7 @@ class ClubSellingCSVView(ClubSellingView):
class ClubEditView(ClubTabsMixin, CanEditMixin, UpdateView): class ClubEditView(ClubTabsMixin, CanEditMixin, UpdateView):
""" """Edit a Club's main informations (for the club's members)."""
Edit a Club's main informations (for the club's members)
"""
model = Club model = Club
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
@ -487,9 +465,7 @@ class ClubEditView(ClubTabsMixin, CanEditMixin, UpdateView):
class ClubEditPropView(ClubTabsMixin, CanEditPropMixin, UpdateView): class ClubEditPropView(ClubTabsMixin, CanEditPropMixin, UpdateView):
""" """Edit the properties of a Club object (for the Sith admins)."""
Edit the properties of a Club object (for the Sith admins)
"""
model = Club model = Club
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
@ -499,9 +475,7 @@ class ClubEditPropView(ClubTabsMixin, CanEditPropMixin, UpdateView):
class ClubCreateView(CanCreateMixin, CreateView): class ClubCreateView(CanCreateMixin, CreateView):
""" """Create a club (for the Sith admin)."""
Create a club (for the Sith admin)
"""
model = Club model = Club
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
@ -510,9 +484,7 @@ class ClubCreateView(CanCreateMixin, CreateView):
class MembershipSetOldView(CanEditMixin, DetailView): class MembershipSetOldView(CanEditMixin, DetailView):
""" """Set a membership as beeing old."""
Set a membership as beeing old
"""
model = Membership model = Membership
pk_url_kwarg = "membership_id" pk_url_kwarg = "membership_id"
@ -541,9 +513,7 @@ class MembershipSetOldView(CanEditMixin, DetailView):
class MembershipDeleteView(UserIsRootMixin, DeleteView): class MembershipDeleteView(UserIsRootMixin, DeleteView):
""" """Delete a membership (for admins only)."""
Delete a membership (for admins only)
"""
model = Membership model = Membership
pk_url_kwarg = "membership_id" pk_url_kwarg = "membership_id"
@ -563,9 +533,7 @@ class ClubStatView(TemplateView):
class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView): class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
""" """A list of mailing for a given club."""
A list of mailing for a given club
"""
model = Club model = Club
form_class = MailingForm form_class = MailingForm
@ -603,9 +571,7 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
return kwargs return kwargs
def add_new_mailing(self, cleaned_data) -> ValidationError | None: def add_new_mailing(self, cleaned_data) -> ValidationError | None:
""" """Create a new mailing list from the form."""
Create a new mailing list from the form
"""
mailing = Mailing( mailing = Mailing(
club=self.get_object(), club=self.get_object(),
email=cleaned_data["mailing_email"], email=cleaned_data["mailing_email"],
@ -620,9 +586,7 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
return None return None
def add_new_subscription(self, cleaned_data) -> ValidationError | None: def add_new_subscription(self, cleaned_data) -> ValidationError | None:
""" """Add mailing subscriptions for each user given and/or for the specified email in form."""
Add mailing subscriptions for each user given and/or for the specified email in form
"""
users_to_save = [] users_to_save = []
for user in cleaned_data["subscription_users"]: for user in cleaned_data["subscription_users"]:
@ -656,9 +620,7 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
return None return None
def remove_subscription(self, cleaned_data): def remove_subscription(self, cleaned_data):
""" """Remove specified users from a mailing list."""
Remove specified users from a mailing list
"""
fields = [ fields = [
cleaned_data[key] cleaned_data[key]
for key in cleaned_data.keys() for key in cleaned_data.keys()
@ -742,7 +704,7 @@ class MailingAutoGenerationView(View):
class PosterListView(ClubTabsMixin, PosterListBaseView, CanViewMixin): class PosterListView(ClubTabsMixin, PosterListBaseView, CanViewMixin):
"""List communication posters""" """List communication posters."""
def get_object(self): def get_object(self):
return self.club return self.club
@ -755,7 +717,7 @@ class PosterListView(ClubTabsMixin, PosterListBaseView, CanViewMixin):
class PosterCreateView(PosterCreateBaseView, CanCreateMixin): class PosterCreateView(PosterCreateBaseView, CanCreateMixin):
"""Create communication poster""" """Create communication poster."""
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
@ -770,7 +732,7 @@ class PosterCreateView(PosterCreateBaseView, CanCreateMixin):
class PosterEditView(ClubTabsMixin, PosterEditBaseView, CanEditMixin): class PosterEditView(ClubTabsMixin, PosterEditBaseView, CanEditMixin):
"""Edit communication poster""" """Edit communication poster."""
def get_success_url(self): def get_success_url(self):
return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id}) return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id})
@ -782,7 +744,7 @@ class PosterEditView(ClubTabsMixin, PosterEditBaseView, CanEditMixin):
class PosterDeleteView(PosterDeleteBaseView, ClubTabsMixin, CanEditMixin): class PosterDeleteView(PosterDeleteBaseView, ClubTabsMixin, CanEditMixin):
"""Delete communication poster""" """Delete communication poster."""
def get_success_url(self): def get_success_url(self):
return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id}) return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id})

View File

@ -39,7 +39,7 @@ from core.models import Notification, Preferences, RealGroup, User
class Sith(models.Model): class Sith(models.Model):
"""A one instance class storing all the modifiable infos""" """A one instance class storing all the modifiable infos."""
alert_msg = models.TextField(_("alert message"), default="", blank=True) alert_msg = models.TextField(_("alert message"), default="", blank=True)
info_msg = models.TextField(_("info message"), default="", blank=True) info_msg = models.TextField(_("info message"), default="", blank=True)
@ -64,7 +64,7 @@ NEWS_TYPES = [
class News(models.Model): class News(models.Model):
"""The news class""" """The news class."""
title = models.CharField(_("title"), max_length=64) title = models.CharField(_("title"), max_length=64)
summary = models.TextField(_("summary")) summary = models.TextField(_("summary"))
@ -143,8 +143,7 @@ def news_notification_callback(notif):
class NewsDate(models.Model): class NewsDate(models.Model):
""" """A date class, useful for weekly events, or for events that just have no date.
A date class, useful for weekly events, or for events that just have no date
This class allows more flexibilty managing the dates related to a news, particularly when this news is weekly, since This class allows more flexibilty managing the dates related to a news, particularly when this news is weekly, since
we don't have to make copies we don't have to make copies
@ -164,8 +163,7 @@ class NewsDate(models.Model):
class Weekmail(models.Model): class Weekmail(models.Model):
""" """The weekmail class.
The weekmail class
:ivar title: Title of the weekmail :ivar title: Title of the weekmail
:ivar intro: Introduction of the weekmail :ivar intro: Introduction of the weekmail
@ -189,8 +187,8 @@ class Weekmail(models.Model):
return f"Weekmail {self.id} (sent: {self.sent}) - {self.title}" return f"Weekmail {self.id} (sent: {self.sent}) - {self.title}"
def send(self): def send(self):
""" """Send the weekmail to all users with the receive weekmail option opt-in.
Send the weekmail to all users with the receive weekmail option opt-in.
Also send the weekmail to the mailing list in settings.SITH_COM_EMAIL. Also send the weekmail to the mailing list in settings.SITH_COM_EMAIL.
""" """
dest = [ dest = [
@ -214,33 +212,25 @@ class Weekmail(models.Model):
Weekmail().save() Weekmail().save()
def render_text(self): def render_text(self):
""" """Renders a pure text version of the mail for readers without HTML support."""
Renders a pure text version of the mail for readers without HTML support.
"""
return render( return render(
None, "com/weekmail_renderer_text.jinja", context={"weekmail": self} None, "com/weekmail_renderer_text.jinja", context={"weekmail": self}
).content.decode("utf-8") ).content.decode("utf-8")
def render_html(self): def render_html(self):
""" """Renders an HTML version of the mail with images and fancy CSS."""
Renders an HTML version of the mail with images and fancy CSS.
"""
return render( return render(
None, "com/weekmail_renderer_html.jinja", context={"weekmail": self} None, "com/weekmail_renderer_html.jinja", context={"weekmail": self}
).content.decode("utf-8") ).content.decode("utf-8")
def get_banner(self): def get_banner(self):
""" """Return an absolute link to the banner."""
Return an absolute link to the banner.
"""
return ( return (
"http://" + settings.SITH_URL + static("com/img/weekmail_bannerV2P22.png") "http://" + settings.SITH_URL + static("com/img/weekmail_bannerV2P22.png")
) )
def get_footer(self): def get_footer(self):
""" """Return an absolute link to the footer."""
Return an absolute link to the footer.
"""
return "http://" + settings.SITH_URL + static("com/img/weekmail_footerP22.png") return "http://" + settings.SITH_URL + static("com/img/weekmail_footerP22.png")
def is_owned_by(self, user): def is_owned_by(self, user):

View File

@ -115,10 +115,7 @@ class ComTest(TestCase):
class SithTest(TestCase): class SithTest(TestCase):
def test_sith_owner(self): def test_sith_owner(self):
""" """Test that the sith instance is owned by com admins and nobody else."""
Test that the sith instance is owned by com admins
and nobody else
"""
sith: Sith = Sith.objects.first() sith: Sith = Sith.objects.first()
com_admin = User.objects.get(username="comunity") com_admin = User.objects.get(username="comunity")
@ -148,20 +145,17 @@ class NewsTest(TestCase):
cls.anonymous = AnonymousUser() cls.anonymous = AnonymousUser()
def test_news_owner(self): def test_news_owner(self):
"""Test that news are owned by com admins
or by their author but nobody else.
""" """
Test that news are owned by com admins
or by their author but nobody else
"""
assert self.new.is_owned_by(self.com_admin) assert self.new.is_owned_by(self.com_admin)
assert self.new.is_owned_by(self.author) assert self.new.is_owned_by(self.author)
assert not self.new.is_owned_by(self.anonymous) assert not self.new.is_owned_by(self.anonymous)
assert not self.new.is_owned_by(self.sli) assert not self.new.is_owned_by(self.sli)
def test_news_viewer(self): def test_news_viewer(self):
""" """Test that moderated news can be viewed by anyone
Test that moderated news can be viewed by anyone and not moderated news only by com admins.
and not moderated news only by com admins
""" """
# by default a news isn't moderated # by default a news isn't moderated
assert self.new.can_be_viewed_by(self.com_admin) assert self.new.can_be_viewed_by(self.com_admin)
@ -177,9 +171,7 @@ class NewsTest(TestCase):
assert self.new.can_be_viewed_by(self.author) assert self.new.can_be_viewed_by(self.author)
def test_news_editor(self): def test_news_editor(self):
""" """Test that only com admins can edit news."""
Test that only com admins can edit news
"""
assert self.new.can_be_edited_by(self.com_admin) assert self.new.can_be_edited_by(self.com_admin)
assert not self.new.can_be_edited_by(self.sli) assert not self.new.can_be_edited_by(self.sli)
assert not self.new.can_be_edited_by(self.anonymous) assert not self.new.can_be_edited_by(self.anonymous)
@ -203,9 +195,7 @@ class WeekmailArticleTest(TestCase):
cls.anonymous = AnonymousUser() cls.anonymous = AnonymousUser()
def test_weekmail_owner(self): def test_weekmail_owner(self):
""" """Test that weekmails are owned only by com admins."""
Test that weekmails are owned only by com admins
"""
assert self.article.is_owned_by(self.com_admin) assert self.article.is_owned_by(self.com_admin)
assert not self.article.is_owned_by(self.author) assert not self.article.is_owned_by(self.author)
assert not self.article.is_owned_by(self.anonymous) assert not self.article.is_owned_by(self.anonymous)
@ -229,9 +219,7 @@ class PosterTest(TestCase):
cls.anonymous = AnonymousUser() cls.anonymous = AnonymousUser()
def test_poster_owner(self): def test_poster_owner(self):
""" """Test that poster are owned by com admins and board members in clubs."""
Test that poster are owned by com admins and board members in clubs
"""
assert self.poster.is_owned_by(self.com_admin) assert self.poster.is_owned_by(self.com_admin)
assert not self.poster.is_owned_by(self.anonymous) assert not self.poster.is_owned_by(self.anonymous)

View File

@ -427,7 +427,7 @@ class WeekmailPreviewView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, Detai
return self.model.objects.filter(sent=False).order_by("-id").first() return self.model.objects.filter(sent=False).order_by("-id").first()
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add rendered weekmail""" """Add rendered weekmail."""
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["weekmail_rendered"] = self.object.render_html() kwargs["weekmail_rendered"] = self.object.render_html()
kwargs["bad_recipients"] = self.bad_recipients kwargs["bad_recipients"] = self.bad_recipients
@ -507,7 +507,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add orphan articles""" """Add orphan articles."""
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["orphans"] = WeekmailArticle.objects.filter(weekmail=None) kwargs["orphans"] = WeekmailArticle.objects.filter(weekmail=None)
return kwargs return kwargs
@ -516,7 +516,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
class WeekmailArticleEditView( class WeekmailArticleEditView(
ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateView ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateView
): ):
"""Edit an article""" """Edit an article."""
model = WeekmailArticle model = WeekmailArticle
form_class = modelform_factory( form_class = modelform_factory(
@ -532,7 +532,7 @@ class WeekmailArticleEditView(
class WeekmailArticleCreateView(QuickNotifMixin, CreateView): class WeekmailArticleCreateView(QuickNotifMixin, CreateView):
"""Post an article""" """Post an article."""
model = WeekmailArticle model = WeekmailArticle
form_class = modelform_factory( form_class = modelform_factory(
@ -574,7 +574,7 @@ class WeekmailArticleCreateView(QuickNotifMixin, CreateView):
class WeekmailArticleDeleteView(CanEditPropMixin, DeleteView): class WeekmailArticleDeleteView(CanEditPropMixin, DeleteView):
"""Delete an article""" """Delete an article."""
model = WeekmailArticle model = WeekmailArticle
template_name = "core/delete_confirm.jinja" template_name = "core/delete_confirm.jinja"
@ -614,7 +614,7 @@ class MailingModerateView(View):
class PosterListBaseView(ListView): class PosterListBaseView(ListView):
"""List communication posters""" """List communication posters."""
current_tab = "posters" current_tab = "posters"
model = Poster model = Poster
@ -641,7 +641,7 @@ class PosterListBaseView(ListView):
class PosterCreateBaseView(CreateView): class PosterCreateBaseView(CreateView):
"""Create communication poster""" """Create communication poster."""
current_tab = "posters" current_tab = "posters"
form_class = PosterForm form_class = PosterForm
@ -673,7 +673,7 @@ class PosterCreateBaseView(CreateView):
class PosterEditBaseView(UpdateView): class PosterEditBaseView(UpdateView):
"""Edit communication poster""" """Edit communication poster."""
pk_url_kwarg = "poster_id" pk_url_kwarg = "poster_id"
current_tab = "posters" current_tab = "posters"
@ -721,7 +721,7 @@ class PosterEditBaseView(UpdateView):
class PosterDeleteBaseView(DeleteView): class PosterDeleteBaseView(DeleteView):
"""Edit communication poster""" """Edit communication poster."""
pk_url_kwarg = "poster_id" pk_url_kwarg = "poster_id"
current_tab = "posters" current_tab = "posters"
@ -738,7 +738,7 @@ class PosterDeleteBaseView(DeleteView):
class PosterListView(IsComAdminMixin, ComTabsMixin, PosterListBaseView): class PosterListView(IsComAdminMixin, ComTabsMixin, PosterListBaseView):
"""List communication posters""" """List communication posters."""
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
@ -747,7 +747,7 @@ class PosterListView(IsComAdminMixin, ComTabsMixin, PosterListBaseView):
class PosterCreateView(IsComAdminMixin, ComTabsMixin, PosterCreateBaseView): class PosterCreateView(IsComAdminMixin, ComTabsMixin, PosterCreateBaseView):
"""Create communication poster""" """Create communication poster."""
success_url = reverse_lazy("com:poster_list") success_url = reverse_lazy("com:poster_list")
@ -758,7 +758,7 @@ class PosterCreateView(IsComAdminMixin, ComTabsMixin, PosterCreateBaseView):
class PosterEditView(IsComAdminMixin, ComTabsMixin, PosterEditBaseView): class PosterEditView(IsComAdminMixin, ComTabsMixin, PosterEditBaseView):
"""Edit communication poster""" """Edit communication poster."""
success_url = reverse_lazy("com:poster_list") success_url = reverse_lazy("com:poster_list")
@ -769,13 +769,13 @@ class PosterEditView(IsComAdminMixin, ComTabsMixin, PosterEditBaseView):
class PosterDeleteView(IsComAdminMixin, ComTabsMixin, PosterDeleteBaseView): class PosterDeleteView(IsComAdminMixin, ComTabsMixin, PosterDeleteBaseView):
"""Delete communication poster""" """Delete communication poster."""
success_url = reverse_lazy("com:poster_list") success_url = reverse_lazy("com:poster_list")
class PosterModerateListView(IsComAdminMixin, ComTabsMixin, ListView): class PosterModerateListView(IsComAdminMixin, ComTabsMixin, ListView):
"""Moderate list communication poster""" """Moderate list communication poster."""
current_tab = "posters" current_tab = "posters"
model = Poster model = Poster
@ -789,7 +789,7 @@ class PosterModerateListView(IsComAdminMixin, ComTabsMixin, ListView):
class PosterModerateView(IsComAdminMixin, ComTabsMixin, View): class PosterModerateView(IsComAdminMixin, ComTabsMixin, View):
"""Moderate communication poster""" """Moderate communication poster."""
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
obj = get_object_or_404(Poster, pk=kwargs["object_id"]) obj = get_object_or_404(Poster, pk=kwargs["object_id"])
@ -807,7 +807,7 @@ class PosterModerateView(IsComAdminMixin, ComTabsMixin, View):
class ScreenListView(IsComAdminMixin, ComTabsMixin, ListView): class ScreenListView(IsComAdminMixin, ComTabsMixin, ListView):
"""List communication screens""" """List communication screens."""
current_tab = "screens" current_tab = "screens"
model = Screen model = Screen
@ -815,7 +815,7 @@ class ScreenListView(IsComAdminMixin, ComTabsMixin, ListView):
class ScreenSlideshowView(DetailView): class ScreenSlideshowView(DetailView):
"""Slideshow of actives posters""" """Slideshow of actives posters."""
pk_url_kwarg = "screen_id" pk_url_kwarg = "screen_id"
model = Screen model = Screen
@ -828,7 +828,7 @@ class ScreenSlideshowView(DetailView):
class ScreenCreateView(IsComAdminMixin, ComTabsMixin, CreateView): class ScreenCreateView(IsComAdminMixin, ComTabsMixin, CreateView):
"""Create communication screen""" """Create communication screen."""
current_tab = "screens" current_tab = "screens"
model = Screen model = Screen
@ -838,7 +838,7 @@ class ScreenCreateView(IsComAdminMixin, ComTabsMixin, CreateView):
class ScreenEditView(IsComAdminMixin, ComTabsMixin, UpdateView): class ScreenEditView(IsComAdminMixin, ComTabsMixin, UpdateView):
"""Edit communication screen""" """Edit communication screen."""
pk_url_kwarg = "screen_id" pk_url_kwarg = "screen_id"
current_tab = "screens" current_tab = "screens"
@ -849,7 +849,7 @@ class ScreenEditView(IsComAdminMixin, ComTabsMixin, UpdateView):
class ScreenDeleteView(IsComAdminMixin, ComTabsMixin, DeleteView): class ScreenDeleteView(IsComAdminMixin, ComTabsMixin, DeleteView):
"""Delete communication screen""" """Delete communication screen."""
pk_url_kwarg = "screen_id" pk_url_kwarg = "screen_id"
current_tab = "screens" current_tab = "screens"

View File

@ -19,9 +19,7 @@ class TwoDigitMonthConverter:
class BooleanStringConverter: class BooleanStringConverter:
""" """Converter whose regex match either True or False."""
Converter whose regex match either True or False
"""
regex = r"(True)|(False)" regex = r"(True)|(False)"

View File

@ -90,12 +90,15 @@ def list_tags(s):
yield parts[1][len(tag_prefix) :] yield parts[1][len(tag_prefix) :]
def parse_semver(s): def parse_semver(s) -> tuple[int, int, int] | None:
""" """Parse a semver string.
Turns a semver string into a 3-tuple or None if the parsing failed, it is a
prerelease or it has build metadata.
See https://semver.org See https://semver.org
Returns:
A tuple, if the parsing was successful, else None.
In the latter case, it must probably be a prerelease
or include build metadata.
""" """
m = semver_regex.match(s) m = semver_regex.match(s)
@ -106,7 +109,7 @@ def parse_semver(s):
): ):
return None return None
return (int(m.group("major")), int(m.group("minor")), int(m.group("patch"))) return int(m.group("major")), int(m.group("minor")), int(m.group("patch"))
def semver_to_s(t): def semver_to_s(t):

View File

@ -29,9 +29,7 @@ from django.core.management.commands import compilemessages
class Command(compilemessages.Command): class Command(compilemessages.Command):
""" """Wrap call to compilemessages to avoid building whole env."""
Wrap call to compilemessages to avoid building whole env
"""
help = """ help = """
The usage is the same as the real compilemessages The usage is the same as the real compilemessages

View File

@ -30,9 +30,7 @@ from django.core.management.base import BaseCommand
class Command(BaseCommand): class Command(BaseCommand):
""" """Compiles scss in static folder for production."""
Compiles scss in static folder for production
"""
help = "Compile scss files from static folder" help = "Compile scss files from static folder"

View File

@ -53,13 +53,14 @@ _threadlocal = threading.local()
def get_signal_request(): def get_signal_request():
""" """Allow to access current request in signals.
!!! Do not use if your operation is asynchronus !!!
Allow to access current request in signals
This is a hack that looks into the thread
Mainly used for log purpose
"""
This is a hack that looks into the thread
Mainly used for log purpose.
!!!danger
Do not use if your operation is asynchronous.
"""
return getattr(_threadlocal, "request", None) return getattr(_threadlocal, "request", None)

View File

@ -21,11 +21,13 @@
# Place - Suite 330, Boston, MA 02111-1307, USA. # Place - Suite 330, Boston, MA 02111-1307, USA.
# #
# #
from __future__ import annotations
import importlib import importlib
import os import os
import unicodedata import unicodedata
from datetime import date, timedelta from datetime import date, timedelta
from typing import List, Optional, Union from typing import TYPE_CHECKING, Optional
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import ( from django.contrib.auth.models import (
@ -56,6 +58,9 @@ from phonenumber_field.modelfields import PhoneNumberField
from core import utils from core import utils
if TYPE_CHECKING:
from club.models import Club
class RealGroupManager(AuthGroupManager): class RealGroupManager(AuthGroupManager):
def get_queryset(self): def get_queryset(self):
@ -68,8 +73,7 @@ class MetaGroupManager(AuthGroupManager):
class Group(AuthGroup): class Group(AuthGroup):
""" """Implement both RealGroups and Meta groups.
Implement both RealGroups and Meta groups
Groups are sorted by their is_meta property Groups are sorted by their is_meta property
""" """
@ -87,9 +91,6 @@ class Group(AuthGroup):
ordering = ["name"] ordering = ["name"]
def get_absolute_url(self): def get_absolute_url(self):
"""
This is needed for black magic powered UpdateView's children
"""
return reverse("core:group_list") return reverse("core:group_list")
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -104,8 +105,8 @@ class Group(AuthGroup):
class MetaGroup(Group): class MetaGroup(Group):
""" """MetaGroups are dynamically created groups.
MetaGroups are dynamically created groups.
Generally used with clubs where creating a club creates two groups: Generally used with clubs where creating a club creates two groups:
* club-SITH_BOARD_SUFFIX * club-SITH_BOARD_SUFFIX
@ -123,14 +124,14 @@ class MetaGroup(Group):
self.is_meta = True self.is_meta = True
@cached_property @cached_property
def associated_club(self): def associated_club(self) -> Club | None:
""" """Return the group associated with this meta group.
Return the group associated with this meta group
The result of this function is cached The result of this function is cached
:return: The associated club if it exists, else None
:rtype: club.models.Club | None Returns:
The associated club if it exists, else None
""" """
from club.models import Club from club.models import Club
@ -150,8 +151,8 @@ class MetaGroup(Group):
class RealGroup(Group): class RealGroup(Group):
""" """RealGroups are created by the developer.
RealGroups are created by the developer.
Most of the time they match a number in settings to be easily used for permissions. Most of the time they match a number in settings to be easily used for permissions.
""" """
@ -173,22 +174,26 @@ def validate_promo(value):
def get_group(*, pk: int = None, name: str = None) -> Optional[Group]: def get_group(*, pk: int = None, name: str = None) -> Optional[Group]:
""" """Search for a group by its primary key or its name.
Search for a group by its primary key or its name.
Either one of the two must be set. Either one of the two must be set.
The result is cached for the default duration (should be 5 minutes). The result is cached for the default duration (should be 5 minutes).
:param pk: The primary key of the group Args:
:param name: The name of the group pk: The primary key of the group
:return: The group if it exists, else None name: The name of the group
:raise ValueError: If no group matches the criteria
Returns:
The group if it exists, else None
Raises:
ValueError: If no group matches the criteria
""" """
if pk is None and name is None: if pk is None and name is None:
raise ValueError("Either pk or name must be set") raise ValueError("Either pk or name must be set")
# replace space characters to hide warnings with memcached backend # replace space characters to hide warnings with memcached backend
pk_or_name: Union[str, int] = pk if pk is not None else name.replace(" ", "_") pk_or_name: str | int = pk if pk is not None else name.replace(" ", "_")
group = cache.get(f"sith_group_{pk_or_name}") group = cache.get(f"sith_group_{pk_or_name}")
if group == "not_found": if group == "not_found":
@ -211,8 +216,7 @@ def get_group(*, pk: int = None, name: str = None) -> Optional[Group]:
class User(AbstractBaseUser): class User(AbstractBaseUser):
""" """Defines the base user class, useable in every app.
Defines the base user class, useable in every app
This is almost the same as the auth module AbstractUser since it inherits from it, This is almost the same as the auth module AbstractUser since it inherits from it,
but some fields are required, and the username is generated automatically with the but some fields are required, and the username is generated automatically with the
@ -382,9 +386,6 @@ class User(AbstractBaseUser):
return self.is_active and self.is_superuser return self.is_active and self.is_superuser
def get_absolute_url(self): def get_absolute_url(self):
"""
This is needed for black magic powered UpdateView's children
"""
return reverse("core:user_profile", kwargs={"user_id": self.pk}) return reverse("core:user_profile", kwargs={"user_id": self.pk})
def __str__(self): def __str__(self):
@ -412,8 +413,7 @@ class User(AbstractBaseUser):
return 0 return 0
def is_in_group(self, *, pk: int = None, name: str = None) -> bool: def is_in_group(self, *, pk: int = None, name: str = None) -> bool:
""" """Check if this user is in the given group.
Check if this user is in the given group.
Either a group id or a group name must be provided. Either a group id or a group name must be provided.
If both are passed, only the id will be considered. If both are passed, only the id will be considered.
@ -421,7 +421,8 @@ class User(AbstractBaseUser):
If no group is found, return False. If no group is found, return False.
If a group is found, check if this user is in the latter. If a group is found, check if this user is in the latter.
:return: True if the user is the group, else False Returns:
True if the user is the group, else False
""" """
if pk is not None: if pk is not None:
group: Optional[Group] = get_group(pk=pk) group: Optional[Group] = get_group(pk=pk)
@ -454,11 +455,12 @@ class User(AbstractBaseUser):
return group in self.cached_groups return group in self.cached_groups
@property @property
def cached_groups(self) -> List[Group]: def cached_groups(self) -> list[Group]:
""" """Get the list of groups this user is in.
Get the list of groups this user is in.
The result is cached for the default duration (should be 5 minutes) The result is cached for the default duration (should be 5 minutes)
:return: A list of all the groups this user is in
Returns: A list of all the groups this user is in.
""" """
groups = cache.get(f"user_{self.id}_groups") groups = cache.get(f"user_{self.id}_groups")
if groups is None: if groups is None:
@ -523,9 +525,8 @@ class User(AbstractBaseUser):
@cached_property @cached_property
def age(self) -> int: def age(self) -> int:
""" """Return the age this user has the day the method is called.
Return the age this user has the day the method is called. If the user has not filled his age, return 0.
If the user has not filled his age, return 0
""" """
if self.date_of_birth is None: if self.date_of_birth is None:
return 0 return 0
@ -576,31 +577,27 @@ class User(AbstractBaseUser):
} }
def get_full_name(self): def get_full_name(self):
""" """Returns the first_name plus the last_name, with a space in between."""
Returns the first_name plus the last_name, with a space in between.
"""
full_name = "%s %s" % (self.first_name, self.last_name) full_name = "%s %s" % (self.first_name, self.last_name)
return full_name.strip() return full_name.strip()
def get_short_name(self): def get_short_name(self):
"Returns the short name for the user." """Returns the short name for the user."""
if self.nick_name: if self.nick_name:
return self.nick_name return self.nick_name
return self.first_name + " " + self.last_name return self.first_name + " " + self.last_name
def get_display_name(self): def get_display_name(self) -> str:
""" """Returns the display name of the user.
Returns the display name of the user.
A nickname if possible, otherwise, the full name A nickname if possible, otherwise, the full name.
""" """
if self.nick_name: if self.nick_name:
return "%s (%s)" % (self.get_full_name(), self.nick_name) return "%s (%s)" % (self.get_full_name(), self.nick_name)
return self.get_full_name() return self.get_full_name()
def get_age(self): def get_age(self):
""" """Returns the age."""
Returns the age
"""
today = timezone.now() today = timezone.now()
born = self.date_of_birth born = self.date_of_birth
return ( return (
@ -608,18 +605,18 @@ class User(AbstractBaseUser):
) )
def email_user(self, subject, message, from_email=None, **kwargs): def email_user(self, subject, message, from_email=None, **kwargs):
""" """Sends an email to this User."""
Sends an email to this User.
"""
if from_email is None: if from_email is None:
from_email = settings.DEFAULT_FROM_EMAIL from_email = settings.DEFAULT_FROM_EMAIL
send_mail(subject, message, from_email, [self.email], **kwargs) send_mail(subject, message, from_email, [self.email], **kwargs)
def generate_username(self): def generate_username(self) -> str:
""" """Generates a unique username based on the first and last names.
Generates a unique username based on the first and last names.
For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.
Returns the generated username
Returns:
The generated username.
""" """
def remove_accents(data): def remove_accents(data):
@ -644,9 +641,7 @@ class User(AbstractBaseUser):
return user_name return user_name
def is_owner(self, obj): def is_owner(self, obj):
""" """Determine if the object is owned by the user."""
Determine if the object is owned by the user
"""
if hasattr(obj, "is_owned_by") and obj.is_owned_by(self): if hasattr(obj, "is_owned_by") and obj.is_owned_by(self):
return True return True
if hasattr(obj, "owner_group") and self.is_in_group(pk=obj.owner_group.id): if hasattr(obj, "owner_group") and self.is_in_group(pk=obj.owner_group.id):
@ -656,9 +651,7 @@ class User(AbstractBaseUser):
return False return False
def can_edit(self, obj): def can_edit(self, obj):
""" """Determine if the object can be edited by the user."""
Determine if the object can be edited by the user
"""
if hasattr(obj, "can_be_edited_by") and obj.can_be_edited_by(self): if hasattr(obj, "can_be_edited_by") and obj.can_be_edited_by(self):
return True return True
if hasattr(obj, "edit_groups"): if hasattr(obj, "edit_groups"):
@ -672,9 +665,7 @@ class User(AbstractBaseUser):
return False return False
def can_view(self, obj): def can_view(self, obj):
""" """Determine if the object can be viewed by the user."""
Determine if the object can be viewed by the user
"""
if hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self): if hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self):
return True return True
if hasattr(obj, "view_groups"): if hasattr(obj, "view_groups"):
@ -730,11 +721,8 @@ class User(AbstractBaseUser):
return infos return infos
@cached_property @cached_property
def clubs_with_rights(self): def clubs_with_rights(self) -> list[Club]:
""" """The list of clubs where the user has rights"""
:return: the list of clubs where the user has rights
:rtype: list[club.models.Club]
"""
memberships = self.memberships.ongoing().board().select_related("club") memberships = self.memberships.ongoing().board().select_related("club")
return [m.club for m in memberships] return [m.club for m in memberships]
@ -796,9 +784,7 @@ class AnonymousUser(AuthAnonymousUser):
raise PermissionDenied raise PermissionDenied
def is_in_group(self, *, pk: int = None, name: str = None) -> bool: def is_in_group(self, *, pk: int = None, name: str = None) -> bool:
""" """The anonymous user is only in the public group."""
The anonymous user is only in the public group
"""
allowed_id = settings.SITH_GROUP_PUBLIC_ID allowed_id = settings.SITH_GROUP_PUBLIC_ID
if pk is not None: if pk is not None:
return pk == allowed_id return pk == allowed_id
@ -957,16 +943,15 @@ class SithFile(models.Model):
).save() ).save()
def can_be_managed_by(self, user: User) -> bool: def can_be_managed_by(self, user: User) -> bool:
""" """Tell if the user can manage the file (edit, delete, etc.) or not.
Tell if the user can manage the file (edit, delete, etc.) or not.
Apply the following rules: Apply the following rules:
- If the file is not in the SAS nor in the profiles directory, it can be "managed" by anyone -> return True - If the file is not in the SAS nor in the profiles directory, it can be "managed" by anyone -> return True
- If the file is in the SAS, only the SAS admins (or roots) can manage it -> return True if the user is in the SAS admin group or is a root - If the file is in the SAS, only the SAS admins (or roots) can manage it -> return True if the user is in the SAS admin group or is a root
- If the file is in the profiles directory, only the roots can manage it -> return True if the user is a root - If the file is in the profiles directory, only the roots can manage it -> return True if the user is a root.
:returns: True if the file is managed by the SAS or within the profiles directory, False otherwise Returns:
True if the file is managed by the SAS or within the profiles directory, False otherwise
""" """
# If the file is not in the SAS nor in the profiles directory, it can be "managed" by anyone # If the file is not in the SAS nor in the profiles directory, it can be "managed" by anyone
profiles_dir = SithFile.objects.filter(name="profiles").first() profiles_dir = SithFile.objects.filter(name="profiles").first()
if not self.is_in_sas and not profiles_dir in self.get_parent_list(): if not self.is_in_sas and not profiles_dir in self.get_parent_list():
@ -1017,9 +1002,7 @@ class SithFile(models.Model):
return super().delete() return super().delete()
def clean(self): def clean(self):
""" """Cleans up the file."""
Cleans up the file
"""
super().clean() super().clean()
if "/" in self.name: if "/" in self.name:
raise ValidationError(_("Character '/' not authorized in name")) raise ValidationError(_("Character '/' not authorized in name"))
@ -1070,15 +1053,14 @@ class SithFile(models.Model):
c.apply_rights_recursively(only_folders=only_folders) c.apply_rights_recursively(only_folders=only_folders)
def copy_rights(self): def copy_rights(self):
"""Copy, if possible, the rights of the parent folder""" """Copy, if possible, the rights of the parent folder."""
if self.parent is not None: if self.parent is not None:
self.edit_groups.set(self.parent.edit_groups.all()) self.edit_groups.set(self.parent.edit_groups.all())
self.view_groups.set(self.parent.view_groups.all()) self.view_groups.set(self.parent.view_groups.all())
self.save() self.save()
def move_to(self, parent): def move_to(self, parent):
""" """Move a file to a new parent.
Move a file to a new parent.
`parent` must be a SithFile with the `is_folder=True` property. Otherwise, this function doesn't change `parent` must be a SithFile with the `is_folder=True` property. Otherwise, this function doesn't change
anything. anything.
This is done only at the DB level, so that it's very fast for the user. Indeed, this function doesn't modify This is done only at the DB level, so that it's very fast for the user. Indeed, this function doesn't modify
@ -1091,10 +1073,7 @@ class SithFile(models.Model):
self.save() self.save()
def _repair_fs(self): def _repair_fs(self):
""" """Rebuilds recursively the filesystem as it should be regarding the DB tree."""
This function rebuilds recursively the filesystem as it should be
regarding the DB tree.
"""
if self.is_folder: if self.is_folder:
for c in self.children.all(): for c in self.children.all():
c._repair_fs() c._repair_fs()
@ -1197,19 +1176,19 @@ class SithFile(models.Model):
class LockError(Exception): class LockError(Exception):
"""There was a lock error on the object""" """There was a lock error on the object."""
pass pass
class AlreadyLocked(LockError): class AlreadyLocked(LockError):
"""The object is already locked""" """The object is already locked."""
pass pass
class NotLocked(LockError): class NotLocked(LockError):
"""The object is not locked""" """The object is not locked."""
pass pass
@ -1220,12 +1199,11 @@ def get_default_owner_group():
class Page(models.Model): class Page(models.Model):
""" """The page class to build a Wiki
The page class to build a Wiki
Each page may have a parent and it's URL is of the form my.site/page/<grd_pa>/<parent>/<mypage> Each page may have a parent and it's URL is of the form my.site/page/<grd_pa>/<parent>/<mypage>
It has an ID field, but don't use it, since it's only there for DB part, and because compound primary key is It has an ID field, but don't use it, since it's only there for DB part, and because compound primary key is
awkward! awkward!
Prefere querying pages with Page.get_page_by_full_name() Prefere querying pages with Page.get_page_by_full_name().
Be careful with the _full_name attribute: this field may not be valid until you call save(). It's made for fast Be careful with the _full_name attribute: this field may not be valid until you call save(). It's made for fast
query, but don't rely on it when playing with a Page object, use get_full_name() instead! query, but don't rely on it when playing with a Page object, use get_full_name() instead!
@ -1294,9 +1272,7 @@ class Page(models.Model):
return self.get_full_name() return self.get_full_name()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" """Performs some needed actions before and after saving a page in database."""
Performs some needed actions before and after saving a page in database
"""
locked = kwargs.pop("force_lock", False) locked = kwargs.pop("force_lock", False)
if not locked: if not locked:
locked = self.is_locked() locked = self.is_locked()
@ -1317,22 +1293,15 @@ class Page(models.Model):
self.unset_lock() self.unset_lock()
def get_absolute_url(self): def get_absolute_url(self):
"""
This is needed for black magic powered UpdateView's children
"""
return reverse("core:page", kwargs={"page_name": self._full_name}) return reverse("core:page", kwargs={"page_name": self._full_name})
@staticmethod @staticmethod
def get_page_by_full_name(name): def get_page_by_full_name(name):
""" """Quicker to get a page with that method rather than building the request every time."""
Quicker to get a page with that method rather than building the request every time
"""
return Page.objects.filter(_full_name=name).first() return Page.objects.filter(_full_name=name).first()
def clean(self): def clean(self):
""" """Cleans up only the name for the moment, but this can be used to make any treatment before saving the object."""
Cleans up only the name for the moment, but this can be used to make any treatment before saving the object
"""
if "/" in self.name: if "/" in self.name:
self.name = self.name.split("/")[-1] self.name = self.name.split("/")[-1]
if ( if (
@ -1367,10 +1336,11 @@ class Page(models.Model):
return l return l
def is_locked(self): def is_locked(self):
""" """Is True if the page is locked, False otherwise.
Is True if the page is locked, False otherwise
This is where the timeout is handled, so a locked page for which the timeout is reach will be unlocked and this This is where the timeout is handled,
function will return False so a locked page for which the timeout is reach will be unlocked and this
function will return False.
""" """
if self.lock_timeout and ( if self.lock_timeout and (
timezone.now() - self.lock_timeout > timedelta(minutes=5) timezone.now() - self.lock_timeout > timedelta(minutes=5)
@ -1384,9 +1354,7 @@ class Page(models.Model):
) )
def set_lock(self, user): def set_lock(self, user):
""" """Sets a lock on the current page or raise an AlreadyLocked exception."""
Sets a lock on the current page or raise an AlreadyLocked exception
"""
if self.is_locked() and self.get_lock() != user: if self.is_locked() and self.get_lock() != user:
raise AlreadyLocked("The page is already locked by someone else") raise AlreadyLocked("The page is already locked by someone else")
self.lock_user = user self.lock_user = user
@ -1395,41 +1363,34 @@ class Page(models.Model):
# print("Locking page") # print("Locking page")
def set_lock_recursive(self, user): def set_lock_recursive(self, user):
""" """Locks recursively all the child pages for editing properties."""
Locks recursively all the child pages for editing properties
"""
for p in self.children.all(): for p in self.children.all():
p.set_lock_recursive(user) p.set_lock_recursive(user)
self.set_lock(user) self.set_lock(user)
def unset_lock_recursive(self): def unset_lock_recursive(self):
""" """Unlocks recursively all the child pages."""
Unlocks recursively all the child pages
"""
for p in self.children.all(): for p in self.children.all():
p.unset_lock_recursive() p.unset_lock_recursive()
self.unset_lock() self.unset_lock()
def unset_lock(self): def unset_lock(self):
"""Always try to unlock, even if there is no lock""" """Always try to unlock, even if there is no lock."""
self.lock_user = None self.lock_user = None
self.lock_timeout = None self.lock_timeout = None
super().save() super().save()
# print("Unlocking page") # print("Unlocking page")
def get_lock(self): def get_lock(self):
""" """Returns the page's mutex containing the time and the user in a dict."""
Returns the page's mutex containing the time and the user in a dict
"""
if self.lock_user: if self.lock_user:
return self.lock_user return self.lock_user
raise NotLocked("The page is not locked and thus can not return its user") raise NotLocked("The page is not locked and thus can not return its user")
def get_full_name(self): def get_full_name(self):
""" """Computes the real full_name of the page based on its name and its parent's name
Computes the real full_name of the page based on its name and its parent's name
You can and must rely on this function when working on a page object that is not freshly fetched from the DB You can and must rely on this function when working on a page object that is not freshly fetched from the DB
(For example when treating a Page object coming from a form) (For example when treating a Page object coming from a form).
""" """
if self.parent is None: if self.parent is None:
return self.name return self.name
@ -1463,8 +1424,8 @@ class Page(models.Model):
class PageRev(models.Model): class PageRev(models.Model):
""" """True content of the page.
This is the true content of the page.
Each page object has a revisions field that is a list of PageRev, ordered by date. Each page object has a revisions field that is a list of PageRev, ordered by date.
my_page.revisions.last() gives the PageRev object that is the most up-to-date, and thus, my_page.revisions.last() gives the PageRev object that is the most up-to-date, and thus,
is the real content of the page. is the real content of the page.
@ -1492,9 +1453,6 @@ class PageRev(models.Model):
self.page.unset_lock() self.page.unset_lock()
def get_absolute_url(self): def get_absolute_url(self):
"""
This is needed for black magic powered UpdateView's children
"""
return reverse("core:page", kwargs={"page_name": self.page._full_name}) return reverse("core:page", kwargs={"page_name": self.page._full_name})
def __getattribute__(self, attr): def __getattribute__(self, attr):
@ -1573,9 +1531,7 @@ class Gift(models.Model):
class OperationLog(models.Model): class OperationLog(models.Model):
""" """General purpose log object to register operations."""
General purpose log object to register operations
"""
date = models.DateTimeField(_("date"), auto_now_add=True) date = models.DateTimeField(_("date"), auto_now_add=True)
label = models.CharField(_("label"), max_length=255) label = models.CharField(_("label"), max_length=255)

View File

@ -21,24 +21,26 @@
# #
# #
""" """Collection of utils for custom migration tricks.
This page is useful for custom migration tricks.
Sometimes, when you need to have a migration hack and you think it can be Sometimes, when you need to have a migration hack,
useful again, put it there, we never know if we might need the hack again. and you think it can be useful again,
put it there, we never know if we might need the hack again.
""" """
from django.db import connection, migrations from django.db import connection, migrations
class PsqlRunOnly(migrations.RunSQL): class PsqlRunOnly(migrations.RunSQL):
""" """SQL runner for PostgreSQL-only queries.
This is an SQL runner that will launch the given command only if
the used DBMS is PostgreSQL.
It may be useful to run Postgres' specific SQL, or to take actions It may be useful to run Postgres' specific SQL, or to take actions
that would be non-senses with backends other than Postgre, such that would be non-senses with backends other than Postgre, such
as disabling particular constraints that would prevent the migration as disabling particular constraints that would prevent the migration
to run successfully. to run successfully.
If used on another DBMS than Postgres, it will be a noop.
See `club/migrations/0010_auto_20170912_2028.py` as an example. See `club/migrations/0010_auto_20170912_2028.py` as an example.
Some explanations can be found here too: Some explanations can be found here too:
https://stackoverflow.com/questions/28429933/django-migrations-using-runpython-to-commit-changes https://stackoverflow.com/questions/28429933/django-migrations-using-runpython-to-commit-changes

View File

@ -31,9 +31,7 @@ from django.core.files.storage import FileSystemStorage
class ScssFinder(FileSystemFinder): class ScssFinder(FileSystemFinder):
""" """Find static *.css files compiled on the fly."""
Find static *.css files compiled on the fly
"""
locations = [] locations = []

View File

@ -35,10 +35,9 @@ from core.scss.storage import ScssFileStorage, find_file
class ScssProcessor(object): class ScssProcessor(object):
""" """If DEBUG mode enabled : compile the scss file
If DEBUG mode enabled : compile the scss file
Else : give the path of the corresponding css supposed to already be compiled Else : give the path of the corresponding css supposed to already be compiled
Don't forget to use compilestatics to compile scss for production Don't forget to use compilestatics to compile scss for production.
""" """
prefix = iri_to_uri(getattr(settings, "STATIC_URL", "/static/")) prefix = iri_to_uri(getattr(settings, "STATIC_URL", "/static/"))

View File

@ -91,9 +91,9 @@ class IndexSignalProcessor(signals.BaseSignalProcessor):
class BigCharFieldIndex(indexes.CharField): class BigCharFieldIndex(indexes.CharField):
""" """Workaround to avoid xapian.InvalidArgument: Term too long (> 245).
Workaround to avoid xapian.InvalidArgument: Term too long (> 245)
See https://groups.google.com/forum/#!topic/django-haystack/hRJKcPNPXqw/discussion See https://groups.google.com/forum/#!topic/django-haystack/hRJKcPNPXqw/discussion.
""" """
def prepare(self, term): def prepare(self, term):

View File

@ -7,9 +7,7 @@ from core.models import User
@receiver(m2m_changed, sender=User.groups.through, dispatch_uid="user_groups_changed") @receiver(m2m_changed, sender=User.groups.through, dispatch_uid="user_groups_changed")
def user_groups_changed(sender, instance: User, **kwargs): def user_groups_changed(sender, instance: User, **kwargs):
""" """Clear the cached groups of the user."""
Clear the cached groups of the user
"""
# As a m2m relationship doesn't live within the model # As a m2m relationship doesn't live within the model
# but rather on an intermediary table, there is no # but rather on an intermediary table, there is no
# model method to override, meaning we must use # model method to override, meaning we must use

View File

@ -30,11 +30,11 @@ from jinja2.parser import Parser
class HoneypotExtension(Extension): class HoneypotExtension(Extension):
""" """Wrapper around the honeypot extension tag.
Wrapper around the honeypot extension tag
Known limitation: doesn't support arguments
Usage: {% render_honeypot_field %} Known limitation: doesn't support arguments.
Usage: `{% render_honeypot_field %}`
""" """
tags = {"render_honeypot_field"} tags = {"render_honeypot_field"}

View File

@ -46,9 +46,7 @@ def markdown(text):
def phonenumber( def phonenumber(
value, country="FR", number_format=phonenumbers.PhoneNumberFormat.NATIONAL value, country="FR", number_format=phonenumbers.PhoneNumberFormat.NATIONAL
): ):
""" # collectivised from https://github.com/foundertherapy/django-phonenumber-filter.
This filter is kindly borrowed from https://github.com/foundertherapy/django-phonenumber-filter
"""
value = str(value) value = str(value)
try: try:
parsed = phonenumbers.parse(value, country) parsed = phonenumbers.parse(value, country)
@ -59,6 +57,12 @@ def phonenumber(
@register.filter(name="truncate_time") @register.filter(name="truncate_time")
def truncate_time(value, time_unit): def truncate_time(value, time_unit):
"""Remove everything in the time format lower than the specified unit.
Args:
value: the value to truncate
time_unit: the lowest unit to display
"""
value = str(value) value = str(value)
return { return {
"millis": lambda: value.split(".")[0], "millis": lambda: value.split(".")[0],
@ -81,8 +85,6 @@ def format_timedelta(value: datetime.timedelta) -> str:
@register.simple_tag() @register.simple_tag()
def scss(path): def scss(path):
""" """Return path of the corresponding css file after compilation."""
Return path of the corresponding css file after compilation
"""
processor = ScssProcessor(path) processor = ScssProcessor(path)
return processor.get_converted_scss() return processor.get_converted_scss()

View File

@ -105,7 +105,7 @@ class TestUserRegistration:
def test_register_fail_with_not_existing_email( def test_register_fail_with_not_existing_email(
self, client: Client, valid_payload, monkeypatch self, client: Client, valid_payload, monkeypatch
): ):
"""Test that, when email is valid but doesn't actually exist, registration fails""" """Test that, when email is valid but doesn't actually exist, registration fails."""
def always_fail(*_args, **_kwargs): def always_fail(*_args, **_kwargs):
raise SMTPException raise SMTPException
@ -127,10 +127,7 @@ class TestUserLogin:
return User.objects.first() return User.objects.first()
def test_login_fail(self, client, user): def test_login_fail(self, client, user):
""" """Should not login a user correctly."""
Should not login a user correctly
"""
response = client.post( response = client.post(
reverse("core:login"), reverse("core:login"),
{ {
@ -158,9 +155,7 @@ class TestUserLogin:
assert response.wsgi_request.user.is_anonymous assert response.wsgi_request.user.is_anonymous
def test_login_success(self, client, user): def test_login_success(self, client, user):
""" """Should login a user correctly."""
Should login a user correctly
"""
response = client.post( response = client.post(
reverse("core:login"), reverse("core:login"),
{ {
@ -210,7 +205,7 @@ class TestUserLogin:
], ],
) )
def test_custom_markdown_syntax(md, html): def test_custom_markdown_syntax(md, html):
"""Test the homemade markdown syntax""" """Test the homemade markdown syntax."""
assert markdown(md) == f"<p>{html}</p>\n" assert markdown(md) == f"<p>{html}</p>\n"
@ -233,7 +228,6 @@ class PageHandlingTest(TestCase):
def test_create_page_ok(self): def test_create_page_ok(self):
"""Should create a page correctly.""" """Should create a page correctly."""
response = self.client.post( response = self.client.post(
reverse("core:page_new"), reverse("core:page_new"),
{"parent": "", "name": "guy", "owner_group": self.root_group.id}, {"parent": "", "name": "guy", "owner_group": self.root_group.id},
@ -274,9 +268,7 @@ class PageHandlingTest(TestCase):
assert '<a href="/page/guy/bibou/">' in str(response.content) assert '<a href="/page/guy/bibou/">' in str(response.content)
def test_access_child_page_ok(self): def test_access_child_page_ok(self):
""" """Should display a page correctly."""
Should display a page correctly
"""
parent = Page(name="guy", owner_group=self.root_group) parent = Page(name="guy", owner_group=self.root_group)
parent.save(force_lock=True) parent.save(force_lock=True)
page = Page(name="bibou", owner_group=self.root_group, parent=parent) page = Page(name="bibou", owner_group=self.root_group, parent=parent)
@ -289,18 +281,14 @@ class PageHandlingTest(TestCase):
self.assertIn('<a href="/page/guy/bibou/edit/">', html) self.assertIn('<a href="/page/guy/bibou/edit/">', html)
def test_access_page_not_found(self): def test_access_page_not_found(self):
""" """Should not display a page correctly."""
Should not display a page correctly
"""
response = self.client.get(reverse("core:page", kwargs={"page_name": "swagg"})) response = self.client.get(reverse("core:page", kwargs={"page_name": "swagg"}))
assert response.status_code == 200 assert response.status_code == 200
html = response.content.decode() html = response.content.decode()
self.assertIn('<a href="/page/create/?page=swagg">', html) self.assertIn('<a href="/page/create/?page=swagg">', html)
def test_create_page_markdown_safe(self): def test_create_page_markdown_safe(self):
""" """Should format the markdown and escape html correctly."""
Should format the markdown and escape html correctly
"""
self.client.post( self.client.post(
reverse("core:page_new"), {"parent": "", "name": "guy", "owner_group": "1"} reverse("core:page_new"), {"parent": "", "name": "guy", "owner_group": "1"}
) )
@ -335,13 +323,13 @@ http://git.an
class UserToolsTest: class UserToolsTest:
def test_anonymous_user_unauthorized(self, client): def test_anonymous_user_unauthorized(self, client):
"""An anonymous user shouldn't have access to the tools page""" """An anonymous user shouldn't have access to the tools page."""
response = client.get(reverse("core:user_tools")) response = client.get(reverse("core:user_tools"))
assert response.status_code == 403 assert response.status_code == 403
@pytest.mark.parametrize("username", ["guy", "root", "skia", "comunity"]) @pytest.mark.parametrize("username", ["guy", "root", "skia", "comunity"])
def test_page_is_working(self, client, username): def test_page_is_working(self, client, username):
"""All existing users should be able to see the test page""" """All existing users should be able to see the test page."""
# Test for simple user # Test for simple user
client.force_login(User.objects.get(username=username)) client.force_login(User.objects.get(username=username))
response = client.get(reverse("core:user_tools")) response = client.get(reverse("core:user_tools"))
@ -391,9 +379,8 @@ class FileHandlingTest(TestCase):
class UserIsInGroupTest(TestCase): class UserIsInGroupTest(TestCase):
""" """Test that the User.is_in_group() and AnonymousUser.is_in_group()
Test that the User.is_in_group() and AnonymousUser.is_in_group() work as intended.
work as intended
""" """
@classmethod @classmethod
@ -450,30 +437,24 @@ class UserIsInGroupTest(TestCase):
assert user.is_in_group(name=meta_groups_members) is False assert user.is_in_group(name=meta_groups_members) is False
def test_anonymous_user(self): def test_anonymous_user(self):
""" """Test that anonymous users are only in the public group."""
Test that anonymous users are only in the public group
"""
user = AnonymousUser() user = AnonymousUser()
self.assert_only_in_public_group(user) self.assert_only_in_public_group(user)
def test_not_subscribed_user(self): def test_not_subscribed_user(self):
""" """Test that users who never subscribed are only in the public group."""
Test that users who never subscribed are only in the public group
"""
self.assert_only_in_public_group(self.toto) self.assert_only_in_public_group(self.toto)
def test_wrong_parameter_fail(self): def test_wrong_parameter_fail(self):
""" """Test that when neither the pk nor the name argument is given,
Test that when neither the pk nor the name argument is given, the function raises a ValueError.
the function raises a ValueError
""" """
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
self.toto.is_in_group() self.toto.is_in_group()
def test_number_queries(self): def test_number_queries(self):
""" """Test that the number of db queries is stable
Test that the number of db queries is stable and that less queries are made when making a new call.
and that less queries are made when making a new call
""" """
# make sure Skia is in at least one group # make sure Skia is in at least one group
self.skia.groups.add(Group.objects.first().pk) self.skia.groups.add(Group.objects.first().pk)
@ -497,9 +478,8 @@ class UserIsInGroupTest(TestCase):
self.skia.is_in_group(pk=group_not_in.id) self.skia.is_in_group(pk=group_not_in.id)
def test_cache_properly_cleared_membership(self): def test_cache_properly_cleared_membership(self):
""" """Test that when the membership of a user end,
Test that when the membership of a user end, the cache is properly invalidated.
the cache is properly invalidated
""" """
membership = Membership.objects.create( membership = Membership.objects.create(
club=self.club, user=self.toto, end_date=None club=self.club, user=self.toto, end_date=None
@ -515,9 +495,8 @@ class UserIsInGroupTest(TestCase):
assert self.toto.is_in_group(name=meta_groups_members) is False assert self.toto.is_in_group(name=meta_groups_members) is False
def test_cache_properly_cleared_group(self): def test_cache_properly_cleared_group(self):
""" """Test that when a user is removed from a group,
Test that when a user is removed from a group, the is_in_group_method return False when calling it again.
the is_in_group_method return False when calling it again
""" """
# testing with pk # testing with pk
self.toto.groups.add(self.com_admin.pk) self.toto.groups.add(self.com_admin.pk)
@ -534,9 +513,8 @@ class UserIsInGroupTest(TestCase):
assert self.toto.is_in_group(name="SAS admin") is False assert self.toto.is_in_group(name="SAS admin") is False
def test_not_existing_group(self): def test_not_existing_group(self):
""" """Test that searching for a not existing group
Test that searching for a not existing group returns False.
returns False
""" """
assert self.skia.is_in_group(name="This doesn't exist") is False assert self.skia.is_in_group(name="This doesn't exist") is False
@ -557,9 +535,7 @@ class DateUtilsTest(TestCase):
cls.spring_first_day = date(2023, cls.spring_month, cls.spring_day) cls.spring_first_day = date(2023, cls.spring_month, cls.spring_day)
def test_get_semester(self): def test_get_semester(self):
""" """Test that the get_semester function returns the correct semester string."""
Test that the get_semester function returns the correct semester string
"""
assert get_semester_code(self.autumn_semester_january) == "A24" assert get_semester_code(self.autumn_semester_january) == "A24"
assert get_semester_code(self.autumn_semester_september) == "A24" assert get_semester_code(self.autumn_semester_september) == "A24"
assert get_semester_code(self.autumn_first_day) == "A24" assert get_semester_code(self.autumn_first_day) == "A24"
@ -568,9 +544,7 @@ class DateUtilsTest(TestCase):
assert get_semester_code(self.spring_first_day) == "P23" assert get_semester_code(self.spring_first_day) == "P23"
def test_get_start_of_semester_fixed_date(self): def test_get_start_of_semester_fixed_date(self):
""" """Test that the get_start_of_semester correctly the starting date of the semester."""
Test that the get_start_of_semester correctly the starting date of the semester.
"""
automn_2024 = date(2024, self.autumn_month, self.autumn_day) automn_2024 = date(2024, self.autumn_month, self.autumn_day)
assert get_start_of_semester(self.autumn_semester_january) == automn_2024 assert get_start_of_semester(self.autumn_semester_january) == automn_2024
assert get_start_of_semester(self.autumn_semester_september) == automn_2024 assert get_start_of_semester(self.autumn_semester_september) == automn_2024
@ -581,9 +555,8 @@ class DateUtilsTest(TestCase):
assert get_start_of_semester(self.spring_first_day) == spring_2023 assert get_start_of_semester(self.spring_first_day) == spring_2023
def test_get_start_of_semester_today(self): def test_get_start_of_semester_today(self):
""" """Test that the get_start_of_semester returns the start of the current semester
Test that the get_start_of_semester returns the start of the current semester when no date is given.
when no date is given
""" """
with freezegun.freeze_time(self.autumn_semester_september): with freezegun.freeze_time(self.autumn_semester_september):
assert get_start_of_semester() == self.autumn_first_day assert get_start_of_semester() == self.autumn_first_day
@ -592,8 +565,7 @@ class DateUtilsTest(TestCase):
assert get_start_of_semester() == self.spring_first_day assert get_start_of_semester() == self.spring_first_day
def test_get_start_of_semester_changing_date(self): def test_get_start_of_semester_changing_date(self):
""" """Test that the get_start_of_semester correctly gives the starting date of the semester,
Test that the get_start_of_semester correctly gives the starting date of the semester,
even when the semester changes while the server isn't restarted. even when the semester changes while the server isn't restarted.
""" """
spring_2023 = date(2023, self.spring_month, self.spring_day) spring_2023 = date(2023, self.spring_month, self.spring_day)

View File

@ -31,9 +31,7 @@ from PIL.Image import Resampling
def get_git_revision_short_hash() -> str: def get_git_revision_short_hash() -> str:
""" """Return the short hash of the current commit."""
Return the short hash of the current commit
"""
try: try:
output = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]) output = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"])
if isinstance(output, bytes): if isinstance(output, bytes):
@ -44,8 +42,7 @@ def get_git_revision_short_hash() -> str:
def get_start_of_semester(today: Optional[date] = None) -> date: def get_start_of_semester(today: Optional[date] = None) -> date:
""" """Return the date of the start of the semester of the given date.
Return the date of the start of the semester of the given date.
If no date is given, return the start date of the current semester. If no date is given, return the start date of the current semester.
The current semester is computed as follows: The current semester is computed as follows:
@ -54,8 +51,11 @@ def get_start_of_semester(today: Optional[date] = None) -> date:
- If the date is between 01/01 and 15/02 => Autumn semester of the previous year. - If the date is between 01/01 and 15/02 => Autumn semester of the previous year.
- If the date is between 15/02 and 15/08 => Spring semester - If the date is between 15/02 and 15/08 => Spring semester
:param today: the date to use to compute the semester. If None, use today's date. Args:
:return: the date of the start of the semester today: the date to use to compute the semester. If None, use today's date.
Returns:
the date of the start of the semester
""" """
if today is None: if today is None:
today = timezone.now().date() today = timezone.now().date()
@ -72,16 +72,18 @@ def get_start_of_semester(today: Optional[date] = None) -> date:
def get_semester_code(d: Optional[date] = None) -> str: def get_semester_code(d: Optional[date] = None) -> str:
""" """Return the semester code of the given date.
Return the semester code of the given date.
If no date is given, return the semester code of the current semester. If no date is given, return the semester code of the current semester.
The semester code is an upper letter (A for autumn, P for spring), The semester code is an upper letter (A for autumn, P for spring),
followed by the last two digits of the year. followed by the last two digits of the year.
For example, the autumn semester of 2018 is "A18". For example, the autumn semester of 2018 is "A18".
:param d: the date to use to compute the semester. If None, use today's date. Args:
:return: the semester code corresponding to the given date d: the date to use to compute the semester. If None, use today's date.
Returns:
the semester code corresponding to the given date
""" """
if d is None: if d is None:
d = timezone.now().date() d = timezone.now().date()
@ -147,8 +149,15 @@ def exif_auto_rotate(image):
return image return image
def doku_to_markdown(text): def doku_to_markdown(text: str) -> str:
"""This is a quite correct doku translator""" """Convert doku text to the corresponding markdown.
Args:
text: the doku text to convert
Returns:
The converted markdown text
"""
text = re.sub( text = re.sub(
r"([^:]|^)\/\/(.*?)\/\/", r"*\2*", text r"([^:]|^)\/\/(.*?)\/\/", r"*\2*", text
) # Italic (prevents protocol:// conflict) ) # Italic (prevents protocol:// conflict)
@ -235,7 +244,14 @@ def doku_to_markdown(text):
def bbcode_to_markdown(text): def bbcode_to_markdown(text):
"""This is a very basic BBcode translator""" """Convert bbcode text to the corresponding markdown.
Args:
text: the bbcode text to convert
Returns:
The converted markdown text
"""
text = re.sub(r"\[b\](.*?)\[\/b\]", r"**\1**", text, flags=re.DOTALL) # Bold text = re.sub(r"\[b\](.*?)\[\/b\]", r"**\1**", text, flags=re.DOTALL) # Bold
text = re.sub(r"\[i\](.*?)\[\/i\]", r"*\1*", text, flags=re.DOTALL) # Italic text = re.sub(r"\[i\](.*?)\[\/i\]", r"*\1*", text, flags=re.DOTALL) # Italic
text = re.sub(r"\[u\](.*?)\[\/u\]", r"__\1__", text, flags=re.DOTALL) # Underline text = re.sub(r"\[u\](.*?)\[\/u\]", r"__\1__", text, flags=re.DOTALL) # Underline

View File

@ -23,6 +23,7 @@
# #
import types import types
from typing import Any
from django.core.exceptions import ( from django.core.exceptions import (
ImproperlyConfigured, ImproperlyConfigured,
@ -39,6 +40,7 @@ from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
from sentry_sdk import last_event_id from sentry_sdk import last_event_id
from core.models import User
from core.views.forms import LoginForm from core.views.forms import LoginForm
@ -60,60 +62,63 @@ def internal_servor_error(request):
return HttpResponseServerError(render(request, "core/500.jinja")) return HttpResponseServerError(render(request, "core/500.jinja"))
def can_edit_prop(obj, user): def can_edit_prop(obj: Any, user: User) -> bool:
""" """Can the user edit the properties of the object.
:param obj: Object to test for permission
:param user: core.models.User to test permissions against
:return: if user is authorized to edit object properties
:rtype: bool
:Example: Args:
obj: Object to test for permission
user: core.models.User to test permissions against
.. code-block:: python Returns:
True if user is authorized to edit object properties else False
Examples:
```python
if not can_edit_prop(self.object ,request.user): if not can_edit_prop(self.object ,request.user):
raise PermissionDenied raise PermissionDenied
```
""" """
if obj is None or user.is_owner(obj): if obj is None or user.is_owner(obj):
return True return True
return False return False
def can_edit(obj, user): def can_edit(obj: Any, user: User):
""" """Can the user edit the object.
:param obj: Object to test for permission
:param user: core.models.User to test permissions against
:return: if user is authorized to edit object
:rtype: bool
:Example: Args:
obj: Object to test for permission
user: core.models.User to test permissions against
.. code-block:: python Returns:
True if user is authorized to edit object else False
if not can_edit(self.object ,request.user): Examples:
```python
if not can_edit(self.object, request.user):
raise PermissionDenied raise PermissionDenied
```
""" """
if obj is None or user.can_edit(obj): if obj is None or user.can_edit(obj):
return True return True
return can_edit_prop(obj, user) return can_edit_prop(obj, user)
def can_view(obj, user): def can_view(obj: Any, user: User):
""" """Can the user see the object.
:param obj: Object to test for permission
:param user: core.models.User to test permissions against
:return: if user is authorized to see object
:rtype: bool
:Example: Args:
obj: Object to test for permission
user: core.models.User to test permissions against
.. code-block:: python Returns:
True if user is authorized to see object else False
Examples:
```python
if not can_view(self.object ,request.user): if not can_view(self.object ,request.user):
raise PermissionDenied raise PermissionDenied
```
""" """
if obj is None or user.can_view(obj): if obj is None or user.can_view(obj):
return True return True
@ -121,20 +126,22 @@ def can_view(obj, user):
class GenericContentPermissionMixinBuilder(View): class GenericContentPermissionMixinBuilder(View):
""" """Used to build permission mixins.
Used to build permission mixins
This view protect any child view that would be showing an object that is restricted based This view protect any child view that would be showing an object that is restricted based
on two properties on two properties.
:prop permission_function: function to test permission with, takes an object and an user an return a bool Attributes:
:prop raised_error: permission to be raised raised_error: permission to be raised
:raises: raised_error
""" """
permission_function = lambda obj, user: False
raised_error = PermissionDenied raised_error = PermissionDenied
@staticmethod
def permission_function(obj: Any, user: User) -> bool:
"""Function to test permission with."""
return False
@classmethod @classmethod
def get_permission_function(cls, obj, user): def get_permission_function(cls, obj, user):
return cls.permission_function(obj, user) return cls.permission_function(obj, user)
@ -162,11 +169,12 @@ class GenericContentPermissionMixinBuilder(View):
class CanCreateMixin(View): class CanCreateMixin(View):
""" """Protect any child view that would create an object.
This view is made to protect any child view that would create an object, and thus, that can not be protected by any
of the following mixin
:raises: PermissionDenied Raises:
PermissionDenied:
If the user has not the necessary permission
to create the object of the view.
""" """
def dispatch(self, request, *arg, **kwargs): def dispatch(self, request, *arg, **kwargs):
@ -183,55 +191,54 @@ class CanCreateMixin(View):
class CanEditPropMixin(GenericContentPermissionMixinBuilder): class CanEditPropMixin(GenericContentPermissionMixinBuilder):
""" """Ensure the user has owner permissions on the child view object.
This view is made to protect any child view that would be showing some properties of an object that are restricted
to only the owner group of the given object.
In other word, you can make a view with this view as parent, and it would be retricted to the users that are in the
object's owner_group
:raises: PermissionDenied In other word, you can make a view with this view as parent,
and it will be retricted to the users that are in the
object's owner_group or that pass the `obj.can_be_viewed_by` test.
Raises:
PermissionDenied: If the user cannot see the object
""" """
permission_function = can_edit_prop permission_function = can_edit_prop
class CanEditMixin(GenericContentPermissionMixinBuilder): class CanEditMixin(GenericContentPermissionMixinBuilder):
""" """Ensure the user has permission to edit this view's object.
This view makes exactly the same thing as its direct parent, but checks the group on the edit_groups field of the
object
:raises: PermissionDenied Raises:
PermissionDenied: if the user cannot edit this view's object.
""" """
permission_function = can_edit permission_function = can_edit
class CanViewMixin(GenericContentPermissionMixinBuilder): class CanViewMixin(GenericContentPermissionMixinBuilder):
""" """Ensure the user has permission to view this view's object.
This view still makes exactly the same thing as its direct parent, but checks the group on the view_groups field of
the object
:raises: PermissionDenied Raises:
PermissionDenied: if the user cannot edit this view's object.
""" """
permission_function = can_view permission_function = can_view
class UserIsRootMixin(GenericContentPermissionMixinBuilder): class UserIsRootMixin(GenericContentPermissionMixinBuilder):
""" """Allow only root admins.
This view check if the user is root
:raises: PermissionDenied Raises:
PermissionDenied: if the user isn't root
""" """
permission_function = lambda obj, user: user.is_root permission_function = lambda obj, user: user.is_root
class FormerSubscriberMixin(View): class FormerSubscriberMixin(View):
""" """Check if the user was at least an old subscriber.
This view check if the user was at least an old subscriber
:raises: PermissionDenied Raises:
PermissionDenied: if the user never subscribed.
""" """
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
@ -241,10 +248,10 @@ class FormerSubscriberMixin(View):
class UserIsLoggedMixin(View): class UserIsLoggedMixin(View):
""" """Check if the user is logged.
This view check if the user is logged
:raises: PermissionDenied Raises:
PermissionDenied:
""" """
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
@ -254,9 +261,7 @@ class UserIsLoggedMixin(View):
class TabedViewMixin(View): class TabedViewMixin(View):
""" """Basic functions for displaying tabs in the template."""
This view provide the basic functions for displaying tabs in the template
"""
def get_tabs_title(self): def get_tabs_title(self):
if hasattr(self, "tabs_title"): if hasattr(self, "tabs_title"):
@ -299,7 +304,7 @@ class QuickNotifMixin:
return ret return ret
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add quick notifications to context""" """Add quick notifications to context."""
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["quick_notifs"] = [] kwargs["quick_notifs"] = []
for n in self.quick_notif_list: for n in self.quick_notif_list:
@ -312,21 +317,15 @@ class QuickNotifMixin:
class DetailFormView(SingleObjectMixin, FormView): class DetailFormView(SingleObjectMixin, FormView):
""" """Class that allow both a detail view and a form view."""
Class that allow both a detail view and a form view
"""
def get_object(self): def get_object(self):
""" """Get current group from id in url."""
Get current group from id in url
"""
return self.cached_object return self.cached_object
@cached_property @cached_property
def cached_object(self): def cached_object(self):
""" """Optimisation on group retrieval."""
Optimisation on group retrieval
"""
return super().get_object() return super().get_object()

View File

@ -42,8 +42,7 @@ from counter.models import Counter
def send_file(request, file_id, file_class=SithFile, file_attr="file"): def send_file(request, file_id, file_class=SithFile, file_attr="file"):
""" """Send a file through Django without loading the whole file into
Send a file through Django without loading the whole file into
memory at once. The FileWrapper will turn the file object into an memory at once. The FileWrapper will turn the file object into an
iterator for chunks of 8KB. iterator for chunks of 8KB.
""" """
@ -268,7 +267,7 @@ class FileEditPropView(CanEditPropMixin, UpdateView):
class FileView(CanViewMixin, DetailView, FormMixin): class FileView(CanViewMixin, DetailView, FormMixin):
"""This class handle the upload of new files into a folder""" """Handle the upload of new files into a folder."""
model = SithFile model = SithFile
pk_url_kwarg = "file_id" pk_url_kwarg = "file_id"
@ -278,8 +277,8 @@ class FileView(CanViewMixin, DetailView, FormMixin):
@staticmethod @staticmethod
def handle_clipboard(request, obj): def handle_clipboard(request, obj):
""" """Handle the clipboard in the view.
This method handles the clipboard in the view.
This method can fail, since it does not catch the exceptions coming from This method can fail, since it does not catch the exceptions coming from
below, allowing proper handling in the calling view. below, allowing proper handling in the calling view.
Use this method like this: Use this method like this:

View File

@ -196,10 +196,9 @@ class RegisteringForm(UserCreationForm):
class UserProfileForm(forms.ModelForm): class UserProfileForm(forms.ModelForm):
""" """Form handling the user profile, managing the files
Form handling the user profile, managing the files
This form is actually pretty bad and was made in the rush before the migration. It should be refactored. This form is actually pretty bad and was made in the rush before the migration. It should be refactored.
TODO: refactor this form TODO: refactor this form.
""" """
class Meta: class Meta:

View File

@ -13,9 +13,7 @@
# #
# #
""" """Views to manage Groups."""
This module contains views to manage Groups
"""
from ajax_select.fields import AutoCompleteSelectMultipleField from ajax_select.fields import AutoCompleteSelectMultipleField
from django import forms from django import forms
@ -31,9 +29,7 @@ from core.views import CanCreateMixin, CanEditMixin, DetailFormView
class EditMembersForm(forms.Form): class EditMembersForm(forms.Form):
""" """Add and remove members from a Group."""
Add and remove members from a Group
"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.current_users = kwargs.pop("users", []) self.current_users = kwargs.pop("users", [])
@ -53,9 +49,7 @@ class EditMembersForm(forms.Form):
) )
def clean_users_added(self): def clean_users_added(self):
""" """Check that the user is not trying to add an user already in the group."""
Check that the user is not trying to add an user already in the group
"""
cleaned_data = super().clean() cleaned_data = super().clean()
users_added = cleaned_data.get("users_added", None) users_added = cleaned_data.get("users_added", None)
if not users_added: if not users_added:
@ -77,9 +71,7 @@ class EditMembersForm(forms.Form):
class GroupListView(CanEditMixin, ListView): class GroupListView(CanEditMixin, ListView):
""" """Displays the Group list."""
Displays the Group list
"""
model = RealGroup model = RealGroup
ordering = ["name"] ordering = ["name"]
@ -87,9 +79,7 @@ class GroupListView(CanEditMixin, ListView):
class GroupEditView(CanEditMixin, UpdateView): class GroupEditView(CanEditMixin, UpdateView):
""" """Edit infos of a Group."""
Edit infos of a Group
"""
model = RealGroup model = RealGroup
pk_url_kwarg = "group_id" pk_url_kwarg = "group_id"
@ -98,9 +88,7 @@ class GroupEditView(CanEditMixin, UpdateView):
class GroupCreateView(CanCreateMixin, CreateView): class GroupCreateView(CanCreateMixin, CreateView):
""" """Add a new Group."""
Add a new Group
"""
model = RealGroup model = RealGroup
template_name = "core/create.jinja" template_name = "core/create.jinja"
@ -108,9 +96,8 @@ class GroupCreateView(CanCreateMixin, CreateView):
class GroupTemplateView(CanEditMixin, DetailFormView): class GroupTemplateView(CanEditMixin, DetailFormView):
""" """Display all users in a given Group
Display all users in a given Group Allow adding and removing users from it.
Allow adding and removing users from it
""" """
model = RealGroup model = RealGroup
@ -143,9 +130,7 @@ class GroupTemplateView(CanEditMixin, DetailFormView):
class GroupDeleteView(CanEditMixin, DeleteView): class GroupDeleteView(CanEditMixin, DeleteView):
""" """Delete a Group."""
Delete a Group
"""
model = RealGroup model = RealGroup
pk_url_kwarg = "group_id" pk_url_kwarg = "group_id"

View File

@ -74,9 +74,7 @@ from trombi.views import UserTrombiForm
@method_decorator(check_honeypot, name="post") @method_decorator(check_honeypot, name="post")
class SithLoginView(views.LoginView): class SithLoginView(views.LoginView):
""" """The login View."""
The login View
"""
template_name = "core/login.jinja" template_name = "core/login.jinja"
authentication_form = LoginForm authentication_form = LoginForm
@ -85,33 +83,25 @@ class SithLoginView(views.LoginView):
class SithPasswordChangeView(views.PasswordChangeView): class SithPasswordChangeView(views.PasswordChangeView):
""" """Allows a user to change its password."""
Allows a user to change its password
"""
template_name = "core/password_change.jinja" template_name = "core/password_change.jinja"
success_url = reverse_lazy("core:password_change_done") success_url = reverse_lazy("core:password_change_done")
class SithPasswordChangeDoneView(views.PasswordChangeDoneView): class SithPasswordChangeDoneView(views.PasswordChangeDoneView):
""" """Allows a user to change its password."""
Allows a user to change its password
"""
template_name = "core/password_change_done.jinja" template_name = "core/password_change_done.jinja"
def logout(request): def logout(request):
""" """The logout view."""
The logout view
"""
return views.logout_then_login(request) return views.logout_then_login(request)
def password_root_change(request, user_id): def password_root_change(request, user_id):
""" """Allows a root user to change someone's password."""
Allows a root user to change someone's password
"""
if not request.user.is_root: if not request.user.is_root:
raise PermissionDenied raise PermissionDenied
user = User.objects.filter(id=user_id).first() user = User.objects.filter(id=user_id).first()
@ -131,9 +121,7 @@ def password_root_change(request, user_id):
@method_decorator(check_honeypot, name="post") @method_decorator(check_honeypot, name="post")
class SithPasswordResetView(views.PasswordResetView): class SithPasswordResetView(views.PasswordResetView):
""" """Allows someone to enter an email address for resetting password."""
Allows someone to enter an email address for resetting password
"""
template_name = "core/password_reset.jinja" template_name = "core/password_reset.jinja"
email_template_name = "core/password_reset_email.jinja" email_template_name = "core/password_reset_email.jinja"
@ -141,26 +129,20 @@ class SithPasswordResetView(views.PasswordResetView):
class SithPasswordResetDoneView(views.PasswordResetDoneView): class SithPasswordResetDoneView(views.PasswordResetDoneView):
""" """Confirm that the reset email has been sent."""
Confirm that the reset email has been sent
"""
template_name = "core/password_reset_done.jinja" template_name = "core/password_reset_done.jinja"
class SithPasswordResetConfirmView(views.PasswordResetConfirmView): class SithPasswordResetConfirmView(views.PasswordResetConfirmView):
""" """Provide a reset password form."""
Provide a reset password form
"""
template_name = "core/password_reset_confirm.jinja" template_name = "core/password_reset_confirm.jinja"
success_url = reverse_lazy("core:password_reset_complete") success_url = reverse_lazy("core:password_reset_complete")
class SithPasswordResetCompleteView(views.PasswordResetCompleteView): class SithPasswordResetCompleteView(views.PasswordResetCompleteView):
""" """Confirm the password has successfully been reset."""
Confirm the password has successfully been reset
"""
template_name = "core/password_reset_complete.jinja" template_name = "core/password_reset_complete.jinja"
@ -302,9 +284,7 @@ class UserTabsMixin(TabedViewMixin):
class UserView(UserTabsMixin, CanViewMixin, DetailView): class UserView(UserTabsMixin, CanViewMixin, DetailView):
""" """Display a user's profile."""
Display a user's profile
"""
model = User model = User
pk_url_kwarg = "user_id" pk_url_kwarg = "user_id"
@ -321,9 +301,7 @@ class UserView(UserTabsMixin, CanViewMixin, DetailView):
class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView): class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView):
""" """Display a user's pictures."""
Display a user's pictures
"""
model = User model = User
pk_url_kwarg = "user_id" pk_url_kwarg = "user_id"
@ -361,9 +339,7 @@ def delete_user_godfather(request, user_id, godfather_id, is_father):
class UserGodfathersView(UserTabsMixin, CanViewMixin, DetailView): class UserGodfathersView(UserTabsMixin, CanViewMixin, DetailView):
""" """Display a user's godfathers."""
Display a user's godfathers
"""
model = User model = User
pk_url_kwarg = "user_id" pk_url_kwarg = "user_id"
@ -394,9 +370,7 @@ class UserGodfathersView(UserTabsMixin, CanViewMixin, DetailView):
class UserGodfathersTreeView(UserTabsMixin, CanViewMixin, DetailView): class UserGodfathersTreeView(UserTabsMixin, CanViewMixin, DetailView):
""" """Display a user's family tree."""
Display a user's family tree
"""
model = User model = User
pk_url_kwarg = "user_id" pk_url_kwarg = "user_id"
@ -415,9 +389,7 @@ class UserGodfathersTreeView(UserTabsMixin, CanViewMixin, DetailView):
class UserGodfathersTreePictureView(CanViewMixin, DetailView): class UserGodfathersTreePictureView(CanViewMixin, DetailView):
""" """Display a user's tree as a picture."""
Display a user's tree as a picture
"""
model = User model = User
pk_url_kwarg = "user_id" pk_url_kwarg = "user_id"
@ -489,9 +461,7 @@ class UserGodfathersTreePictureView(CanViewMixin, DetailView):
class UserStatsView(UserTabsMixin, CanViewMixin, DetailView): class UserStatsView(UserTabsMixin, CanViewMixin, DetailView):
""" """Display a user's stats."""
Display a user's stats
"""
model = User model = User
pk_url_kwarg = "user_id" pk_url_kwarg = "user_id"
@ -591,9 +561,7 @@ class UserStatsView(UserTabsMixin, CanViewMixin, DetailView):
class UserMiniView(CanViewMixin, DetailView): class UserMiniView(CanViewMixin, DetailView):
""" """Display a user's profile."""
Display a user's profile
"""
model = User model = User
pk_url_kwarg = "user_id" pk_url_kwarg = "user_id"
@ -602,18 +570,14 @@ class UserMiniView(CanViewMixin, DetailView):
class UserListView(ListView, CanEditPropMixin): class UserListView(ListView, CanEditPropMixin):
""" """Displays the user list."""
Displays the user list
"""
model = User model = User
template_name = "core/user_list.jinja" template_name = "core/user_list.jinja"
class UserUploadProfilePictView(CanEditMixin, DetailView): class UserUploadProfilePictView(CanEditMixin, DetailView):
""" """Handle the upload of the profile picture taken with webcam in navigator."""
Handle the upload of the profile picture taken with webcam in navigator
"""
model = User model = User
pk_url_kwarg = "user_id" pk_url_kwarg = "user_id"
@ -650,9 +614,7 @@ class UserUploadProfilePictView(CanEditMixin, DetailView):
class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView): class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView):
""" """Edit a user's profile."""
Edit a user's profile
"""
model = User model = User
pk_url_kwarg = "user_id" pk_url_kwarg = "user_id"
@ -663,9 +625,7 @@ class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView):
board_only = [] board_only = []
def remove_restricted_fields(self, request): def remove_restricted_fields(self, request):
""" """Removes edit_once and board_only fields."""
Removes edit_once and board_only fields
"""
for i in self.edit_once: for i in self.edit_once:
if getattr(self.form.instance, i) and not ( if getattr(self.form.instance, i) and not (
request.user.is_board_member or request.user.is_root request.user.is_board_member or request.user.is_root
@ -703,9 +663,7 @@ class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView):
class UserClubView(UserTabsMixin, CanViewMixin, DetailView): class UserClubView(UserTabsMixin, CanViewMixin, DetailView):
""" """Display the user's club(s)."""
Display the user's club(s)
"""
model = User model = User
context_object_name = "profile" context_object_name = "profile"
@ -715,9 +673,7 @@ class UserClubView(UserTabsMixin, CanViewMixin, DetailView):
class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView): class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView):
""" """Edit a user's preferences."""
Edit a user's preferences
"""
model = User model = User
pk_url_kwarg = "user_id" pk_url_kwarg = "user_id"
@ -752,9 +708,7 @@ class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView):
class UserUpdateGroupView(UserTabsMixin, CanEditPropMixin, UpdateView): class UserUpdateGroupView(UserTabsMixin, CanEditPropMixin, UpdateView):
""" """Edit a user's groups."""
Edit a user's groups
"""
model = User model = User
pk_url_kwarg = "user_id" pk_url_kwarg = "user_id"
@ -767,9 +721,7 @@ class UserUpdateGroupView(UserTabsMixin, CanEditPropMixin, UpdateView):
class UserToolsView(QuickNotifMixin, UserTabsMixin, UserIsLoggedMixin, TemplateView): class UserToolsView(QuickNotifMixin, UserTabsMixin, UserIsLoggedMixin, TemplateView):
""" """Displays the logged user's tools."""
Displays the logged user's tools
"""
template_name = "core/user_tools.jinja" template_name = "core/user_tools.jinja"
current_tab = "tools" current_tab = "tools"
@ -786,9 +738,7 @@ class UserToolsView(QuickNotifMixin, UserTabsMixin, UserIsLoggedMixin, TemplateV
class UserAccountBase(UserTabsMixin, DetailView): class UserAccountBase(UserTabsMixin, DetailView):
""" """Base class for UserAccount."""
Base class for UserAccount
"""
model = User model = User
pk_url_kwarg = "user_id" pk_url_kwarg = "user_id"
@ -809,9 +759,7 @@ class UserAccountBase(UserTabsMixin, DetailView):
class UserAccountView(UserAccountBase): class UserAccountView(UserAccountBase):
""" """Display a user's account."""
Display a user's account
"""
template_name = "core/user_account.jinja" template_name = "core/user_account.jinja"
@ -858,9 +806,7 @@ class UserAccountView(UserAccountBase):
class UserAccountDetailView(UserAccountBase, YearMixin, MonthMixin): class UserAccountDetailView(UserAccountBase, YearMixin, MonthMixin):
""" """Display a user's account for month."""
Display a user's account for month
"""
template_name = "core/user_account_detail.jinja" template_name = "core/user_account_detail.jinja"

View File

@ -30,9 +30,8 @@ class BillingInfoForm(forms.ModelForm):
class StudentCardForm(forms.ModelForm): class StudentCardForm(forms.ModelForm):
""" """Form for adding student cards
Form for adding student cards Only used for user profile since CounterClick is to complicated.
Only used for user profile since CounterClick is to complicated
""" """
class Meta: class Meta:
@ -48,8 +47,7 @@ class StudentCardForm(forms.ModelForm):
class GetUserForm(forms.Form): class GetUserForm(forms.Form):
""" """The Form class aims at providing a valid user_id field in its cleaned data, in order to pass it to some view,
The Form class aims at providing a valid user_id field in its cleaned data, in order to pass it to some view,
reverse function, or any other use. reverse function, or any other use.
The Form implements a nice JS widget allowing the user to type a customer account id, or search the database with The Form implements a nice JS widget allowing the user to type a customer account id, or search the database with

View File

@ -44,9 +44,10 @@ from subscription.models import Subscription
class Customer(models.Model): class Customer(models.Model):
""" """Customer data of a User.
This class extends a user to make a customer. It adds some basic customers' information, such as the account ID, and
is used by other accounting classes as reference to the customer, rather than using User It adds some basic customers' information, such as the account ID, and
is used by other accounting classes as reference to the customer, rather than using User.
""" """
user = models.OneToOneField(User, primary_key=True, on_delete=models.CASCADE) user = models.OneToOneField(User, primary_key=True, on_delete=models.CASCADE)
@ -63,10 +64,9 @@ class Customer(models.Model):
return "%s - %s" % (self.user.username, self.account_id) return "%s - %s" % (self.user.username, self.account_id)
def save(self, *args, allow_negative=False, is_selling=False, **kwargs): def save(self, *args, allow_negative=False, is_selling=False, **kwargs):
""" """is_selling : tell if the current action is a selling
is_selling : tell if the current action is a selling
allow_negative : ignored if not a selling. Allow a selling to put the account in negative allow_negative : ignored if not a selling. Allow a selling to put the account in negative
Those two parameters avoid blocking the save method of a customer if his account is negative Those two parameters avoid blocking the save method of a customer if his account is negative.
""" """
if self.amount < 0 and (is_selling and not allow_negative): if self.amount < 0 and (is_selling and not allow_negative):
raise ValidationError(_("Not enough money")) raise ValidationError(_("Not enough money"))
@ -84,9 +84,8 @@ class Customer(models.Model):
@property @property
def can_buy(self) -> bool: def can_buy(self) -> bool:
""" """Check if whether this customer has the right to purchase any item.
Check if whether this customer has the right to
purchase any item.
This must be not confused with the Product.can_be_sold_to(user) This must be not confused with the Product.can_be_sold_to(user)
method as the present method returns an information method as the present method returns an information
about a customer whereas the other tells something about a customer whereas the other tells something
@ -100,8 +99,7 @@ class Customer(models.Model):
@classmethod @classmethod
def get_or_create(cls, user: User) -> Tuple[Customer, bool]: def get_or_create(cls, user: User) -> Tuple[Customer, bool]:
""" """Work in pretty much the same way as the usual get_or_create method,
Work in pretty much the same way as the usual get_or_create method,
but with the default field replaced by some under the hood. but with the default field replaced by some under the hood.
If the user has an account, return it as is. If the user has an account, return it as is.
@ -158,9 +156,8 @@ class Customer(models.Model):
class BillingInfo(models.Model): class BillingInfo(models.Model):
""" """Represent the billing information of a user, which are required
Represent the billing information of a user, which are required by the 3D-Secure v2 system used by the etransaction module.
by the 3D-Secure v2 system used by the etransaction module
""" """
customer = models.OneToOneField( customer = models.OneToOneField(
@ -182,10 +179,9 @@ class BillingInfo(models.Model):
return f"{self.first_name} {self.last_name}" return f"{self.first_name} {self.last_name}"
def to_3dsv2_xml(self) -> str: def to_3dsv2_xml(self) -> str:
""" """Convert the data from this model into a xml usable
Convert the data from this model into a xml usable
by the online paying service of the Crédit Agricole bank. by the online paying service of the Crédit Agricole bank.
see : `https://www.ca-moncommerce.com/espace-client-mon-commerce/up2pay-e-transactions/ma-documentation/manuel-dintegration-focus-3ds-v2/principes-generaux/#integration-3dsv2-developpeur-webmaster` see : `https://www.ca-moncommerce.com/espace-client-mon-commerce/up2pay-e-transactions/ma-documentation/manuel-dintegration-focus-3ds-v2/principes-generaux/#integration-3dsv2-developpeur-webmaster`.
""" """
data = { data = {
"Address": { "Address": {
@ -204,9 +200,9 @@ class BillingInfo(models.Model):
class ProductType(models.Model): class ProductType(models.Model):
""" """A product type.
This describes a product type
Useful only for categorizing, changes are made at the product level for now Useful only for categorizing.
""" """
name = models.CharField(_("name"), max_length=30) name = models.CharField(_("name"), max_length=30)
@ -229,9 +225,7 @@ class ProductType(models.Model):
return reverse("counter:producttype_list") return reverse("counter:producttype_list")
def is_owned_by(self, user): def is_owned_by(self, user):
""" """Method to see if that object can be edited by the given user."""
Method to see if that object can be edited by the given user
"""
if user.is_anonymous: if user.is_anonymous:
return False return False
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
@ -240,9 +234,7 @@ class ProductType(models.Model):
class Product(models.Model): class Product(models.Model):
""" """A product, with all its related information."""
This describes a product, with all its related informations
"""
name = models.CharField(_("name"), max_length=64) name = models.CharField(_("name"), max_length=64)
description = models.TextField(_("description"), blank=True) description = models.TextField(_("description"), blank=True)
@ -297,9 +289,7 @@ class Product(models.Model):
return settings.SITH_ECOCUP_DECO == self.id return settings.SITH_ECOCUP_DECO == self.id
def is_owned_by(self, user): def is_owned_by(self, user):
""" """Method to see if that object can be edited by the given user."""
Method to see if that object can be edited by the given user
"""
if user.is_anonymous: if user.is_anonymous:
return False return False
if user.is_in_group( if user.is_in_group(
@ -309,8 +299,7 @@ class Product(models.Model):
return False return False
def can_be_sold_to(self, user: User) -> bool: def can_be_sold_to(self, user: User) -> bool:
""" """Check if whether the user given in parameter has the right to buy
Check if whether the user given in parameter has the right to buy
this product or not. this product or not.
This must be not confused with the Customer.can_buy() This must be not confused with the Customer.can_buy()
@ -319,7 +308,8 @@ class Product(models.Model):
whereas the other tells something about a Customer whereas the other tells something about a Customer
(and not a user, they are not the same model). (and not a user, they are not the same model).
:return: True if the user can buy this product else False Returns:
True if the user can buy this product else False
""" """
if not self.buying_groups.exists(): if not self.buying_groups.exists():
return True return True
@ -335,15 +325,16 @@ class Product(models.Model):
class CounterQuerySet(models.QuerySet): class CounterQuerySet(models.QuerySet):
def annotate_has_barman(self, user: User) -> CounterQuerySet: def annotate_has_barman(self, user: User) -> CounterQuerySet:
""" """Annotate the queryset with the `user_is_barman` field.
Annotate the queryset with the `user_is_barman` field.
For each counter, this field has value True if the user For each counter, this field has value True if the user
is a barman of this counter, else False. is a barman of this counter, else False.
:param user: the user we want to check if he is a barman Args:
user: the user we want to check if he is a barman
Example::
Examples:
```python
sli = User.objects.get(username="sli") sli = User.objects.get(username="sli")
counters = ( counters = (
Counter.objects Counter.objects
@ -353,6 +344,7 @@ class CounterQuerySet(models.QuerySet):
print("Sli est barman dans les comptoirs suivants :") print("Sli est barman dans les comptoirs suivants :")
for counter in counters: for counter in counters:
print(f"- {counter.name}") print(f"- {counter.name}")
```
""" """
subquery = user.counters.filter(pk=OuterRef("pk")) subquery = user.counters.filter(pk=OuterRef("pk"))
# noinspection PyTypeChecker # noinspection PyTypeChecker
@ -417,23 +409,21 @@ class Counter(models.Model):
return user.is_board_member or user in self.sellers.all() return user.is_board_member or user in self.sellers.all()
def gen_token(self): def gen_token(self):
"""Generate a new token for this counter""" """Generate a new token for this counter."""
self.token = "".join( self.token = "".join(
random.choice(string.ascii_letters + string.digits) for x in range(30) random.choice(string.ascii_letters + string.digits) for x in range(30)
) )
self.save() self.save()
def add_barman(self, user): def add_barman(self, user):
""" """Logs a barman in to the given counter.
Logs a barman in to the given counter
A user is stored as a tuple with its login time A user is stored as a tuple with its login time.
""" """
Permanency(user=user, counter=self, start=timezone.now(), end=None).save() Permanency(user=user, counter=self, start=timezone.now(), end=None).save()
def del_barman(self, user): def del_barman(self, user):
""" """Logs a barman out and store its permanency."""
Logs a barman out and store its permanency
"""
perm = Permanency.objects.filter(counter=self, user=user, end=None).all() perm = Permanency.objects.filter(counter=self, user=user, end=None).all()
for p in perm: for p in perm:
p.end = p.activity p.end = p.activity
@ -444,8 +434,7 @@ class Counter(models.Model):
return self.get_barmen_list() return self.get_barmen_list()
def get_barmen_list(self): def get_barmen_list(self):
""" """Returns the barman list as list of User.
Returns the barman list as list of User
Also handle the timeout of the barmen Also handle the timeout of the barmen
""" """
@ -462,16 +451,12 @@ class Counter(models.Model):
return bl return bl
def get_random_barman(self): def get_random_barman(self):
""" """Return a random user being currently a barman."""
Return a random user being currently a barman
"""
bl = self.get_barmen_list() bl = self.get_barmen_list()
return bl[random.randrange(0, len(bl))] return bl[random.randrange(0, len(bl))]
def update_activity(self): def update_activity(self):
""" """Update the barman activity to prevent timeout."""
Update the barman activity to prevent timeout
"""
for p in Permanency.objects.filter(counter=self, end=None).all(): for p in Permanency.objects.filter(counter=self, end=None).all():
p.save() # Update activity p.save() # Update activity
@ -479,25 +464,18 @@ class Counter(models.Model):
return len(self.barmen_list) > 0 return len(self.barmen_list) > 0
def is_inactive(self): def is_inactive(self):
""" """Returns True if the counter self is inactive from SITH_COUNTER_MINUTE_INACTIVE's value minutes, else False."""
Returns True if the counter self is inactive from SITH_COUNTER_MINUTE_INACTIVE's value minutes, else False
"""
return self.is_open() and ( return self.is_open() and (
(timezone.now() - self.permanencies.order_by("-activity").first().activity) (timezone.now() - self.permanencies.order_by("-activity").first().activity)
> timedelta(minutes=settings.SITH_COUNTER_MINUTE_INACTIVE) > timedelta(minutes=settings.SITH_COUNTER_MINUTE_INACTIVE)
) )
def barman_list(self): def barman_list(self):
""" """Returns the barman id list."""
Returns the barman id list
"""
return [b.id for b in self.get_barmen_list()] return [b.id for b in self.get_barmen_list()]
def can_refill(self): def can_refill(self):
""" """Show if the counter authorize the refilling with physic money."""
Show if the counter authorize the refilling with physic money
"""
if self.type != "BAR": if self.type != "BAR":
return False return False
if self.id in SITH_COUNTER_OFFICES: if self.id in SITH_COUNTER_OFFICES:
@ -511,8 +489,7 @@ class Counter(models.Model):
return is_ae_member return is_ae_member
def get_top_barmen(self) -> QuerySet: def get_top_barmen(self) -> QuerySet:
""" """Return a QuerySet querying the office hours stats of all the barmen of all time
Return a QuerySet querying the office hours stats of all the barmen of all time
of this counter, ordered by descending number of hours. of this counter, ordered by descending number of hours.
Each element of the QuerySet corresponds to a barman and has the following data : Each element of the QuerySet corresponds to a barman and has the following data :
@ -535,16 +512,17 @@ class Counter(models.Model):
) )
def get_top_customers(self, since: datetime | date | None = None) -> QuerySet: def get_top_customers(self, since: datetime | date | None = None) -> QuerySet:
""" """Return a QuerySet querying the money spent by customers of this counter
Return a QuerySet querying the money spent by customers of this counter
since the specified date, ordered by descending amount of money spent. since the specified date, ordered by descending amount of money spent.
Each element of the QuerySet corresponds to a customer and has the following data : Each element of the QuerySet corresponds to a customer and has the following data :
- the full name (first name + last name) of the customer
- the nickname of the customer
- the amount of money spent by the customer
:param since: timestamp from which to perform the calculation - the full name (first name + last name) of the customer
- the nickname of the customer
- the amount of money spent by the customer
Args:
since: timestamp from which to perform the calculation
""" """
if since is None: if since is None:
since = get_start_of_semester() since = get_start_of_semester()
@ -573,12 +551,15 @@ class Counter(models.Model):
) )
def get_total_sales(self, since: datetime | date | None = None) -> CurrencyField: def get_total_sales(self, since: datetime | date | None = None) -> CurrencyField:
""" """Compute and return the total turnover of this counter since the given date.
Compute and return the total turnover of this counter
since the date specified in parameter (by default, since the start of the current By default, the date is the start of the current semester.
semester)
:param since: timestamp from which to perform the calculation Args:
:return: Total revenue earned at this counter since: timestamp from which to perform the calculation
Returns:
Total revenue earned at this counter.
""" """
if since is None: if since is None:
since = get_start_of_semester() since = get_start_of_semester()
@ -591,9 +572,7 @@ class Counter(models.Model):
class Refilling(models.Model): class Refilling(models.Model):
""" """Handle the refilling."""
Handle the refilling
"""
counter = models.ForeignKey( counter = models.ForeignKey(
Counter, related_name="refillings", blank=False, on_delete=models.CASCADE Counter, related_name="refillings", blank=False, on_delete=models.CASCADE
@ -665,9 +644,7 @@ class Refilling(models.Model):
class Selling(models.Model): class Selling(models.Model):
""" """Handle the sellings."""
Handle the sellings
"""
label = models.CharField(_("label"), max_length=64) label = models.CharField(_("label"), max_length=64)
product = models.ForeignKey( product = models.ForeignKey(
@ -724,9 +701,7 @@ class Selling(models.Model):
) )
def save(self, *args, allow_negative=False, **kwargs): def save(self, *args, allow_negative=False, **kwargs):
""" """allow_negative : Allow this selling to use more money than available for this user."""
allow_negative : Allow this selling to use more money than available for this user
"""
if not self.date: if not self.date:
self.date = timezone.now() self.date = timezone.now()
self.full_clean() self.full_clean()
@ -864,8 +839,10 @@ class Selling(models.Model):
class Permanency(models.Model): class Permanency(models.Model):
""" """A permanency of a barman, on a counter.
This class aims at storing a traceability of who was barman where and when
This aims at storing a traceability of who was barman where and when.
Mainly for ~~dick size contest~~ establishing the top 10 barmen of the semester.
""" """
user = models.ForeignKey( user = models.ForeignKey(
@ -971,9 +948,7 @@ class CashRegisterSummary(models.Model):
return object.__getattribute__(self, name) return object.__getattribute__(self, name)
def is_owned_by(self, user): def is_owned_by(self, user):
""" """Method to see if that object can be edited by the given user."""
Method to see if that object can be edited by the given user
"""
if user.is_anonymous: if user.is_anonymous:
return False return False
if user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID): if user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID):
@ -1010,9 +985,7 @@ class CashRegisterSummaryItem(models.Model):
class Eticket(models.Model): class Eticket(models.Model):
""" """Eticket can be linked to a product an allows PDF generation."""
Eticket can be linked to a product an allows PDF generation
"""
product = models.OneToOneField( product = models.OneToOneField(
Product, Product,
@ -1041,9 +1014,7 @@ class Eticket(models.Model):
return reverse("counter:eticket_list") return reverse("counter:eticket_list")
def is_owned_by(self, user): def is_owned_by(self, user):
""" """Method to see if that object can be edited by the given user."""
Method to see if that object can be edited by the given user
"""
if user.is_anonymous: if user.is_anonymous:
return False return False
return user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID) return user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)
@ -1058,11 +1029,11 @@ class Eticket(models.Model):
class StudentCard(models.Model): class StudentCard(models.Model):
""" """Alternative way to connect a customer into a counter.
Alternative way to connect a customer into a counter
We are using Mifare DESFire EV1 specs since it's used for izly cards We are using Mifare DESFire EV1 specs since it's used for izly cards
https://www.nxp.com/docs/en/application-note/AN10927.pdf https://www.nxp.com/docs/en/application-note/AN10927.pdf
UID is 7 byte long that means 14 hexa characters UID is 7 byte long that means 14 hexa characters.
""" """
UID_SIZE = 14 UID_SIZE = 14

View File

@ -140,10 +140,7 @@ class CounterTest(TestCase):
assert response.status_code == 200 assert response.status_code == 200
def test_annotate_has_barman_queryset(self): def test_annotate_has_barman_queryset(self):
""" """Test if the custom queryset method `annotate_has_barman` works as intended."""
Test if the custom queryset method ``annotate_has_barman``
works as intended
"""
self.sli.counters.set([self.foyer, self.mde]) self.sli.counters.set([self.foyer, self.mde])
counters = Counter.objects.annotate_has_barman(self.sli) counters = Counter.objects.annotate_has_barman(self.sli)
for counter in counters: for counter in counters:
@ -265,15 +262,11 @@ class CounterStatsTest(TestCase):
assert response.status_code == 403 assert response.status_code == 403
def test_get_total_sales(self): def test_get_total_sales(self):
""" """Test the result of the Counter.get_total_sales() method."""
Test the result of the Counter.get_total_sales() method
"""
assert self.counter.get_total_sales() == 3102 assert self.counter.get_total_sales() == 3102
def test_top_barmen(self): def test_top_barmen(self):
""" """Test the result of Counter.get_top_barmen() is correct."""
Test the result of Counter.get_top_barmen() is correct
"""
users = [self.skia, self.root, self.sli] users = [self.skia, self.root, self.sli]
perm_times = [ perm_times = [
timedelta(days=16, hours=2, minutes=35, seconds=54), timedelta(days=16, hours=2, minutes=35, seconds=54),
@ -292,9 +285,7 @@ class CounterStatsTest(TestCase):
] ]
def test_top_customer(self): def test_top_customer(self):
""" """Test the result of Counter.get_top_customers() is correct."""
Test the result of Counter.get_top_customers() is correct
"""
users = [self.sli, self.skia, self.krophil, self.root] users = [self.sli, self.skia, self.krophil, self.root]
sale_amounts = [2000, 1000, 100, 2] sale_amounts = [2000, 1000, 100, 2]
assert list(self.counter.get_top_customers()) == [ assert list(self.counter.get_top_customers()) == [
@ -588,9 +579,8 @@ class BarmanConnectionTest(TestCase):
class StudentCardTest(TestCase): class StudentCardTest(TestCase):
""" """Tests for adding and deleting Stundent Cards
Tests for adding and deleting Stundent Cards Test that an user can be found with it's student card.
Test that an user can be found with it's student card
""" """
@classmethod @classmethod

View File

@ -75,9 +75,7 @@ from counter.models import (
class CounterAdminMixin(View): class CounterAdminMixin(View):
""" """Protect counter admin section."""
This view is made to protect counter admin section
"""
edit_group = [settings.SITH_GROUP_COUNTER_ADMIN_ID] edit_group = [settings.SITH_GROUP_COUNTER_ADMIN_ID]
edit_club = [] edit_club = []
@ -105,9 +103,7 @@ class CounterAdminMixin(View):
class StudentCardDeleteView(DeleteView, CanEditMixin): class StudentCardDeleteView(DeleteView, CanEditMixin):
""" """View used to delete a card from a user."""
View used to delete a card from a user
"""
model = StudentCard model = StudentCard
template_name = "core/delete_confirm.jinja" template_name = "core/delete_confirm.jinja"
@ -210,9 +206,7 @@ class CounterTabsMixin(TabedViewMixin):
class CounterMain( class CounterMain(
CounterTabsMixin, CanViewMixin, DetailView, ProcessFormView, FormMixin CounterTabsMixin, CanViewMixin, DetailView, ProcessFormView, FormMixin
): ):
""" """The public (barman) view."""
The public (barman) view
"""
model = Counter model = Counter
template_name = "counter/counter_main.jinja" template_name = "counter/counter_main.jinja"
@ -239,9 +233,7 @@ class CounterMain(
return super().post(request, *args, **kwargs) return super().post(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" """We handle here the login form for the barman."""
We handle here the login form for the barman
"""
if self.request.method == "POST": if self.request.method == "POST":
self.object = self.get_object() self.object = self.get_object()
self.object.update_activity() self.object.update_activity()
@ -275,9 +267,7 @@ class CounterMain(
return kwargs return kwargs
def form_valid(self, form): def form_valid(self, form):
""" """We handle here the redirection, passing the user id of the asked customer."""
We handle here the redirection, passing the user id of the asked customer
"""
self.kwargs["user_id"] = form.cleaned_data["user_id"] self.kwargs["user_id"] = form.cleaned_data["user_id"]
return super().form_valid(form) return super().form_valid(form)
@ -286,10 +276,9 @@ class CounterMain(
class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
""" """The click view
The click view
This is a detail view not to have to worry about loading the counter This is a detail view not to have to worry about loading the counter
Everything is made by hand in the post method Everything is made by hand in the post method.
""" """
model = Counter model = Counter
@ -347,7 +336,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
"""Simple get view""" """Simple get view."""
if "basket" not in request.session.keys(): # Init the basket session entry if "basket" not in request.session.keys(): # Init the basket session entry
request.session["basket"] = {} request.session["basket"] = {}
request.session["basket_total"] = 0 request.session["basket_total"] = 0
@ -364,7 +353,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
return ret return ret
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
"""Handle the many possibilities of the post request""" """Handle the many possibilities of the post request."""
self.object = self.get_object() self.object = self.get_object()
self.refill_form = None self.refill_form = None
if (self.object.type != "BAR" and not request.user.is_authenticated) or ( if (self.object.type != "BAR" and not request.user.is_authenticated) or (
@ -481,10 +470,9 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
return len(request.POST) == 0 and len(request.body) != 0 return len(request.POST) == 0 and len(request.body) != 0
def add_product(self, request, q=1, p=None): def add_product(self, request, q=1, p=None):
""" """Add a product to the basket
Add a product to the basket
q is the quantity passed as integer q is the quantity passed as integer
p is the product id, passed as an integer p is the product id, passed as an integer.
""" """
pid = p or parse_qs(request.body.decode())["product_id"][0] pid = p or parse_qs(request.body.decode())["product_id"][0]
pid = str(pid) pid = str(pid)
@ -543,9 +531,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
return True return True
def add_student_card(self, request): def add_student_card(self, request):
""" """Add a new student card on the customer account."""
Add a new student card on the customer account
"""
uid = request.POST["student_card_uid"] uid = request.POST["student_card_uid"]
uid = str(uid) uid = str(uid)
if not StudentCard.is_valid(uid): if not StudentCard.is_valid(uid):
@ -564,7 +550,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
return True return True
def del_product(self, request): def del_product(self, request):
"""Delete a product from the basket""" """Delete a product from the basket."""
pid = parse_qs(request.body.decode())["product_id"][0] pid = parse_qs(request.body.decode())["product_id"][0]
product = self.get_product(pid) product = self.get_product(pid)
if pid in request.session["basket"]: if pid in request.session["basket"]:
@ -581,11 +567,11 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
request.session.modified = True request.session.modified = True
def parse_code(self, request): def parse_code(self, request):
""" """Parse the string entered by the barman.
Parse the string entered by the barman
This can be of two forms : This can be of two forms :
- <str>, where the string is the code of the product - `<str>`, where the string is the code of the product
- <int>X<str>, where the integer is the quantity and str the code - `<int>X<str>`, where the integer is the quantity and str the code.
""" """
string = parse_qs(request.body.decode()).get("code", [""])[0].upper() string = parse_qs(request.body.decode()).get("code", [""])[0].upper()
if string == "FIN": if string == "FIN":
@ -605,7 +591,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
return self.render_to_response(context) return self.render_to_response(context)
def finish(self, request): def finish(self, request):
"""Finish the click session, and validate the basket""" """Finish the click session, and validate the basket."""
with transaction.atomic(): with transaction.atomic():
request.session["last_basket"] = [] request.session["last_basket"] = []
if self.sum_basket(request) > self.customer.amount: if self.sum_basket(request) > self.customer.amount:
@ -657,7 +643,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
) )
def cancel(self, request): def cancel(self, request):
"""Cancel the click session""" """Cancel the click session."""
kwargs = {"counter_id": self.object.id} kwargs = {"counter_id": self.object.id}
request.session.pop("basket", None) request.session.pop("basket", None)
return HttpResponseRedirect( return HttpResponseRedirect(
@ -665,7 +651,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
) )
def refill(self, request): def refill(self, request):
"""Refill the customer's account""" """Refill the customer's account."""
if not self.object.can_refill(): if not self.object.can_refill():
raise PermissionDenied raise PermissionDenied
form = RefillForm(request.POST) form = RefillForm(request.POST)
@ -678,7 +664,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
self.refill_form = form self.refill_form = form
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add customer to the context""" """Add customer to the context."""
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
products = self.object.products.select_related("product_type") products = self.object.products.select_related("product_type")
if self.customer_is_barman(): if self.customer_is_barman():
@ -701,8 +687,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
class CounterLogin(RedirectView): class CounterLogin(RedirectView):
""" """Handle the login of a barman.
Handle the login of a barman
Logged barmen are stored in the Permanency model Logged barmen are stored in the Permanency model
""" """
@ -710,9 +695,7 @@ class CounterLogin(RedirectView):
permanent = False permanent = False
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
""" """Register the logged user as barman for this counter."""
Register the logged user as barman for this counter
"""
self.counter_id = kwargs["counter_id"] self.counter_id = kwargs["counter_id"]
self.counter = Counter.objects.filter(id=kwargs["counter_id"]).first() self.counter = Counter.objects.filter(id=kwargs["counter_id"]).first()
form = LoginForm(request, data=request.POST) form = LoginForm(request, data=request.POST)
@ -745,9 +728,7 @@ class CounterLogout(RedirectView):
permanent = False permanent = False
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
""" """Unregister the user from the barman."""
Unregister the user from the barman
"""
self.counter = Counter.objects.filter(id=kwargs["counter_id"]).first() self.counter = Counter.objects.filter(id=kwargs["counter_id"]).first()
user = User.objects.filter(id=request.POST["user_id"]).first() user = User.objects.filter(id=request.POST["user_id"]).first()
self.counter.del_barman(user) self.counter.del_barman(user)
@ -803,9 +784,7 @@ class CounterAdminTabsMixin(TabedViewMixin):
class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView): class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView):
""" """A list view for the admins."""
A list view for the admins
"""
model = Counter model = Counter
template_name = "counter/counter_list.jinja" template_name = "counter/counter_list.jinja"
@ -813,9 +792,7 @@ class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView):
class CounterEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): class CounterEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
""" """Edit a counter's main informations (for the counter's manager)."""
Edit a counter's main informations (for the counter's manager)
"""
model = Counter model = Counter
form_class = CounterEditForm form_class = CounterEditForm
@ -833,9 +810,7 @@ class CounterEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
class CounterEditPropView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): class CounterEditPropView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
""" """Edit a counter's main informations (for the counter's admin)."""
Edit a counter's main informations (for the counter's admin)
"""
model = Counter model = Counter
form_class = modelform_factory(Counter, fields=["name", "club", "type"]) form_class = modelform_factory(Counter, fields=["name", "club", "type"])
@ -845,9 +820,7 @@ class CounterEditPropView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
""" """Create a counter (for the admins)."""
Create a counter (for the admins)
"""
model = Counter model = Counter
form_class = modelform_factory( form_class = modelform_factory(
@ -860,9 +833,7 @@ class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView): class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView):
""" """Delete a counter (for the admins)."""
Delete a counter (for the admins)
"""
model = Counter model = Counter
pk_url_kwarg = "counter_id" pk_url_kwarg = "counter_id"
@ -875,9 +846,7 @@ class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView):
class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
""" """A list view for the admins."""
A list view for the admins
"""
model = ProductType model = ProductType
template_name = "counter/producttype_list.jinja" template_name = "counter/producttype_list.jinja"
@ -885,9 +854,7 @@ class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
""" """A create view for the admins."""
A create view for the admins
"""
model = ProductType model = ProductType
fields = ["name", "description", "comment", "icon", "priority"] fields = ["name", "description", "comment", "icon", "priority"]
@ -896,9 +863,7 @@ class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView
class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
""" """An edit view for the admins."""
An edit view for the admins
"""
model = ProductType model = ProductType
template_name = "core/edit.jinja" template_name = "core/edit.jinja"
@ -908,9 +873,7 @@ class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
class ProductArchivedListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): class ProductArchivedListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
""" """A list view for the admins."""
A list view for the admins
"""
model = Product model = Product
template_name = "counter/product_list.jinja" template_name = "counter/product_list.jinja"
@ -920,9 +883,7 @@ class ProductArchivedListView(CounterAdminTabsMixin, CounterAdminMixin, ListView
class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
""" """A list view for the admins."""
A list view for the admins
"""
model = Product model = Product
template_name = "counter/product_list.jinja" template_name = "counter/product_list.jinja"
@ -932,9 +893,7 @@ class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
""" """A create view for the admins."""
A create view for the admins
"""
model = Product model = Product
form_class = ProductEditForm form_class = ProductEditForm
@ -943,9 +902,7 @@ class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
""" """An edit view for the admins."""
An edit view for the admins
"""
model = Product model = Product
form_class = ProductEditForm form_class = ProductEditForm
@ -955,18 +912,14 @@ class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
class RefillingDeleteView(DeleteView): class RefillingDeleteView(DeleteView):
""" """Delete a refilling (for the admins)."""
Delete a refilling (for the admins)
"""
model = Refilling model = Refilling
pk_url_kwarg = "refilling_id" pk_url_kwarg = "refilling_id"
template_name = "core/delete_confirm.jinja" template_name = "core/delete_confirm.jinja"
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
""" """We have here a very particular right handling, we can't inherit from CanEditPropMixin."""
We have here a very particular right handling, we can't inherit from CanEditPropMixin
"""
self.object = self.get_object() self.object = self.get_object()
if ( if (
timezone.now() - self.object.date timezone.now() - self.object.date
@ -990,18 +943,14 @@ class RefillingDeleteView(DeleteView):
class SellingDeleteView(DeleteView): class SellingDeleteView(DeleteView):
""" """Delete a selling (for the admins)."""
Delete a selling (for the admins)
"""
model = Selling model = Selling
pk_url_kwarg = "selling_id" pk_url_kwarg = "selling_id"
template_name = "core/delete_confirm.jinja" template_name = "core/delete_confirm.jinja"
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
""" """We have here a very particular right handling, we can't inherit from CanEditPropMixin."""
We have here a very particular right handling, we can't inherit from CanEditPropMixin
"""
self.object = self.get_object() self.object = self.get_object()
if ( if (
timezone.now() - self.object.date timezone.now() - self.object.date
@ -1028,9 +977,7 @@ class SellingDeleteView(DeleteView):
class CashRegisterSummaryForm(forms.Form): class CashRegisterSummaryForm(forms.Form):
""" """Provide the cash summary form."""
Provide the cash summary form
"""
ten_cents = forms.IntegerField(label=_("10 cents"), required=False, min_value=0) ten_cents = forms.IntegerField(label=_("10 cents"), required=False, min_value=0)
twenty_cents = forms.IntegerField(label=_("20 cents"), required=False, min_value=0) twenty_cents = forms.IntegerField(label=_("20 cents"), required=False, min_value=0)
@ -1238,9 +1185,7 @@ class CashRegisterSummaryForm(forms.Form):
class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView): class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
""" """Provide the last operations to allow barmen to delete them."""
Provide the last operations to allow barmen to delete them
"""
model = Counter model = Counter
pk_url_kwarg = "counter_id" pk_url_kwarg = "counter_id"
@ -1248,9 +1193,7 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
current_tab = "last_ops" current_tab = "last_ops"
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
""" """We have here again a very particular right handling."""
We have here again a very particular right handling
"""
self.object = self.get_object() self.object = self.get_object()
if ( if (
self.object.get_barmen_list() self.object.get_barmen_list()
@ -1267,7 +1210,7 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
) )
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add form to the context""" """Add form to the context."""
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
threshold = timezone.now() - timedelta( threshold = timezone.now() - timedelta(
minutes=settings.SITH_LAST_OPERATIONS_LIMIT minutes=settings.SITH_LAST_OPERATIONS_LIMIT
@ -1282,9 +1225,7 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView): class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView):
""" """Provide the cash summary form."""
Provide the cash summary form
"""
model = Counter model = Counter
pk_url_kwarg = "counter_id" pk_url_kwarg = "counter_id"
@ -1292,9 +1233,7 @@ class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView):
current_tab = "cash_summary" current_tab = "cash_summary"
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
""" """We have here again a very particular right handling."""
We have here again a very particular right handling
"""
self.object = self.get_object() self.object = self.get_object()
if ( if (
self.object.get_barmen_list() self.object.get_barmen_list()
@ -1327,16 +1266,14 @@ class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView):
return reverse_lazy("counter:details", kwargs={"counter_id": self.object.id}) return reverse_lazy("counter:details", kwargs={"counter_id": self.object.id})
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add form to the context""" """Add form to the context."""
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["form"] = self.form kwargs["form"] = self.form
return kwargs return kwargs
class CounterActivityView(DetailView): class CounterActivityView(DetailView):
""" """Show the bar activity."""
Show the bar activity
"""
model = Counter model = Counter
pk_url_kwarg = "counter_id" pk_url_kwarg = "counter_id"
@ -1344,16 +1281,14 @@ class CounterActivityView(DetailView):
class CounterStatView(DetailView, CounterAdminMixin): class CounterStatView(DetailView, CounterAdminMixin):
""" """Show the bar stats."""
Show the bar stats
"""
model = Counter model = Counter
pk_url_kwarg = "counter_id" pk_url_kwarg = "counter_id"
template_name = "counter/stats.jinja" template_name = "counter/stats.jinja"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add stats to the context""" """Add stats to the context."""
counter: Counter = self.object counter: Counter = self.object
semester_start = get_start_of_semester() semester_start = get_start_of_semester()
office_hours = counter.get_top_barmen() office_hours = counter.get_top_barmen()
@ -1386,7 +1321,7 @@ class CounterStatView(DetailView, CounterAdminMixin):
class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
"""Edit cash summaries""" """Edit cash summaries."""
model = CashRegisterSummary model = CashRegisterSummary
template_name = "counter/cash_register_summary.jinja" template_name = "counter/cash_register_summary.jinja"
@ -1400,7 +1335,7 @@ class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
"""Display a list of cash summaries""" """Display a list of cash summaries."""
model = CashRegisterSummary model = CashRegisterSummary
template_name = "counter/cash_summary_list.jinja" template_name = "counter/cash_summary_list.jinja"
@ -1410,7 +1345,7 @@ class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add sums to the context""" """Add sums to the context."""
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
form = CashSummaryFormBase(self.request.GET) form = CashSummaryFormBase(self.request.GET)
kwargs["form"] = form kwargs["form"] = form
@ -1461,7 +1396,7 @@ class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView):
current_tab = "invoices_call" current_tab = "invoices_call"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add sums to the context""" """Add sums to the context."""
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC") kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC")
if "month" in self.request.GET: if "month" in self.request.GET:
@ -1522,9 +1457,7 @@ class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView):
class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
""" """A list view for the admins."""
A list view for the admins
"""
model = Eticket model = Eticket
template_name = "counter/eticket_list.jinja" template_name = "counter/eticket_list.jinja"
@ -1533,9 +1466,7 @@ class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
class EticketCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): class EticketCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
""" """Create an eticket."""
Create an eticket
"""
model = Eticket model = Eticket
template_name = "core/create.jinja" template_name = "core/create.jinja"
@ -1544,9 +1475,7 @@ class EticketCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
class EticketEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): class EticketEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
""" """Edit an eticket."""
Edit an eticket
"""
model = Eticket model = Eticket
template_name = "core/edit.jinja" template_name = "core/edit.jinja"
@ -1556,9 +1485,7 @@ class EticketEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
class EticketPDFView(CanViewMixin, DetailView): class EticketPDFView(CanViewMixin, DetailView):
""" """Display the PDF of an eticket."""
Display the PDF of an eticket
"""
model = Selling model = Selling
pk_url_kwarg = "selling_id" pk_url_kwarg = "selling_id"
@ -1647,9 +1574,7 @@ class EticketPDFView(CanViewMixin, DetailView):
class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
""" """List of refillings on a counter."""
List of refillings on a counter
"""
model = Refilling model = Refilling
template_name = "counter/refilling_list.jinja" template_name = "counter/refilling_list.jinja"
@ -1668,9 +1593,7 @@ class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListVie
class StudentCardFormView(FormView): class StudentCardFormView(FormView):
""" """Add a new student card."""
Add a new student card
"""
form_class = StudentCardForm form_class = StudentCardForm
template_name = "core/create.jinja" template_name = "core/create.jinja"

View File

@ -21,11 +21,10 @@
class PaymentResultConverter: class PaymentResultConverter:
""" """Converter used for url mapping of the `eboutic.views.payment_result` view.
Converter used for url mapping of the ``eboutic.views.payment_result``
view.
It's meant to build an url that can match It's meant to build an url that can match
either ``/eboutic/pay/success/`` or ``/eboutic/pay/failure/`` either `/eboutic/pay/success/` or `/eboutic/pay/failure/`
but nothing else. but nothing else.
""" """

View File

@ -34,9 +34,8 @@ from eboutic.models import get_eboutic_products
class BasketForm: class BasketForm:
""" """Class intended to perform checks on the request sended to the server when
Class intended to perform checks on the request sended to the server when the user submits his basket from /eboutic/.
the user submits his basket from /eboutic/
Because it must check an unknown number of fields, coming from a cookie Because it must check an unknown number of fields, coming from a cookie
and needing some databases checks to be performed, inheriting from forms.Form and needing some databases checks to be performed, inheriting from forms.Form
@ -45,6 +44,7 @@ class BasketForm:
However, it still tries to share some similarities with a standard django Form. However, it still tries to share some similarities with a standard django Form.
Example: Example:
-------
:: ::
def my_view(request): def my_view(request):
@ -62,6 +62,7 @@ class BasketForm:
You can also use a little shortcut by directly calling `form.is_valid()` You can also use a little shortcut by directly calling `form.is_valid()`
without calling `form.clean()`. In this case, the latter method shall be without calling `form.clean()`. In this case, the latter method shall be
implicitly called. implicitly called.
""" """
# check the json is an array containing non-nested objects. # check the json is an array containing non-nested objects.
@ -85,8 +86,7 @@ class BasketForm:
self.correct_cookie = [] self.correct_cookie = []
def clean(self) -> None: def clean(self) -> None:
""" """Perform all the checks, but return nothing.
Perform all the checks, but return nothing.
To know if the form is valid, the `is_valid()` method must be used. To know if the form is valid, the `is_valid()` method must be used.
The form shall be considered as valid if it meets all the following conditions : The form shall be considered as valid if it meets all the following conditions :
@ -170,9 +170,9 @@ class BasketForm:
# the form is invalid # the form is invalid
def is_valid(self) -> bool: def is_valid(self) -> bool:
""" """Return True if the form is correct else False.
return True if the form is correct else False.
If the `clean()` method has not been called beforehand, call it If the `clean()` method has not been called beforehand, call it.
""" """
if self.error_messages == set() and self.correct_cookie == []: if self.error_messages == set() and self.correct_cookie == []:
self.clean() self.clean()

View File

@ -12,10 +12,10 @@
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #
from __future__ import annotations
import hmac import hmac
import typing
from datetime import datetime from datetime import datetime
from typing import List
from dict2xml import dict2xml from dict2xml import dict2xml
from django.conf import settings from django.conf import settings
@ -29,7 +29,7 @@ from core.models import User
from counter.models import BillingInfo, Counter, Customer, Product, Refilling, Selling from counter.models import BillingInfo, Counter, Customer, Product, Refilling, Selling
def get_eboutic_products(user: User) -> List[Product]: def get_eboutic_products(user: User) -> list[Product]:
products = ( products = (
Counter.objects.get(type="EBOUTIC") Counter.objects.get(type="EBOUTIC")
.products.filter(product_type__isnull=False) .products.filter(product_type__isnull=False)
@ -43,9 +43,7 @@ def get_eboutic_products(user: User) -> List[Product]:
class Basket(models.Model): class Basket(models.Model):
""" """Basket is built when the user connects to an eboutic page."""
Basket is built when the user connects to an eboutic page
"""
user = models.ForeignKey( user = models.ForeignKey(
User, User,
@ -60,8 +58,7 @@ class Basket(models.Model):
return f"{self.user}'s basket ({self.items.all().count()} items)" return f"{self.user}'s basket ({self.items.all().count()} items)"
def add_product(self, p: Product, q: int = 1): def add_product(self, p: Product, q: int = 1):
""" """Given p an object of the Product model and q an integer,
Given p an object of the Product model and q an integer,
add q items corresponding to this Product from the basket. add q items corresponding to this Product from the basket.
If this function is called with a product not in the basket, no error will be raised If this function is called with a product not in the basket, no error will be raised
@ -81,8 +78,7 @@ class Basket(models.Model):
item.save() item.save()
def del_product(self, p: Product, q: int = 1): def del_product(self, p: Product, q: int = 1):
""" """Given p an object of the Product model and q an integer
Given p an object of the Product model and q an integer,
remove q items corresponding to this Product from the basket. remove q items corresponding to this Product from the basket.
If this function is called with a product not in the basket, no error will be raised If this function is called with a product not in the basket, no error will be raised
@ -98,9 +94,7 @@ class Basket(models.Model):
item.save() item.save()
def clear(self) -> None: def clear(self) -> None:
""" """Remove all items from this basket without deleting the basket."""
Remove all items from this basket without deleting the basket
"""
self.items.all().delete() self.items.all().delete()
@cached_property @cached_property
@ -116,11 +110,8 @@ class Basket(models.Model):
return float(total) if total is not None else 0 return float(total) if total is not None else 0
@classmethod @classmethod
def from_session(cls, session) -> typing.Union["Basket", None]: def from_session(cls, session) -> Basket | None:
""" """The basket stored in the session object, if it exists."""
Given an HttpRequest django object, return the basket used in the current session
if it exists else None
"""
if "basket_id" in session: if "basket_id" in session:
try: try:
return cls.objects.get(id=session["basket_id"]) return cls.objects.get(id=session["basket_id"])
@ -129,23 +120,22 @@ class Basket(models.Model):
return None return None
def generate_sales(self, counter, seller: User, payment_method: str): def generate_sales(self, counter, seller: User, payment_method: str):
""" """Generate a list of sold items corresponding to the items
Generate a list of sold items corresponding to the items of this basket WITHOUT saving them NOR deleting the basket.
of this basket WITHOUT saving them NOR deleting the basket
Example: Example:
:: ```python
counter = Counter.objects.get(name="Eboutic")
sales = basket.generate_sales(counter, "SITH_ACCOUNT")
# here the basket is in the same state as before the method call
counter = Counter.objects.get(name="Eboutic") with transaction.atomic():
sales = basket.generate_sales(counter, "SITH_ACCOUNT") for sale in sales:
# here the basket is in the same state as before the method call sale.save()
basket.delete()
with transaction.atomic(): # all the basket items are deleted by the on_delete=CASCADE relation
for sale in sales: # thus only the sales remain
sale.save() ```
basket.delete()
# all the basket items are deleted by the on_delete=CASCADE relation
# thus only the sales remain
""" """
# I must proceed with two distinct requests instead of # I must proceed with two distinct requests instead of
# only one with a join because the AbstractBaseItem model has been # only one with a join because the AbstractBaseItem model has been
@ -212,9 +202,7 @@ class Basket(models.Model):
class Invoice(models.Model): class Invoice(models.Model):
""" """Invoices are generated once the payment has been validated."""
Invoices are generated once the payment has been validated
"""
user = models.ForeignKey( user = models.ForeignKey(
User, User,
@ -297,11 +285,12 @@ class BasketItem(AbstractBaseItem):
@classmethod @classmethod
def from_product(cls, product: Product, quantity: int): def from_product(cls, product: Product, quantity: int):
""" """Create a BasketItem with the same characteristics as the
Create a BasketItem with the same characteristics as the product passed in parameters, with the specified quantity.
product passed in parameters, with the specified quantity
WARNING : the basket field is not filled, so you must set Warnings:
it yourself before saving the model the basket field is not filled, so you must set
it yourself before saving the model.
""" """
return cls( return cls(
product_id=product.id, product_id=product.id,

View File

@ -52,9 +52,9 @@ class EbouticTest(TestCase):
cls.public = User.objects.get(username="public") cls.public = User.objects.get(username="public")
def get_busy_basket(self, user) -> Basket: def get_busy_basket(self, user) -> Basket:
""" """Create and return a basket with 3 barbar and 1 cotis in it.
Create and return a basket with 3 barbar and 1 cotis in it.
Edit the client session to store the basket id in it Edit the client session to store the basket id in it.
""" """
session = self.client.session session = self.client.session
basket = Basket.objects.create(user=user) basket = Basket.objects.create(user=user)

View File

@ -43,8 +43,8 @@ from eboutic.models import Basket, Invoice, InvoiceItem, get_eboutic_products
@login_required @login_required
@require_GET @require_GET
def eboutic_main(request: HttpRequest) -> HttpResponse: def eboutic_main(request: HttpRequest) -> HttpResponse:
""" """Main view of the eboutic application.
Main view of the eboutic application.
Return an Http response whose content is of type text/html. Return an Http response whose content is of type text/html.
The latter represents the page from which a user can see The latter represents the page from which a user can see
the catalogue of products that he can buy and fill the catalogue of products that he can buy and fill

View File

@ -7,9 +7,7 @@ from core.models import Group, User
class Election(models.Model): class Election(models.Model):
""" """This class allows to create a new election."""
This class allows to create a new election
"""
title = models.CharField(_("title"), max_length=255) title = models.CharField(_("title"), max_length=255)
description = models.TextField(_("description"), null=True, blank=True) description = models.TextField(_("description"), null=True, blank=True)
@ -105,9 +103,7 @@ class Election(models.Model):
class Role(OrderedModel): class Role(OrderedModel):
""" """This class allows to create a new role avaliable for a candidature."""
This class allows to create a new role avaliable for a candidature
"""
election = models.ForeignKey( election = models.ForeignKey(
Election, Election,
@ -151,9 +147,7 @@ class Role(OrderedModel):
class ElectionList(models.Model): class ElectionList(models.Model):
""" """To allow per list vote."""
To allow per list vote
"""
title = models.CharField(_("title"), max_length=255) title = models.CharField(_("title"), max_length=255)
election = models.ForeignKey( election = models.ForeignKey(
@ -176,9 +170,7 @@ class ElectionList(models.Model):
class Candidature(models.Model): class Candidature(models.Model):
""" """This class is a component of responsability."""
This class is a component of responsability
"""
role = models.ForeignKey( role = models.ForeignKey(
Role, Role,
@ -214,9 +206,7 @@ class Candidature(models.Model):
class Vote(models.Model): class Vote(models.Model):
""" """This class allows to vote for candidates."""
This class allows to vote for candidates
"""
role = models.ForeignKey( role = models.ForeignKey(
Role, related_name="votes", verbose_name=_("role"), on_delete=models.CASCADE Role, related_name="votes", verbose_name=_("role"), on_delete=models.CASCADE

View File

@ -19,10 +19,7 @@ from election.models import Candidature, Election, ElectionList, Role, Vote
class LimitedCheckboxField(forms.ModelMultipleChoiceField): class LimitedCheckboxField(forms.ModelMultipleChoiceField):
""" """A `ModelMultipleChoiceField`, with a max limit of selectable inputs."""
Used to replace ModelMultipleChoiceField but with
automatic backend verification
"""
def __init__(self, queryset, max_choice, **kwargs): def __init__(self, queryset, max_choice, **kwargs):
self.max_choice = max_choice self.max_choice = max_choice
@ -45,7 +42,7 @@ class LimitedCheckboxField(forms.ModelMultipleChoiceField):
class CandidateForm(forms.ModelForm): class CandidateForm(forms.ModelForm):
"""Form to candidate""" """Form to candidate."""
class Meta: class Meta:
model = Candidature model = Candidature
@ -91,7 +88,7 @@ class VoteForm(forms.Form):
class RoleForm(forms.ModelForm): class RoleForm(forms.ModelForm):
"""Form for creating a role""" """Form for creating a role."""
class Meta: class Meta:
model = Role model = Role
@ -175,9 +172,7 @@ class ElectionForm(forms.ModelForm):
class ElectionsListView(CanViewMixin, ListView): class ElectionsListView(CanViewMixin, ListView):
""" """A list of all non archived elections visible."""
A list of all non archived elections visible
"""
model = Election model = Election
ordering = ["-id"] ordering = ["-id"]
@ -189,9 +184,7 @@ class ElectionsListView(CanViewMixin, ListView):
class ElectionListArchivedView(CanViewMixin, ListView): class ElectionListArchivedView(CanViewMixin, ListView):
""" """A list of all archived elections visible."""
A list of all archived elections visible
"""
model = Election model = Election
ordering = ["-id"] ordering = ["-id"]
@ -203,9 +196,7 @@ class ElectionListArchivedView(CanViewMixin, ListView):
class ElectionDetailView(CanViewMixin, DetailView): class ElectionDetailView(CanViewMixin, DetailView):
""" """Details an election responsability by responsability."""
Details an election responsability by responsability
"""
model = Election model = Election
template_name = "election/election_detail.jinja" template_name = "election/election_detail.jinja"
@ -232,7 +223,7 @@ class ElectionDetailView(CanViewMixin, DetailView):
return response return response
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add additionnal data to the template""" """Add additionnal data to the template."""
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["election_form"] = VoteForm(self.object, self.request.user) kwargs["election_form"] = VoteForm(self.object, self.request.user)
kwargs["election_results"] = self.object.results kwargs["election_results"] = self.object.results
@ -243,9 +234,7 @@ class ElectionDetailView(CanViewMixin, DetailView):
class VoteFormView(CanCreateMixin, FormView): class VoteFormView(CanCreateMixin, FormView):
""" """Alows users to vote."""
Alows users to vote
"""
form_class = VoteForm form_class = VoteForm
template_name = "election/election_detail.jinja" template_name = "election/election_detail.jinja"
@ -278,9 +267,7 @@ class VoteFormView(CanCreateMixin, FormView):
return kwargs return kwargs
def form_valid(self, form): def form_valid(self, form):
""" """Verify that the user is part in a vote group."""
Verify that the user is part in a vote group
"""
data = form.clean() data = form.clean()
res = super(FormView, self).form_valid(form) res = super(FormView, self).form_valid(form)
for grp_id in self.election.vote_groups.values_list("pk", flat=True): for grp_id in self.election.vote_groups.values_list("pk", flat=True):
@ -293,7 +280,7 @@ class VoteFormView(CanCreateMixin, FormView):
return reverse_lazy("election:detail", kwargs={"election_id": self.election.id}) return reverse_lazy("election:detail", kwargs={"election_id": self.election.id})
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add additionnal data to the template""" """Add additionnal data to the template."""
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["object"] = self.election kwargs["object"] = self.election
kwargs["election"] = self.election kwargs["election"] = self.election
@ -305,9 +292,7 @@ class VoteFormView(CanCreateMixin, FormView):
class CandidatureCreateView(CanCreateMixin, CreateView): class CandidatureCreateView(CanCreateMixin, CreateView):
""" """View dedicated to a cundidature creation."""
View dedicated to a cundidature creation
"""
form_class = CandidateForm form_class = CandidateForm
model = Candidature model = Candidature
@ -330,9 +315,7 @@ class CandidatureCreateView(CanCreateMixin, CreateView):
return kwargs return kwargs
def form_valid(self, form): def form_valid(self, form):
""" """Verify that the selected user is in candidate group."""
Verify that the selected user is in candidate group
"""
obj = form.instance obj = form.instance
obj.election = Election.objects.get(id=self.election.id) obj.election = Election.objects.get(id=self.election.id)
if (obj.election.can_candidate(obj.user)) and ( if (obj.election.can_candidate(obj.user)) and (
@ -361,10 +344,7 @@ class ElectionCreateView(CanCreateMixin, CreateView):
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def form_valid(self, form): def form_valid(self, form):
""" """Allow every user that had passed the dispatch to create an election."""
Allow every users that had passed the dispatch
to create an election
"""
return super(CreateView, self).form_valid(form) return super(CreateView, self).form_valid(form)
def get_success_url(self, **kwargs): def get_success_url(self, **kwargs):
@ -388,9 +368,7 @@ class RoleCreateView(CanCreateMixin, CreateView):
return init return init
def form_valid(self, form): def form_valid(self, form):
""" """Verify that the user can edit properly."""
Verify that the user can edit properly
"""
obj: Role = form.instance obj: Role = form.instance
user: User = self.request.user user: User = self.request.user
if obj.election: if obj.election:
@ -432,9 +410,7 @@ class ElectionListCreateView(CanCreateMixin, CreateView):
return kwargs return kwargs
def form_valid(self, form): def form_valid(self, form):
""" """Verify that the user can vote on this election."""
Verify that the user can vote on this election
"""
obj: ElectionList = form.instance obj: ElectionList = form.instance
user: User = self.request.user user: User = self.request.user
if obj.election: if obj.election:

View File

@ -48,8 +48,7 @@ def get_default_view_group():
class Forum(models.Model): class Forum(models.Model):
""" """The Forum class, made as a tree to allow nice tidy organization.
The Forum class, made as a tree to allow nice tidy organization
owner_club allows club members to moderate there own topics owner_club allows club members to moderate there own topics
edit_groups allows to put any group as a forum admin edit_groups allows to put any group as a forum admin
@ -157,7 +156,7 @@ class Forum(models.Model):
c.apply_rights_recursively() c.apply_rights_recursively()
def copy_rights(self): def copy_rights(self):
"""Copy, if possible, the rights of the parent folder""" """Copy, if possible, the rights of the parent folder."""
if self.parent is not None: if self.parent is not None:
self.owner_club = self.parent.owner_club self.owner_club = self.parent.owner_club
self.edit_groups.set(self.parent.edit_groups.all()) self.edit_groups.set(self.parent.edit_groups.all())
@ -187,7 +186,7 @@ class Forum(models.Model):
return False return False
def check_loop(self): def check_loop(self):
"""Raise a validation error when a loop is found within the parent list""" """Raise a validation error when a loop is found within the parent list."""
objs = [] objs = []
cur = self cur = self
while cur.parent is not None: while cur.parent is not None:
@ -299,9 +298,7 @@ class ForumTopic(models.Model):
class ForumMessage(models.Model): class ForumMessage(models.Model):
""" """A message in the forum (thx Cpt. Obvious.)."""
"A ForumMessage object represents a message in the forum" -- Cpt. Obvious
"""
topic = models.ForeignKey( topic = models.ForeignKey(
ForumTopic, related_name="messages", on_delete=models.CASCADE ForumTopic, related_name="messages", on_delete=models.CASCADE
@ -425,7 +422,8 @@ class ForumMessageMeta(models.Model):
class ForumUserInfo(models.Model): class ForumUserInfo(models.Model):
""" """The forum infos of a user.
This currently stores only the last date a user clicked "Mark all as read". This currently stores only the last date a user clicked "Mark all as read".
However, this can be extended with lot of user preferences dedicated to a However, this can be extended with lot of user preferences dedicated to a
user, such as the favourite topics, the signature, and so on... user, such as the favourite topics, the signature, and so on...

View File

@ -118,11 +118,11 @@ class Command(BaseCommand):
self.make_important_citizen(u) self.make_important_citizen(u)
def make_clubs(self): def make_clubs(self):
""" """Create all the clubs (:class:`club.models.Club`).
Create all the clubs (:class:`club.models.Club`)
and store them in `self.clubs` for fast access later. After creation, the clubs are stored in `self.clubs` for fast access later.
Don't create the meta groups (:class:`core.models.MetaGroup`) Don't create the meta groups (:class:`core.models.MetaGroup`)
nor the pages of the clubs (:class:`core.models.Page`) nor the pages of the clubs (:class:`core.models.Page`).
""" """
self.clubs = [] self.clubs = []
for i in range(self.NB_CLUBS): for i in range(self.NB_CLUBS):
@ -132,8 +132,7 @@ class Command(BaseCommand):
self.clubs = list(Club.objects.filter(unix_name__startswith="galaxy-").all()) self.clubs = list(Club.objects.filter(unix_name__startswith="galaxy-").all())
def make_users(self): def make_users(self):
""" """Create all the users and store them in `self.users` for fast access later.
Create all the users and store them in `self.users` for fast access later.
Also create a subscription for all the generated users. Also create a subscription for all the generated users.
""" """
@ -167,8 +166,7 @@ class Command(BaseCommand):
Subscription.objects.bulk_create(subs) Subscription.objects.bulk_create(subs)
def make_families(self): def make_families(self):
""" """Generate the godfather/godchild relations for the users contained in :attr:`self.users`.
Generate the godfather/godchild relations for the users contained in :attr:`self.users`.
The :meth:`make_users` method must have been called beforehand. The :meth:`make_users` method must have been called beforehand.
@ -194,8 +192,7 @@ class Command(BaseCommand):
User.godfathers.through.objects.bulk_create(godfathers) User.godfathers.through.objects.bulk_create(godfathers)
def make_club_memberships(self): def make_club_memberships(self):
""" """Assign users to clubs and give them a role in a pseudo-random way.
Assign users to clubs and give them a role in a pseudo-random way.
The :meth:`make_users` and :meth:`make_clubs` methods The :meth:`make_users` and :meth:`make_clubs` methods
must have been called beforehand. must have been called beforehand.
@ -265,8 +262,7 @@ class Command(BaseCommand):
Membership.objects.bulk_create(memberships) Membership.objects.bulk_create(memberships)
def make_pictures(self): def make_pictures(self):
""" """Create pictures for users to be tagged on later.
Create pictures for users to be tagged on later.
The :meth:`make_users` method must have been called beforehand. The :meth:`make_users` method must have been called beforehand.
""" """
@ -301,11 +297,10 @@ class Command(BaseCommand):
self.picts = list(Picture.objects.filter(name__startswith="galaxy-").all()) self.picts = list(Picture.objects.filter(name__startswith="galaxy-").all())
def make_pictures_memberships(self): def make_pictures_memberships(self):
""" """Assign users to pictures and make enough of them for our
Assign users to pictures and make enough of them for our
created users to be eligible for promotion as citizen. created users to be eligible for promotion as citizen.
See :meth:`galaxy.models.Galaxy.rule` for details on promotion to citizen. See `galaxy.models.Galaxy.rule` for details on promotion to citizen.
""" """
self.pictures_tags = [] self.pictures_tags = []
@ -363,9 +358,9 @@ class Command(BaseCommand):
PeoplePictureRelation.objects.bulk_create(self.pictures_tags) PeoplePictureRelation.objects.bulk_create(self.pictures_tags)
def make_important_citizen(self, uid: int): def make_important_citizen(self, uid: int):
""" """Make the user whose uid is given in parameter a more important citizen.
Make the user whose uid is given in parameter a more important citizen,
thus triggering many more connections to others (lanes) This will trigger many more connections to others (lanes)
and dragging him towards the center of the Galaxy. and dragging him towards the center of the Galaxy.
This promotion is obtained by adding more family links This promotion is obtained by adding more family links
@ -375,7 +370,8 @@ class Command(BaseCommand):
also be tagged in more pictures, thus making them also also be tagged in more pictures, thus making them also
more important. more important.
:param uid: the id of the user to make more important Args:
uid: the id of the user to make more important
""" """
u1 = self.users[uid] u1 = self.users[uid]
u2 = self.users[uid - 100] u2 = self.users[uid - 100]

View File

@ -26,7 +26,7 @@ from __future__ import annotations
import logging import logging
import math import math
import time import time
from typing import List, NamedTuple, Optional, TypedDict, Union from typing import NamedTuple, TypedDict
from django.db import models from django.db import models
from django.db.models import Case, Count, F, Q, Value, When from django.db.models import Case, Count, F, Q, Value, When
@ -40,9 +40,9 @@ from sas.models import Picture
class GalaxyStar(models.Model): class GalaxyStar(models.Model):
""" """Define a star (vertex -> user) in the galaxy graph.
Define a star (vertex -> user) in the galaxy graph,
storing a reference to its owner citizen. Store a reference to its owner citizen.
Stars are linked to each others through the :class:`GalaxyLane` model. Stars are linked to each others through the :class:`GalaxyLane` model.
@ -74,13 +74,14 @@ class GalaxyStar(models.Model):
@property @property
def current_star(self) -> Optional[GalaxyStar]: def current_star(self) -> GalaxyStar | None:
""" """The star of this user in the :class:`Galaxy`.
The star of this user in the :class:`Galaxy`.
Only take into account the most recent active galaxy. Only take into account the most recent active galaxy.
:return: The star of this user if there is an active Galaxy Returns:
and this user is a citizen of it, else ``None`` The star of this user if there is an active Galaxy
and this user is a citizen of it, else `None`
""" """
return self.stars.filter(galaxy=Galaxy.get_current_galaxy()).last() return self.stars.filter(galaxy=Galaxy.get_current_galaxy()).last()
@ -90,10 +91,9 @@ User.current_star = current_star
class GalaxyLane(models.Model): class GalaxyLane(models.Model):
""" """Define a lane (edge -> link between galaxy citizen) in the galaxy map.
Define a lane (edge -> link between galaxy citizen)
in the galaxy map, storing a reference to both its Store a reference to both its ends and the distance it covers.
ends and the distance it covers.
Score details between citizen owning the stars is also stored here. Score details between citizen owning the stars is also stored here.
""" """
@ -138,8 +138,8 @@ class StarDict(TypedDict):
class GalaxyDict(TypedDict): class GalaxyDict(TypedDict):
nodes: List[StarDict] nodes: list[StarDict]
links: List links: list
class RelationScore(NamedTuple): class RelationScore(NamedTuple):
@ -149,8 +149,8 @@ class RelationScore(NamedTuple):
class Galaxy(models.Model): class Galaxy(models.Model):
""" """The Galaxy, a graph linking the active users between each others.
The Galaxy, a graph linking the active users between each others.
The distance between two users is given by a relation score which takes The distance between two users is given by a relation score which takes
into account a few parameter like the number of pictures they are both tagged on, into account a few parameter like the number of pictures they are both tagged on,
the time during which they were in the same clubs and whether they are the time during which they were in the same clubs and whether they are
@ -204,8 +204,8 @@ class Galaxy(models.Model):
@classmethod @classmethod
def compute_user_score(cls, user: User) -> int: def compute_user_score(cls, user: User) -> int:
""" """Compute an individual score for each citizen.
Compute an individual score for each citizen.
It will later be used by the graph algorithm to push It will later be used by the graph algorithm to push
higher scores towards the center of the galaxy. higher scores towards the center of the galaxy.
@ -231,10 +231,7 @@ class Galaxy(models.Model):
@classmethod @classmethod
def query_user_score(cls, user: User) -> int: def query_user_score(cls, user: User) -> int:
""" """Get the individual score of the given user in the galaxy."""
Perform the db query to get the individual score
of the given user in the galaxy.
"""
score_query = ( score_query = (
User.objects.filter(id=user.id) User.objects.filter(id=user.id)
.annotate( .annotate(
@ -262,9 +259,9 @@ class Galaxy(models.Model):
@classmethod @classmethod
def compute_users_score(cls, user1: User, user2: User) -> RelationScore: def compute_users_score(cls, user1: User, user2: User) -> RelationScore:
""" """Compute the relationship scores of the two given users.
Compute the relationship scores of the two given users
in the following fields : The computation is done with the following fields :
- family: if they have some godfather/godchild relation - family: if they have some godfather/godchild relation
- pictures: in how many pictures are both tagged - pictures: in how many pictures are both tagged
@ -277,11 +274,12 @@ class Galaxy(models.Model):
@classmethod @classmethod
def compute_users_family_score(cls, user1: User, user2: User) -> int: def compute_users_family_score(cls, user1: User, user2: User) -> int:
""" """Compute the family score of the relation between the given users.
Compute the family score of the relation between the given users.
This takes into account mutual godfathers. This takes into account mutual godfathers.
:return: 366 if user1 is the godfather of user2 (or vice versa) else 0 Returns:
366 if user1 is the godfather of user2 (or vice versa) else 0
""" """
link_count = User.objects.filter( link_count = User.objects.filter(
Q(id=user1.id, godfathers=user2) | Q(id=user2.id, godfathers=user1) Q(id=user1.id, godfathers=user2) | Q(id=user2.id, godfathers=user1)
@ -294,14 +292,14 @@ class Galaxy(models.Model):
@classmethod @classmethod
def compute_users_pictures_score(cls, user1: User, user2: User) -> int: def compute_users_pictures_score(cls, user1: User, user2: User) -> int:
""" """Compute the pictures score of the relation between the given users.
Compute the pictures score of the relation between the given users.
The pictures score is obtained by counting the number The pictures score is obtained by counting the number
of :class:`Picture` in which they have been both identified. of :class:`Picture` in which they have been both identified.
This score is then multiplied by 2. This score is then multiplied by 2.
:return: The number of pictures both users have in common, times 2 Returns:
The number of pictures both users have in common, times 2
""" """
picture_count = ( picture_count = (
Picture.objects.filter(people__user__in=(user1,)) Picture.objects.filter(people__user__in=(user1,))
@ -316,8 +314,7 @@ class Galaxy(models.Model):
@classmethod @classmethod
def compute_users_clubs_score(cls, user1: User, user2: User) -> int: def compute_users_clubs_score(cls, user1: User, user2: User) -> int:
""" """Compute the clubs score of the relation between the given users.
Compute the clubs score of the relation between the given users.
The club score is obtained by counting the number of days The club score is obtained by counting the number of days
during which the memberships (see :class:`club.models.Membership`) during which the memberships (see :class:`club.models.Membership`)
@ -328,7 +325,8 @@ class Galaxy(models.Model):
31/12/2022 (also two years, but with an offset of one year), then their 31/12/2022 (also two years, but with an offset of one year), then their
club score is 365. club score is 365.
:return: the number of days during which both users were in the same club Returns:
the number of days during which both users were in the same club
""" """
common_clubs = Club.objects.filter(members__in=user1.memberships.all()).filter( common_clubs = Club.objects.filter(members__in=user1.memberships.all()).filter(
members__in=user2.memberships.all() members__in=user2.memberships.all()
@ -380,13 +378,13 @@ class Galaxy(models.Model):
################### ###################
@classmethod @classmethod
def scale_distance(cls, value: Union[int, float]) -> int: def scale_distance(cls, value: int | float) -> int:
""" """Given a numeric value, return a scaled value which can
Given a numeric value, return a scaled value which can
be used in the Galaxy's graphical interface to set the distance be used in the Galaxy's graphical interface to set the distance
between two stars between two stars.
:return: the scaled value usable in the Galaxy's 3d graph Returns:
the scaled value usable in the Galaxy's 3d graph
""" """
# TODO: this will need adjustements with the real, typical data on Taiste # TODO: this will need adjustements with the real, typical data on Taiste
if value == 0: if value == 0:
@ -409,8 +407,8 @@ class Galaxy(models.Model):
return int(value) return int(value)
def rule(self, picture_count_threshold=10) -> None: def rule(self, picture_count_threshold=10) -> None:
""" """Main function of the Galaxy.
Main function of the Galaxy.
Iterate over all the rulable users to promote them to citizens. Iterate over all the rulable users to promote them to citizens.
A citizen is a user who has a corresponding star in the Galaxy. A citizen is a user who has a corresponding star in the Galaxy.
Also build up the lanes, which are the links between the different citizen. Also build up the lanes, which are the links between the different citizen.
@ -566,9 +564,7 @@ class Galaxy(models.Model):
) )
def make_state(self) -> None: def make_state(self) -> None:
""" """Compute JSON structure to send to 3d-force-graph: https://github.com/vasturiano/3d-force-graph/."""
Compute JSON structure to send to 3d-force-graph: https://github.com/vasturiano/3d-force-graph/
"""
self.logger.info( self.logger.info(
"Caching current Galaxy state for a quicker display of the Empire's power." "Caching current Galaxy state for a quicker display of the Empire's power."
) )

View File

@ -46,9 +46,7 @@ class GalaxyTestModel(TestCase):
cls.com = User.objects.get(username="comunity") cls.com = User.objects.get(username="comunity")
def test_user_self_score(self): def test_user_self_score(self):
""" """Test that individual user scores are correct."""
Test that individual user scores are correct
"""
with self.assertNumQueries(8): with self.assertNumQueries(8):
assert Galaxy.compute_user_score(self.root) == 9 assert Galaxy.compute_user_score(self.root) == 9
assert Galaxy.compute_user_score(self.skia) == 10 assert Galaxy.compute_user_score(self.skia) == 10
@ -60,9 +58,8 @@ class GalaxyTestModel(TestCase):
assert Galaxy.compute_user_score(self.com) == 1 assert Galaxy.compute_user_score(self.com) == 1
def test_users_score(self): def test_users_score(self):
""" """Test on the default dataset generated by the `populate` command
Test on the default dataset generated by the `populate` command that the relation scores are correct.
that the relation scores are correct
""" """
expected_scores = { expected_scores = {
"krophil": { "krophil": {
@ -138,8 +135,7 @@ class GalaxyTestModel(TestCase):
self.assertDictEqual(expected_scores, computed_scores) self.assertDictEqual(expected_scores, computed_scores)
def test_rule(self): def test_rule(self):
""" """Test on the default dataset generated by the `populate` command
Test on the default dataset generated by the `populate` command
that the number of queries to rule the galaxy is stable. that the number of queries to rule the galaxy is stable.
""" """
galaxy = Galaxy.objects.create() galaxy = Galaxy.objects.create()
@ -151,18 +147,14 @@ class GalaxyTestModel(TestCase):
class GalaxyTestView(TestCase): class GalaxyTestView(TestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
""" """Generate a plausible Galaxy once for every test."""
Generate a plausible Galaxy once for every test
"""
call_command("generate_galaxy_test_data", "-v", "0") call_command("generate_galaxy_test_data", "-v", "0")
galaxy = Galaxy.objects.create() galaxy = Galaxy.objects.create()
galaxy.rule(26) # We want a fast test galaxy.rule(26) # We want a fast test
cls.root = User.objects.get(username="root") cls.root = User.objects.get(username="root")
def test_page_is_citizen(self): def test_page_is_citizen(self):
""" """Test that users can access the galaxy page of users who are citizens."""
Test that users can access the galaxy page of users who are citizens
"""
self.client.force_login(self.root) self.client.force_login(self.root)
user = User.objects.get(last_name="n°500") user = User.objects.get(last_name="n°500")
response = self.client.get(reverse("galaxy:user", args=[user.id])) response = self.client.get(reverse("galaxy:user", args=[user.id]))
@ -173,20 +165,19 @@ class GalaxyTestView(TestCase):
) )
def test_page_not_citizen(self): def test_page_not_citizen(self):
""" """Test that trying to access the galaxy page of non-citizen users return a 404."""
Test that trying to access the galaxy page of a user who is not
citizens return a 404
"""
self.client.force_login(self.root) self.client.force_login(self.root)
user = User.objects.get(last_name="n°1") user = User.objects.get(last_name="n°1")
response = self.client.get(reverse("galaxy:user", args=[user.id])) response = self.client.get(reverse("galaxy:user", args=[user.id]))
assert response.status_code == 404 assert response.status_code == 404
def test_full_galaxy_state(self): def test_full_galaxy_state(self):
""" """Test with a more complete galaxy.
Test on the more complex dataset generated by the `generate_galaxy_test_data`
command that the relation scores are correct, and that the view exposes the This time, the test is done on
right data. the more complex dataset generated by the `generate_galaxy_test_data`
command that the relation scores are correct,
and that the view exposes the right data.
""" """
self.client.force_login(self.root) self.client.force_login(self.root)
response = self.client.get(reverse("galaxy:data")) response = self.client.get(reverse("galaxy:data"))

View File

@ -44,9 +44,7 @@ class Launderette(models.Model):
return reverse("launderette:launderette_list") return reverse("launderette:launderette_list")
def is_owned_by(self, user): def is_owned_by(self, user):
""" """Method to see if that object can be edited by the given user."""
Method to see if that object can be edited by the given user
"""
if user.is_anonymous: if user.is_anonymous:
return False return False
launderette_club = Club.objects.filter( launderette_club = Club.objects.filter(
@ -108,9 +106,7 @@ class Machine(models.Model):
) )
def is_owned_by(self, user): def is_owned_by(self, user):
""" """Method to see if that object can be edited by the given user."""
Method to see if that object can be edited by the given user
"""
if user.is_anonymous: if user.is_anonymous:
return False return False
launderette_club = Club.objects.filter( launderette_club = Club.objects.filter(
@ -161,9 +157,7 @@ class Token(models.Model):
super().save(*args, **kwargs) super().save(*args, **kwargs)
def is_owned_by(self, user): def is_owned_by(self, user):
""" """Method to see if that object can be edited by the given user."""
Method to see if that object can be edited by the given user
"""
if user.is_anonymous: if user.is_anonymous:
return False return False
launderette_club = Club.objects.filter( launderette_club = Club.objects.filter(

View File

@ -38,26 +38,26 @@ from launderette.models import Launderette, Machine, Slot, Token
class LaunderetteMainView(TemplateView): class LaunderetteMainView(TemplateView):
"""Main presentation view""" """Main presentation view."""
template_name = "launderette/launderette_main.jinja" template_name = "launderette/launderette_main.jinja"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add page to the context""" """Add page to the context."""
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["page"] = Page.objects.filter(name="launderette").first() kwargs["page"] = Page.objects.filter(name="launderette").first()
return kwargs return kwargs
class LaunderetteBookMainView(CanViewMixin, ListView): class LaunderetteBookMainView(CanViewMixin, ListView):
"""Choose which launderette to book""" """Choose which launderette to book."""
model = Launderette model = Launderette
template_name = "launderette/launderette_book_choose.jinja" template_name = "launderette/launderette_book_choose.jinja"
class LaunderetteBookView(CanViewMixin, DetailView): class LaunderetteBookView(CanViewMixin, DetailView):
"""Display the launderette schedule""" """Display the launderette schedule."""
model = Launderette model = Launderette
pk_url_kwarg = "launderette_id" pk_url_kwarg = "launderette_id"
@ -133,7 +133,7 @@ class LaunderetteBookView(CanViewMixin, DetailView):
currentDate += delta currentDate += delta
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add page to the context""" """Add page to the context."""
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["planning"] = OrderedDict() kwargs["planning"] = OrderedDict()
kwargs["slot_type"] = self.slot_type kwargs["slot_type"] = self.slot_type
@ -166,7 +166,7 @@ class LaunderetteBookView(CanViewMixin, DetailView):
class SlotDeleteView(CanEditPropMixin, DeleteView): class SlotDeleteView(CanEditPropMixin, DeleteView):
"""Delete a slot""" """Delete a slot."""
model = Slot model = Slot
pk_url_kwarg = "slot_id" pk_url_kwarg = "slot_id"
@ -180,14 +180,14 @@ class SlotDeleteView(CanEditPropMixin, DeleteView):
class LaunderetteListView(CanEditPropMixin, ListView): class LaunderetteListView(CanEditPropMixin, ListView):
"""Choose which launderette to administer""" """Choose which launderette to administer."""
model = Launderette model = Launderette
template_name = "launderette/launderette_list.jinja" template_name = "launderette/launderette_list.jinja"
class LaunderetteEditView(CanEditPropMixin, UpdateView): class LaunderetteEditView(CanEditPropMixin, UpdateView):
"""Edit a launderette""" """Edit a launderette."""
model = Launderette model = Launderette
pk_url_kwarg = "launderette_id" pk_url_kwarg = "launderette_id"
@ -196,7 +196,7 @@ class LaunderetteEditView(CanEditPropMixin, UpdateView):
class LaunderetteCreateView(CanCreateMixin, CreateView): class LaunderetteCreateView(CanCreateMixin, CreateView):
"""Create a new launderette""" """Create a new launderette."""
model = Launderette model = Launderette
fields = ["name"] fields = ["name"]
@ -275,7 +275,7 @@ class ManageTokenForm(forms.Form):
class LaunderetteAdminView(CanEditPropMixin, BaseFormView, DetailView): class LaunderetteAdminView(CanEditPropMixin, BaseFormView, DetailView):
"""The admin page of the launderette""" """The admin page of the launderette."""
model = Launderette model = Launderette
pk_url_kwarg = "launderette_id" pk_url_kwarg = "launderette_id"
@ -297,9 +297,7 @@ class LaunderetteAdminView(CanEditPropMixin, BaseFormView, DetailView):
return self.form_invalid(form) return self.form_invalid(form)
def form_valid(self, form): def form_valid(self, form):
""" """We handle here the redirection, passing the user id of the asked customer."""
We handle here the redirection, passing the user id of the asked customer
"""
form.process(self.object) form.process(self.object)
if form.is_valid(): if form.is_valid():
return super().form_valid(form) return super().form_valid(form)
@ -307,9 +305,7 @@ class LaunderetteAdminView(CanEditPropMixin, BaseFormView, DetailView):
return super().form_invalid(form) return super().form_invalid(form)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" """We handle here the login form for the barman."""
We handle here the login form for the barman
"""
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
if self.request.method == "GET": if self.request.method == "GET":
kwargs["form"] = self.get_form() kwargs["form"] = self.get_form()
@ -331,7 +327,7 @@ class GetLaunderetteUserForm(GetUserForm):
class LaunderetteMainClickView(CanEditMixin, BaseFormView, DetailView): class LaunderetteMainClickView(CanEditMixin, BaseFormView, DetailView):
"""The click page of the launderette""" """The click page of the launderette."""
model = Launderette model = Launderette
pk_url_kwarg = "launderette_id" pk_url_kwarg = "launderette_id"
@ -347,16 +343,12 @@ class LaunderetteMainClickView(CanEditMixin, BaseFormView, DetailView):
return super().post(request, *args, **kwargs) return super().post(request, *args, **kwargs)
def form_valid(self, form): def form_valid(self, form):
""" """We handle here the redirection, passing the user id of the asked customer."""
We handle here the redirection, passing the user id of the asked customer
"""
self.kwargs["user_id"] = form.cleaned_data["user_id"] self.kwargs["user_id"] = form.cleaned_data["user_id"]
return super().form_valid(form) return super().form_valid(form)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" """We handle here the login form for the barman."""
We handle here the login form for the barman
"""
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["counter"] = self.object.counter kwargs["counter"] = self.object.counter
kwargs["form"] = self.get_form() kwargs["form"] = self.get_form()
@ -417,7 +409,7 @@ class ClickTokenForm(forms.BaseForm):
class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView): class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView):
"""The click page of the launderette""" """The click page of the launderette."""
model = Launderette model = Launderette
pk_url_kwarg = "launderette_id" pk_url_kwarg = "launderette_id"
@ -465,14 +457,14 @@ class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView):
return type("ClickForm", (ClickTokenForm,), kwargs) return type("ClickForm", (ClickTokenForm,), kwargs)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
"""Simple get view""" """Simple get view."""
self.customer = Customer.objects.filter(user__id=self.kwargs["user_id"]).first() self.customer = Customer.objects.filter(user__id=self.kwargs["user_id"]).first()
self.subscriber = self.customer.user self.subscriber = self.customer.user
self.operator = request.user self.operator = request.user
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
"""Handle the many possibilities of the post request""" """Handle the many possibilities of the post request."""
self.object = self.get_object() self.object = self.get_object()
self.customer = Customer.objects.filter(user__id=self.kwargs["user_id"]).first() self.customer = Customer.objects.filter(user__id=self.kwargs["user_id"]).first()
self.subscriber = self.customer.user self.subscriber = self.customer.user
@ -480,16 +472,12 @@ class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView):
return super().post(request, *args, **kwargs) return super().post(request, *args, **kwargs)
def form_valid(self, form): def form_valid(self, form):
""" """We handle here the redirection, passing the user id of the asked customer."""
We handle here the redirection, passing the user id of the asked customer
"""
self.request.session.update(form.last_basket) self.request.session.update(form.last_basket)
return super().form_valid(form) return super().form_valid(form)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" """We handle here the login form for the barman."""
We handle here the login form for the barman
"""
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
if "form" not in kwargs.keys(): if "form" not in kwargs.keys():
kwargs["form"] = self.get_form() kwargs["form"] = self.get_form()
@ -505,7 +493,7 @@ class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView):
class MachineEditView(CanEditPropMixin, UpdateView): class MachineEditView(CanEditPropMixin, UpdateView):
"""Edit a machine""" """Edit a machine."""
model = Machine model = Machine
pk_url_kwarg = "machine_id" pk_url_kwarg = "machine_id"
@ -514,7 +502,7 @@ class MachineEditView(CanEditPropMixin, UpdateView):
class MachineDeleteView(CanEditPropMixin, DeleteView): class MachineDeleteView(CanEditPropMixin, DeleteView):
"""Edit a machine""" """Edit a machine."""
model = Machine model = Machine
pk_url_kwarg = "machine_id" pk_url_kwarg = "machine_id"
@ -523,7 +511,7 @@ class MachineDeleteView(CanEditPropMixin, DeleteView):
class MachineCreateView(CanCreateMixin, CreateView): class MachineCreateView(CanCreateMixin, CreateView):
"""Create a new machine""" """Create a new machine."""
model = Machine model = Machine
fields = ["name", "launderette", "type"] fields = ["name", "launderette", "type"]

View File

@ -155,9 +155,7 @@ class SearchFormListView(FormerSubscriberMixin, SingleObjectMixin, ListView):
class SearchFormView(FormerSubscriberMixin, FormView): class SearchFormView(FormerSubscriberMixin, FormView):
""" """Allows users to search inside the user list."""
Allows users to search inside the user list
"""
form_class = SearchForm form_class = SearchForm
@ -200,9 +198,7 @@ class SearchQuickFormView(SearchFormView):
class SearchClearFormView(FormerSubscriberMixin, View): class SearchClearFormView(FormerSubscriberMixin, View):
""" """Clear SearchFormView and redirect to it."""
Clear SearchFormView and redirect to it
"""
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
super().dispatch(request, *args, **kwargs) super().dispatch(request, *args, **kwargs)

View File

@ -30,9 +30,7 @@ from pedagogy.models import UV, UVComment, UVCommentReport
class UVForm(forms.ModelForm): class UVForm(forms.ModelForm):
""" """Form handeling creation and edit of an UV."""
Form handeling creation and edit of an UV
"""
class Meta: class Meta:
model = UV model = UV
@ -85,9 +83,7 @@ class StarList(forms.NumberInput):
class UVCommentForm(forms.ModelForm): class UVCommentForm(forms.ModelForm):
""" """Form handeling creation and edit of an UVComment."""
Form handeling creation and edit of an UVComment
"""
class Meta: class Meta:
model = UVComment model = UVComment
@ -137,9 +133,7 @@ class UVCommentForm(forms.ModelForm):
class UVCommentReportForm(forms.ModelForm): class UVCommentReportForm(forms.ModelForm):
""" """Form handeling creation and edit of an UVReport."""
Form handeling creation and edit of an UVReport
"""
class Meta: class Meta:
model = UVCommentReport model = UVCommentReport
@ -159,9 +153,7 @@ class UVCommentReportForm(forms.ModelForm):
class UVCommentModerationForm(forms.Form): class UVCommentModerationForm(forms.Form):
""" """Form handeling bulk comment deletion."""
Form handeling bulk comment deletion
"""
accepted_reports = forms.ModelMultipleChoiceField( accepted_reports = forms.ModelMultipleChoiceField(
UVCommentReport.objects.all(), UVCommentReport.objects.all(),

View File

@ -36,9 +36,7 @@ from core.models import User
class UV(models.Model): class UV(models.Model):
""" """Contains infos about an UV (course)."""
Contains infos about an UV (course)
"""
code = models.CharField( code = models.CharField(
_("code"), _("code"),
@ -148,15 +146,11 @@ class UV(models.Model):
return reverse("pedagogy:uv_detail", kwargs={"uv_id": self.id}) return reverse("pedagogy:uv_detail", kwargs={"uv_id": self.id})
def is_owned_by(self, user): def is_owned_by(self, user):
""" """Can be created by superuser, root or pedagogy admin user."""
Can be created by superuser, root or pedagogy admin user
"""
return user.is_in_group(pk=settings.SITH_GROUP_PEDAGOGY_ADMIN_ID) return user.is_in_group(pk=settings.SITH_GROUP_PEDAGOGY_ADMIN_ID)
def can_be_viewed_by(self, user): def can_be_viewed_by(self, user):
""" """Only visible by subscribers."""
Only visible by subscribers
"""
return user.is_subscribed return user.is_subscribed
def __grade_average_generic(self, field): def __grade_average_generic(self, field):
@ -166,14 +160,13 @@ class UV(models.Model):
return int(sum(comments.values_list(field, flat=True)) / comments.count()) return int(sum(comments.values_list(field, flat=True)) / comments.count())
def has_user_already_commented(self, user): def has_user_already_commented(self, user: User) -> bool:
""" """Help prevent multiples comments from the same user.
Help prevent multiples comments from the same user
This function checks that no other comment has been posted by a specified user
:param user: core.models.User This function checks that no other comment has been posted by a specified user.
:return: if the user has already posted a comment on this UV
:rtype: bool Returns:
True if the user has already posted a comment on this UV, else False.
""" """
return self.comments.filter(author=user).exists() return self.comments.filter(author=user).exists()
@ -199,9 +192,7 @@ class UV(models.Model):
class UVComment(models.Model): class UVComment(models.Model):
""" """A comment about an UV."""
A comment about an UV
"""
author = models.ForeignKey( author = models.ForeignKey(
User, User,
@ -261,28 +252,30 @@ class UVComment(models.Model):
super().save(*args, **kwargs) super().save(*args, **kwargs)
def is_owned_by(self, user): def is_owned_by(self, user):
""" """Is owned by a pedagogy admin, a superuser or the author himself."""
Is owned by a pedagogy admin, a superuser or the author himself
"""
return self.author == user or user.is_owner(self.uv) return self.author == user or user.is_owner(self.uv)
@cached_property @cached_property
def is_reported(self): def is_reported(self):
""" """Return True if someone reported this UV."""
Return True if someone reported this UV
"""
return self.reports.exists() return self.reports.exists()
# TODO : it seems that some views were meant to be implemented
# to use this model.
# However, it seems that the implementation finally didn't happen.
# It should be discussed, when possible, of what to do with that :
# - go on and finally implement the UV results features ?
# - or fuck go back and remove this model ?
class UVResult(models.Model): class UVResult(models.Model):
""" """Results got to an UV.
Results got to an UV
Views will be implemented after the first release Views will be implemented after the first release
Will list every UV done by an user Will list every UV done by an user
Linked to user Linked to user
uv uv
Contains a grade settings.SITH_PEDAGOGY_UV_RESULT_GRADE Contains a grade settings.SITH_PEDAGOGY_UV_RESULT_GRADE
a semester (P/A)20xx a semester (P/A)20xx.
""" """
uv = models.ForeignKey( uv = models.ForeignKey(
@ -308,9 +301,7 @@ class UVResult(models.Model):
class UVCommentReport(models.Model): class UVCommentReport(models.Model):
""" """Report an inapropriate comment."""
Report an inapropriate comment
"""
comment = models.ForeignKey( comment = models.ForeignKey(
UVComment, UVComment,
@ -334,9 +325,7 @@ class UVCommentReport(models.Model):
return self.comment.uv return self.comment.uv
def is_owned_by(self, user): def is_owned_by(self, user):
""" """Can be created by a pedagogy admin, a superuser or a subscriber."""
Can be created by a pedagogy admin, a superuser or a subscriber
"""
return user.is_subscribed or user.is_owner(self.comment.uv) return user.is_subscribed or user.is_owner(self.comment.uv)
@ -344,9 +333,9 @@ class UVCommentReport(models.Model):
class UVSerializer(serializers.ModelSerializer): class UVSerializer(serializers.ModelSerializer):
""" """Custom seralizer for UVs.
Custom seralizer for UVs
Allow adding more informations like absolute_url Allow adding more informations like absolute_url.
""" """
class Meta: class Meta:

View File

@ -29,9 +29,7 @@ from pedagogy.models import UV
class IndexSignalProcessor(signals.BaseSignalProcessor): class IndexSignalProcessor(signals.BaseSignalProcessor):
""" """Auto update index on CRUD operations."""
Auto update index on CRUD operations
"""
def setup(self): def setup(self):
# Listen only to the ``UV`` model. # Listen only to the ``UV`` model.
@ -45,9 +43,7 @@ class IndexSignalProcessor(signals.BaseSignalProcessor):
class UVIndex(indexes.SearchIndex, indexes.Indexable): class UVIndex(indexes.SearchIndex, indexes.Indexable):
""" """Indexer class for UVs."""
Indexer class for UVs
"""
text = BigCharFieldIndex(document=True, use_template=True) text = BigCharFieldIndex(document=True, use_template=True)
auto = indexes.EdgeNgramField(use_template=True) auto = indexes.EdgeNgramField(use_template=True)

View File

@ -32,9 +32,7 @@ from pedagogy.models import UV, UVComment, UVCommentReport
def create_uv_template(user_id, code="IFC1", exclude_list=None): def create_uv_template(user_id, code="IFC1", exclude_list=None):
""" """Factory to help UV creation/update in post requests."""
Factory to help UV creation/update in post requests
"""
if exclude_list is None: if exclude_list is None:
exclude_list = [] exclude_list = []
uv = { uv = {
@ -79,9 +77,7 @@ def create_uv_template(user_id, code="IFC1", exclude_list=None):
class UVCreation(TestCase): class UVCreation(TestCase):
""" """Test uv creation."""
Test uv creation
"""
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -291,9 +287,7 @@ class UVUpdateTest(TestCase):
def create_uv_comment_template(user_id, uv_code="PA00", exclude_list=None): def create_uv_comment_template(user_id, uv_code="PA00", exclude_list=None):
""" """Factory to help UVComment creation/update in post requests."""
Factory to help UVComment creation/update in post requests
"""
if exclude_list is None: if exclude_list is None:
exclude_list = [] exclude_list = []
comment = { comment = {
@ -312,9 +306,9 @@ def create_uv_comment_template(user_id, uv_code="PA00", exclude_list=None):
class UVCommentCreationAndDisplay(TestCase): class UVCommentCreationAndDisplay(TestCase):
""" """Test UVComment creation and its display.
Test UVComment creation and it's display
Display and creation are the same view Display and creation are the same view.
""" """
@classmethod @classmethod
@ -575,10 +569,7 @@ class UVCommentUpdateTest(TestCase):
class UVSearchTest(TestCase): class UVSearchTest(TestCase):
""" """Test UV guide rights for view and API."""
Test UV guide rights for view and API
Test that the API is working well
"""
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -751,10 +742,7 @@ class UVSearchTest(TestCase):
class UVModerationFormTest(TestCase): class UVModerationFormTest(TestCase):
""" """Assert access rights and if the form works well."""
Test moderation view
Assert access rights and if the form works well
"""
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -967,9 +955,9 @@ class UVModerationFormTest(TestCase):
class UVCommentReportCreateTest(TestCase): class UVCommentReportCreateTest(TestCase):
""" """Test report creation view.
Test report creation view view
Assert access rights and if you can create with it Assert access rights and if you can create with it.
""" """
def setUp(self): def setUp(self):

View File

@ -57,21 +57,15 @@ from pedagogy.models import UV, UVComment, UVCommentReport, UVSerializer
class CanCreateUVFunctionMixin(View): class CanCreateUVFunctionMixin(View):
""" """Add the function can_create_uv(user) into the template."""
Add the function can_create_uv(user) into the template
"""
@staticmethod @staticmethod
def can_create_uv(user): def can_create_uv(user):
""" """Creates a dummy instance of UV and test is_owner."""
Creates a dummy instance of UV and test is_owner
"""
return user.is_owner(UV()) return user.is_owner(UV())
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" """Pass the function to the template."""
Pass the function to the template
"""
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["can_create_uv"] = self.can_create_uv kwargs["can_create_uv"] = self.can_create_uv
return kwargs return kwargs
@ -81,9 +75,9 @@ class CanCreateUVFunctionMixin(View):
class UVDetailFormView(CanViewMixin, CanCreateUVFunctionMixin, DetailFormView): class UVDetailFormView(CanViewMixin, CanCreateUVFunctionMixin, DetailFormView):
""" """Display every comment of an UV and detailed infos about it.
Dispaly every comment of an UV and detailed infos about it
Allow to comment the UV Allow to comment the UV.
""" """
model = UV model = UV
@ -109,9 +103,7 @@ class UVDetailFormView(CanViewMixin, CanCreateUVFunctionMixin, DetailFormView):
class UVCommentUpdateView(CanEditPropMixin, UpdateView): class UVCommentUpdateView(CanEditPropMixin, UpdateView):
""" """Allow edit of a given comment."""
Allow edit of a given comment
"""
model = UVComment model = UVComment
form_class = UVCommentForm form_class = UVCommentForm
@ -132,9 +124,7 @@ class UVCommentUpdateView(CanEditPropMixin, UpdateView):
class UVCommentDeleteView(CanEditPropMixin, DeleteView): class UVCommentDeleteView(CanEditPropMixin, DeleteView):
""" """Allow delete of a given comment."""
Allow delete of a given comment
"""
model = UVComment model = UVComment
pk_url_kwarg = "comment_id" pk_url_kwarg = "comment_id"
@ -145,9 +135,7 @@ class UVCommentDeleteView(CanEditPropMixin, DeleteView):
class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView): class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView):
""" """UV guide main page."""
UV guide main page
"""
# This is very basic and is prone to changment # This is very basic and is prone to changment
@ -208,9 +196,7 @@ class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView):
class UVCommentReportCreateView(CanCreateMixin, CreateView): class UVCommentReportCreateView(CanCreateMixin, CreateView):
""" """Create a new report for an inapropriate comment."""
Create a new report for an inapropriate comment
"""
model = UVCommentReport model = UVCommentReport
form_class = UVCommentReportForm form_class = UVCommentReportForm
@ -253,9 +239,7 @@ class UVCommentReportCreateView(CanCreateMixin, CreateView):
class UVModerationFormView(FormView): class UVModerationFormView(FormView):
""" """Moderation interface (Privileged)."""
Moderation interface (Privileged)
"""
form_class = UVCommentModerationForm form_class = UVCommentModerationForm
template_name = "pedagogy/moderation.jinja" template_name = "pedagogy/moderation.jinja"
@ -286,9 +270,7 @@ class UVModerationFormView(FormView):
class UVCreateView(CanCreateMixin, CreateView): class UVCreateView(CanCreateMixin, CreateView):
""" """Add a new UV (Privileged)."""
Add a new UV (Privileged)
"""
model = UV model = UV
form_class = UVForm form_class = UVForm
@ -304,9 +286,7 @@ class UVCreateView(CanCreateMixin, CreateView):
class UVDeleteView(CanEditPropMixin, DeleteView): class UVDeleteView(CanEditPropMixin, DeleteView):
""" """Allow to delete an UV (Privileged)."""
Allow to delete an UV (Privileged)
"""
model = UV model = UV
pk_url_kwarg = "uv_id" pk_url_kwarg = "uv_id"
@ -317,9 +297,7 @@ class UVDeleteView(CanEditPropMixin, DeleteView):
class UVUpdateView(CanEditPropMixin, UpdateView): class UVUpdateView(CanEditPropMixin, UpdateView):
""" """Allow to edit an UV (Privilegied)."""
Allow to edit an UV (Privilegied)
"""
model = UV model = UV
form_class = UVForm form_class = UVForm

View File

@ -100,6 +100,9 @@ ignore = [
"DJ001", # null=True in CharField/TextField. this one would require a migration "DJ001", # null=True in CharField/TextField. this one would require a migration
] ]
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.pytest.ini_options] [tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "sith.settings" DJANGO_SETTINGS_MODULE = "sith.settings"
python_files = ["tests.py", "test_*.py", "*_tests.py"] python_files = ["tests.py", "test_*.py", "*_tests.py"]

View File

@ -29,9 +29,7 @@ from rootplace.views import delete_all_forum_user_messages
class Command(BaseCommand): class Command(BaseCommand):
""" """Delete all forum messages from a user."""
Delete all forum messages from a user
"""
help = "Delete all user's forum message" help = "Delete all user's forum message"

View File

@ -38,8 +38,8 @@ from forum.models import ForumMessageMeta
def __merge_subscriptions(u1: User, u2: User): def __merge_subscriptions(u1: User, u2: User):
""" """Give all the subscriptions of the second user to first one.
Give all the subscriptions of the second user to first one
If some subscriptions are still active, update their end date If some subscriptions are still active, update their end date
to increase the overall subscription time of the first user. to increase the overall subscription time of the first user.
@ -87,8 +87,8 @@ def __merge_pictures(u1: User, u2: User) -> None:
def merge_users(u1: User, u2: User) -> User: def merge_users(u1: User, u2: User) -> User:
""" """Merge u2 into u1.
Merge u2 into u1
This means that u1 shall receive everything that belonged to u2 : This means that u1 shall receive everything that belonged to u2 :
- pictures - pictures
@ -134,11 +134,12 @@ def merge_users(u1: User, u2: User) -> User:
def delete_all_forum_user_messages(user, moderator, *, verbose=False): def delete_all_forum_user_messages(user, moderator, *, verbose=False):
""" """Soft delete all messages of a user.
Create a ForumMessageMeta that says a forum
message is deleted on every forum message of an user Args:
user: the user to delete messages from user: the user to delete messages from
moderator: the one marked as the moderator moderator: the one marked as the moderator.
verbose: it True, print the deleted messages
""" """
for message in user.forum_messages.all(): for message in user.forum_messages.all():
if message.is_deleted(): if message.is_deleted():
@ -184,10 +185,10 @@ class MergeUsersView(FormView):
class DeleteAllForumUserMessagesView(FormView): class DeleteAllForumUserMessagesView(FormView):
""" """Delete all forum messages from an user.
Delete all forum messages from an user
Messages are soft deleted and are still visible from admins Messages are soft deleted and are still visible from admins
GUI frontend to the dedicated command GUI frontend to the dedicated command.
""" """
template_name = "rootplace/delete_user_messages.jinja" template_name = "rootplace/delete_user_messages.jinja"
@ -209,9 +210,7 @@ class DeleteAllForumUserMessagesView(FormView):
class OperationLogListView(ListView, CanEditPropMixin): class OperationLogListView(ListView, CanEditPropMixin):
""" """List all logs."""
List all logs
"""
model = OperationLog model = OperationLog
template_name = "rootplace/logs.jinja" template_name = "rootplace/logs.jinja"

View File

@ -221,10 +221,7 @@ def sas_notification_callback(notif):
class PeoplePictureRelation(models.Model): class PeoplePictureRelation(models.Model):
""" """The PeoplePictureRelation class makes the connection between User and Picture."""
The PeoplePictureRelation class makes the connection between User and Picture
"""
user = models.ForeignKey( user = models.ForeignKey(
User, User,

View File

@ -22,8 +22,7 @@
# #
# #
""" """Django settings for sith project.
Django settings for sith project.
Generated by 'django-admin startproject' using Django 1.8.6. Generated by 'django-admin startproject' using Django 1.8.6.

View File

@ -13,22 +13,6 @@
# #
# #
"""sith URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.8/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf
1. Add an import: from blog import urls as blog_urls
2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls))
"""
from ajax_select import urls as ajax_select_urls from ajax_select import urls as ajax_select_urls
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static

View File

@ -13,8 +13,7 @@
# #
# #
""" """WSGI config for sith project.
WSGI config for sith project.
It exposes the WSGI callable as a module-level variable named ``application``. It exposes the WSGI callable as a module-level variable named ``application``.

View File

@ -31,9 +31,7 @@ from counter.models import Counter, ProductType
class Stock(models.Model): class Stock(models.Model):
""" """The Stock class, this one is used to know how many products are left for a specific counter."""
The Stock class, this one is used to know how many products are left for a specific counter
"""
name = models.CharField(_("name"), max_length=64) name = models.CharField(_("name"), max_length=64)
counter = models.OneToOneField( counter = models.OneToOneField(
@ -54,9 +52,7 @@ class Stock(models.Model):
class StockItem(models.Model): class StockItem(models.Model):
""" """The StockItem class, element of the stock."""
The StockItem class, element of the stock
"""
name = models.CharField(_("name"), max_length=64) name = models.CharField(_("name"), max_length=64)
unit_quantity = models.IntegerField( unit_quantity = models.IntegerField(
@ -95,9 +91,7 @@ class StockItem(models.Model):
class ShoppingList(models.Model): class ShoppingList(models.Model):
""" """The ShoppingList class, used to make an history of the shopping lists."""
The ShoppingList class, used to make an history of the shopping lists
"""
date = models.DateTimeField(_("date")) date = models.DateTimeField(_("date"))
name = models.CharField(_("name"), max_length=64) name = models.CharField(_("name"), max_length=64)
@ -118,7 +112,7 @@ class ShoppingList(models.Model):
class ShoppingListItem(models.Model): class ShoppingListItem(models.Model):
"""""" """An Item on a shopping list."""
shopping_lists = models.ManyToManyField( shopping_lists = models.ManyToManyField(
ShoppingList, ShoppingList,

View File

@ -41,9 +41,7 @@ from stock.models import ShoppingList, ShoppingListItem, Stock, StockItem
class StockItemList(CounterAdminTabsMixin, CanCreateMixin, ListView): class StockItemList(CounterAdminTabsMixin, CanCreateMixin, ListView):
""" """The stockitems list view for the counter owner."""
The stockitems list view for the counter owner
"""
model = Stock model = Stock
template_name = "stock/stock_item_list.jinja" template_name = "stock/stock_item_list.jinja"
@ -58,9 +56,7 @@ class StockItemList(CounterAdminTabsMixin, CanCreateMixin, ListView):
class StockListView(CounterAdminTabsMixin, CanViewMixin, ListView): class StockListView(CounterAdminTabsMixin, CanViewMixin, ListView):
""" """A list view for the admins."""
A list view for the admins
"""
model = Stock model = Stock
template_name = "stock/stock_list.jinja" template_name = "stock/stock_list.jinja"
@ -68,9 +64,7 @@ class StockListView(CounterAdminTabsMixin, CanViewMixin, ListView):
class StockEditForm(forms.ModelForm): class StockEditForm(forms.ModelForm):
""" """A form to change stock's characteristics."""
A form to change stock's characteristics
"""
class Meta: class Meta:
model = Stock model = Stock
@ -84,9 +78,7 @@ class StockEditForm(forms.ModelForm):
class StockEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView): class StockEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView):
""" """An edit view for the stock."""
An edit view for the stock
"""
model = Stock model = Stock
form_class = modelform_factory(Stock, fields=["name", "counter"]) form_class = modelform_factory(Stock, fields=["name", "counter"])
@ -96,9 +88,7 @@ class StockEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView):
class StockItemEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView): class StockItemEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView):
""" """An edit view for a stock item."""
An edit view for a stock item
"""
model = StockItem model = StockItem
form_class = modelform_factory( form_class = modelform_factory(
@ -118,9 +108,7 @@ class StockItemEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView):
class StockCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView): class StockCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView):
""" """A create view for a new Stock."""
A create view for a new Stock
"""
model = Stock model = Stock
form_class = modelform_factory(Stock, fields=["name", "counter"]) form_class = modelform_factory(Stock, fields=["name", "counter"])
@ -137,9 +125,7 @@ class StockCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView):
class StockItemCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView): class StockItemCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView):
""" """A create view for a new StockItem."""
A create view for a new StockItem
"""
model = StockItem model = StockItem
form_class = modelform_factory( form_class = modelform_factory(
@ -170,9 +156,7 @@ class StockItemCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView):
class StockShoppingListView(CounterAdminTabsMixin, CanViewMixin, ListView): class StockShoppingListView(CounterAdminTabsMixin, CanViewMixin, ListView):
""" """A list view for the people to know the item to buy."""
A list view for the people to know the item to buy
"""
model = Stock model = Stock
template_name = "stock/stock_shopping_list.jinja" template_name = "stock/stock_shopping_list.jinja"
@ -225,9 +209,7 @@ class StockItemQuantityForm(forms.BaseForm):
class StockItemQuantityBaseFormView( class StockItemQuantityBaseFormView(
CounterAdminTabsMixin, CanEditMixin, DetailView, BaseFormView CounterAdminTabsMixin, CanEditMixin, DetailView, BaseFormView
): ):
""" """docstring for StockItemOutList."""
docstring for StockItemOutList
"""
model = StockItem model = StockItem
template_name = "stock/shopping_list_quantity.jinja" template_name = "stock/shopping_list_quantity.jinja"
@ -266,16 +248,12 @@ class StockItemQuantityBaseFormView(
return type("StockItemQuantityForm", (StockItemQuantityForm,), kwargs) return type("StockItemQuantityForm", (StockItemQuantityForm,), kwargs)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
""" """Simple get view."""
Simple get view
"""
self.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first() self.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first()
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
""" """Handle the many possibilities of the post request."""
Handle the many possibilities of the post request
"""
self.object = self.get_object() self.object = self.get_object()
self.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first() self.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first()
return super().post(request, *args, **kwargs) return super().post(request, *args, **kwargs)
@ -297,7 +275,7 @@ class StockItemQuantityBaseFormView(
class StockShoppingListItemListView(CounterAdminTabsMixin, CanViewMixin, ListView): class StockShoppingListItemListView(CounterAdminTabsMixin, CanViewMixin, ListView):
"""docstring for StockShoppingListItemListView""" """docstring for StockShoppingListItemListView."""
model = ShoppingList model = ShoppingList
template_name = "stock/shopping_list_items.jinja" template_name = "stock/shopping_list_items.jinja"
@ -314,9 +292,7 @@ class StockShoppingListItemListView(CounterAdminTabsMixin, CanViewMixin, ListVie
class StockShoppingListDeleteView(CounterAdminTabsMixin, CanEditMixin, DeleteView): class StockShoppingListDeleteView(CounterAdminTabsMixin, CanEditMixin, DeleteView):
""" """Delete a ShoppingList (for the resonsible account)."""
Delete a ShoppingList (for the resonsible account)
"""
model = ShoppingList model = ShoppingList
pk_url_kwarg = "shoppinglist_id" pk_url_kwarg = "shoppinglist_id"
@ -330,9 +306,7 @@ class StockShoppingListDeleteView(CounterAdminTabsMixin, CanEditMixin, DeleteVie
class StockShopppingListSetDone(CanEditMixin, DetailView): class StockShopppingListSetDone(CanEditMixin, DetailView):
""" """Set a ShoppingList as done."""
Set a ShoppingList as done
"""
model = ShoppingList model = ShoppingList
pk_url_kwarg = "shoppinglist_id" pk_url_kwarg = "shoppinglist_id"
@ -361,9 +335,7 @@ class StockShopppingListSetDone(CanEditMixin, DetailView):
class StockShopppingListSetTodo(CanEditMixin, DetailView): class StockShopppingListSetTodo(CanEditMixin, DetailView):
""" """Set a ShoppingList as done."""
Set a ShoppingList as done
"""
model = ShoppingList model = ShoppingList
pk_url_kwarg = "shoppinglist_id" pk_url_kwarg = "shoppinglist_id"
@ -415,9 +387,7 @@ class StockUpdateAfterShopppingForm(forms.BaseForm):
class StockUpdateAfterShopppingBaseFormView( class StockUpdateAfterShopppingBaseFormView(
CounterAdminTabsMixin, CanEditMixin, DetailView, BaseFormView CounterAdminTabsMixin, CanEditMixin, DetailView, BaseFormView
): ):
""" """docstring for StockUpdateAfterShopppingBaseFormView."""
docstring for StockUpdateAfterShopppingBaseFormView
"""
model = ShoppingList model = ShoppingList
template_name = "stock/update_after_shopping.jinja" template_name = "stock/update_after_shopping.jinja"
@ -453,9 +423,7 @@ class StockUpdateAfterShopppingBaseFormView(
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
""" """Handle the many possibilities of the post request."""
Handle the many possibilities of the post request
"""
self.object = self.get_object() self.object = self.get_object()
self.shoppinglist = ShoppingList.objects.filter( self.shoppinglist = ShoppingList.objects.filter(
id=self.kwargs["shoppinglist_id"] id=self.kwargs["shoppinglist_id"]
@ -463,9 +431,7 @@ class StockUpdateAfterShopppingBaseFormView(
return super().post(request, *args, **kwargs) return super().post(request, *args, **kwargs)
def form_valid(self, form): def form_valid(self, form):
""" """We handle here the redirection."""
We handle here the redirection
"""
return super().form_valid(form) return super().form_valid(form)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -484,9 +450,7 @@ class StockUpdateAfterShopppingBaseFormView(
class StockTakeItemsForm(forms.BaseForm): class StockTakeItemsForm(forms.BaseForm):
""" """docstring for StockTakeItemsFormView."""
docstring for StockTakeItemsFormView
"""
def clean(self): def clean(self):
with transaction.atomic(): with transaction.atomic():
@ -502,9 +466,7 @@ class StockTakeItemsForm(forms.BaseForm):
class StockTakeItemsBaseFormView( class StockTakeItemsBaseFormView(
CounterTabsMixin, CanEditMixin, DetailView, BaseFormView CounterTabsMixin, CanEditMixin, DetailView, BaseFormView
): ):
""" """docstring for StockTakeItemsBaseFormView."""
docstring for StockTakeItemsBaseFormView
"""
model = StockItem model = StockItem
template_name = "stock/stock_take_items.jinja" template_name = "stock/stock_take_items.jinja"
@ -535,16 +497,11 @@ class StockTakeItemsBaseFormView(
return type("StockTakeItemsForm", (StockTakeItemsForm,), kwargs) return type("StockTakeItemsForm", (StockTakeItemsForm,), kwargs)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
"""
Simple get view
"""
self.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first() self.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first()
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
""" """Handle the many possibilities of the post request."""
Handle the many possibilities of the post request
"""
self.object = self.get_object() self.object = self.get_object()
self.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first() self.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first()
if self.stock.counter.type == "BAR" and not ( if self.stock.counter.type == "BAR" and not (

View File

@ -108,14 +108,15 @@ class Subscription(models.Model):
@staticmethod @staticmethod
def compute_start(d: date = None, duration: int = 1, user: User = None) -> date: def compute_start(d: date = None, duration: int = 1, user: User = None) -> date:
""" """Computes the start date of the subscription.
This function computes the start date of the subscription with respect to the given date (default is today),
The computation is done with respect to the given date (default is today)
and the start date given in settings.SITH_SEMESTER_START_AUTUMN. and the start date given in settings.SITH_SEMESTER_START_AUTUMN.
It takes the nearest past start date. It takes the nearest past start date.
Exemples: with SITH_SEMESTER_START_AUTUMN = (8, 15) Exemples: with SITH_SEMESTER_START_AUTUMN = (8, 15)
Today -> Start date Today -> Start date
2015-03-17 -> 2015-02-15 2015-03-17 -> 2015-02-15
2015-01-11 -> 2014-08-15 2015-01-11 -> 2014-08-15.
""" """
if not d: if not d:
d = date.today() d = date.today()
@ -129,14 +130,21 @@ class Subscription(models.Model):
@staticmethod @staticmethod
def compute_end(duration: int, start: date = None, user: User = None) -> date: def compute_end(duration: int, start: date = None, user: User = None) -> date:
""" """Compute the end date of the subscription.
This function compute the end date of the subscription given a start date and a duration in number of semester
Exemple: Args:
duration:
the duration of the subscription, in semester
(for example, 2 => 2 semesters => 1 year)
start: The start date of the subscription
user: the user which is (or will be) subscribed
Exemples:
Start - Duration -> End date Start - Duration -> End date
2015-09-18 - 1 -> 2016-03-18 2015-09-18 - 1 -> 2016-03-18
2015-09-18 - 2 -> 2016-09-18 2015-09-18 - 2 -> 2016-09-18
2015-09-18 - 3 -> 2017-03-18 2015-09-18 - 3 -> 2017-03-18
2015-09-18 - 4 -> 2017-09-18 2015-09-18 - 4 -> 2017-09-18.
""" """
if start is None: if start is None:
start = Subscription.compute_start(duration=duration, user=user) start = Subscription.compute_start(duration=duration, user=user)

View File

@ -45,8 +45,8 @@ class AvailableTrombiManager(models.Manager):
class Trombi(models.Model): class Trombi(models.Model):
""" """Main class of the trombi, the Trombi itself.
This is the main class, the Trombi itself.
It contains the deadlines for the users, and the link to the club that makes It contains the deadlines for the users, and the link to the club that makes
its Trombi. its Trombi.
""" """
@ -103,10 +103,10 @@ class Trombi(models.Model):
class TrombiUser(models.Model): class TrombiUser(models.Model):
""" """Bound between a `User` and a `Trombi`.
This class is only here to avoid cross references between the core, club,
and trombi modules. It binds a User to a Trombi without needing to import This class is here to avoid cross-references between the core, club,
Trombi into the core. and trombi modules.
It also adds the pictures to the profile without needing all the security It also adds the pictures to the profile without needing all the security
like the other SithFiles. like the other SithFiles.
""" """
@ -172,10 +172,7 @@ class TrombiUser(models.Model):
class TrombiComment(models.Model): class TrombiComment(models.Model):
""" """A comment given by someone to someone else in the same Trombi instance."""
This represent a comment given by someone to someone else in the same Trombi
instance.
"""
author = models.ForeignKey( author = models.ForeignKey(
TrombiUser, TrombiUser,
@ -202,9 +199,7 @@ class TrombiComment(models.Model):
class TrombiClubMembership(models.Model): class TrombiClubMembership(models.Model):
""" """A membership in a club."""
This represent a membership to a club
"""
user = models.ForeignKey( user = models.ForeignKey(
TrombiUser, TrombiUser,

View File

@ -94,9 +94,7 @@ class TrombiTabsMixin(TabedViewMixin):
class UserIsInATrombiMixin(View): class UserIsInATrombiMixin(View):
""" """Check if the requested user has a trombi_user attribute."""
This view check if the requested user has a trombi_user attribute
"""
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
if not hasattr(self.request.user, "trombi_user"): if not hasattr(self.request.user, "trombi_user"):
@ -118,18 +116,14 @@ class TrombiForm(forms.ModelForm):
class TrombiCreateView(CanCreateMixin, CreateView): class TrombiCreateView(CanCreateMixin, CreateView):
""" """Create a trombi for a club."""
Create a trombi for a club
"""
model = Trombi model = Trombi
form_class = TrombiForm form_class = TrombiForm
template_name = "core/create.jinja" template_name = "core/create.jinja"
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
""" """Affect club."""
Affect club
"""
form = self.get_form() form = self.get_form()
if form.is_valid(): if form.is_valid():
club = get_object_or_404(Club, id=self.kwargs["club_id"]) club = get_object_or_404(Club, id=self.kwargs["club_id"])
@ -304,9 +298,7 @@ class UserTrombiForm(forms.Form):
class UserTrombiToolsView( class UserTrombiToolsView(
QuickNotifMixin, TrombiTabsMixin, UserIsLoggedMixin, TemplateView QuickNotifMixin, TrombiTabsMixin, UserIsLoggedMixin, TemplateView
): ):
""" """Display a user's trombi tools."""
Display a user's trombi tools
"""
template_name = "trombi/user_tools.jinja" template_name = "trombi/user_tools.jinja"
current_tab = "tools" current_tab = "tools"
@ -466,9 +458,7 @@ class UserTrombiProfileView(TrombiTabsMixin, DetailView):
class TrombiCommentFormView(LoginRequiredMixin, View): class TrombiCommentFormView(LoginRequiredMixin, View):
""" """Create/edit a trombi comment."""
Create/edit a trombi comment
"""
model = TrombiComment model = TrombiComment
fields = ["content"] fields = ["content"]