mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-21 13:43:20 +00:00
use google convention for docstrings
This commit is contained in:
parent
07b625d4aa
commit
8c69a94488
@ -29,9 +29,7 @@ from core.models import SithFile, User
|
||||
|
||||
|
||||
class CurrencyField(models.DecimalField):
|
||||
"""
|
||||
This is a custom database field used for currency
|
||||
"""
|
||||
"""Custom database field used for currency."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs["max_digits"] = 12
|
||||
@ -71,30 +69,22 @@ class Company(models.Model):
|
||||
return self.name
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
"""Check if that object can be edited by the given user."""
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_be_edited_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
for club in user.memberships.filter(end_date=None).all():
|
||||
if club and club.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
||||
return True
|
||||
return False
|
||||
"""Check if that object can be edited by the given user."""
|
||||
return user.memberships.filter(
|
||||
end_date=None, club__role=settings.SITH_CLUB_ROLES_ID["Treasurer"]
|
||||
).exists()
|
||||
|
||||
def can_be_viewed_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be viewed by the given user
|
||||
"""
|
||||
for club in user.memberships.filter(end_date=None).all():
|
||||
if club and club.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
||||
return True
|
||||
return False
|
||||
"""Check if that object can be viewed by the given user."""
|
||||
return user.memberships.filter(
|
||||
end_date=None, club__role_gte=settings.SITH_CLUB_ROLES_ID["Treasurer"]
|
||||
).exists()
|
||||
|
||||
|
||||
class BankAccount(models.Model):
|
||||
@ -119,9 +109,7 @@ class BankAccount(models.Model):
|
||||
return reverse("accounting:bank_details", kwargs={"b_account_id": self.id})
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
"""Check if that object can be edited by the given user."""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
@ -158,9 +146,7 @@ class ClubAccount(models.Model):
|
||||
return reverse("accounting:club_details", kwargs={"c_account_id": self.id})
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
"""Check if that object can be edited by the given user."""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
@ -168,18 +154,14 @@ class ClubAccount(models.Model):
|
||||
return False
|
||||
|
||||
def can_be_edited_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
"""Check if that object can be edited by the given user."""
|
||||
m = self.club.get_membership_for(user)
|
||||
if m and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_be_viewed_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be viewed by the given user
|
||||
"""
|
||||
"""Check if that object can be viewed by the given user."""
|
||||
m = self.club.get_membership_for(user)
|
||||
if m and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
||||
return True
|
||||
@ -202,9 +184,7 @@ class ClubAccount(models.Model):
|
||||
|
||||
|
||||
class GeneralJournal(models.Model):
|
||||
"""
|
||||
Class storing all the operations for a period of time
|
||||
"""
|
||||
"""Class storing all the operations for a period of time."""
|
||||
|
||||
start_date = models.DateField(_("start date"))
|
||||
end_date = models.DateField(_("end date"), null=True, blank=True, default=None)
|
||||
@ -231,9 +211,7 @@ class GeneralJournal(models.Model):
|
||||
return reverse("accounting:journal_details", kwargs={"j_id": self.id})
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
"""Check if that object can be edited by the given user."""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
@ -243,9 +221,7 @@ class GeneralJournal(models.Model):
|
||||
return False
|
||||
|
||||
def can_be_edited_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
"""Check if that object can be edited by the given user."""
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
return True
|
||||
if self.club_account.can_be_edited_by(user):
|
||||
@ -271,9 +247,7 @@ class GeneralJournal(models.Model):
|
||||
|
||||
|
||||
class Operation(models.Model):
|
||||
"""
|
||||
An operation is a line in the journal, a debit or a credit
|
||||
"""
|
||||
"""An operation is a line in the journal, a debit or a credit."""
|
||||
|
||||
number = models.IntegerField(_("number"))
|
||||
journal = models.ForeignKey(
|
||||
@ -422,9 +396,7 @@ class Operation(models.Model):
|
||||
return tar
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
"""Check if that object can be edited by the given user."""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
@ -437,9 +409,7 @@ class Operation(models.Model):
|
||||
return False
|
||||
|
||||
def can_be_edited_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
"""Check if that object can be edited by the given user."""
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
return True
|
||||
if self.journal.closed:
|
||||
@ -451,10 +421,9 @@ class Operation(models.Model):
|
||||
|
||||
|
||||
class AccountingType(models.Model):
|
||||
"""
|
||||
Class describing the accounting types.
|
||||
"""Accounting types.
|
||||
|
||||
Thoses are numbers used in accounting to classify operations
|
||||
Those are numbers used in accounting to classify operations
|
||||
"""
|
||||
|
||||
code = models.CharField(
|
||||
@ -488,9 +457,7 @@ class AccountingType(models.Model):
|
||||
return reverse("accounting:type_list")
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
"""Check if that object can be edited by the given user."""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
@ -499,9 +466,7 @@ class AccountingType(models.Model):
|
||||
|
||||
|
||||
class SimplifiedAccountingType(models.Model):
|
||||
"""
|
||||
Class describing the simplified accounting types.
|
||||
"""
|
||||
"""Simplified version of `AccountingType`."""
|
||||
|
||||
label = models.CharField(_("label"), max_length=128)
|
||||
accounting_type = models.ForeignKey(
|
||||
@ -533,7 +498,7 @@ class SimplifiedAccountingType(models.Model):
|
||||
|
||||
|
||||
class Label(models.Model):
|
||||
"""Label allow a club to sort its operations"""
|
||||
"""Label allow a club to sort its operations."""
|
||||
|
||||
name = models.CharField(_("label"), max_length=64)
|
||||
club_account = models.ForeignKey(
|
||||
|
@ -53,9 +53,7 @@ from counter.models import Counter, Product, Selling
|
||||
|
||||
|
||||
class BankAccountListView(CanViewMixin, ListView):
|
||||
"""
|
||||
A list view for the admins
|
||||
"""
|
||||
"""A list view for the admins."""
|
||||
|
||||
model = BankAccount
|
||||
template_name = "accounting/bank_account_list.jinja"
|
||||
@ -66,18 +64,14 @@ class BankAccountListView(CanViewMixin, ListView):
|
||||
|
||||
|
||||
class SimplifiedAccountingTypeListView(CanViewMixin, ListView):
|
||||
"""
|
||||
A list view for the admins
|
||||
"""
|
||||
"""A list view for the admins."""
|
||||
|
||||
model = SimplifiedAccountingType
|
||||
template_name = "accounting/simplifiedaccountingtype_list.jinja"
|
||||
|
||||
|
||||
class SimplifiedAccountingTypeEditView(CanViewMixin, UpdateView):
|
||||
"""
|
||||
An edit view for the admins
|
||||
"""
|
||||
"""An edit view for the admins."""
|
||||
|
||||
model = SimplifiedAccountingType
|
||||
pk_url_kwarg = "type_id"
|
||||
@ -86,9 +80,7 @@ class SimplifiedAccountingTypeEditView(CanViewMixin, UpdateView):
|
||||
|
||||
|
||||
class SimplifiedAccountingTypeCreateView(CanCreateMixin, CreateView):
|
||||
"""
|
||||
Create an accounting type (for the admins)
|
||||
"""
|
||||
"""Create an accounting type (for the admins)."""
|
||||
|
||||
model = SimplifiedAccountingType
|
||||
fields = ["label", "accounting_type"]
|
||||
@ -99,18 +91,14 @@ class SimplifiedAccountingTypeCreateView(CanCreateMixin, CreateView):
|
||||
|
||||
|
||||
class AccountingTypeListView(CanViewMixin, ListView):
|
||||
"""
|
||||
A list view for the admins
|
||||
"""
|
||||
"""A list view for the admins."""
|
||||
|
||||
model = AccountingType
|
||||
template_name = "accounting/accountingtype_list.jinja"
|
||||
|
||||
|
||||
class AccountingTypeEditView(CanViewMixin, UpdateView):
|
||||
"""
|
||||
An edit view for the admins
|
||||
"""
|
||||
"""An edit view for the admins."""
|
||||
|
||||
model = AccountingType
|
||||
pk_url_kwarg = "type_id"
|
||||
@ -119,9 +107,7 @@ class AccountingTypeEditView(CanViewMixin, UpdateView):
|
||||
|
||||
|
||||
class AccountingTypeCreateView(CanCreateMixin, CreateView):
|
||||
"""
|
||||
Create an accounting type (for the admins)
|
||||
"""
|
||||
"""Create an accounting type (for the admins)."""
|
||||
|
||||
model = AccountingType
|
||||
fields = ["code", "label", "movement_type"]
|
||||
@ -132,9 +118,7 @@ class AccountingTypeCreateView(CanCreateMixin, CreateView):
|
||||
|
||||
|
||||
class BankAccountEditView(CanViewMixin, UpdateView):
|
||||
"""
|
||||
An edit view for the admins
|
||||
"""
|
||||
"""An edit view for the admins."""
|
||||
|
||||
model = BankAccount
|
||||
pk_url_kwarg = "b_account_id"
|
||||
@ -143,9 +127,7 @@ class BankAccountEditView(CanViewMixin, UpdateView):
|
||||
|
||||
|
||||
class BankAccountDetailView(CanViewMixin, DetailView):
|
||||
"""
|
||||
A detail view, listing every club account
|
||||
"""
|
||||
"""A detail view, listing every club account."""
|
||||
|
||||
model = BankAccount
|
||||
pk_url_kwarg = "b_account_id"
|
||||
@ -153,9 +135,7 @@ class BankAccountDetailView(CanViewMixin, DetailView):
|
||||
|
||||
|
||||
class BankAccountCreateView(CanCreateMixin, CreateView):
|
||||
"""
|
||||
Create a bank account (for the admins)
|
||||
"""
|
||||
"""Create a bank account (for the admins)."""
|
||||
|
||||
model = BankAccount
|
||||
fields = ["name", "club", "iban", "number"]
|
||||
@ -165,9 +145,7 @@ class BankAccountCreateView(CanCreateMixin, CreateView):
|
||||
class BankAccountDeleteView(
|
||||
CanEditPropMixin, DeleteView
|
||||
): # TODO change Delete to Close
|
||||
"""
|
||||
Delete a bank account (for the admins)
|
||||
"""
|
||||
"""Delete a bank account (for the admins)."""
|
||||
|
||||
model = BankAccount
|
||||
pk_url_kwarg = "b_account_id"
|
||||
@ -179,9 +157,7 @@ class BankAccountDeleteView(
|
||||
|
||||
|
||||
class ClubAccountEditView(CanViewMixin, UpdateView):
|
||||
"""
|
||||
An edit view for the admins
|
||||
"""
|
||||
"""An edit view for the admins."""
|
||||
|
||||
model = ClubAccount
|
||||
pk_url_kwarg = "c_account_id"
|
||||
@ -190,9 +166,7 @@ class ClubAccountEditView(CanViewMixin, UpdateView):
|
||||
|
||||
|
||||
class ClubAccountDetailView(CanViewMixin, DetailView):
|
||||
"""
|
||||
A detail view, listing every journal
|
||||
"""
|
||||
"""A detail view, listing every journal."""
|
||||
|
||||
model = ClubAccount
|
||||
pk_url_kwarg = "c_account_id"
|
||||
@ -200,9 +174,7 @@ class ClubAccountDetailView(CanViewMixin, DetailView):
|
||||
|
||||
|
||||
class ClubAccountCreateView(CanCreateMixin, CreateView):
|
||||
"""
|
||||
Create a club account (for the admins)
|
||||
"""
|
||||
"""Create a club account (for the admins)."""
|
||||
|
||||
model = ClubAccount
|
||||
fields = ["name", "club", "bank_account"]
|
||||
@ -220,9 +192,7 @@ class ClubAccountCreateView(CanCreateMixin, CreateView):
|
||||
class ClubAccountDeleteView(
|
||||
CanEditPropMixin, DeleteView
|
||||
): # TODO change Delete to Close
|
||||
"""
|
||||
Delete a club account (for the admins)
|
||||
"""
|
||||
"""Delete a club account (for the admins)."""
|
||||
|
||||
model = ClubAccount
|
||||
pk_url_kwarg = "c_account_id"
|
||||
@ -282,9 +252,7 @@ class JournalTabsMixin(TabedViewMixin):
|
||||
|
||||
|
||||
class JournalCreateView(CanCreateMixin, CreateView):
|
||||
"""
|
||||
Create a general journal
|
||||
"""
|
||||
"""Create a general journal."""
|
||||
|
||||
model = GeneralJournal
|
||||
form_class = modelform_factory(
|
||||
@ -304,9 +272,7 @@ class JournalCreateView(CanCreateMixin, CreateView):
|
||||
|
||||
|
||||
class JournalDetailView(JournalTabsMixin, CanViewMixin, DetailView):
|
||||
"""
|
||||
A detail view, listing every operation
|
||||
"""
|
||||
"""A detail view, listing every operation."""
|
||||
|
||||
model = GeneralJournal
|
||||
pk_url_kwarg = "j_id"
|
||||
@ -315,9 +281,7 @@ class JournalDetailView(JournalTabsMixin, CanViewMixin, DetailView):
|
||||
|
||||
|
||||
class JournalEditView(CanEditMixin, UpdateView):
|
||||
"""
|
||||
Update a general journal
|
||||
"""
|
||||
"""Update a general journal."""
|
||||
|
||||
model = GeneralJournal
|
||||
pk_url_kwarg = "j_id"
|
||||
@ -326,9 +290,7 @@ class JournalEditView(CanEditMixin, UpdateView):
|
||||
|
||||
|
||||
class JournalDeleteView(CanEditPropMixin, DeleteView):
|
||||
"""
|
||||
Delete a club account (for the admins)
|
||||
"""
|
||||
"""Delete a club account (for the admins)."""
|
||||
|
||||
model = GeneralJournal
|
||||
pk_url_kwarg = "j_id"
|
||||
@ -467,9 +429,7 @@ class OperationForm(forms.ModelForm):
|
||||
|
||||
|
||||
class OperationCreateView(CanCreateMixin, CreateView):
|
||||
"""
|
||||
Create an operation
|
||||
"""
|
||||
"""Create an operation."""
|
||||
|
||||
model = Operation
|
||||
form_class = OperationForm
|
||||
@ -487,7 +447,7 @@ class OperationCreateView(CanCreateMixin, CreateView):
|
||||
return ret
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add journal to the context"""
|
||||
"""Add journal to the context."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
if self.journal:
|
||||
kwargs["object"] = self.journal
|
||||
@ -495,9 +455,7 @@ class OperationCreateView(CanCreateMixin, CreateView):
|
||||
|
||||
|
||||
class OperationEditView(CanEditMixin, UpdateView):
|
||||
"""
|
||||
An edit view, working as detail for the moment
|
||||
"""
|
||||
"""An edit view, working as detail for the moment."""
|
||||
|
||||
model = Operation
|
||||
pk_url_kwarg = "op_id"
|
||||
@ -505,16 +463,14 @@ class OperationEditView(CanEditMixin, UpdateView):
|
||||
template_name = "accounting/operation_edit.jinja"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add journal to the context"""
|
||||
"""Add journal to the context."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["object"] = self.object.journal
|
||||
return kwargs
|
||||
|
||||
|
||||
class OperationPDFView(CanViewMixin, DetailView):
|
||||
"""
|
||||
Display the PDF of a given operation
|
||||
"""
|
||||
"""Display the PDF of a given operation."""
|
||||
|
||||
model = Operation
|
||||
pk_url_kwarg = "op_id"
|
||||
@ -666,9 +622,7 @@ class OperationPDFView(CanViewMixin, DetailView):
|
||||
|
||||
|
||||
class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
||||
"""
|
||||
Display a statement sorted by labels
|
||||
"""
|
||||
"""Display a statement sorted by labels."""
|
||||
|
||||
model = GeneralJournal
|
||||
pk_url_kwarg = "j_id"
|
||||
@ -726,16 +680,14 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
||||
return statement
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add infos to the context"""
|
||||
"""Add infos to the context."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["statement"] = self.big_statement()
|
||||
return kwargs
|
||||
|
||||
|
||||
class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
||||
"""
|
||||
Calculate a dictionary with operation target and sum of operations
|
||||
"""
|
||||
"""Calculate a dictionary with operation target and sum of operations."""
|
||||
|
||||
model = GeneralJournal
|
||||
pk_url_kwarg = "j_id"
|
||||
@ -765,7 +717,7 @@ class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
||||
return sum(self.statement(movement_type).values())
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add journal to the context"""
|
||||
"""Add journal to the context."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["credit_statement"] = self.statement("CREDIT")
|
||||
kwargs["debit_statement"] = self.statement("DEBIT")
|
||||
@ -775,9 +727,7 @@ class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
||||
|
||||
|
||||
class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
||||
"""
|
||||
Calculate a dictionary with operation type and sum of operations
|
||||
"""
|
||||
"""Calculate a dictionary with operation type and sum of operations."""
|
||||
|
||||
model = GeneralJournal
|
||||
pk_url_kwarg = "j_id"
|
||||
@ -795,7 +745,7 @@ class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView)
|
||||
return statement
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add journal to the context"""
|
||||
"""Add journal to the context."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["statement"] = self.statement()
|
||||
return kwargs
|
||||
@ -810,9 +760,7 @@ class CompanyListView(CanViewMixin, ListView):
|
||||
|
||||
|
||||
class CompanyCreateView(CanCreateMixin, CreateView):
|
||||
"""
|
||||
Create a company
|
||||
"""
|
||||
"""Create a company."""
|
||||
|
||||
model = Company
|
||||
fields = ["name"]
|
||||
@ -821,9 +769,7 @@ class CompanyCreateView(CanCreateMixin, CreateView):
|
||||
|
||||
|
||||
class CompanyEditView(CanCreateMixin, UpdateView):
|
||||
"""
|
||||
Edit a company
|
||||
"""
|
||||
"""Edit a company."""
|
||||
|
||||
model = Company
|
||||
pk_url_kwarg = "co_id"
|
||||
@ -882,9 +828,7 @@ class CloseCustomerAccountForm(forms.Form):
|
||||
|
||||
|
||||
class RefoundAccountView(FormView):
|
||||
"""
|
||||
Create a selling with the same amount than the current user money
|
||||
"""
|
||||
"""Create a selling with the same amount than the current user money."""
|
||||
|
||||
template_name = "accounting/refound_account.jinja"
|
||||
form_class = CloseCustomerAccountForm
|
||||
|
@ -23,9 +23,9 @@ from core.views import can_edit, can_view
|
||||
|
||||
|
||||
def check_if(obj, user, test):
|
||||
"""
|
||||
Detect if it's a single object or a queryset
|
||||
aply a given test on individual object and return global permission
|
||||
"""Detect if it's a single object or a queryset.
|
||||
|
||||
Apply a given test on individual object and return global permission.
|
||||
"""
|
||||
if isinstance(obj, QuerySet):
|
||||
for o in obj:
|
||||
@ -39,9 +39,7 @@ def check_if(obj, user, test):
|
||||
class ManageModelMixin:
|
||||
@action(detail=True)
|
||||
def id(self, request, pk=None):
|
||||
"""
|
||||
Get by id (api/v1/router/{pk}/id/)
|
||||
"""
|
||||
"""Get by id (api/v1/router/{pk}/id/)."""
|
||||
self.queryset = get_object_or_404(self.queryset.filter(id=pk))
|
||||
serializer = self.get_serializer(self.queryset)
|
||||
return Response(serializer.data)
|
||||
|
@ -23,9 +23,7 @@ from core.templatetags.renderer import markdown
|
||||
@api_view(["POST"])
|
||||
@renderer_classes((StaticHTMLRenderer,))
|
||||
def RenderMarkdown(request):
|
||||
"""
|
||||
Render Markdown
|
||||
"""
|
||||
"""Render Markdown."""
|
||||
try:
|
||||
data = markdown(request.POST["text"])
|
||||
except:
|
||||
|
@ -31,9 +31,7 @@ class ClubSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class ClubViewSet(RightModelViewSet):
|
||||
"""
|
||||
Manage Clubs (api/v1/club/)
|
||||
"""
|
||||
"""Manage Clubs (api/v1/club/)."""
|
||||
|
||||
serializer_class = ClubSerializer
|
||||
queryset = Club.objects.all()
|
||||
|
@ -33,18 +33,14 @@ class CounterSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class CounterViewSet(RightModelViewSet):
|
||||
"""
|
||||
Manage Counters (api/v1/counter/)
|
||||
"""
|
||||
"""Manage Counters (api/v1/counter/)."""
|
||||
|
||||
serializer_class = CounterSerializer
|
||||
queryset = Counter.objects.all()
|
||||
|
||||
@action(detail=False)
|
||||
def bar(self, request):
|
||||
"""
|
||||
Return all bars (api/v1/counter/bar/)
|
||||
"""
|
||||
"""Return all bars (api/v1/counter/bar/)."""
|
||||
self.queryset = self.queryset.filter(type="BAR")
|
||||
serializer = self.get_serializer(self.queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
@ -25,9 +25,7 @@ class GroupSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class GroupViewSet(RightModelViewSet):
|
||||
"""
|
||||
Manage Groups (api/v1/group/)
|
||||
"""
|
||||
"""Manage Groups (api/v1/group/)."""
|
||||
|
||||
serializer_class = GroupSerializer
|
||||
queryset = RealGroup.objects.all()
|
||||
|
@ -60,54 +60,42 @@ class LaunderetteTokenSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class LaunderettePlaceViewSet(RightModelViewSet):
|
||||
"""
|
||||
Manage Launderette (api/v1/launderette/place/)
|
||||
"""
|
||||
"""Manage Launderette (api/v1/launderette/place/)."""
|
||||
|
||||
serializer_class = LaunderettePlaceSerializer
|
||||
queryset = Launderette.objects.all()
|
||||
|
||||
|
||||
class LaunderetteMachineViewSet(RightModelViewSet):
|
||||
"""
|
||||
Manage Washing Machines (api/v1/launderette/machine/)
|
||||
"""
|
||||
"""Manage Washing Machines (api/v1/launderette/machine/)."""
|
||||
|
||||
serializer_class = LaunderetteMachineSerializer
|
||||
queryset = Machine.objects.all()
|
||||
|
||||
|
||||
class LaunderetteTokenViewSet(RightModelViewSet):
|
||||
"""
|
||||
Manage Launderette's tokens (api/v1/launderette/token/)
|
||||
"""
|
||||
"""Manage Launderette's tokens (api/v1/launderette/token/)."""
|
||||
|
||||
serializer_class = LaunderetteTokenSerializer
|
||||
queryset = Token.objects.all()
|
||||
|
||||
@action(detail=False)
|
||||
def washing(self, request):
|
||||
"""
|
||||
Return all washing tokens (api/v1/launderette/token/washing)
|
||||
"""
|
||||
"""Return all washing tokens (api/v1/launderette/token/washing)."""
|
||||
self.queryset = self.queryset.filter(type="WASHING")
|
||||
serializer = self.get_serializer(self.queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False)
|
||||
def drying(self, request):
|
||||
"""
|
||||
Return all drying tokens (api/v1/launderette/token/drying)
|
||||
"""
|
||||
"""Return all drying tokens (api/v1/launderette/token/drying)."""
|
||||
self.queryset = self.queryset.filter(type="DRYING")
|
||||
serializer = self.get_serializer(self.queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False)
|
||||
def avaliable(self, request):
|
||||
"""
|
||||
Return all avaliable tokens (api/v1/launderette/token/avaliable)
|
||||
"""
|
||||
"""Return all avaliable tokens (api/v1/launderette/token/avaliable)."""
|
||||
self.queryset = self.queryset.filter(
|
||||
borrow_date__isnull=True, user__isnull=True
|
||||
)
|
||||
@ -116,9 +104,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
|
||||
|
||||
@action(detail=False)
|
||||
def unavaliable(self, request):
|
||||
"""
|
||||
Return all unavaliable tokens (api/v1/launderette/token/unavaliable)
|
||||
"""
|
||||
"""Return all unavaliable tokens (api/v1/launderette/token/unavaliable)."""
|
||||
self.queryset = self.queryset.filter(
|
||||
borrow_date__isnull=False, user__isnull=False
|
||||
)
|
||||
|
@ -39,9 +39,9 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class UserViewSet(RightModelViewSet):
|
||||
"""
|
||||
Manage Users (api/v1/user/)
|
||||
Only show active users
|
||||
"""Manage Users (api/v1/user/).
|
||||
|
||||
Only show active users.
|
||||
"""
|
||||
|
||||
serializer_class = UserSerializer
|
||||
@ -49,9 +49,7 @@ class UserViewSet(RightModelViewSet):
|
||||
|
||||
@action(detail=False)
|
||||
def birthday(self, request):
|
||||
"""
|
||||
Return all users born today (api/v1/user/birstdays)
|
||||
"""
|
||||
"""Return all users born today (api/v1/user/birstdays)."""
|
||||
date = datetime.datetime.today()
|
||||
self.queryset = self.queryset.filter(date_of_birth=date)
|
||||
serializer = self.get_serializer(self.queryset, many=True)
|
||||
|
@ -28,10 +28,10 @@ def uv_endpoint(request):
|
||||
return Response(make_clean_uv(short_uv, full_uv))
|
||||
|
||||
|
||||
def find_uv(lang, year, code):
|
||||
"""
|
||||
Uses the UTBM API to find an UV.
|
||||
short_uv is the UV entry in the UV list. It is returned as it contains
|
||||
def find_uv(lang: str, year: int | str, code: str) -> tuple[dict | None, dict | None]:
|
||||
"""Uses the UTBM API to find an UV.
|
||||
|
||||
Short_uv is the UV entry in the UV list. It is returned as it contains
|
||||
information which are not in full_uv.
|
||||
full_uv is the detailed representation of an UV.
|
||||
"""
|
||||
@ -44,7 +44,7 @@ def find_uv(lang, year, code):
|
||||
# find the first UV which matches the code
|
||||
short_uv = next(uv for uv in uvs if uv["code"] == code)
|
||||
except StopIteration:
|
||||
return (None, None)
|
||||
return None, None
|
||||
|
||||
# get detailed information about the UV
|
||||
uv_url = settings.SITH_PEDAGOGY_UTBM_API + "/uv/{}/{}/{}/{}".format(
|
||||
@ -53,13 +53,11 @@ def find_uv(lang, year, code):
|
||||
response = urllib.request.urlopen(uv_url)
|
||||
full_uv = json.loads(response.read().decode("utf-8"))
|
||||
|
||||
return (short_uv, full_uv)
|
||||
return short_uv, full_uv
|
||||
|
||||
|
||||
def make_clean_uv(short_uv, full_uv):
|
||||
"""
|
||||
Cleans the data up so that it corresponds to our data representation.
|
||||
"""
|
||||
def make_clean_uv(short_uv: dict, full_uv: dict):
|
||||
"""Cleans the data up so that it corresponds to our data representation."""
|
||||
res = {}
|
||||
|
||||
res["credit_type"] = short_uv["codeCategorie"]
|
||||
|
@ -44,9 +44,7 @@ class ClubEditForm(forms.ModelForm):
|
||||
|
||||
|
||||
class MailingForm(forms.Form):
|
||||
"""
|
||||
Form handling mailing lists right
|
||||
"""
|
||||
"""Form handling mailing lists right."""
|
||||
|
||||
ACTION_NEW_MAILING = 1
|
||||
ACTION_NEW_SUBSCRIPTION = 2
|
||||
@ -105,16 +103,12 @@ class MailingForm(forms.Form):
|
||||
)
|
||||
|
||||
def check_required(self, cleaned_data, field):
|
||||
"""
|
||||
If the given field doesn't exist or has no value, add a required error on it
|
||||
"""
|
||||
"""If the given field doesn't exist or has no value, add a required error on it."""
|
||||
if not cleaned_data.get(field, None):
|
||||
self.add_error(field, _("This field is required"))
|
||||
|
||||
def clean_subscription_users(self):
|
||||
"""
|
||||
Convert given users into real users and check their validity
|
||||
"""
|
||||
"""Convert given users into real users and check their validity."""
|
||||
cleaned_data = super().clean()
|
||||
users = []
|
||||
for user in cleaned_data["subscription_users"]:
|
||||
@ -177,9 +171,7 @@ class SellingsForm(forms.Form):
|
||||
|
||||
|
||||
class ClubMemberForm(forms.Form):
|
||||
"""
|
||||
Form handling the members of a club
|
||||
"""
|
||||
"""Form handling the members of a club."""
|
||||
|
||||
error_css_class = "error"
|
||||
required_css_class = "required"
|
||||
@ -236,9 +228,9 @@ class ClubMemberForm(forms.Form):
|
||||
self.fields.pop("start_date")
|
||||
|
||||
def clean_users(self):
|
||||
"""
|
||||
Check that the user is not trying to add an user already in the club
|
||||
Also check that the user is valid and has a valid subscription
|
||||
"""Check that the user is not trying to add an user already in the club.
|
||||
|
||||
Also check that the user is valid and has a valid subscription.
|
||||
"""
|
||||
cleaned_data = super().clean()
|
||||
users = []
|
||||
@ -260,9 +252,7 @@ class ClubMemberForm(forms.Form):
|
||||
return users
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Check user rights for adding an user
|
||||
"""
|
||||
"""Check user rights for adding an user."""
|
||||
cleaned_data = super().clean()
|
||||
|
||||
if "start_date" in cleaned_data and not cleaned_data["start_date"]:
|
||||
|
@ -21,7 +21,7 @@
|
||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#
|
||||
#
|
||||
from typing import Optional
|
||||
from __future__ import annotations
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import validators
|
||||
@ -46,9 +46,7 @@ def get_default_owner_group():
|
||||
|
||||
|
||||
class Club(models.Model):
|
||||
"""
|
||||
The Club class, made as a tree to allow nice tidy organization
|
||||
"""
|
||||
"""The Club class, made as a tree to allow nice tidy organization."""
|
||||
|
||||
id = models.AutoField(primary_key=True, db_index=True)
|
||||
name = models.CharField(_("name"), max_length=64)
|
||||
@ -141,7 +139,7 @@ class Club(models.Model):
|
||||
).first()
|
||||
|
||||
def check_loop(self):
|
||||
"""Raise a validation error when a loop is found within the parent list"""
|
||||
"""Raise a validation error when a loop is found within the parent list."""
|
||||
objs = []
|
||||
cur = self
|
||||
while cur.parent is not None:
|
||||
@ -223,9 +221,7 @@ class Club(models.Model):
|
||||
return self.name
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be super edited by the given user
|
||||
"""
|
||||
"""Method to see if that object can be super edited by the given user."""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
return user.is_board_member
|
||||
@ -234,24 +230,21 @@ class Club(models.Model):
|
||||
return "https://%s%s" % (settings.SITH_URL, self.logo.url)
|
||||
|
||||
def can_be_edited_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
"""Method to see if that object can be edited by the given user."""
|
||||
return self.has_rights_in_club(user)
|
||||
|
||||
def can_be_viewed_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be seen by the given user
|
||||
"""
|
||||
"""Method to see if that object can be seen by the given user."""
|
||||
sub = User.objects.filter(pk=user.pk).first()
|
||||
if sub is None:
|
||||
return False
|
||||
return sub.was_subscribed
|
||||
|
||||
def get_membership_for(self, user: User) -> Optional["Membership"]:
|
||||
"""
|
||||
Return the current membership the given user.
|
||||
The result is cached.
|
||||
def get_membership_for(self, user: User) -> Membership | None:
|
||||
"""Return the current membership the given user.
|
||||
|
||||
Note:
|
||||
The result is cached.
|
||||
"""
|
||||
if user.is_anonymous:
|
||||
return None
|
||||
@ -273,15 +266,12 @@ class Club(models.Model):
|
||||
|
||||
class MembershipQuerySet(models.QuerySet):
|
||||
def ongoing(self) -> "MembershipQuerySet":
|
||||
"""
|
||||
Filter all memberships which are not finished yet
|
||||
"""
|
||||
"""Filter all memberships which are not finished yet."""
|
||||
# noinspection PyTypeChecker
|
||||
return self.filter(Q(end_date=None) | Q(end_date__gte=timezone.now()))
|
||||
|
||||
def board(self) -> "MembershipQuerySet":
|
||||
"""
|
||||
Filter all memberships where the user is/was in the board.
|
||||
"""Filter all memberships where the user is/was in the board.
|
||||
|
||||
Be aware that users who were in the board in the past
|
||||
are included, even if there are no more members.
|
||||
@ -293,9 +283,9 @@ class MembershipQuerySet(models.QuerySet):
|
||||
return self.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE)
|
||||
|
||||
def update(self, **kwargs):
|
||||
"""
|
||||
Work just like the default Django's update() method,
|
||||
but add a cache refresh for the elements of the queryset.
|
||||
"""Refresh the cache for the elements of the queryset.
|
||||
|
||||
Besides that, does the same job as a regular update method.
|
||||
|
||||
Be aware that this adds a db query to retrieve the updated objects
|
||||
"""
|
||||
@ -315,8 +305,7 @@ class MembershipQuerySet(models.QuerySet):
|
||||
)
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Work just like the default Django's delete() method,
|
||||
"""Work just like the default Django's delete() method,
|
||||
but add a cache invalidation for the elements of the queryset
|
||||
before the deletion.
|
||||
|
||||
@ -332,8 +321,7 @@ class MembershipQuerySet(models.QuerySet):
|
||||
|
||||
|
||||
class Membership(models.Model):
|
||||
"""
|
||||
The Membership class makes the connection between User and Clubs
|
||||
"""The Membership class makes the connection between User and Clubs.
|
||||
|
||||
Both Users and Clubs can have many Membership objects:
|
||||
- a user can be a member of many clubs at a time
|
||||
@ -390,17 +378,13 @@ class Membership(models.Model):
|
||||
return reverse("club:club_members", kwargs={"club_id": self.club_id})
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be super edited by the given user
|
||||
"""
|
||||
"""Method to see if that object can be super edited by the given user."""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
return user.is_board_member
|
||||
|
||||
def can_be_edited_by(self, user: User) -> bool:
|
||||
"""
|
||||
Check if that object can be edited by the given user
|
||||
"""
|
||||
"""Check if that object can be edited by the given user."""
|
||||
if user.is_root or user.is_board_member:
|
||||
return True
|
||||
membership = self.club.get_membership_for(user)
|
||||
@ -414,9 +398,10 @@ class Membership(models.Model):
|
||||
|
||||
|
||||
class Mailing(models.Model):
|
||||
"""
|
||||
This class correspond to a mailing list
|
||||
Remember that mailing lists should be validated by UTBM
|
||||
"""A Mailing list for a club.
|
||||
|
||||
Warning:
|
||||
Remember that mailing lists should be validated by UTBM.
|
||||
"""
|
||||
|
||||
club = models.ForeignKey(
|
||||
@ -508,9 +493,7 @@ class Mailing(models.Model):
|
||||
|
||||
|
||||
class MailingSubscription(models.Model):
|
||||
"""
|
||||
This class makes the link between user and mailing list
|
||||
"""
|
||||
"""Link between user and mailing list."""
|
||||
|
||||
mailing = models.ForeignKey(
|
||||
Mailing,
|
||||
|
126
club/tests.py
126
club/tests.py
@ -29,8 +29,8 @@ from sith.settings import SITH_BAR_MANAGER, SITH_MAIN_CLUB_ID
|
||||
|
||||
|
||||
class ClubTest(TestCase):
|
||||
"""
|
||||
Set up data for test cases related to clubs and membership
|
||||
"""Set up data for test cases related to clubs and membership.
|
||||
|
||||
The generated dataset is the one created by the populate command,
|
||||
plus the following modifications :
|
||||
|
||||
@ -94,8 +94,7 @@ class ClubTest(TestCase):
|
||||
|
||||
class MembershipQuerySetTest(ClubTest):
|
||||
def test_ongoing(self):
|
||||
"""
|
||||
Test that the ongoing queryset method returns the memberships that
|
||||
"""Test that the ongoing queryset method returns the memberships that
|
||||
are not ended.
|
||||
"""
|
||||
current_members = list(self.club.members.ongoing().order_by("id"))
|
||||
@ -108,9 +107,8 @@ class MembershipQuerySetTest(ClubTest):
|
||||
assert current_members == expected
|
||||
|
||||
def test_board(self):
|
||||
"""
|
||||
Test that the board queryset method returns the memberships
|
||||
of user in the club board
|
||||
"""Test that the board queryset method returns the memberships
|
||||
of user in the club board.
|
||||
"""
|
||||
board_members = list(self.club.members.board().order_by("id"))
|
||||
expected = [
|
||||
@ -123,9 +121,8 @@ class MembershipQuerySetTest(ClubTest):
|
||||
assert board_members == expected
|
||||
|
||||
def test_ongoing_board(self):
|
||||
"""
|
||||
Test that combining ongoing and board returns users
|
||||
who are currently board members of the club
|
||||
"""Test that combining ongoing and board returns users
|
||||
who are currently board members of the club.
|
||||
"""
|
||||
members = list(self.club.members.ongoing().board().order_by("id"))
|
||||
expected = [
|
||||
@ -136,9 +133,7 @@ class MembershipQuerySetTest(ClubTest):
|
||||
assert members == expected
|
||||
|
||||
def test_update_invalidate_cache(self):
|
||||
"""
|
||||
Test that the `update` queryset method properly invalidate cache
|
||||
"""
|
||||
"""Test that the `update` queryset method properly invalidate cache."""
|
||||
mem_skia = self.skia.memberships.get(club=self.club)
|
||||
cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia)
|
||||
self.skia.memberships.update(end_date=localtime(now()).date())
|
||||
@ -157,10 +152,7 @@ class MembershipQuerySetTest(ClubTest):
|
||||
assert new_mem.role == 5
|
||||
|
||||
def test_delete_invalidate_cache(self):
|
||||
"""
|
||||
Test that the `delete` queryset properly invalidate cache
|
||||
"""
|
||||
|
||||
"""Test that the `delete` queryset properly invalidate cache."""
|
||||
mem_skia = self.skia.memberships.get(club=self.club)
|
||||
mem_comptable = self.comptable.memberships.get(club=self.club)
|
||||
cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia)
|
||||
@ -180,9 +172,7 @@ class MembershipQuerySetTest(ClubTest):
|
||||
|
||||
class ClubModelTest(ClubTest):
|
||||
def assert_membership_started_today(self, user: User, role: int):
|
||||
"""
|
||||
Assert that the given membership is active and started today
|
||||
"""
|
||||
"""Assert that the given membership is active and started today."""
|
||||
membership = user.memberships.ongoing().filter(club=self.club).first()
|
||||
assert membership is not None
|
||||
assert localtime(now()).date() == membership.start_date
|
||||
@ -195,17 +185,14 @@ class ClubModelTest(ClubTest):
|
||||
assert user.is_in_group(name=board_group)
|
||||
|
||||
def assert_membership_ended_today(self, user: User):
|
||||
"""
|
||||
Assert that the given user have a membership which ended today
|
||||
"""
|
||||
"""Assert that the given user have a membership which ended today."""
|
||||
today = localtime(now()).date()
|
||||
assert user.memberships.filter(club=self.club, end_date=today).exists()
|
||||
assert self.club.get_membership_for(user) is None
|
||||
|
||||
def test_access_unauthorized(self):
|
||||
"""
|
||||
Test that users who never subscribed and anonymous users
|
||||
cannot see the page
|
||||
"""Test that users who never subscribed and anonymous users
|
||||
cannot see the page.
|
||||
"""
|
||||
response = self.client.post(self.members_url)
|
||||
assert response.status_code == 403
|
||||
@ -215,8 +202,7 @@ class ClubModelTest(ClubTest):
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_display(self):
|
||||
"""
|
||||
Test that a GET request return a page where the requested
|
||||
"""Test that a GET request return a page where the requested
|
||||
information are displayed.
|
||||
"""
|
||||
self.client.force_login(self.skia)
|
||||
@ -251,9 +237,7 @@ class ClubModelTest(ClubTest):
|
||||
self.assertInHTML(expected_html, response.content.decode())
|
||||
|
||||
def test_root_add_one_club_member(self):
|
||||
"""
|
||||
Test that root users can add members to clubs, one at a time
|
||||
"""
|
||||
"""Test that root users can add members to clubs, one at a time."""
|
||||
self.client.force_login(self.root)
|
||||
response = self.client.post(
|
||||
self.members_url,
|
||||
@ -264,9 +248,7 @@ class ClubModelTest(ClubTest):
|
||||
self.assert_membership_started_today(self.subscriber, role=3)
|
||||
|
||||
def test_root_add_multiple_club_member(self):
|
||||
"""
|
||||
Test that root users can add multiple members at once to clubs
|
||||
"""
|
||||
"""Test that root users can add multiple members at once to clubs."""
|
||||
self.client.force_login(self.root)
|
||||
response = self.client.post(
|
||||
self.members_url,
|
||||
@ -281,8 +263,7 @@ class ClubModelTest(ClubTest):
|
||||
self.assert_membership_started_today(self.krophil, role=3)
|
||||
|
||||
def test_add_unauthorized_members(self):
|
||||
"""
|
||||
Test that users who are not currently subscribed
|
||||
"""Test that users who are not currently subscribed
|
||||
cannot be members of clubs.
|
||||
"""
|
||||
self.client.force_login(self.root)
|
||||
@ -302,9 +283,8 @@ class ClubModelTest(ClubTest):
|
||||
assert '<ul class="errorlist"><li>' in response.content.decode()
|
||||
|
||||
def test_add_members_already_members(self):
|
||||
"""
|
||||
Test that users who are already members of a club
|
||||
cannot be added again to this club
|
||||
"""Test that users who are already members of a club
|
||||
cannot be added again to this club.
|
||||
"""
|
||||
self.client.force_login(self.root)
|
||||
current_membership = self.skia.memberships.ongoing().get(club=self.club)
|
||||
@ -320,8 +300,7 @@ class ClubModelTest(ClubTest):
|
||||
assert self.club.get_membership_for(self.skia) == new_membership
|
||||
|
||||
def test_add_not_existing_users(self):
|
||||
"""
|
||||
Test that not existing users cannot be added in clubs.
|
||||
"""Test that not existing users cannot be added in clubs.
|
||||
If one user in the request is invalid, no membership creation at all
|
||||
can take place.
|
||||
"""
|
||||
@ -349,9 +328,7 @@ class ClubModelTest(ClubTest):
|
||||
assert self.club.members.count() == nb_memberships
|
||||
|
||||
def test_president_add_members(self):
|
||||
"""
|
||||
Test that the president of the club can add members
|
||||
"""
|
||||
"""Test that the president of the club can add members."""
|
||||
president = self.club.members.get(role=10).user
|
||||
nb_club_membership = self.club.members.count()
|
||||
nb_subscriber_memberships = self.subscriber.memberships.count()
|
||||
@ -368,8 +345,7 @@ class ClubModelTest(ClubTest):
|
||||
self.assert_membership_started_today(self.subscriber, role=9)
|
||||
|
||||
def test_add_member_greater_role(self):
|
||||
"""
|
||||
Test that a member of the club member cannot create
|
||||
"""Test that a member of the club member cannot create
|
||||
a membership with a greater role than its own.
|
||||
"""
|
||||
self.client.force_login(self.skia)
|
||||
@ -388,9 +364,7 @@ class ClubModelTest(ClubTest):
|
||||
assert not self.subscriber.memberships.filter(club=self.club).exists()
|
||||
|
||||
def test_add_member_without_role(self):
|
||||
"""
|
||||
Test that trying to add members without specifying their role fails
|
||||
"""
|
||||
"""Test that trying to add members without specifying their role fails."""
|
||||
self.client.force_login(self.root)
|
||||
response = self.client.post(
|
||||
self.members_url,
|
||||
@ -402,9 +376,7 @@ class ClubModelTest(ClubTest):
|
||||
)
|
||||
|
||||
def test_end_membership_self(self):
|
||||
"""
|
||||
Test that a member can end its own membership
|
||||
"""
|
||||
"""Test that a member can end its own membership."""
|
||||
self.client.force_login(self.skia)
|
||||
self.client.post(
|
||||
self.members_url,
|
||||
@ -414,9 +386,8 @@ class ClubModelTest(ClubTest):
|
||||
self.assert_membership_ended_today(self.skia)
|
||||
|
||||
def test_end_membership_lower_role(self):
|
||||
"""
|
||||
Test that board members of the club can end memberships
|
||||
of users with lower roles
|
||||
"""Test that board members of the club can end memberships
|
||||
of users with lower roles.
|
||||
"""
|
||||
# remainder : skia has role 3, comptable has role 10, richard has role 1
|
||||
self.client.force_login(self.skia)
|
||||
@ -429,9 +400,8 @@ class ClubModelTest(ClubTest):
|
||||
self.assert_membership_ended_today(self.richard)
|
||||
|
||||
def test_end_membership_higher_role(self):
|
||||
"""
|
||||
Test that board members of the club cannot end memberships
|
||||
of users with higher roles
|
||||
"""Test that board members of the club cannot end memberships
|
||||
of users with higher roles.
|
||||
"""
|
||||
membership = self.comptable.memberships.filter(club=self.club).first()
|
||||
self.client.force_login(self.skia)
|
||||
@ -448,9 +418,8 @@ class ClubModelTest(ClubTest):
|
||||
assert membership.end_date is None
|
||||
|
||||
def test_end_membership_as_main_club_board(self):
|
||||
"""
|
||||
Test that board members of the main club can end the membership
|
||||
of anyone
|
||||
"""Test that board members of the main club can end the membership
|
||||
of anyone.
|
||||
"""
|
||||
# make subscriber a board member
|
||||
self.subscriber.memberships.all().delete()
|
||||
@ -467,9 +436,7 @@ class ClubModelTest(ClubTest):
|
||||
assert self.club.members.ongoing().count() == nb_memberships - 1
|
||||
|
||||
def test_end_membership_as_root(self):
|
||||
"""
|
||||
Test that root users can end the membership of anyone
|
||||
"""
|
||||
"""Test that root users can end the membership of anyone."""
|
||||
nb_memberships = self.club.members.count()
|
||||
self.client.force_login(self.root)
|
||||
response = self.client.post(
|
||||
@ -482,9 +449,7 @@ class ClubModelTest(ClubTest):
|
||||
assert self.club.members.count() == nb_memberships
|
||||
|
||||
def test_end_membership_as_foreigner(self):
|
||||
"""
|
||||
Test that users who are not in this club cannot end its memberships
|
||||
"""
|
||||
"""Test that users who are not in this club cannot end its memberships."""
|
||||
nb_memberships = self.club.members.count()
|
||||
membership = self.richard.memberships.filter(club=self.club).first()
|
||||
self.client.force_login(self.subscriber)
|
||||
@ -498,9 +463,8 @@ class ClubModelTest(ClubTest):
|
||||
assert membership == new_mem
|
||||
|
||||
def test_delete_remove_from_meta_group(self):
|
||||
"""
|
||||
Test that when a club is deleted, all its members are removed from the
|
||||
associated metagroup
|
||||
"""Test that when a club is deleted, all its members are removed from the
|
||||
associated metagroup.
|
||||
"""
|
||||
memberships = self.club.members.select_related("user")
|
||||
users = [membership.user for membership in memberships]
|
||||
@ -511,9 +475,7 @@ class ClubModelTest(ClubTest):
|
||||
assert not user.is_in_group(name=meta_group)
|
||||
|
||||
def test_add_to_meta_group(self):
|
||||
"""
|
||||
Test that when a membership begins, the user is added to the meta group
|
||||
"""
|
||||
"""Test that when a membership begins, the user is added to the meta group."""
|
||||
group_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX
|
||||
board_members = self.club.unix_name + settings.SITH_BOARD_SUFFIX
|
||||
assert not self.subscriber.is_in_group(name=group_members)
|
||||
@ -523,9 +485,7 @@ class ClubModelTest(ClubTest):
|
||||
assert self.subscriber.is_in_group(name=board_members)
|
||||
|
||||
def test_remove_from_meta_group(self):
|
||||
"""
|
||||
Test that when a membership ends, the user is removed from meta group
|
||||
"""
|
||||
"""Test that when a membership ends, the user is removed from meta group."""
|
||||
group_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX
|
||||
board_members = self.club.unix_name + settings.SITH_BOARD_SUFFIX
|
||||
assert self.comptable.is_in_group(name=group_members)
|
||||
@ -535,9 +495,7 @@ class ClubModelTest(ClubTest):
|
||||
assert not self.comptable.is_in_group(name=board_members)
|
||||
|
||||
def test_club_owner(self):
|
||||
"""
|
||||
Test that a club is owned only by board members of the main club
|
||||
"""
|
||||
"""Test that a club is owned only by board members of the main club."""
|
||||
anonymous = AnonymousUser()
|
||||
assert not self.club.is_owned_by(anonymous)
|
||||
assert not self.club.is_owned_by(self.subscriber)
|
||||
@ -549,7 +507,7 @@ class ClubModelTest(ClubTest):
|
||||
|
||||
|
||||
class MailingFormTest(TestCase):
|
||||
"""Perform validation tests for MailingForm"""
|
||||
"""Perform validation tests for MailingForm."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -865,9 +823,7 @@ class MailingFormTest(TestCase):
|
||||
|
||||
|
||||
class ClubSellingViewTest(TestCase):
|
||||
"""
|
||||
Perform basics tests to ensure that the page is available
|
||||
"""
|
||||
"""Perform basics tests to ensure that the page is available."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -875,9 +831,7 @@ class ClubSellingViewTest(TestCase):
|
||||
cls.skia = User.objects.get(username="skia")
|
||||
|
||||
def test_page_not_internal_error(self):
|
||||
"""
|
||||
Test that the page does not return and internal error
|
||||
"""
|
||||
"""Test that the page does not return and internal error."""
|
||||
self.client.force_login(self.skia)
|
||||
response = self.client.get(
|
||||
reverse("club:club_sellings", kwargs={"club_id": self.ae.id})
|
||||
|
@ -175,18 +175,14 @@ class ClubTabsMixin(TabedViewMixin):
|
||||
|
||||
|
||||
class ClubListView(ListView):
|
||||
"""
|
||||
List the Clubs
|
||||
"""
|
||||
"""List the Clubs."""
|
||||
|
||||
model = Club
|
||||
template_name = "club/club_list.jinja"
|
||||
|
||||
|
||||
class ClubView(ClubTabsMixin, DetailView):
|
||||
"""
|
||||
Front page of a Club
|
||||
"""
|
||||
"""Front page of a Club."""
|
||||
|
||||
model = Club
|
||||
pk_url_kwarg = "club_id"
|
||||
@ -201,9 +197,7 @@ class ClubView(ClubTabsMixin, DetailView):
|
||||
|
||||
|
||||
class ClubRevView(ClubView):
|
||||
"""
|
||||
Display a specific page revision
|
||||
"""
|
||||
"""Display a specific page revision."""
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
@ -235,9 +229,7 @@ class ClubPageEditView(ClubTabsMixin, PageEditViewBase):
|
||||
|
||||
|
||||
class ClubPageHistView(ClubTabsMixin, CanViewMixin, DetailView):
|
||||
"""
|
||||
Modification hostory of the page
|
||||
"""
|
||||
"""Modification hostory of the page."""
|
||||
|
||||
model = Club
|
||||
pk_url_kwarg = "club_id"
|
||||
@ -246,9 +238,7 @@ class ClubPageHistView(ClubTabsMixin, CanViewMixin, DetailView):
|
||||
|
||||
|
||||
class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView):
|
||||
"""
|
||||
Tools page of a Club
|
||||
"""
|
||||
"""Tools page of a Club."""
|
||||
|
||||
model = Club
|
||||
pk_url_kwarg = "club_id"
|
||||
@ -257,9 +247,7 @@ class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView):
|
||||
|
||||
|
||||
class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
|
||||
"""
|
||||
View of a club's members
|
||||
"""
|
||||
"""View of a club's members."""
|
||||
|
||||
model = Club
|
||||
pk_url_kwarg = "club_id"
|
||||
@ -280,9 +268,7 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
Check user rights
|
||||
"""
|
||||
"""Check user rights."""
|
||||
resp = super().form_valid(form)
|
||||
|
||||
data = form.clean()
|
||||
@ -307,9 +293,7 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
|
||||
|
||||
|
||||
class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView):
|
||||
"""
|
||||
Old members of a club
|
||||
"""
|
||||
"""Old members of a club."""
|
||||
|
||||
model = Club
|
||||
pk_url_kwarg = "club_id"
|
||||
@ -318,9 +302,7 @@ class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView):
|
||||
|
||||
|
||||
class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
||||
"""
|
||||
Sellings of a club
|
||||
"""
|
||||
"""Sellings of a club."""
|
||||
|
||||
model = Club
|
||||
pk_url_kwarg = "club_id"
|
||||
@ -396,12 +378,10 @@ class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
||||
|
||||
|
||||
class ClubSellingCSVView(ClubSellingView):
|
||||
"""
|
||||
Generate sellings in csv for a given period
|
||||
"""
|
||||
"""Generate sellings in csv for a given period."""
|
||||
|
||||
class StreamWriter:
|
||||
"""Implements a file-like interface for streaming the CSV"""
|
||||
"""Implements a file-like interface for streaming the CSV."""
|
||||
|
||||
def write(self, value):
|
||||
"""Write the value by returning it, instead of storing in a buffer."""
|
||||
@ -475,9 +455,7 @@ class ClubSellingCSVView(ClubSellingView):
|
||||
|
||||
|
||||
class ClubEditView(ClubTabsMixin, CanEditMixin, UpdateView):
|
||||
"""
|
||||
Edit a Club's main informations (for the club's members)
|
||||
"""
|
||||
"""Edit a Club's main informations (for the club's members)."""
|
||||
|
||||
model = Club
|
||||
pk_url_kwarg = "club_id"
|
||||
@ -487,9 +465,7 @@ class ClubEditView(ClubTabsMixin, CanEditMixin, UpdateView):
|
||||
|
||||
|
||||
class ClubEditPropView(ClubTabsMixin, CanEditPropMixin, UpdateView):
|
||||
"""
|
||||
Edit the properties of a Club object (for the Sith admins)
|
||||
"""
|
||||
"""Edit the properties of a Club object (for the Sith admins)."""
|
||||
|
||||
model = Club
|
||||
pk_url_kwarg = "club_id"
|
||||
@ -499,9 +475,7 @@ class ClubEditPropView(ClubTabsMixin, CanEditPropMixin, UpdateView):
|
||||
|
||||
|
||||
class ClubCreateView(CanCreateMixin, CreateView):
|
||||
"""
|
||||
Create a club (for the Sith admin)
|
||||
"""
|
||||
"""Create a club (for the Sith admin)."""
|
||||
|
||||
model = Club
|
||||
pk_url_kwarg = "club_id"
|
||||
@ -510,9 +484,7 @@ class ClubCreateView(CanCreateMixin, CreateView):
|
||||
|
||||
|
||||
class MembershipSetOldView(CanEditMixin, DetailView):
|
||||
"""
|
||||
Set a membership as beeing old
|
||||
"""
|
||||
"""Set a membership as beeing old."""
|
||||
|
||||
model = Membership
|
||||
pk_url_kwarg = "membership_id"
|
||||
@ -541,9 +513,7 @@ class MembershipSetOldView(CanEditMixin, DetailView):
|
||||
|
||||
|
||||
class MembershipDeleteView(UserIsRootMixin, DeleteView):
|
||||
"""
|
||||
Delete a membership (for admins only)
|
||||
"""
|
||||
"""Delete a membership (for admins only)."""
|
||||
|
||||
model = Membership
|
||||
pk_url_kwarg = "membership_id"
|
||||
@ -563,9 +533,7 @@ class ClubStatView(TemplateView):
|
||||
|
||||
|
||||
class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
||||
"""
|
||||
A list of mailing for a given club
|
||||
"""
|
||||
"""A list of mailing for a given club."""
|
||||
|
||||
model = Club
|
||||
form_class = MailingForm
|
||||
@ -603,9 +571,7 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
||||
return kwargs
|
||||
|
||||
def add_new_mailing(self, cleaned_data) -> ValidationError | None:
|
||||
"""
|
||||
Create a new mailing list from the form
|
||||
"""
|
||||
"""Create a new mailing list from the form."""
|
||||
mailing = Mailing(
|
||||
club=self.get_object(),
|
||||
email=cleaned_data["mailing_email"],
|
||||
@ -620,9 +586,7 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
||||
return None
|
||||
|
||||
def add_new_subscription(self, cleaned_data) -> ValidationError | None:
|
||||
"""
|
||||
Add mailing subscriptions for each user given and/or for the specified email in form
|
||||
"""
|
||||
"""Add mailing subscriptions for each user given and/or for the specified email in form."""
|
||||
users_to_save = []
|
||||
|
||||
for user in cleaned_data["subscription_users"]:
|
||||
@ -656,9 +620,7 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
||||
return None
|
||||
|
||||
def remove_subscription(self, cleaned_data):
|
||||
"""
|
||||
Remove specified users from a mailing list
|
||||
"""
|
||||
"""Remove specified users from a mailing list."""
|
||||
fields = [
|
||||
cleaned_data[key]
|
||||
for key in cleaned_data.keys()
|
||||
@ -742,7 +704,7 @@ class MailingAutoGenerationView(View):
|
||||
|
||||
|
||||
class PosterListView(ClubTabsMixin, PosterListBaseView, CanViewMixin):
|
||||
"""List communication posters"""
|
||||
"""List communication posters."""
|
||||
|
||||
def get_object(self):
|
||||
return self.club
|
||||
@ -755,7 +717,7 @@ class PosterListView(ClubTabsMixin, PosterListBaseView, CanViewMixin):
|
||||
|
||||
|
||||
class PosterCreateView(PosterCreateBaseView, CanCreateMixin):
|
||||
"""Create communication poster"""
|
||||
"""Create communication poster."""
|
||||
|
||||
pk_url_kwarg = "club_id"
|
||||
|
||||
@ -770,7 +732,7 @@ class PosterCreateView(PosterCreateBaseView, CanCreateMixin):
|
||||
|
||||
|
||||
class PosterEditView(ClubTabsMixin, PosterEditBaseView, CanEditMixin):
|
||||
"""Edit communication poster"""
|
||||
"""Edit communication poster."""
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id})
|
||||
@ -782,7 +744,7 @@ class PosterEditView(ClubTabsMixin, PosterEditBaseView, CanEditMixin):
|
||||
|
||||
|
||||
class PosterDeleteView(PosterDeleteBaseView, ClubTabsMixin, CanEditMixin):
|
||||
"""Delete communication poster"""
|
||||
"""Delete communication poster."""
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id})
|
||||
|
@ -39,7 +39,7 @@ from core.models import Notification, Preferences, RealGroup, User
|
||||
|
||||
|
||||
class Sith(models.Model):
|
||||
"""A one instance class storing all the modifiable infos"""
|
||||
"""A one instance class storing all the modifiable infos."""
|
||||
|
||||
alert_msg = models.TextField(_("alert message"), default="", blank=True)
|
||||
info_msg = models.TextField(_("info message"), default="", blank=True)
|
||||
@ -64,7 +64,7 @@ NEWS_TYPES = [
|
||||
|
||||
|
||||
class News(models.Model):
|
||||
"""The news class"""
|
||||
"""The news class."""
|
||||
|
||||
title = models.CharField(_("title"), max_length=64)
|
||||
summary = models.TextField(_("summary"))
|
||||
@ -143,8 +143,7 @@ def news_notification_callback(notif):
|
||||
|
||||
|
||||
class NewsDate(models.Model):
|
||||
"""
|
||||
A date class, useful for weekly events, or for events that just have no date
|
||||
"""A date class, useful for weekly events, or for events that just have no date.
|
||||
|
||||
This class allows more flexibilty managing the dates related to a news, particularly when this news is weekly, since
|
||||
we don't have to make copies
|
||||
@ -164,8 +163,7 @@ class NewsDate(models.Model):
|
||||
|
||||
|
||||
class Weekmail(models.Model):
|
||||
"""
|
||||
The weekmail class
|
||||
"""The weekmail class.
|
||||
|
||||
:ivar title: Title of the weekmail
|
||||
:ivar intro: Introduction of the weekmail
|
||||
@ -189,8 +187,8 @@ class Weekmail(models.Model):
|
||||
return f"Weekmail {self.id} (sent: {self.sent}) - {self.title}"
|
||||
|
||||
def send(self):
|
||||
"""
|
||||
Send the weekmail to all users with the receive weekmail option opt-in.
|
||||
"""Send the weekmail to all users with the receive weekmail option opt-in.
|
||||
|
||||
Also send the weekmail to the mailing list in settings.SITH_COM_EMAIL.
|
||||
"""
|
||||
dest = [
|
||||
@ -214,33 +212,25 @@ class Weekmail(models.Model):
|
||||
Weekmail().save()
|
||||
|
||||
def render_text(self):
|
||||
"""
|
||||
Renders a pure text version of the mail for readers without HTML support.
|
||||
"""
|
||||
"""Renders a pure text version of the mail for readers without HTML support."""
|
||||
return render(
|
||||
None, "com/weekmail_renderer_text.jinja", context={"weekmail": self}
|
||||
).content.decode("utf-8")
|
||||
|
||||
def render_html(self):
|
||||
"""
|
||||
Renders an HTML version of the mail with images and fancy CSS.
|
||||
"""
|
||||
"""Renders an HTML version of the mail with images and fancy CSS."""
|
||||
return render(
|
||||
None, "com/weekmail_renderer_html.jinja", context={"weekmail": self}
|
||||
).content.decode("utf-8")
|
||||
|
||||
def get_banner(self):
|
||||
"""
|
||||
Return an absolute link to the banner.
|
||||
"""
|
||||
"""Return an absolute link to the banner."""
|
||||
return (
|
||||
"http://" + settings.SITH_URL + static("com/img/weekmail_bannerV2P22.png")
|
||||
)
|
||||
|
||||
def get_footer(self):
|
||||
"""
|
||||
Return an absolute link to the footer.
|
||||
"""
|
||||
"""Return an absolute link to the footer."""
|
||||
return "http://" + settings.SITH_URL + static("com/img/weekmail_footerP22.png")
|
||||
|
||||
def is_owned_by(self, user):
|
||||
|
28
com/tests.py
28
com/tests.py
@ -115,10 +115,7 @@ class ComTest(TestCase):
|
||||
|
||||
class SithTest(TestCase):
|
||||
def test_sith_owner(self):
|
||||
"""
|
||||
Test that the sith instance is owned by com admins
|
||||
and nobody else
|
||||
"""
|
||||
"""Test that the sith instance is owned by com admins and nobody else."""
|
||||
sith: Sith = Sith.objects.first()
|
||||
|
||||
com_admin = User.objects.get(username="comunity")
|
||||
@ -148,20 +145,17 @@ class NewsTest(TestCase):
|
||||
cls.anonymous = AnonymousUser()
|
||||
|
||||
def test_news_owner(self):
|
||||
"""Test that news are owned by com admins
|
||||
or by their author but nobody else.
|
||||
"""
|
||||
Test that news are owned by com admins
|
||||
or by their author but nobody else
|
||||
"""
|
||||
|
||||
assert self.new.is_owned_by(self.com_admin)
|
||||
assert self.new.is_owned_by(self.author)
|
||||
assert not self.new.is_owned_by(self.anonymous)
|
||||
assert not self.new.is_owned_by(self.sli)
|
||||
|
||||
def test_news_viewer(self):
|
||||
"""
|
||||
Test that moderated news can be viewed by anyone
|
||||
and not moderated news only by com admins
|
||||
"""Test that moderated news can be viewed by anyone
|
||||
and not moderated news only by com admins.
|
||||
"""
|
||||
# by default a news isn't moderated
|
||||
assert self.new.can_be_viewed_by(self.com_admin)
|
||||
@ -177,9 +171,7 @@ class NewsTest(TestCase):
|
||||
assert self.new.can_be_viewed_by(self.author)
|
||||
|
||||
def test_news_editor(self):
|
||||
"""
|
||||
Test that only com admins can edit news
|
||||
"""
|
||||
"""Test that only com admins can edit news."""
|
||||
assert self.new.can_be_edited_by(self.com_admin)
|
||||
assert not self.new.can_be_edited_by(self.sli)
|
||||
assert not self.new.can_be_edited_by(self.anonymous)
|
||||
@ -203,9 +195,7 @@ class WeekmailArticleTest(TestCase):
|
||||
cls.anonymous = AnonymousUser()
|
||||
|
||||
def test_weekmail_owner(self):
|
||||
"""
|
||||
Test that weekmails are owned only by com admins
|
||||
"""
|
||||
"""Test that weekmails are owned only by com admins."""
|
||||
assert self.article.is_owned_by(self.com_admin)
|
||||
assert not self.article.is_owned_by(self.author)
|
||||
assert not self.article.is_owned_by(self.anonymous)
|
||||
@ -229,9 +219,7 @@ class PosterTest(TestCase):
|
||||
cls.anonymous = AnonymousUser()
|
||||
|
||||
def test_poster_owner(self):
|
||||
"""
|
||||
Test that poster are owned by com admins and board members in clubs
|
||||
"""
|
||||
"""Test that poster are owned by com admins and board members in clubs."""
|
||||
assert self.poster.is_owned_by(self.com_admin)
|
||||
assert not self.poster.is_owned_by(self.anonymous)
|
||||
|
||||
|
40
com/views.py
40
com/views.py
@ -427,7 +427,7 @@ class WeekmailPreviewView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, Detai
|
||||
return self.model.objects.filter(sent=False).order_by("-id").first()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add rendered weekmail"""
|
||||
"""Add rendered weekmail."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["weekmail_rendered"] = self.object.render_html()
|
||||
kwargs["bad_recipients"] = self.bad_recipients
|
||||
@ -507,7 +507,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add orphan articles"""
|
||||
"""Add orphan articles."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["orphans"] = WeekmailArticle.objects.filter(weekmail=None)
|
||||
return kwargs
|
||||
@ -516,7 +516,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
|
||||
class WeekmailArticleEditView(
|
||||
ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateView
|
||||
):
|
||||
"""Edit an article"""
|
||||
"""Edit an article."""
|
||||
|
||||
model = WeekmailArticle
|
||||
form_class = modelform_factory(
|
||||
@ -532,7 +532,7 @@ class WeekmailArticleEditView(
|
||||
|
||||
|
||||
class WeekmailArticleCreateView(QuickNotifMixin, CreateView):
|
||||
"""Post an article"""
|
||||
"""Post an article."""
|
||||
|
||||
model = WeekmailArticle
|
||||
form_class = modelform_factory(
|
||||
@ -574,7 +574,7 @@ class WeekmailArticleCreateView(QuickNotifMixin, CreateView):
|
||||
|
||||
|
||||
class WeekmailArticleDeleteView(CanEditPropMixin, DeleteView):
|
||||
"""Delete an article"""
|
||||
"""Delete an article."""
|
||||
|
||||
model = WeekmailArticle
|
||||
template_name = "core/delete_confirm.jinja"
|
||||
@ -614,7 +614,7 @@ class MailingModerateView(View):
|
||||
|
||||
|
||||
class PosterListBaseView(ListView):
|
||||
"""List communication posters"""
|
||||
"""List communication posters."""
|
||||
|
||||
current_tab = "posters"
|
||||
model = Poster
|
||||
@ -641,7 +641,7 @@ class PosterListBaseView(ListView):
|
||||
|
||||
|
||||
class PosterCreateBaseView(CreateView):
|
||||
"""Create communication poster"""
|
||||
"""Create communication poster."""
|
||||
|
||||
current_tab = "posters"
|
||||
form_class = PosterForm
|
||||
@ -673,7 +673,7 @@ class PosterCreateBaseView(CreateView):
|
||||
|
||||
|
||||
class PosterEditBaseView(UpdateView):
|
||||
"""Edit communication poster"""
|
||||
"""Edit communication poster."""
|
||||
|
||||
pk_url_kwarg = "poster_id"
|
||||
current_tab = "posters"
|
||||
@ -721,7 +721,7 @@ class PosterEditBaseView(UpdateView):
|
||||
|
||||
|
||||
class PosterDeleteBaseView(DeleteView):
|
||||
"""Edit communication poster"""
|
||||
"""Edit communication poster."""
|
||||
|
||||
pk_url_kwarg = "poster_id"
|
||||
current_tab = "posters"
|
||||
@ -738,7 +738,7 @@ class PosterDeleteBaseView(DeleteView):
|
||||
|
||||
|
||||
class PosterListView(IsComAdminMixin, ComTabsMixin, PosterListBaseView):
|
||||
"""List communication posters"""
|
||||
"""List communication posters."""
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
@ -747,7 +747,7 @@ class PosterListView(IsComAdminMixin, ComTabsMixin, PosterListBaseView):
|
||||
|
||||
|
||||
class PosterCreateView(IsComAdminMixin, ComTabsMixin, PosterCreateBaseView):
|
||||
"""Create communication poster"""
|
||||
"""Create communication poster."""
|
||||
|
||||
success_url = reverse_lazy("com:poster_list")
|
||||
|
||||
@ -758,7 +758,7 @@ class PosterCreateView(IsComAdminMixin, ComTabsMixin, PosterCreateBaseView):
|
||||
|
||||
|
||||
class PosterEditView(IsComAdminMixin, ComTabsMixin, PosterEditBaseView):
|
||||
"""Edit communication poster"""
|
||||
"""Edit communication poster."""
|
||||
|
||||
success_url = reverse_lazy("com:poster_list")
|
||||
|
||||
@ -769,13 +769,13 @@ class PosterEditView(IsComAdminMixin, ComTabsMixin, PosterEditBaseView):
|
||||
|
||||
|
||||
class PosterDeleteView(IsComAdminMixin, ComTabsMixin, PosterDeleteBaseView):
|
||||
"""Delete communication poster"""
|
||||
"""Delete communication poster."""
|
||||
|
||||
success_url = reverse_lazy("com:poster_list")
|
||||
|
||||
|
||||
class PosterModerateListView(IsComAdminMixin, ComTabsMixin, ListView):
|
||||
"""Moderate list communication poster"""
|
||||
"""Moderate list communication poster."""
|
||||
|
||||
current_tab = "posters"
|
||||
model = Poster
|
||||
@ -789,7 +789,7 @@ class PosterModerateListView(IsComAdminMixin, ComTabsMixin, ListView):
|
||||
|
||||
|
||||
class PosterModerateView(IsComAdminMixin, ComTabsMixin, View):
|
||||
"""Moderate communication poster"""
|
||||
"""Moderate communication poster."""
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
obj = get_object_or_404(Poster, pk=kwargs["object_id"])
|
||||
@ -807,7 +807,7 @@ class PosterModerateView(IsComAdminMixin, ComTabsMixin, View):
|
||||
|
||||
|
||||
class ScreenListView(IsComAdminMixin, ComTabsMixin, ListView):
|
||||
"""List communication screens"""
|
||||
"""List communication screens."""
|
||||
|
||||
current_tab = "screens"
|
||||
model = Screen
|
||||
@ -815,7 +815,7 @@ class ScreenListView(IsComAdminMixin, ComTabsMixin, ListView):
|
||||
|
||||
|
||||
class ScreenSlideshowView(DetailView):
|
||||
"""Slideshow of actives posters"""
|
||||
"""Slideshow of actives posters."""
|
||||
|
||||
pk_url_kwarg = "screen_id"
|
||||
model = Screen
|
||||
@ -828,7 +828,7 @@ class ScreenSlideshowView(DetailView):
|
||||
|
||||
|
||||
class ScreenCreateView(IsComAdminMixin, ComTabsMixin, CreateView):
|
||||
"""Create communication screen"""
|
||||
"""Create communication screen."""
|
||||
|
||||
current_tab = "screens"
|
||||
model = Screen
|
||||
@ -838,7 +838,7 @@ class ScreenCreateView(IsComAdminMixin, ComTabsMixin, CreateView):
|
||||
|
||||
|
||||
class ScreenEditView(IsComAdminMixin, ComTabsMixin, UpdateView):
|
||||
"""Edit communication screen"""
|
||||
"""Edit communication screen."""
|
||||
|
||||
pk_url_kwarg = "screen_id"
|
||||
current_tab = "screens"
|
||||
@ -849,7 +849,7 @@ class ScreenEditView(IsComAdminMixin, ComTabsMixin, UpdateView):
|
||||
|
||||
|
||||
class ScreenDeleteView(IsComAdminMixin, ComTabsMixin, DeleteView):
|
||||
"""Delete communication screen"""
|
||||
"""Delete communication screen."""
|
||||
|
||||
pk_url_kwarg = "screen_id"
|
||||
current_tab = "screens"
|
||||
|
@ -19,9 +19,7 @@ class TwoDigitMonthConverter:
|
||||
|
||||
|
||||
class BooleanStringConverter:
|
||||
"""
|
||||
Converter whose regex match either True or False
|
||||
"""
|
||||
"""Converter whose regex match either True or False."""
|
||||
|
||||
regex = r"(True)|(False)"
|
||||
|
||||
|
@ -90,12 +90,15 @@ def list_tags(s):
|
||||
yield parts[1][len(tag_prefix) :]
|
||||
|
||||
|
||||
def parse_semver(s):
|
||||
"""
|
||||
Turns a semver string into a 3-tuple or None if the parsing failed, it is a
|
||||
prerelease or it has build metadata.
|
||||
def parse_semver(s) -> tuple[int, int, int] | None:
|
||||
"""Parse a semver string.
|
||||
|
||||
See https://semver.org
|
||||
|
||||
Returns:
|
||||
A tuple, if the parsing was successful, else None.
|
||||
In the latter case, it must probably be a prerelease
|
||||
or include build metadata.
|
||||
"""
|
||||
m = semver_regex.match(s)
|
||||
|
||||
@ -106,7 +109,7 @@ def parse_semver(s):
|
||||
):
|
||||
return None
|
||||
|
||||
return (int(m.group("major")), int(m.group("minor")), int(m.group("patch")))
|
||||
return int(m.group("major")), int(m.group("minor")), int(m.group("patch"))
|
||||
|
||||
|
||||
def semver_to_s(t):
|
||||
|
@ -29,9 +29,7 @@ from django.core.management.commands import compilemessages
|
||||
|
||||
|
||||
class Command(compilemessages.Command):
|
||||
"""
|
||||
Wrap call to compilemessages to avoid building whole env
|
||||
"""
|
||||
"""Wrap call to compilemessages to avoid building whole env."""
|
||||
|
||||
help = """
|
||||
The usage is the same as the real compilemessages
|
||||
|
@ -30,9 +30,7 @@ from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Compiles scss in static folder for production
|
||||
"""
|
||||
"""Compiles scss in static folder for production."""
|
||||
|
||||
help = "Compile scss files from static folder"
|
||||
|
||||
|
@ -53,13 +53,14 @@ _threadlocal = threading.local()
|
||||
|
||||
|
||||
def get_signal_request():
|
||||
"""
|
||||
!!! Do not use if your operation is asynchronus !!!
|
||||
Allow to access current request in signals
|
||||
This is a hack that looks into the thread
|
||||
Mainly used for log purpose
|
||||
"""
|
||||
"""Allow to access current request in signals.
|
||||
|
||||
This is a hack that looks into the thread
|
||||
Mainly used for log purpose.
|
||||
|
||||
!!!danger
|
||||
Do not use if your operation is asynchronous.
|
||||
"""
|
||||
return getattr(_threadlocal, "request", None)
|
||||
|
||||
|
||||
|
224
core/models.py
224
core/models.py
@ -21,11 +21,13 @@
|
||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import unicodedata
|
||||
from datetime import date, timedelta
|
||||
from typing import List, Optional, Union
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import (
|
||||
@ -56,6 +58,9 @@ from phonenumber_field.modelfields import PhoneNumberField
|
||||
|
||||
from core import utils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from club.models import Club
|
||||
|
||||
|
||||
class RealGroupManager(AuthGroupManager):
|
||||
def get_queryset(self):
|
||||
@ -68,8 +73,7 @@ class MetaGroupManager(AuthGroupManager):
|
||||
|
||||
|
||||
class Group(AuthGroup):
|
||||
"""
|
||||
Implement both RealGroups and Meta groups
|
||||
"""Implement both RealGroups and Meta groups.
|
||||
|
||||
Groups are sorted by their is_meta property
|
||||
"""
|
||||
@ -87,9 +91,6 @@ class Group(AuthGroup):
|
||||
ordering = ["name"]
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""
|
||||
This is needed for black magic powered UpdateView's children
|
||||
"""
|
||||
return reverse("core:group_list")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
@ -104,8 +105,8 @@ class Group(AuthGroup):
|
||||
|
||||
|
||||
class MetaGroup(Group):
|
||||
"""
|
||||
MetaGroups are dynamically created groups.
|
||||
"""MetaGroups are dynamically created groups.
|
||||
|
||||
Generally used with clubs where creating a club creates two groups:
|
||||
|
||||
* club-SITH_BOARD_SUFFIX
|
||||
@ -123,14 +124,14 @@ class MetaGroup(Group):
|
||||
self.is_meta = True
|
||||
|
||||
@cached_property
|
||||
def associated_club(self):
|
||||
"""
|
||||
Return the group associated with this meta group
|
||||
def associated_club(self) -> Club | None:
|
||||
"""Return the group associated with this meta group.
|
||||
|
||||
The result of this function is cached
|
||||
|
||||
:return: The associated club if it exists, else None
|
||||
:rtype: club.models.Club | None
|
||||
|
||||
Returns:
|
||||
The associated club if it exists, else None
|
||||
"""
|
||||
from club.models import Club
|
||||
|
||||
@ -150,8 +151,8 @@ class MetaGroup(Group):
|
||||
|
||||
|
||||
class RealGroup(Group):
|
||||
"""
|
||||
RealGroups are created by the developer.
|
||||
"""RealGroups are created by the developer.
|
||||
|
||||
Most of the time they match a number in settings to be easily used for permissions.
|
||||
"""
|
||||
|
||||
@ -173,22 +174,26 @@ def validate_promo(value):
|
||||
|
||||
|
||||
def get_group(*, pk: int = None, name: str = None) -> Optional[Group]:
|
||||
"""
|
||||
Search for a group by its primary key or its name.
|
||||
"""Search for a group by its primary key or its name.
|
||||
Either one of the two must be set.
|
||||
|
||||
The result is cached for the default duration (should be 5 minutes).
|
||||
|
||||
:param pk: The primary key of the group
|
||||
:param name: The name of the group
|
||||
:return: The group if it exists, else None
|
||||
:raise ValueError: If no group matches the criteria
|
||||
Args:
|
||||
pk: The primary key of the group
|
||||
name: The name of the group
|
||||
|
||||
Returns:
|
||||
The group if it exists, else None
|
||||
|
||||
Raises:
|
||||
ValueError: If no group matches the criteria
|
||||
"""
|
||||
if pk is None and name is None:
|
||||
raise ValueError("Either pk or name must be set")
|
||||
|
||||
# replace space characters to hide warnings with memcached backend
|
||||
pk_or_name: Union[str, int] = pk if pk is not None else name.replace(" ", "_")
|
||||
pk_or_name: str | int = pk if pk is not None else name.replace(" ", "_")
|
||||
group = cache.get(f"sith_group_{pk_or_name}")
|
||||
|
||||
if group == "not_found":
|
||||
@ -211,8 +216,7 @@ def get_group(*, pk: int = None, name: str = None) -> Optional[Group]:
|
||||
|
||||
|
||||
class User(AbstractBaseUser):
|
||||
"""
|
||||
Defines the base user class, useable in every app
|
||||
"""Defines the base user class, useable in every app.
|
||||
|
||||
This is almost the same as the auth module AbstractUser since it inherits from it,
|
||||
but some fields are required, and the username is generated automatically with the
|
||||
@ -382,9 +386,6 @@ class User(AbstractBaseUser):
|
||||
return self.is_active and self.is_superuser
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""
|
||||
This is needed for black magic powered UpdateView's children
|
||||
"""
|
||||
return reverse("core:user_profile", kwargs={"user_id": self.pk})
|
||||
|
||||
def __str__(self):
|
||||
@ -412,8 +413,7 @@ class User(AbstractBaseUser):
|
||||
return 0
|
||||
|
||||
def is_in_group(self, *, pk: int = None, name: str = None) -> bool:
|
||||
"""
|
||||
Check if this user is in the given group.
|
||||
"""Check if this user is in the given group.
|
||||
Either a group id or a group name must be provided.
|
||||
If both are passed, only the id will be considered.
|
||||
|
||||
@ -421,7 +421,8 @@ class User(AbstractBaseUser):
|
||||
If no group is found, return False.
|
||||
If a group is found, check if this user is in the latter.
|
||||
|
||||
:return: True if the user is the group, else False
|
||||
Returns:
|
||||
True if the user is the group, else False
|
||||
"""
|
||||
if pk is not None:
|
||||
group: Optional[Group] = get_group(pk=pk)
|
||||
@ -454,11 +455,12 @@ class User(AbstractBaseUser):
|
||||
return group in self.cached_groups
|
||||
|
||||
@property
|
||||
def cached_groups(self) -> List[Group]:
|
||||
"""
|
||||
Get the list of groups this user is in.
|
||||
def cached_groups(self) -> list[Group]:
|
||||
"""Get the list of groups this user is in.
|
||||
|
||||
The result is cached for the default duration (should be 5 minutes)
|
||||
:return: A list of all the groups this user is in
|
||||
|
||||
Returns: A list of all the groups this user is in.
|
||||
"""
|
||||
groups = cache.get(f"user_{self.id}_groups")
|
||||
if groups is None:
|
||||
@ -523,9 +525,8 @@ class User(AbstractBaseUser):
|
||||
|
||||
@cached_property
|
||||
def age(self) -> int:
|
||||
"""
|
||||
Return the age this user has the day the method is called.
|
||||
If the user has not filled his age, return 0
|
||||
"""Return the age this user has the day the method is called.
|
||||
If the user has not filled his age, return 0.
|
||||
"""
|
||||
if self.date_of_birth is None:
|
||||
return 0
|
||||
@ -576,31 +577,27 @@ class User(AbstractBaseUser):
|
||||
}
|
||||
|
||||
def get_full_name(self):
|
||||
"""
|
||||
Returns the first_name plus the last_name, with a space in between.
|
||||
"""
|
||||
"""Returns the first_name plus the last_name, with a space in between."""
|
||||
full_name = "%s %s" % (self.first_name, self.last_name)
|
||||
return full_name.strip()
|
||||
|
||||
def get_short_name(self):
|
||||
"Returns the short name for the user."
|
||||
"""Returns the short name for the user."""
|
||||
if self.nick_name:
|
||||
return self.nick_name
|
||||
return self.first_name + " " + self.last_name
|
||||
|
||||
def get_display_name(self):
|
||||
"""
|
||||
Returns the display name of the user.
|
||||
A nickname if possible, otherwise, the full name
|
||||
def get_display_name(self) -> str:
|
||||
"""Returns the display name of the user.
|
||||
|
||||
A nickname if possible, otherwise, the full name.
|
||||
"""
|
||||
if self.nick_name:
|
||||
return "%s (%s)" % (self.get_full_name(), self.nick_name)
|
||||
return self.get_full_name()
|
||||
|
||||
def get_age(self):
|
||||
"""
|
||||
Returns the age
|
||||
"""
|
||||
"""Returns the age."""
|
||||
today = timezone.now()
|
||||
born = self.date_of_birth
|
||||
return (
|
||||
@ -608,18 +605,18 @@ class User(AbstractBaseUser):
|
||||
)
|
||||
|
||||
def email_user(self, subject, message, from_email=None, **kwargs):
|
||||
"""
|
||||
Sends an email to this User.
|
||||
"""
|
||||
"""Sends an email to this User."""
|
||||
if from_email is None:
|
||||
from_email = settings.DEFAULT_FROM_EMAIL
|
||||
send_mail(subject, message, from_email, [self.email], **kwargs)
|
||||
|
||||
def generate_username(self):
|
||||
"""
|
||||
Generates a unique username based on the first and last names.
|
||||
For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists
|
||||
Returns the generated username
|
||||
def generate_username(self) -> str:
|
||||
"""Generates a unique username based on the first and last names.
|
||||
|
||||
For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.
|
||||
|
||||
Returns:
|
||||
The generated username.
|
||||
"""
|
||||
|
||||
def remove_accents(data):
|
||||
@ -644,9 +641,7 @@ class User(AbstractBaseUser):
|
||||
return user_name
|
||||
|
||||
def is_owner(self, obj):
|
||||
"""
|
||||
Determine if the object is owned by the user
|
||||
"""
|
||||
"""Determine if the object is owned by the user."""
|
||||
if hasattr(obj, "is_owned_by") and obj.is_owned_by(self):
|
||||
return True
|
||||
if hasattr(obj, "owner_group") and self.is_in_group(pk=obj.owner_group.id):
|
||||
@ -656,9 +651,7 @@ class User(AbstractBaseUser):
|
||||
return False
|
||||
|
||||
def can_edit(self, obj):
|
||||
"""
|
||||
Determine if the object can be edited by the user
|
||||
"""
|
||||
"""Determine if the object can be edited by the user."""
|
||||
if hasattr(obj, "can_be_edited_by") and obj.can_be_edited_by(self):
|
||||
return True
|
||||
if hasattr(obj, "edit_groups"):
|
||||
@ -672,9 +665,7 @@ class User(AbstractBaseUser):
|
||||
return False
|
||||
|
||||
def can_view(self, obj):
|
||||
"""
|
||||
Determine if the object can be viewed by the user
|
||||
"""
|
||||
"""Determine if the object can be viewed by the user."""
|
||||
if hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self):
|
||||
return True
|
||||
if hasattr(obj, "view_groups"):
|
||||
@ -730,11 +721,8 @@ class User(AbstractBaseUser):
|
||||
return infos
|
||||
|
||||
@cached_property
|
||||
def clubs_with_rights(self):
|
||||
"""
|
||||
:return: the list of clubs where the user has rights
|
||||
:rtype: list[club.models.Club]
|
||||
"""
|
||||
def clubs_with_rights(self) -> list[Club]:
|
||||
"""The list of clubs where the user has rights"""
|
||||
memberships = self.memberships.ongoing().board().select_related("club")
|
||||
return [m.club for m in memberships]
|
||||
|
||||
@ -796,9 +784,7 @@ class AnonymousUser(AuthAnonymousUser):
|
||||
raise PermissionDenied
|
||||
|
||||
def is_in_group(self, *, pk: int = None, name: str = None) -> bool:
|
||||
"""
|
||||
The anonymous user is only in the public group
|
||||
"""
|
||||
"""The anonymous user is only in the public group."""
|
||||
allowed_id = settings.SITH_GROUP_PUBLIC_ID
|
||||
if pk is not None:
|
||||
return pk == allowed_id
|
||||
@ -957,16 +943,15 @@ class SithFile(models.Model):
|
||||
).save()
|
||||
|
||||
def can_be_managed_by(self, user: User) -> bool:
|
||||
"""
|
||||
Tell if the user can manage the file (edit, delete, etc.) or not.
|
||||
"""Tell if the user can manage the file (edit, delete, etc.) or not.
|
||||
Apply the following rules:
|
||||
- If the file is not in the SAS nor in the profiles directory, it can be "managed" by anyone -> return True
|
||||
- If the file is in the SAS, only the SAS admins (or roots) can manage it -> return True if the user is in the SAS admin group or is a root
|
||||
- If the file is in the profiles directory, only the roots can manage it -> return True if the user is a root
|
||||
- If the file is in the profiles directory, only the roots can manage it -> return True if the user is a root.
|
||||
|
||||
:returns: True if the file is managed by the SAS or within the profiles directory, False otherwise
|
||||
Returns:
|
||||
True if the file is managed by the SAS or within the profiles directory, False otherwise
|
||||
"""
|
||||
|
||||
# If the file is not in the SAS nor in the profiles directory, it can be "managed" by anyone
|
||||
profiles_dir = SithFile.objects.filter(name="profiles").first()
|
||||
if not self.is_in_sas and not profiles_dir in self.get_parent_list():
|
||||
@ -1017,9 +1002,7 @@ class SithFile(models.Model):
|
||||
return super().delete()
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Cleans up the file
|
||||
"""
|
||||
"""Cleans up the file."""
|
||||
super().clean()
|
||||
if "/" in self.name:
|
||||
raise ValidationError(_("Character '/' not authorized in name"))
|
||||
@ -1070,15 +1053,14 @@ class SithFile(models.Model):
|
||||
c.apply_rights_recursively(only_folders=only_folders)
|
||||
|
||||
def copy_rights(self):
|
||||
"""Copy, if possible, the rights of the parent folder"""
|
||||
"""Copy, if possible, the rights of the parent folder."""
|
||||
if self.parent is not None:
|
||||
self.edit_groups.set(self.parent.edit_groups.all())
|
||||
self.view_groups.set(self.parent.view_groups.all())
|
||||
self.save()
|
||||
|
||||
def move_to(self, parent):
|
||||
"""
|
||||
Move a file to a new parent.
|
||||
"""Move a file to a new parent.
|
||||
`parent` must be a SithFile with the `is_folder=True` property. Otherwise, this function doesn't change
|
||||
anything.
|
||||
This is done only at the DB level, so that it's very fast for the user. Indeed, this function doesn't modify
|
||||
@ -1091,10 +1073,7 @@ class SithFile(models.Model):
|
||||
self.save()
|
||||
|
||||
def _repair_fs(self):
|
||||
"""
|
||||
This function rebuilds recursively the filesystem as it should be
|
||||
regarding the DB tree.
|
||||
"""
|
||||
"""Rebuilds recursively the filesystem as it should be regarding the DB tree."""
|
||||
if self.is_folder:
|
||||
for c in self.children.all():
|
||||
c._repair_fs()
|
||||
@ -1197,19 +1176,19 @@ class SithFile(models.Model):
|
||||
|
||||
|
||||
class LockError(Exception):
|
||||
"""There was a lock error on the object"""
|
||||
"""There was a lock error on the object."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AlreadyLocked(LockError):
|
||||
"""The object is already locked"""
|
||||
"""The object is already locked."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NotLocked(LockError):
|
||||
"""The object is not locked"""
|
||||
"""The object is not locked."""
|
||||
|
||||
pass
|
||||
|
||||
@ -1220,12 +1199,11 @@ def get_default_owner_group():
|
||||
|
||||
|
||||
class Page(models.Model):
|
||||
"""
|
||||
The page class to build a Wiki
|
||||
"""The page class to build a Wiki
|
||||
Each page may have a parent and it's URL is of the form my.site/page/<grd_pa>/<parent>/<mypage>
|
||||
It has an ID field, but don't use it, since it's only there for DB part, and because compound primary key is
|
||||
awkward!
|
||||
Prefere querying pages with Page.get_page_by_full_name()
|
||||
Prefere querying pages with Page.get_page_by_full_name().
|
||||
|
||||
Be careful with the _full_name attribute: this field may not be valid until you call save(). It's made for fast
|
||||
query, but don't rely on it when playing with a Page object, use get_full_name() instead!
|
||||
@ -1294,9 +1272,7 @@ class Page(models.Model):
|
||||
return self.get_full_name()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
Performs some needed actions before and after saving a page in database
|
||||
"""
|
||||
"""Performs some needed actions before and after saving a page in database."""
|
||||
locked = kwargs.pop("force_lock", False)
|
||||
if not locked:
|
||||
locked = self.is_locked()
|
||||
@ -1317,22 +1293,15 @@ class Page(models.Model):
|
||||
self.unset_lock()
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""
|
||||
This is needed for black magic powered UpdateView's children
|
||||
"""
|
||||
return reverse("core:page", kwargs={"page_name": self._full_name})
|
||||
|
||||
@staticmethod
|
||||
def get_page_by_full_name(name):
|
||||
"""
|
||||
Quicker to get a page with that method rather than building the request every time
|
||||
"""
|
||||
"""Quicker to get a page with that method rather than building the request every time."""
|
||||
return Page.objects.filter(_full_name=name).first()
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Cleans up only the name for the moment, but this can be used to make any treatment before saving the object
|
||||
"""
|
||||
"""Cleans up only the name for the moment, but this can be used to make any treatment before saving the object."""
|
||||
if "/" in self.name:
|
||||
self.name = self.name.split("/")[-1]
|
||||
if (
|
||||
@ -1367,10 +1336,11 @@ class Page(models.Model):
|
||||
return l
|
||||
|
||||
def is_locked(self):
|
||||
"""
|
||||
Is True if the page is locked, False otherwise
|
||||
This is where the timeout is handled, so a locked page for which the timeout is reach will be unlocked and this
|
||||
function will return False
|
||||
"""Is True if the page is locked, False otherwise.
|
||||
|
||||
This is where the timeout is handled,
|
||||
so a locked page for which the timeout is reach will be unlocked and this
|
||||
function will return False.
|
||||
"""
|
||||
if self.lock_timeout and (
|
||||
timezone.now() - self.lock_timeout > timedelta(minutes=5)
|
||||
@ -1384,9 +1354,7 @@ class Page(models.Model):
|
||||
)
|
||||
|
||||
def set_lock(self, user):
|
||||
"""
|
||||
Sets a lock on the current page or raise an AlreadyLocked exception
|
||||
"""
|
||||
"""Sets a lock on the current page or raise an AlreadyLocked exception."""
|
||||
if self.is_locked() and self.get_lock() != user:
|
||||
raise AlreadyLocked("The page is already locked by someone else")
|
||||
self.lock_user = user
|
||||
@ -1395,41 +1363,34 @@ class Page(models.Model):
|
||||
# print("Locking page")
|
||||
|
||||
def set_lock_recursive(self, user):
|
||||
"""
|
||||
Locks recursively all the child pages for editing properties
|
||||
"""
|
||||
"""Locks recursively all the child pages for editing properties."""
|
||||
for p in self.children.all():
|
||||
p.set_lock_recursive(user)
|
||||
self.set_lock(user)
|
||||
|
||||
def unset_lock_recursive(self):
|
||||
"""
|
||||
Unlocks recursively all the child pages
|
||||
"""
|
||||
"""Unlocks recursively all the child pages."""
|
||||
for p in self.children.all():
|
||||
p.unset_lock_recursive()
|
||||
self.unset_lock()
|
||||
|
||||
def unset_lock(self):
|
||||
"""Always try to unlock, even if there is no lock"""
|
||||
"""Always try to unlock, even if there is no lock."""
|
||||
self.lock_user = None
|
||||
self.lock_timeout = None
|
||||
super().save()
|
||||
# print("Unlocking page")
|
||||
|
||||
def get_lock(self):
|
||||
"""
|
||||
Returns the page's mutex containing the time and the user in a dict
|
||||
"""
|
||||
"""Returns the page's mutex containing the time and the user in a dict."""
|
||||
if self.lock_user:
|
||||
return self.lock_user
|
||||
raise NotLocked("The page is not locked and thus can not return its user")
|
||||
|
||||
def get_full_name(self):
|
||||
"""
|
||||
Computes the real full_name of the page based on its name and its parent's name
|
||||
"""Computes the real full_name of the page based on its name and its parent's name
|
||||
You can and must rely on this function when working on a page object that is not freshly fetched from the DB
|
||||
(For example when treating a Page object coming from a form)
|
||||
(For example when treating a Page object coming from a form).
|
||||
"""
|
||||
if self.parent is None:
|
||||
return self.name
|
||||
@ -1463,8 +1424,8 @@ class Page(models.Model):
|
||||
|
||||
|
||||
class PageRev(models.Model):
|
||||
"""
|
||||
This is the true content of the page.
|
||||
"""True content of the page.
|
||||
|
||||
Each page object has a revisions field that is a list of PageRev, ordered by date.
|
||||
my_page.revisions.last() gives the PageRev object that is the most up-to-date, and thus,
|
||||
is the real content of the page.
|
||||
@ -1492,9 +1453,6 @@ class PageRev(models.Model):
|
||||
self.page.unset_lock()
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""
|
||||
This is needed for black magic powered UpdateView's children
|
||||
"""
|
||||
return reverse("core:page", kwargs={"page_name": self.page._full_name})
|
||||
|
||||
def __getattribute__(self, attr):
|
||||
@ -1573,9 +1531,7 @@ class Gift(models.Model):
|
||||
|
||||
|
||||
class OperationLog(models.Model):
|
||||
"""
|
||||
General purpose log object to register operations
|
||||
"""
|
||||
"""General purpose log object to register operations."""
|
||||
|
||||
date = models.DateTimeField(_("date"), auto_now_add=True)
|
||||
label = models.CharField(_("label"), max_length=255)
|
||||
|
@ -21,24 +21,26 @@
|
||||
#
|
||||
#
|
||||
|
||||
"""
|
||||
This page is useful for custom migration tricks.
|
||||
Sometimes, when you need to have a migration hack and you think it can be
|
||||
useful again, put it there, we never know if we might need the hack again.
|
||||
"""Collection of utils for custom migration tricks.
|
||||
|
||||
Sometimes, when you need to have a migration hack,
|
||||
and you think it can be useful again,
|
||||
put it there, we never know if we might need the hack again.
|
||||
"""
|
||||
|
||||
from django.db import connection, migrations
|
||||
|
||||
|
||||
class PsqlRunOnly(migrations.RunSQL):
|
||||
"""
|
||||
This is an SQL runner that will launch the given command only if
|
||||
the used DBMS is PostgreSQL.
|
||||
"""SQL runner for PostgreSQL-only queries.
|
||||
|
||||
It may be useful to run Postgres' specific SQL, or to take actions
|
||||
that would be non-senses with backends other than Postgre, such
|
||||
as disabling particular constraints that would prevent the migration
|
||||
to run successfully.
|
||||
|
||||
If used on another DBMS than Postgres, it will be a noop.
|
||||
|
||||
See `club/migrations/0010_auto_20170912_2028.py` as an example.
|
||||
Some explanations can be found here too:
|
||||
https://stackoverflow.com/questions/28429933/django-migrations-using-runpython-to-commit-changes
|
||||
|
@ -31,9 +31,7 @@ from django.core.files.storage import FileSystemStorage
|
||||
|
||||
|
||||
class ScssFinder(FileSystemFinder):
|
||||
"""
|
||||
Find static *.css files compiled on the fly
|
||||
"""
|
||||
"""Find static *.css files compiled on the fly."""
|
||||
|
||||
locations = []
|
||||
|
||||
|
@ -35,10 +35,9 @@ from core.scss.storage import ScssFileStorage, find_file
|
||||
|
||||
|
||||
class ScssProcessor(object):
|
||||
"""
|
||||
If DEBUG mode enabled : compile the scss file
|
||||
"""If DEBUG mode enabled : compile the scss file
|
||||
Else : give the path of the corresponding css supposed to already be compiled
|
||||
Don't forget to use compilestatics to compile scss for production
|
||||
Don't forget to use compilestatics to compile scss for production.
|
||||
"""
|
||||
|
||||
prefix = iri_to_uri(getattr(settings, "STATIC_URL", "/static/"))
|
||||
|
@ -91,9 +91,9 @@ class IndexSignalProcessor(signals.BaseSignalProcessor):
|
||||
|
||||
|
||||
class BigCharFieldIndex(indexes.CharField):
|
||||
"""
|
||||
Workaround to avoid xapian.InvalidArgument: Term too long (> 245)
|
||||
See https://groups.google.com/forum/#!topic/django-haystack/hRJKcPNPXqw/discussion
|
||||
"""Workaround to avoid xapian.InvalidArgument: Term too long (> 245).
|
||||
|
||||
See https://groups.google.com/forum/#!topic/django-haystack/hRJKcPNPXqw/discussion.
|
||||
"""
|
||||
|
||||
def prepare(self, term):
|
||||
|
@ -7,9 +7,7 @@ from core.models import User
|
||||
|
||||
@receiver(m2m_changed, sender=User.groups.through, dispatch_uid="user_groups_changed")
|
||||
def user_groups_changed(sender, instance: User, **kwargs):
|
||||
"""
|
||||
Clear the cached groups of the user
|
||||
"""
|
||||
"""Clear the cached groups of the user."""
|
||||
# As a m2m relationship doesn't live within the model
|
||||
# but rather on an intermediary table, there is no
|
||||
# model method to override, meaning we must use
|
||||
|
@ -30,11 +30,11 @@ from jinja2.parser import Parser
|
||||
|
||||
|
||||
class HoneypotExtension(Extension):
|
||||
"""
|
||||
Wrapper around the honeypot extension tag
|
||||
Known limitation: doesn't support arguments
|
||||
"""Wrapper around the honeypot extension tag.
|
||||
|
||||
Usage: {% render_honeypot_field %}
|
||||
Known limitation: doesn't support arguments.
|
||||
|
||||
Usage: `{% render_honeypot_field %}`
|
||||
"""
|
||||
|
||||
tags = {"render_honeypot_field"}
|
||||
|
@ -46,9 +46,7 @@ def markdown(text):
|
||||
def phonenumber(
|
||||
value, country="FR", number_format=phonenumbers.PhoneNumberFormat.NATIONAL
|
||||
):
|
||||
"""
|
||||
This filter is kindly borrowed from https://github.com/foundertherapy/django-phonenumber-filter
|
||||
"""
|
||||
# collectivised from https://github.com/foundertherapy/django-phonenumber-filter.
|
||||
value = str(value)
|
||||
try:
|
||||
parsed = phonenumbers.parse(value, country)
|
||||
@ -59,6 +57,12 @@ def phonenumber(
|
||||
|
||||
@register.filter(name="truncate_time")
|
||||
def truncate_time(value, time_unit):
|
||||
"""Remove everything in the time format lower than the specified unit.
|
||||
|
||||
Args:
|
||||
value: the value to truncate
|
||||
time_unit: the lowest unit to display
|
||||
"""
|
||||
value = str(value)
|
||||
return {
|
||||
"millis": lambda: value.split(".")[0],
|
||||
@ -81,8 +85,6 @@ def format_timedelta(value: datetime.timedelta) -> str:
|
||||
|
||||
@register.simple_tag()
|
||||
def scss(path):
|
||||
"""
|
||||
Return path of the corresponding css file after compilation
|
||||
"""
|
||||
"""Return path of the corresponding css file after compilation."""
|
||||
processor = ScssProcessor(path)
|
||||
return processor.get_converted_scss()
|
||||
|
@ -105,7 +105,7 @@ class TestUserRegistration:
|
||||
def test_register_fail_with_not_existing_email(
|
||||
self, client: Client, valid_payload, monkeypatch
|
||||
):
|
||||
"""Test that, when email is valid but doesn't actually exist, registration fails"""
|
||||
"""Test that, when email is valid but doesn't actually exist, registration fails."""
|
||||
|
||||
def always_fail(*_args, **_kwargs):
|
||||
raise SMTPException
|
||||
@ -127,10 +127,7 @@ class TestUserLogin:
|
||||
return User.objects.first()
|
||||
|
||||
def test_login_fail(self, client, user):
|
||||
"""
|
||||
Should not login a user correctly
|
||||
"""
|
||||
|
||||
"""Should not login a user correctly."""
|
||||
response = client.post(
|
||||
reverse("core:login"),
|
||||
{
|
||||
@ -158,9 +155,7 @@ class TestUserLogin:
|
||||
assert response.wsgi_request.user.is_anonymous
|
||||
|
||||
def test_login_success(self, client, user):
|
||||
"""
|
||||
Should login a user correctly
|
||||
"""
|
||||
"""Should login a user correctly."""
|
||||
response = client.post(
|
||||
reverse("core:login"),
|
||||
{
|
||||
@ -210,7 +205,7 @@ class TestUserLogin:
|
||||
],
|
||||
)
|
||||
def test_custom_markdown_syntax(md, html):
|
||||
"""Test the homemade markdown syntax"""
|
||||
"""Test the homemade markdown syntax."""
|
||||
assert markdown(md) == f"<p>{html}</p>\n"
|
||||
|
||||
|
||||
@ -233,7 +228,6 @@ class PageHandlingTest(TestCase):
|
||||
|
||||
def test_create_page_ok(self):
|
||||
"""Should create a page correctly."""
|
||||
|
||||
response = self.client.post(
|
||||
reverse("core:page_new"),
|
||||
{"parent": "", "name": "guy", "owner_group": self.root_group.id},
|
||||
@ -274,9 +268,7 @@ class PageHandlingTest(TestCase):
|
||||
assert '<a href="/page/guy/bibou/">' in str(response.content)
|
||||
|
||||
def test_access_child_page_ok(self):
|
||||
"""
|
||||
Should display a page correctly
|
||||
"""
|
||||
"""Should display a page correctly."""
|
||||
parent = Page(name="guy", owner_group=self.root_group)
|
||||
parent.save(force_lock=True)
|
||||
page = Page(name="bibou", owner_group=self.root_group, parent=parent)
|
||||
@ -289,18 +281,14 @@ class PageHandlingTest(TestCase):
|
||||
self.assertIn('<a href="/page/guy/bibou/edit/">', html)
|
||||
|
||||
def test_access_page_not_found(self):
|
||||
"""
|
||||
Should not display a page correctly
|
||||
"""
|
||||
"""Should not display a page correctly."""
|
||||
response = self.client.get(reverse("core:page", kwargs={"page_name": "swagg"}))
|
||||
assert response.status_code == 200
|
||||
html = response.content.decode()
|
||||
self.assertIn('<a href="/page/create/?page=swagg">', html)
|
||||
|
||||
def test_create_page_markdown_safe(self):
|
||||
"""
|
||||
Should format the markdown and escape html correctly
|
||||
"""
|
||||
"""Should format the markdown and escape html correctly."""
|
||||
self.client.post(
|
||||
reverse("core:page_new"), {"parent": "", "name": "guy", "owner_group": "1"}
|
||||
)
|
||||
@ -335,13 +323,13 @@ http://git.an
|
||||
|
||||
class UserToolsTest:
|
||||
def test_anonymous_user_unauthorized(self, client):
|
||||
"""An anonymous user shouldn't have access to the tools page"""
|
||||
"""An anonymous user shouldn't have access to the tools page."""
|
||||
response = client.get(reverse("core:user_tools"))
|
||||
assert response.status_code == 403
|
||||
|
||||
@pytest.mark.parametrize("username", ["guy", "root", "skia", "comunity"])
|
||||
def test_page_is_working(self, client, username):
|
||||
"""All existing users should be able to see the test page"""
|
||||
"""All existing users should be able to see the test page."""
|
||||
# Test for simple user
|
||||
client.force_login(User.objects.get(username=username))
|
||||
response = client.get(reverse("core:user_tools"))
|
||||
@ -391,9 +379,8 @@ class FileHandlingTest(TestCase):
|
||||
|
||||
|
||||
class UserIsInGroupTest(TestCase):
|
||||
"""
|
||||
Test that the User.is_in_group() and AnonymousUser.is_in_group()
|
||||
work as intended
|
||||
"""Test that the User.is_in_group() and AnonymousUser.is_in_group()
|
||||
work as intended.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@ -450,30 +437,24 @@ class UserIsInGroupTest(TestCase):
|
||||
assert user.is_in_group(name=meta_groups_members) is False
|
||||
|
||||
def test_anonymous_user(self):
|
||||
"""
|
||||
Test that anonymous users are only in the public group
|
||||
"""
|
||||
"""Test that anonymous users are only in the public group."""
|
||||
user = AnonymousUser()
|
||||
self.assert_only_in_public_group(user)
|
||||
|
||||
def test_not_subscribed_user(self):
|
||||
"""
|
||||
Test that users who never subscribed are only in the public group
|
||||
"""
|
||||
"""Test that users who never subscribed are only in the public group."""
|
||||
self.assert_only_in_public_group(self.toto)
|
||||
|
||||
def test_wrong_parameter_fail(self):
|
||||
"""
|
||||
Test that when neither the pk nor the name argument is given,
|
||||
the function raises a ValueError
|
||||
"""Test that when neither the pk nor the name argument is given,
|
||||
the function raises a ValueError.
|
||||
"""
|
||||
with self.assertRaises(ValueError):
|
||||
self.toto.is_in_group()
|
||||
|
||||
def test_number_queries(self):
|
||||
"""
|
||||
Test that the number of db queries is stable
|
||||
and that less queries are made when making a new call
|
||||
"""Test that the number of db queries is stable
|
||||
and that less queries are made when making a new call.
|
||||
"""
|
||||
# make sure Skia is in at least one group
|
||||
self.skia.groups.add(Group.objects.first().pk)
|
||||
@ -497,9 +478,8 @@ class UserIsInGroupTest(TestCase):
|
||||
self.skia.is_in_group(pk=group_not_in.id)
|
||||
|
||||
def test_cache_properly_cleared_membership(self):
|
||||
"""
|
||||
Test that when the membership of a user end,
|
||||
the cache is properly invalidated
|
||||
"""Test that when the membership of a user end,
|
||||
the cache is properly invalidated.
|
||||
"""
|
||||
membership = Membership.objects.create(
|
||||
club=self.club, user=self.toto, end_date=None
|
||||
@ -515,9 +495,8 @@ class UserIsInGroupTest(TestCase):
|
||||
assert self.toto.is_in_group(name=meta_groups_members) is False
|
||||
|
||||
def test_cache_properly_cleared_group(self):
|
||||
"""
|
||||
Test that when a user is removed from a group,
|
||||
the is_in_group_method return False when calling it again
|
||||
"""Test that when a user is removed from a group,
|
||||
the is_in_group_method return False when calling it again.
|
||||
"""
|
||||
# testing with pk
|
||||
self.toto.groups.add(self.com_admin.pk)
|
||||
@ -534,9 +513,8 @@ class UserIsInGroupTest(TestCase):
|
||||
assert self.toto.is_in_group(name="SAS admin") is False
|
||||
|
||||
def test_not_existing_group(self):
|
||||
"""
|
||||
Test that searching for a not existing group
|
||||
returns False
|
||||
"""Test that searching for a not existing group
|
||||
returns False.
|
||||
"""
|
||||
assert self.skia.is_in_group(name="This doesn't exist") is False
|
||||
|
||||
@ -557,9 +535,7 @@ class DateUtilsTest(TestCase):
|
||||
cls.spring_first_day = date(2023, cls.spring_month, cls.spring_day)
|
||||
|
||||
def test_get_semester(self):
|
||||
"""
|
||||
Test that the get_semester function returns the correct semester string
|
||||
"""
|
||||
"""Test that the get_semester function returns the correct semester string."""
|
||||
assert get_semester_code(self.autumn_semester_january) == "A24"
|
||||
assert get_semester_code(self.autumn_semester_september) == "A24"
|
||||
assert get_semester_code(self.autumn_first_day) == "A24"
|
||||
@ -568,9 +544,7 @@ class DateUtilsTest(TestCase):
|
||||
assert get_semester_code(self.spring_first_day) == "P23"
|
||||
|
||||
def test_get_start_of_semester_fixed_date(self):
|
||||
"""
|
||||
Test that the get_start_of_semester correctly the starting date of the semester.
|
||||
"""
|
||||
"""Test that the get_start_of_semester correctly the starting date of the semester."""
|
||||
automn_2024 = date(2024, self.autumn_month, self.autumn_day)
|
||||
assert get_start_of_semester(self.autumn_semester_january) == automn_2024
|
||||
assert get_start_of_semester(self.autumn_semester_september) == automn_2024
|
||||
@ -581,9 +555,8 @@ class DateUtilsTest(TestCase):
|
||||
assert get_start_of_semester(self.spring_first_day) == spring_2023
|
||||
|
||||
def test_get_start_of_semester_today(self):
|
||||
"""
|
||||
Test that the get_start_of_semester returns the start of the current semester
|
||||
when no date is given
|
||||
"""Test that the get_start_of_semester returns the start of the current semester
|
||||
when no date is given.
|
||||
"""
|
||||
with freezegun.freeze_time(self.autumn_semester_september):
|
||||
assert get_start_of_semester() == self.autumn_first_day
|
||||
@ -592,8 +565,7 @@ class DateUtilsTest(TestCase):
|
||||
assert get_start_of_semester() == self.spring_first_day
|
||||
|
||||
def test_get_start_of_semester_changing_date(self):
|
||||
"""
|
||||
Test that the get_start_of_semester correctly gives the starting date of the semester,
|
||||
"""Test that the get_start_of_semester correctly gives the starting date of the semester,
|
||||
even when the semester changes while the server isn't restarted.
|
||||
"""
|
||||
spring_2023 = date(2023, self.spring_month, self.spring_day)
|
||||
|
@ -31,9 +31,7 @@ from PIL.Image import Resampling
|
||||
|
||||
|
||||
def get_git_revision_short_hash() -> str:
|
||||
"""
|
||||
Return the short hash of the current commit
|
||||
"""
|
||||
"""Return the short hash of the current commit."""
|
||||
try:
|
||||
output = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"])
|
||||
if isinstance(output, bytes):
|
||||
@ -44,8 +42,7 @@ def get_git_revision_short_hash() -> str:
|
||||
|
||||
|
||||
def get_start_of_semester(today: Optional[date] = None) -> date:
|
||||
"""
|
||||
Return the date of the start of the semester of the given date.
|
||||
"""Return the date of the start of the semester of the given date.
|
||||
If no date is given, return the start date of the current semester.
|
||||
|
||||
The current semester is computed as follows:
|
||||
@ -54,8 +51,11 @@ def get_start_of_semester(today: Optional[date] = None) -> date:
|
||||
- If the date is between 01/01 and 15/02 => Autumn semester of the previous year.
|
||||
- If the date is between 15/02 and 15/08 => Spring semester
|
||||
|
||||
:param today: the date to use to compute the semester. If None, use today's date.
|
||||
:return: the date of the start of the semester
|
||||
Args:
|
||||
today: the date to use to compute the semester. If None, use today's date.
|
||||
|
||||
Returns:
|
||||
the date of the start of the semester
|
||||
"""
|
||||
if today is None:
|
||||
today = timezone.now().date()
|
||||
@ -72,16 +72,18 @@ def get_start_of_semester(today: Optional[date] = None) -> date:
|
||||
|
||||
|
||||
def get_semester_code(d: Optional[date] = None) -> str:
|
||||
"""
|
||||
Return the semester code of the given date.
|
||||
"""Return the semester code of the given date.
|
||||
If no date is given, return the semester code of the current semester.
|
||||
|
||||
The semester code is an upper letter (A for autumn, P for spring),
|
||||
followed by the last two digits of the year.
|
||||
For example, the autumn semester of 2018 is "A18".
|
||||
|
||||
:param d: the date to use to compute the semester. If None, use today's date.
|
||||
:return: the semester code corresponding to the given date
|
||||
Args:
|
||||
d: the date to use to compute the semester. If None, use today's date.
|
||||
|
||||
Returns:
|
||||
the semester code corresponding to the given date
|
||||
"""
|
||||
if d is None:
|
||||
d = timezone.now().date()
|
||||
@ -147,8 +149,15 @@ def exif_auto_rotate(image):
|
||||
return image
|
||||
|
||||
|
||||
def doku_to_markdown(text):
|
||||
"""This is a quite correct doku translator"""
|
||||
def doku_to_markdown(text: str) -> str:
|
||||
"""Convert doku text to the corresponding markdown.
|
||||
|
||||
Args:
|
||||
text: the doku text to convert
|
||||
|
||||
Returns:
|
||||
The converted markdown text
|
||||
"""
|
||||
text = re.sub(
|
||||
r"([^:]|^)\/\/(.*?)\/\/", r"*\2*", text
|
||||
) # Italic (prevents protocol:// conflict)
|
||||
@ -235,7 +244,14 @@ def doku_to_markdown(text):
|
||||
|
||||
|
||||
def bbcode_to_markdown(text):
|
||||
"""This is a very basic BBcode translator"""
|
||||
"""Convert bbcode text to the corresponding markdown.
|
||||
|
||||
Args:
|
||||
text: the bbcode text to convert
|
||||
|
||||
Returns:
|
||||
The converted markdown text
|
||||
"""
|
||||
text = re.sub(r"\[b\](.*?)\[\/b\]", r"**\1**", text, flags=re.DOTALL) # Bold
|
||||
text = re.sub(r"\[i\](.*?)\[\/i\]", r"*\1*", text, flags=re.DOTALL) # Italic
|
||||
text = re.sub(r"\[u\](.*?)\[\/u\]", r"__\1__", text, flags=re.DOTALL) # Underline
|
||||
|
@ -23,6 +23,7 @@
|
||||
#
|
||||
|
||||
import types
|
||||
from typing import Any
|
||||
|
||||
from django.core.exceptions import (
|
||||
ImproperlyConfigured,
|
||||
@ -39,6 +40,7 @@ from django.views.generic.detail import SingleObjectMixin
|
||||
from django.views.generic.edit import FormView
|
||||
from sentry_sdk import last_event_id
|
||||
|
||||
from core.models import User
|
||||
from core.views.forms import LoginForm
|
||||
|
||||
|
||||
@ -60,60 +62,63 @@ def internal_servor_error(request):
|
||||
return HttpResponseServerError(render(request, "core/500.jinja"))
|
||||
|
||||
|
||||
def can_edit_prop(obj, user):
|
||||
"""
|
||||
:param obj: Object to test for permission
|
||||
:param user: core.models.User to test permissions against
|
||||
:return: if user is authorized to edit object properties
|
||||
:rtype: bool
|
||||
def can_edit_prop(obj: Any, user: User) -> bool:
|
||||
"""Can the user edit the properties of the object.
|
||||
|
||||
:Example:
|
||||
Args:
|
||||
obj: Object to test for permission
|
||||
user: core.models.User to test permissions against
|
||||
|
||||
.. code-block:: python
|
||||
Returns:
|
||||
True if user is authorized to edit object properties else False
|
||||
|
||||
Examples:
|
||||
```python
|
||||
if not can_edit_prop(self.object ,request.user):
|
||||
raise PermissionDenied
|
||||
|
||||
```
|
||||
"""
|
||||
if obj is None or user.is_owner(obj):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def can_edit(obj, user):
|
||||
"""
|
||||
:param obj: Object to test for permission
|
||||
:param user: core.models.User to test permissions against
|
||||
:return: if user is authorized to edit object
|
||||
:rtype: bool
|
||||
def can_edit(obj: Any, user: User):
|
||||
"""Can the user edit the object.
|
||||
|
||||
:Example:
|
||||
Args:
|
||||
obj: Object to test for permission
|
||||
user: core.models.User to test permissions against
|
||||
|
||||
.. code-block:: python
|
||||
Returns:
|
||||
True if user is authorized to edit object else False
|
||||
|
||||
if not can_edit(self.object ,request.user):
|
||||
Examples:
|
||||
```python
|
||||
if not can_edit(self.object, request.user):
|
||||
raise PermissionDenied
|
||||
|
||||
```
|
||||
"""
|
||||
if obj is None or user.can_edit(obj):
|
||||
return True
|
||||
return can_edit_prop(obj, user)
|
||||
|
||||
|
||||
def can_view(obj, user):
|
||||
"""
|
||||
:param obj: Object to test for permission
|
||||
:param user: core.models.User to test permissions against
|
||||
:return: if user is authorized to see object
|
||||
:rtype: bool
|
||||
def can_view(obj: Any, user: User):
|
||||
"""Can the user see the object.
|
||||
|
||||
:Example:
|
||||
Args:
|
||||
obj: Object to test for permission
|
||||
user: core.models.User to test permissions against
|
||||
|
||||
.. code-block:: python
|
||||
Returns:
|
||||
True if user is authorized to see object else False
|
||||
|
||||
Examples:
|
||||
```python
|
||||
if not can_view(self.object ,request.user):
|
||||
raise PermissionDenied
|
||||
|
||||
```
|
||||
"""
|
||||
if obj is None or user.can_view(obj):
|
||||
return True
|
||||
@ -121,20 +126,22 @@ def can_view(obj, user):
|
||||
|
||||
|
||||
class GenericContentPermissionMixinBuilder(View):
|
||||
"""
|
||||
Used to build permission mixins
|
||||
"""Used to build permission mixins.
|
||||
|
||||
This view protect any child view that would be showing an object that is restricted based
|
||||
on two properties
|
||||
on two properties.
|
||||
|
||||
:prop permission_function: function to test permission with, takes an object and an user an return a bool
|
||||
:prop raised_error: permission to be raised
|
||||
|
||||
:raises: raised_error
|
||||
Attributes:
|
||||
raised_error: permission to be raised
|
||||
"""
|
||||
|
||||
permission_function = lambda obj, user: False
|
||||
raised_error = PermissionDenied
|
||||
|
||||
@staticmethod
|
||||
def permission_function(obj: Any, user: User) -> bool:
|
||||
"""Function to test permission with."""
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_permission_function(cls, obj, user):
|
||||
return cls.permission_function(obj, user)
|
||||
@ -162,11 +169,12 @@ class GenericContentPermissionMixinBuilder(View):
|
||||
|
||||
|
||||
class CanCreateMixin(View):
|
||||
"""
|
||||
This view is made to protect any child view that would create an object, and thus, that can not be protected by any
|
||||
of the following mixin
|
||||
"""Protect any child view that would create an object.
|
||||
|
||||
:raises: PermissionDenied
|
||||
Raises:
|
||||
PermissionDenied:
|
||||
If the user has not the necessary permission
|
||||
to create the object of the view.
|
||||
"""
|
||||
|
||||
def dispatch(self, request, *arg, **kwargs):
|
||||
@ -183,55 +191,54 @@ class CanCreateMixin(View):
|
||||
|
||||
|
||||
class CanEditPropMixin(GenericContentPermissionMixinBuilder):
|
||||
"""
|
||||
This view is made to protect any child view that would be showing some properties of an object that are restricted
|
||||
to only the owner group of the given object.
|
||||
In other word, you can make a view with this view as parent, and it would be retricted to the users that are in the
|
||||
object's owner_group
|
||||
"""Ensure the user has owner permissions on the child view object.
|
||||
|
||||
:raises: PermissionDenied
|
||||
In other word, you can make a view with this view as parent,
|
||||
and it will be retricted to the users that are in the
|
||||
object's owner_group or that pass the `obj.can_be_viewed_by` test.
|
||||
|
||||
Raises:
|
||||
PermissionDenied: If the user cannot see the object
|
||||
"""
|
||||
|
||||
permission_function = can_edit_prop
|
||||
|
||||
|
||||
class CanEditMixin(GenericContentPermissionMixinBuilder):
|
||||
"""
|
||||
This view makes exactly the same thing as its direct parent, but checks the group on the edit_groups field of the
|
||||
object
|
||||
"""Ensure the user has permission to edit this view's object.
|
||||
|
||||
:raises: PermissionDenied
|
||||
Raises:
|
||||
PermissionDenied: if the user cannot edit this view's object.
|
||||
"""
|
||||
|
||||
permission_function = can_edit
|
||||
|
||||
|
||||
class CanViewMixin(GenericContentPermissionMixinBuilder):
|
||||
"""
|
||||
This view still makes exactly the same thing as its direct parent, but checks the group on the view_groups field of
|
||||
the object
|
||||
"""Ensure the user has permission to view this view's object.
|
||||
|
||||
:raises: PermissionDenied
|
||||
Raises:
|
||||
PermissionDenied: if the user cannot edit this view's object.
|
||||
"""
|
||||
|
||||
permission_function = can_view
|
||||
|
||||
|
||||
class UserIsRootMixin(GenericContentPermissionMixinBuilder):
|
||||
"""
|
||||
This view check if the user is root
|
||||
"""Allow only root admins.
|
||||
|
||||
:raises: PermissionDenied
|
||||
Raises:
|
||||
PermissionDenied: if the user isn't root
|
||||
"""
|
||||
|
||||
permission_function = lambda obj, user: user.is_root
|
||||
|
||||
|
||||
class FormerSubscriberMixin(View):
|
||||
"""
|
||||
This view check if the user was at least an old subscriber
|
||||
"""Check if the user was at least an old subscriber.
|
||||
|
||||
:raises: PermissionDenied
|
||||
Raises:
|
||||
PermissionDenied: if the user never subscribed.
|
||||
"""
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
@ -241,10 +248,10 @@ class FormerSubscriberMixin(View):
|
||||
|
||||
|
||||
class UserIsLoggedMixin(View):
|
||||
"""
|
||||
This view check if the user is logged
|
||||
"""Check if the user is logged.
|
||||
|
||||
:raises: PermissionDenied
|
||||
Raises:
|
||||
PermissionDenied:
|
||||
"""
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
@ -254,9 +261,7 @@ class UserIsLoggedMixin(View):
|
||||
|
||||
|
||||
class TabedViewMixin(View):
|
||||
"""
|
||||
This view provide the basic functions for displaying tabs in the template
|
||||
"""
|
||||
"""Basic functions for displaying tabs in the template."""
|
||||
|
||||
def get_tabs_title(self):
|
||||
if hasattr(self, "tabs_title"):
|
||||
@ -299,7 +304,7 @@ class QuickNotifMixin:
|
||||
return ret
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add quick notifications to context"""
|
||||
"""Add quick notifications to context."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["quick_notifs"] = []
|
||||
for n in self.quick_notif_list:
|
||||
@ -312,21 +317,15 @@ class QuickNotifMixin:
|
||||
|
||||
|
||||
class DetailFormView(SingleObjectMixin, FormView):
|
||||
"""
|
||||
Class that allow both a detail view and a form view
|
||||
"""
|
||||
"""Class that allow both a detail view and a form view."""
|
||||
|
||||
def get_object(self):
|
||||
"""
|
||||
Get current group from id in url
|
||||
"""
|
||||
"""Get current group from id in url."""
|
||||
return self.cached_object
|
||||
|
||||
@cached_property
|
||||
def cached_object(self):
|
||||
"""
|
||||
Optimisation on group retrieval
|
||||
"""
|
||||
"""Optimisation on group retrieval."""
|
||||
return super().get_object()
|
||||
|
||||
|
||||
|
@ -42,8 +42,7 @@ from counter.models import Counter
|
||||
|
||||
|
||||
def send_file(request, file_id, file_class=SithFile, file_attr="file"):
|
||||
"""
|
||||
Send a file through Django without loading the whole file into
|
||||
"""Send a file through Django without loading the whole file into
|
||||
memory at once. The FileWrapper will turn the file object into an
|
||||
iterator for chunks of 8KB.
|
||||
"""
|
||||
@ -268,7 +267,7 @@ class FileEditPropView(CanEditPropMixin, UpdateView):
|
||||
|
||||
|
||||
class FileView(CanViewMixin, DetailView, FormMixin):
|
||||
"""This class handle the upload of new files into a folder"""
|
||||
"""Handle the upload of new files into a folder."""
|
||||
|
||||
model = SithFile
|
||||
pk_url_kwarg = "file_id"
|
||||
@ -278,8 +277,8 @@ class FileView(CanViewMixin, DetailView, FormMixin):
|
||||
|
||||
@staticmethod
|
||||
def handle_clipboard(request, obj):
|
||||
"""
|
||||
This method handles the clipboard in the view.
|
||||
"""Handle the clipboard in the view.
|
||||
|
||||
This method can fail, since it does not catch the exceptions coming from
|
||||
below, allowing proper handling in the calling view.
|
||||
Use this method like this:
|
||||
|
@ -196,10 +196,9 @@ class RegisteringForm(UserCreationForm):
|
||||
|
||||
|
||||
class UserProfileForm(forms.ModelForm):
|
||||
"""
|
||||
Form handling the user profile, managing the files
|
||||
"""Form handling the user profile, managing the files
|
||||
This form is actually pretty bad and was made in the rush before the migration. It should be refactored.
|
||||
TODO: refactor this form
|
||||
TODO: refactor this form.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
|
@ -13,9 +13,7 @@
|
||||
#
|
||||
#
|
||||
|
||||
"""
|
||||
This module contains views to manage Groups
|
||||
"""
|
||||
"""Views to manage Groups."""
|
||||
|
||||
from ajax_select.fields import AutoCompleteSelectMultipleField
|
||||
from django import forms
|
||||
@ -31,9 +29,7 @@ from core.views import CanCreateMixin, CanEditMixin, DetailFormView
|
||||
|
||||
|
||||
class EditMembersForm(forms.Form):
|
||||
"""
|
||||
Add and remove members from a Group
|
||||
"""
|
||||
"""Add and remove members from a Group."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.current_users = kwargs.pop("users", [])
|
||||
@ -53,9 +49,7 @@ class EditMembersForm(forms.Form):
|
||||
)
|
||||
|
||||
def clean_users_added(self):
|
||||
"""
|
||||
Check that the user is not trying to add an user already in the group
|
||||
"""
|
||||
"""Check that the user is not trying to add an user already in the group."""
|
||||
cleaned_data = super().clean()
|
||||
users_added = cleaned_data.get("users_added", None)
|
||||
if not users_added:
|
||||
@ -77,9 +71,7 @@ class EditMembersForm(forms.Form):
|
||||
|
||||
|
||||
class GroupListView(CanEditMixin, ListView):
|
||||
"""
|
||||
Displays the Group list
|
||||
"""
|
||||
"""Displays the Group list."""
|
||||
|
||||
model = RealGroup
|
||||
ordering = ["name"]
|
||||
@ -87,9 +79,7 @@ class GroupListView(CanEditMixin, ListView):
|
||||
|
||||
|
||||
class GroupEditView(CanEditMixin, UpdateView):
|
||||
"""
|
||||
Edit infos of a Group
|
||||
"""
|
||||
"""Edit infos of a Group."""
|
||||
|
||||
model = RealGroup
|
||||
pk_url_kwarg = "group_id"
|
||||
@ -98,9 +88,7 @@ class GroupEditView(CanEditMixin, UpdateView):
|
||||
|
||||
|
||||
class GroupCreateView(CanCreateMixin, CreateView):
|
||||
"""
|
||||
Add a new Group
|
||||
"""
|
||||
"""Add a new Group."""
|
||||
|
||||
model = RealGroup
|
||||
template_name = "core/create.jinja"
|
||||
@ -108,9 +96,8 @@ class GroupCreateView(CanCreateMixin, CreateView):
|
||||
|
||||
|
||||
class GroupTemplateView(CanEditMixin, DetailFormView):
|
||||
"""
|
||||
Display all users in a given Group
|
||||
Allow adding and removing users from it
|
||||
"""Display all users in a given Group
|
||||
Allow adding and removing users from it.
|
||||
"""
|
||||
|
||||
model = RealGroup
|
||||
@ -143,9 +130,7 @@ class GroupTemplateView(CanEditMixin, DetailFormView):
|
||||
|
||||
|
||||
class GroupDeleteView(CanEditMixin, DeleteView):
|
||||
"""
|
||||
Delete a Group
|
||||
"""
|
||||
"""Delete a Group."""
|
||||
|
||||
model = RealGroup
|
||||
pk_url_kwarg = "group_id"
|
||||
|
@ -74,9 +74,7 @@ from trombi.views import UserTrombiForm
|
||||
|
||||
@method_decorator(check_honeypot, name="post")
|
||||
class SithLoginView(views.LoginView):
|
||||
"""
|
||||
The login View
|
||||
"""
|
||||
"""The login View."""
|
||||
|
||||
template_name = "core/login.jinja"
|
||||
authentication_form = LoginForm
|
||||
@ -85,33 +83,25 @@ class SithLoginView(views.LoginView):
|
||||
|
||||
|
||||
class SithPasswordChangeView(views.PasswordChangeView):
|
||||
"""
|
||||
Allows a user to change its password
|
||||
"""
|
||||
"""Allows a user to change its password."""
|
||||
|
||||
template_name = "core/password_change.jinja"
|
||||
success_url = reverse_lazy("core:password_change_done")
|
||||
|
||||
|
||||
class SithPasswordChangeDoneView(views.PasswordChangeDoneView):
|
||||
"""
|
||||
Allows a user to change its password
|
||||
"""
|
||||
"""Allows a user to change its password."""
|
||||
|
||||
template_name = "core/password_change_done.jinja"
|
||||
|
||||
|
||||
def logout(request):
|
||||
"""
|
||||
The logout view
|
||||
"""
|
||||
"""The logout view."""
|
||||
return views.logout_then_login(request)
|
||||
|
||||
|
||||
def password_root_change(request, user_id):
|
||||
"""
|
||||
Allows a root user to change someone's password
|
||||
"""
|
||||
"""Allows a root user to change someone's password."""
|
||||
if not request.user.is_root:
|
||||
raise PermissionDenied
|
||||
user = User.objects.filter(id=user_id).first()
|
||||
@ -131,9 +121,7 @@ def password_root_change(request, user_id):
|
||||
|
||||
@method_decorator(check_honeypot, name="post")
|
||||
class SithPasswordResetView(views.PasswordResetView):
|
||||
"""
|
||||
Allows someone to enter an email address for resetting password
|
||||
"""
|
||||
"""Allows someone to enter an email address for resetting password."""
|
||||
|
||||
template_name = "core/password_reset.jinja"
|
||||
email_template_name = "core/password_reset_email.jinja"
|
||||
@ -141,26 +129,20 @@ class SithPasswordResetView(views.PasswordResetView):
|
||||
|
||||
|
||||
class SithPasswordResetDoneView(views.PasswordResetDoneView):
|
||||
"""
|
||||
Confirm that the reset email has been sent
|
||||
"""
|
||||
"""Confirm that the reset email has been sent."""
|
||||
|
||||
template_name = "core/password_reset_done.jinja"
|
||||
|
||||
|
||||
class SithPasswordResetConfirmView(views.PasswordResetConfirmView):
|
||||
"""
|
||||
Provide a reset password form
|
||||
"""
|
||||
"""Provide a reset password form."""
|
||||
|
||||
template_name = "core/password_reset_confirm.jinja"
|
||||
success_url = reverse_lazy("core:password_reset_complete")
|
||||
|
||||
|
||||
class SithPasswordResetCompleteView(views.PasswordResetCompleteView):
|
||||
"""
|
||||
Confirm the password has successfully been reset
|
||||
"""
|
||||
"""Confirm the password has successfully been reset."""
|
||||
|
||||
template_name = "core/password_reset_complete.jinja"
|
||||
|
||||
@ -302,9 +284,7 @@ class UserTabsMixin(TabedViewMixin):
|
||||
|
||||
|
||||
class UserView(UserTabsMixin, CanViewMixin, DetailView):
|
||||
"""
|
||||
Display a user's profile
|
||||
"""
|
||||
"""Display a user's profile."""
|
||||
|
||||
model = User
|
||||
pk_url_kwarg = "user_id"
|
||||
@ -321,9 +301,7 @@ class UserView(UserTabsMixin, CanViewMixin, DetailView):
|
||||
|
||||
|
||||
class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView):
|
||||
"""
|
||||
Display a user's pictures
|
||||
"""
|
||||
"""Display a user's pictures."""
|
||||
|
||||
model = User
|
||||
pk_url_kwarg = "user_id"
|
||||
@ -361,9 +339,7 @@ def delete_user_godfather(request, user_id, godfather_id, is_father):
|
||||
|
||||
|
||||
class UserGodfathersView(UserTabsMixin, CanViewMixin, DetailView):
|
||||
"""
|
||||
Display a user's godfathers
|
||||
"""
|
||||
"""Display a user's godfathers."""
|
||||
|
||||
model = User
|
||||
pk_url_kwarg = "user_id"
|
||||
@ -394,9 +370,7 @@ class UserGodfathersView(UserTabsMixin, CanViewMixin, DetailView):
|
||||
|
||||
|
||||
class UserGodfathersTreeView(UserTabsMixin, CanViewMixin, DetailView):
|
||||
"""
|
||||
Display a user's family tree
|
||||
"""
|
||||
"""Display a user's family tree."""
|
||||
|
||||
model = User
|
||||
pk_url_kwarg = "user_id"
|
||||
@ -415,9 +389,7 @@ class UserGodfathersTreeView(UserTabsMixin, CanViewMixin, DetailView):
|
||||
|
||||
|
||||
class UserGodfathersTreePictureView(CanViewMixin, DetailView):
|
||||
"""
|
||||
Display a user's tree as a picture
|
||||
"""
|
||||
"""Display a user's tree as a picture."""
|
||||
|
||||
model = User
|
||||
pk_url_kwarg = "user_id"
|
||||
@ -489,9 +461,7 @@ class UserGodfathersTreePictureView(CanViewMixin, DetailView):
|
||||
|
||||
|
||||
class UserStatsView(UserTabsMixin, CanViewMixin, DetailView):
|
||||
"""
|
||||
Display a user's stats
|
||||
"""
|
||||
"""Display a user's stats."""
|
||||
|
||||
model = User
|
||||
pk_url_kwarg = "user_id"
|
||||
@ -591,9 +561,7 @@ class UserStatsView(UserTabsMixin, CanViewMixin, DetailView):
|
||||
|
||||
|
||||
class UserMiniView(CanViewMixin, DetailView):
|
||||
"""
|
||||
Display a user's profile
|
||||
"""
|
||||
"""Display a user's profile."""
|
||||
|
||||
model = User
|
||||
pk_url_kwarg = "user_id"
|
||||
@ -602,18 +570,14 @@ class UserMiniView(CanViewMixin, DetailView):
|
||||
|
||||
|
||||
class UserListView(ListView, CanEditPropMixin):
|
||||
"""
|
||||
Displays the user list
|
||||
"""
|
||||
"""Displays the user list."""
|
||||
|
||||
model = User
|
||||
template_name = "core/user_list.jinja"
|
||||
|
||||
|
||||
class UserUploadProfilePictView(CanEditMixin, DetailView):
|
||||
"""
|
||||
Handle the upload of the profile picture taken with webcam in navigator
|
||||
"""
|
||||
"""Handle the upload of the profile picture taken with webcam in navigator."""
|
||||
|
||||
model = User
|
||||
pk_url_kwarg = "user_id"
|
||||
@ -650,9 +614,7 @@ class UserUploadProfilePictView(CanEditMixin, DetailView):
|
||||
|
||||
|
||||
class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView):
|
||||
"""
|
||||
Edit a user's profile
|
||||
"""
|
||||
"""Edit a user's profile."""
|
||||
|
||||
model = User
|
||||
pk_url_kwarg = "user_id"
|
||||
@ -663,9 +625,7 @@ class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView):
|
||||
board_only = []
|
||||
|
||||
def remove_restricted_fields(self, request):
|
||||
"""
|
||||
Removes edit_once and board_only fields
|
||||
"""
|
||||
"""Removes edit_once and board_only fields."""
|
||||
for i in self.edit_once:
|
||||
if getattr(self.form.instance, i) and not (
|
||||
request.user.is_board_member or request.user.is_root
|
||||
@ -703,9 +663,7 @@ class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView):
|
||||
|
||||
|
||||
class UserClubView(UserTabsMixin, CanViewMixin, DetailView):
|
||||
"""
|
||||
Display the user's club(s)
|
||||
"""
|
||||
"""Display the user's club(s)."""
|
||||
|
||||
model = User
|
||||
context_object_name = "profile"
|
||||
@ -715,9 +673,7 @@ class UserClubView(UserTabsMixin, CanViewMixin, DetailView):
|
||||
|
||||
|
||||
class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView):
|
||||
"""
|
||||
Edit a user's preferences
|
||||
"""
|
||||
"""Edit a user's preferences."""
|
||||
|
||||
model = User
|
||||
pk_url_kwarg = "user_id"
|
||||
@ -752,9 +708,7 @@ class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView):
|
||||
|
||||
|
||||
class UserUpdateGroupView(UserTabsMixin, CanEditPropMixin, UpdateView):
|
||||
"""
|
||||
Edit a user's groups
|
||||
"""
|
||||
"""Edit a user's groups."""
|
||||
|
||||
model = User
|
||||
pk_url_kwarg = "user_id"
|
||||
@ -767,9 +721,7 @@ class UserUpdateGroupView(UserTabsMixin, CanEditPropMixin, UpdateView):
|
||||
|
||||
|
||||
class UserToolsView(QuickNotifMixin, UserTabsMixin, UserIsLoggedMixin, TemplateView):
|
||||
"""
|
||||
Displays the logged user's tools
|
||||
"""
|
||||
"""Displays the logged user's tools."""
|
||||
|
||||
template_name = "core/user_tools.jinja"
|
||||
current_tab = "tools"
|
||||
@ -786,9 +738,7 @@ class UserToolsView(QuickNotifMixin, UserTabsMixin, UserIsLoggedMixin, TemplateV
|
||||
|
||||
|
||||
class UserAccountBase(UserTabsMixin, DetailView):
|
||||
"""
|
||||
Base class for UserAccount
|
||||
"""
|
||||
"""Base class for UserAccount."""
|
||||
|
||||
model = User
|
||||
pk_url_kwarg = "user_id"
|
||||
@ -809,9 +759,7 @@ class UserAccountBase(UserTabsMixin, DetailView):
|
||||
|
||||
|
||||
class UserAccountView(UserAccountBase):
|
||||
"""
|
||||
Display a user's account
|
||||
"""
|
||||
"""Display a user's account."""
|
||||
|
||||
template_name = "core/user_account.jinja"
|
||||
|
||||
@ -858,9 +806,7 @@ class UserAccountView(UserAccountBase):
|
||||
|
||||
|
||||
class UserAccountDetailView(UserAccountBase, YearMixin, MonthMixin):
|
||||
"""
|
||||
Display a user's account for month
|
||||
"""
|
||||
"""Display a user's account for month."""
|
||||
|
||||
template_name = "core/user_account_detail.jinja"
|
||||
|
||||
|
@ -30,9 +30,8 @@ class BillingInfoForm(forms.ModelForm):
|
||||
|
||||
|
||||
class StudentCardForm(forms.ModelForm):
|
||||
"""
|
||||
Form for adding student cards
|
||||
Only used for user profile since CounterClick is to complicated
|
||||
"""Form for adding student cards
|
||||
Only used for user profile since CounterClick is to complicated.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
@ -48,8 +47,7 @@ class StudentCardForm(forms.ModelForm):
|
||||
|
||||
|
||||
class GetUserForm(forms.Form):
|
||||
"""
|
||||
The Form class aims at providing a valid user_id field in its cleaned data, in order to pass it to some view,
|
||||
"""The Form class aims at providing a valid user_id field in its cleaned data, in order to pass it to some view,
|
||||
reverse function, or any other use.
|
||||
|
||||
The Form implements a nice JS widget allowing the user to type a customer account id, or search the database with
|
||||
|
@ -44,9 +44,10 @@ from subscription.models import Subscription
|
||||
|
||||
|
||||
class Customer(models.Model):
|
||||
"""
|
||||
This class extends a user to make a customer. It adds some basic customers' information, such as the account ID, and
|
||||
is used by other accounting classes as reference to the customer, rather than using User
|
||||
"""Customer data of a User.
|
||||
|
||||
It adds some basic customers' information, such as the account ID, and
|
||||
is used by other accounting classes as reference to the customer, rather than using User.
|
||||
"""
|
||||
|
||||
user = models.OneToOneField(User, primary_key=True, on_delete=models.CASCADE)
|
||||
@ -63,10 +64,9 @@ class Customer(models.Model):
|
||||
return "%s - %s" % (self.user.username, self.account_id)
|
||||
|
||||
def save(self, *args, allow_negative=False, is_selling=False, **kwargs):
|
||||
"""
|
||||
is_selling : tell if the current action is a selling
|
||||
"""is_selling : tell if the current action is a selling
|
||||
allow_negative : ignored if not a selling. Allow a selling to put the account in negative
|
||||
Those two parameters avoid blocking the save method of a customer if his account is negative
|
||||
Those two parameters avoid blocking the save method of a customer if his account is negative.
|
||||
"""
|
||||
if self.amount < 0 and (is_selling and not allow_negative):
|
||||
raise ValidationError(_("Not enough money"))
|
||||
@ -84,9 +84,8 @@ class Customer(models.Model):
|
||||
|
||||
@property
|
||||
def can_buy(self) -> bool:
|
||||
"""
|
||||
Check if whether this customer has the right to
|
||||
purchase any item.
|
||||
"""Check if whether this customer has the right to purchase any item.
|
||||
|
||||
This must be not confused with the Product.can_be_sold_to(user)
|
||||
method as the present method returns an information
|
||||
about a customer whereas the other tells something
|
||||
@ -100,8 +99,7 @@ class Customer(models.Model):
|
||||
|
||||
@classmethod
|
||||
def get_or_create(cls, user: User) -> Tuple[Customer, bool]:
|
||||
"""
|
||||
Work in pretty much the same way as the usual get_or_create method,
|
||||
"""Work in pretty much the same way as the usual get_or_create method,
|
||||
but with the default field replaced by some under the hood.
|
||||
|
||||
If the user has an account, return it as is.
|
||||
@ -158,9 +156,8 @@ class Customer(models.Model):
|
||||
|
||||
|
||||
class BillingInfo(models.Model):
|
||||
"""
|
||||
Represent the billing information of a user, which are required
|
||||
by the 3D-Secure v2 system used by the etransaction module
|
||||
"""Represent the billing information of a user, which are required
|
||||
by the 3D-Secure v2 system used by the etransaction module.
|
||||
"""
|
||||
|
||||
customer = models.OneToOneField(
|
||||
@ -182,10 +179,9 @@ class BillingInfo(models.Model):
|
||||
return f"{self.first_name} {self.last_name}"
|
||||
|
||||
def to_3dsv2_xml(self) -> str:
|
||||
"""
|
||||
Convert the data from this model into a xml usable
|
||||
"""Convert the data from this model into a xml usable
|
||||
by the online paying service of the Crédit Agricole bank.
|
||||
see : `https://www.ca-moncommerce.com/espace-client-mon-commerce/up2pay-e-transactions/ma-documentation/manuel-dintegration-focus-3ds-v2/principes-generaux/#integration-3dsv2-developpeur-webmaster`
|
||||
see : `https://www.ca-moncommerce.com/espace-client-mon-commerce/up2pay-e-transactions/ma-documentation/manuel-dintegration-focus-3ds-v2/principes-generaux/#integration-3dsv2-developpeur-webmaster`.
|
||||
"""
|
||||
data = {
|
||||
"Address": {
|
||||
@ -204,9 +200,9 @@ class BillingInfo(models.Model):
|
||||
|
||||
|
||||
class ProductType(models.Model):
|
||||
"""
|
||||
This describes a product type
|
||||
Useful only for categorizing, changes are made at the product level for now
|
||||
"""A product type.
|
||||
|
||||
Useful only for categorizing.
|
||||
"""
|
||||
|
||||
name = models.CharField(_("name"), max_length=30)
|
||||
@ -229,9 +225,7 @@ class ProductType(models.Model):
|
||||
return reverse("counter:producttype_list")
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
"""Method to see if that object can be edited by the given user."""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
@ -240,9 +234,7 @@ class ProductType(models.Model):
|
||||
|
||||
|
||||
class Product(models.Model):
|
||||
"""
|
||||
This describes a product, with all its related informations
|
||||
"""
|
||||
"""A product, with all its related information."""
|
||||
|
||||
name = models.CharField(_("name"), max_length=64)
|
||||
description = models.TextField(_("description"), blank=True)
|
||||
@ -297,9 +289,7 @@ class Product(models.Model):
|
||||
return settings.SITH_ECOCUP_DECO == self.id
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
"""Method to see if that object can be edited by the given user."""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
if user.is_in_group(
|
||||
@ -309,8 +299,7 @@ class Product(models.Model):
|
||||
return False
|
||||
|
||||
def can_be_sold_to(self, user: User) -> bool:
|
||||
"""
|
||||
Check if whether the user given in parameter has the right to buy
|
||||
"""Check if whether the user given in parameter has the right to buy
|
||||
this product or not.
|
||||
|
||||
This must be not confused with the Customer.can_buy()
|
||||
@ -319,7 +308,8 @@ class Product(models.Model):
|
||||
whereas the other tells something about a Customer
|
||||
(and not a user, they are not the same model).
|
||||
|
||||
:return: True if the user can buy this product else False
|
||||
Returns:
|
||||
True if the user can buy this product else False
|
||||
"""
|
||||
if not self.buying_groups.exists():
|
||||
return True
|
||||
@ -335,15 +325,16 @@ class Product(models.Model):
|
||||
|
||||
class CounterQuerySet(models.QuerySet):
|
||||
def annotate_has_barman(self, user: User) -> CounterQuerySet:
|
||||
"""
|
||||
Annotate the queryset with the `user_is_barman` field.
|
||||
"""Annotate the queryset with the `user_is_barman` field.
|
||||
|
||||
For each counter, this field has value True if the user
|
||||
is a barman of this counter, else False.
|
||||
|
||||
:param user: the user we want to check if he is a barman
|
||||
|
||||
Example::
|
||||
Args:
|
||||
user: the user we want to check if he is a barman
|
||||
|
||||
Examples:
|
||||
```python
|
||||
sli = User.objects.get(username="sli")
|
||||
counters = (
|
||||
Counter.objects
|
||||
@ -353,6 +344,7 @@ class CounterQuerySet(models.QuerySet):
|
||||
print("Sli est barman dans les comptoirs suivants :")
|
||||
for counter in counters:
|
||||
print(f"- {counter.name}")
|
||||
```
|
||||
"""
|
||||
subquery = user.counters.filter(pk=OuterRef("pk"))
|
||||
# noinspection PyTypeChecker
|
||||
@ -417,23 +409,21 @@ class Counter(models.Model):
|
||||
return user.is_board_member or user in self.sellers.all()
|
||||
|
||||
def gen_token(self):
|
||||
"""Generate a new token for this counter"""
|
||||
"""Generate a new token for this counter."""
|
||||
self.token = "".join(
|
||||
random.choice(string.ascii_letters + string.digits) for x in range(30)
|
||||
)
|
||||
self.save()
|
||||
|
||||
def add_barman(self, user):
|
||||
"""
|
||||
Logs a barman in to the given counter
|
||||
A user is stored as a tuple with its login time
|
||||
"""Logs a barman in to the given counter.
|
||||
|
||||
A user is stored as a tuple with its login time.
|
||||
"""
|
||||
Permanency(user=user, counter=self, start=timezone.now(), end=None).save()
|
||||
|
||||
def del_barman(self, user):
|
||||
"""
|
||||
Logs a barman out and store its permanency
|
||||
"""
|
||||
"""Logs a barman out and store its permanency."""
|
||||
perm = Permanency.objects.filter(counter=self, user=user, end=None).all()
|
||||
for p in perm:
|
||||
p.end = p.activity
|
||||
@ -444,8 +434,7 @@ class Counter(models.Model):
|
||||
return self.get_barmen_list()
|
||||
|
||||
def get_barmen_list(self):
|
||||
"""
|
||||
Returns the barman list as list of User
|
||||
"""Returns the barman list as list of User.
|
||||
|
||||
Also handle the timeout of the barmen
|
||||
"""
|
||||
@ -462,16 +451,12 @@ class Counter(models.Model):
|
||||
return bl
|
||||
|
||||
def get_random_barman(self):
|
||||
"""
|
||||
Return a random user being currently a barman
|
||||
"""
|
||||
"""Return a random user being currently a barman."""
|
||||
bl = self.get_barmen_list()
|
||||
return bl[random.randrange(0, len(bl))]
|
||||
|
||||
def update_activity(self):
|
||||
"""
|
||||
Update the barman activity to prevent timeout
|
||||
"""
|
||||
"""Update the barman activity to prevent timeout."""
|
||||
for p in Permanency.objects.filter(counter=self, end=None).all():
|
||||
p.save() # Update activity
|
||||
|
||||
@ -479,25 +464,18 @@ class Counter(models.Model):
|
||||
return len(self.barmen_list) > 0
|
||||
|
||||
def is_inactive(self):
|
||||
"""
|
||||
Returns True if the counter self is inactive from SITH_COUNTER_MINUTE_INACTIVE's value minutes, else False
|
||||
"""
|
||||
"""Returns True if the counter self is inactive from SITH_COUNTER_MINUTE_INACTIVE's value minutes, else False."""
|
||||
return self.is_open() and (
|
||||
(timezone.now() - self.permanencies.order_by("-activity").first().activity)
|
||||
> timedelta(minutes=settings.SITH_COUNTER_MINUTE_INACTIVE)
|
||||
)
|
||||
|
||||
def barman_list(self):
|
||||
"""
|
||||
Returns the barman id list
|
||||
"""
|
||||
"""Returns the barman id list."""
|
||||
return [b.id for b in self.get_barmen_list()]
|
||||
|
||||
def can_refill(self):
|
||||
"""
|
||||
Show if the counter authorize the refilling with physic money
|
||||
"""
|
||||
|
||||
"""Show if the counter authorize the refilling with physic money."""
|
||||
if self.type != "BAR":
|
||||
return False
|
||||
if self.id in SITH_COUNTER_OFFICES:
|
||||
@ -511,8 +489,7 @@ class Counter(models.Model):
|
||||
return is_ae_member
|
||||
|
||||
def get_top_barmen(self) -> QuerySet:
|
||||
"""
|
||||
Return a QuerySet querying the office hours stats of all the barmen of all time
|
||||
"""Return a QuerySet querying the office hours stats of all the barmen of all time
|
||||
of this counter, ordered by descending number of hours.
|
||||
|
||||
Each element of the QuerySet corresponds to a barman and has the following data :
|
||||
@ -535,16 +512,17 @@ class Counter(models.Model):
|
||||
)
|
||||
|
||||
def get_top_customers(self, since: datetime | date | None = None) -> QuerySet:
|
||||
"""
|
||||
Return a QuerySet querying the money spent by customers of this counter
|
||||
"""Return a QuerySet querying the money spent by customers of this counter
|
||||
since the specified date, ordered by descending amount of money spent.
|
||||
|
||||
Each element of the QuerySet corresponds to a customer and has the following data :
|
||||
- the full name (first name + last name) of the customer
|
||||
- the nickname of the customer
|
||||
- the amount of money spent by the customer
|
||||
|
||||
:param since: timestamp from which to perform the calculation
|
||||
- the full name (first name + last name) of the customer
|
||||
- the nickname of the customer
|
||||
- the amount of money spent by the customer
|
||||
|
||||
Args:
|
||||
since: timestamp from which to perform the calculation
|
||||
"""
|
||||
if since is None:
|
||||
since = get_start_of_semester()
|
||||
@ -573,12 +551,15 @@ class Counter(models.Model):
|
||||
)
|
||||
|
||||
def get_total_sales(self, since: datetime | date | None = None) -> CurrencyField:
|
||||
"""
|
||||
Compute and return the total turnover of this counter
|
||||
since the date specified in parameter (by default, since the start of the current
|
||||
semester)
|
||||
:param since: timestamp from which to perform the calculation
|
||||
:return: Total revenue earned at this counter
|
||||
"""Compute and return the total turnover of this counter since the given date.
|
||||
|
||||
By default, the date is the start of the current semester.
|
||||
|
||||
Args:
|
||||
since: timestamp from which to perform the calculation
|
||||
|
||||
Returns:
|
||||
Total revenue earned at this counter.
|
||||
"""
|
||||
if since is None:
|
||||
since = get_start_of_semester()
|
||||
@ -591,9 +572,7 @@ class Counter(models.Model):
|
||||
|
||||
|
||||
class Refilling(models.Model):
|
||||
"""
|
||||
Handle the refilling
|
||||
"""
|
||||
"""Handle the refilling."""
|
||||
|
||||
counter = models.ForeignKey(
|
||||
Counter, related_name="refillings", blank=False, on_delete=models.CASCADE
|
||||
@ -665,9 +644,7 @@ class Refilling(models.Model):
|
||||
|
||||
|
||||
class Selling(models.Model):
|
||||
"""
|
||||
Handle the sellings
|
||||
"""
|
||||
"""Handle the sellings."""
|
||||
|
||||
label = models.CharField(_("label"), max_length=64)
|
||||
product = models.ForeignKey(
|
||||
@ -724,9 +701,7 @@ class Selling(models.Model):
|
||||
)
|
||||
|
||||
def save(self, *args, allow_negative=False, **kwargs):
|
||||
"""
|
||||
allow_negative : Allow this selling to use more money than available for this user
|
||||
"""
|
||||
"""allow_negative : Allow this selling to use more money than available for this user."""
|
||||
if not self.date:
|
||||
self.date = timezone.now()
|
||||
self.full_clean()
|
||||
@ -864,8 +839,10 @@ class Selling(models.Model):
|
||||
|
||||
|
||||
class Permanency(models.Model):
|
||||
"""
|
||||
This class aims at storing a traceability of who was barman where and when
|
||||
"""A permanency of a barman, on a counter.
|
||||
|
||||
This aims at storing a traceability of who was barman where and when.
|
||||
Mainly for ~~dick size contest~~ establishing the top 10 barmen of the semester.
|
||||
"""
|
||||
|
||||
user = models.ForeignKey(
|
||||
@ -971,9 +948,7 @@ class CashRegisterSummary(models.Model):
|
||||
return object.__getattribute__(self, name)
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
"""Method to see if that object can be edited by the given user."""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID):
|
||||
@ -1010,9 +985,7 @@ class CashRegisterSummaryItem(models.Model):
|
||||
|
||||
|
||||
class Eticket(models.Model):
|
||||
"""
|
||||
Eticket can be linked to a product an allows PDF generation
|
||||
"""
|
||||
"""Eticket can be linked to a product an allows PDF generation."""
|
||||
|
||||
product = models.OneToOneField(
|
||||
Product,
|
||||
@ -1041,9 +1014,7 @@ class Eticket(models.Model):
|
||||
return reverse("counter:eticket_list")
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
"""Method to see if that object can be edited by the given user."""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
return user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)
|
||||
@ -1058,11 +1029,11 @@ class Eticket(models.Model):
|
||||
|
||||
|
||||
class StudentCard(models.Model):
|
||||
"""
|
||||
Alternative way to connect a customer into a counter
|
||||
"""Alternative way to connect a customer into a counter.
|
||||
|
||||
We are using Mifare DESFire EV1 specs since it's used for izly cards
|
||||
https://www.nxp.com/docs/en/application-note/AN10927.pdf
|
||||
UID is 7 byte long that means 14 hexa characters
|
||||
UID is 7 byte long that means 14 hexa characters.
|
||||
"""
|
||||
|
||||
UID_SIZE = 14
|
||||
|
@ -140,10 +140,7 @@ class CounterTest(TestCase):
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_annotate_has_barman_queryset(self):
|
||||
"""
|
||||
Test if the custom queryset method ``annotate_has_barman``
|
||||
works as intended
|
||||
"""
|
||||
"""Test if the custom queryset method `annotate_has_barman` works as intended."""
|
||||
self.sli.counters.set([self.foyer, self.mde])
|
||||
counters = Counter.objects.annotate_has_barman(self.sli)
|
||||
for counter in counters:
|
||||
@ -265,15 +262,11 @@ class CounterStatsTest(TestCase):
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_get_total_sales(self):
|
||||
"""
|
||||
Test the result of the Counter.get_total_sales() method
|
||||
"""
|
||||
"""Test the result of the Counter.get_total_sales() method."""
|
||||
assert self.counter.get_total_sales() == 3102
|
||||
|
||||
def test_top_barmen(self):
|
||||
"""
|
||||
Test the result of Counter.get_top_barmen() is correct
|
||||
"""
|
||||
"""Test the result of Counter.get_top_barmen() is correct."""
|
||||
users = [self.skia, self.root, self.sli]
|
||||
perm_times = [
|
||||
timedelta(days=16, hours=2, minutes=35, seconds=54),
|
||||
@ -292,9 +285,7 @@ class CounterStatsTest(TestCase):
|
||||
]
|
||||
|
||||
def test_top_customer(self):
|
||||
"""
|
||||
Test the result of Counter.get_top_customers() is correct
|
||||
"""
|
||||
"""Test the result of Counter.get_top_customers() is correct."""
|
||||
users = [self.sli, self.skia, self.krophil, self.root]
|
||||
sale_amounts = [2000, 1000, 100, 2]
|
||||
assert list(self.counter.get_top_customers()) == [
|
||||
@ -588,9 +579,8 @@ class BarmanConnectionTest(TestCase):
|
||||
|
||||
|
||||
class StudentCardTest(TestCase):
|
||||
"""
|
||||
Tests for adding and deleting Stundent Cards
|
||||
Test that an user can be found with it's student card
|
||||
"""Tests for adding and deleting Stundent Cards
|
||||
Test that an user can be found with it's student card.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
|
197
counter/views.py
197
counter/views.py
@ -75,9 +75,7 @@ from counter.models import (
|
||||
|
||||
|
||||
class CounterAdminMixin(View):
|
||||
"""
|
||||
This view is made to protect counter admin section
|
||||
"""
|
||||
"""Protect counter admin section."""
|
||||
|
||||
edit_group = [settings.SITH_GROUP_COUNTER_ADMIN_ID]
|
||||
edit_club = []
|
||||
@ -105,9 +103,7 @@ class CounterAdminMixin(View):
|
||||
|
||||
|
||||
class StudentCardDeleteView(DeleteView, CanEditMixin):
|
||||
"""
|
||||
View used to delete a card from a user
|
||||
"""
|
||||
"""View used to delete a card from a user."""
|
||||
|
||||
model = StudentCard
|
||||
template_name = "core/delete_confirm.jinja"
|
||||
@ -210,9 +206,7 @@ class CounterTabsMixin(TabedViewMixin):
|
||||
class CounterMain(
|
||||
CounterTabsMixin, CanViewMixin, DetailView, ProcessFormView, FormMixin
|
||||
):
|
||||
"""
|
||||
The public (barman) view
|
||||
"""
|
||||
"""The public (barman) view."""
|
||||
|
||||
model = Counter
|
||||
template_name = "counter/counter_main.jinja"
|
||||
@ -239,9 +233,7 @@ class CounterMain(
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""
|
||||
We handle here the login form for the barman
|
||||
"""
|
||||
"""We handle here the login form for the barman."""
|
||||
if self.request.method == "POST":
|
||||
self.object = self.get_object()
|
||||
self.object.update_activity()
|
||||
@ -275,9 +267,7 @@ class CounterMain(
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
We handle here the redirection, passing the user id of the asked customer
|
||||
"""
|
||||
"""We handle here the redirection, passing the user id of the asked customer."""
|
||||
self.kwargs["user_id"] = form.cleaned_data["user_id"]
|
||||
return super().form_valid(form)
|
||||
|
||||
@ -286,10 +276,9 @@ class CounterMain(
|
||||
|
||||
|
||||
class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
"""
|
||||
The click view
|
||||
"""The click view
|
||||
This is a detail view not to have to worry about loading the counter
|
||||
Everything is made by hand in the post method
|
||||
Everything is made by hand in the post method.
|
||||
"""
|
||||
|
||||
model = Counter
|
||||
@ -347,7 +336,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Simple get view"""
|
||||
"""Simple get view."""
|
||||
if "basket" not in request.session.keys(): # Init the basket session entry
|
||||
request.session["basket"] = {}
|
||||
request.session["basket_total"] = 0
|
||||
@ -364,7 +353,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
return ret
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Handle the many possibilities of the post request"""
|
||||
"""Handle the many possibilities of the post request."""
|
||||
self.object = self.get_object()
|
||||
self.refill_form = None
|
||||
if (self.object.type != "BAR" and not request.user.is_authenticated) or (
|
||||
@ -481,10 +470,9 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
return len(request.POST) == 0 and len(request.body) != 0
|
||||
|
||||
def add_product(self, request, q=1, p=None):
|
||||
"""
|
||||
Add a product to the basket
|
||||
"""Add a product to the basket
|
||||
q is the quantity passed as integer
|
||||
p is the product id, passed as an integer
|
||||
p is the product id, passed as an integer.
|
||||
"""
|
||||
pid = p or parse_qs(request.body.decode())["product_id"][0]
|
||||
pid = str(pid)
|
||||
@ -543,9 +531,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
return True
|
||||
|
||||
def add_student_card(self, request):
|
||||
"""
|
||||
Add a new student card on the customer account
|
||||
"""
|
||||
"""Add a new student card on the customer account."""
|
||||
uid = request.POST["student_card_uid"]
|
||||
uid = str(uid)
|
||||
if not StudentCard.is_valid(uid):
|
||||
@ -564,7 +550,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
return True
|
||||
|
||||
def del_product(self, request):
|
||||
"""Delete a product from the basket"""
|
||||
"""Delete a product from the basket."""
|
||||
pid = parse_qs(request.body.decode())["product_id"][0]
|
||||
product = self.get_product(pid)
|
||||
if pid in request.session["basket"]:
|
||||
@ -581,11 +567,11 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
request.session.modified = True
|
||||
|
||||
def parse_code(self, request):
|
||||
"""
|
||||
Parse the string entered by the barman
|
||||
"""Parse the string entered by the barman.
|
||||
|
||||
This can be of two forms :
|
||||
- <str>, where the string is the code of the product
|
||||
- <int>X<str>, where the integer is the quantity and str the code
|
||||
- `<str>`, where the string is the code of the product
|
||||
- `<int>X<str>`, where the integer is the quantity and str the code.
|
||||
"""
|
||||
string = parse_qs(request.body.decode()).get("code", [""])[0].upper()
|
||||
if string == "FIN":
|
||||
@ -605,7 +591,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
return self.render_to_response(context)
|
||||
|
||||
def finish(self, request):
|
||||
"""Finish the click session, and validate the basket"""
|
||||
"""Finish the click session, and validate the basket."""
|
||||
with transaction.atomic():
|
||||
request.session["last_basket"] = []
|
||||
if self.sum_basket(request) > self.customer.amount:
|
||||
@ -657,7 +643,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
)
|
||||
|
||||
def cancel(self, request):
|
||||
"""Cancel the click session"""
|
||||
"""Cancel the click session."""
|
||||
kwargs = {"counter_id": self.object.id}
|
||||
request.session.pop("basket", None)
|
||||
return HttpResponseRedirect(
|
||||
@ -665,7 +651,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
)
|
||||
|
||||
def refill(self, request):
|
||||
"""Refill the customer's account"""
|
||||
"""Refill the customer's account."""
|
||||
if not self.object.can_refill():
|
||||
raise PermissionDenied
|
||||
form = RefillForm(request.POST)
|
||||
@ -678,7 +664,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
self.refill_form = form
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add customer to the context"""
|
||||
"""Add customer to the context."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
products = self.object.products.select_related("product_type")
|
||||
if self.customer_is_barman():
|
||||
@ -701,8 +687,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
|
||||
|
||||
class CounterLogin(RedirectView):
|
||||
"""
|
||||
Handle the login of a barman
|
||||
"""Handle the login of a barman.
|
||||
|
||||
Logged barmen are stored in the Permanency model
|
||||
"""
|
||||
@ -710,9 +695,7 @@ class CounterLogin(RedirectView):
|
||||
permanent = False
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Register the logged user as barman for this counter
|
||||
"""
|
||||
"""Register the logged user as barman for this counter."""
|
||||
self.counter_id = kwargs["counter_id"]
|
||||
self.counter = Counter.objects.filter(id=kwargs["counter_id"]).first()
|
||||
form = LoginForm(request, data=request.POST)
|
||||
@ -745,9 +728,7 @@ class CounterLogout(RedirectView):
|
||||
permanent = False
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Unregister the user from the barman
|
||||
"""
|
||||
"""Unregister the user from the barman."""
|
||||
self.counter = Counter.objects.filter(id=kwargs["counter_id"]).first()
|
||||
user = User.objects.filter(id=request.POST["user_id"]).first()
|
||||
self.counter.del_barman(user)
|
||||
@ -803,9 +784,7 @@ class CounterAdminTabsMixin(TabedViewMixin):
|
||||
|
||||
|
||||
class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView):
|
||||
"""
|
||||
A list view for the admins
|
||||
"""
|
||||
"""A list view for the admins."""
|
||||
|
||||
model = Counter
|
||||
template_name = "counter/counter_list.jinja"
|
||||
@ -813,9 +792,7 @@ class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView):
|
||||
|
||||
|
||||
class CounterEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
||||
"""
|
||||
Edit a counter's main informations (for the counter's manager)
|
||||
"""
|
||||
"""Edit a counter's main informations (for the counter's manager)."""
|
||||
|
||||
model = Counter
|
||||
form_class = CounterEditForm
|
||||
@ -833,9 +810,7 @@ class CounterEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
||||
|
||||
|
||||
class CounterEditPropView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
||||
"""
|
||||
Edit a counter's main informations (for the counter's admin)
|
||||
"""
|
||||
"""Edit a counter's main informations (for the counter's admin)."""
|
||||
|
||||
model = Counter
|
||||
form_class = modelform_factory(Counter, fields=["name", "club", "type"])
|
||||
@ -845,9 +820,7 @@ class CounterEditPropView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
||||
|
||||
|
||||
class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
|
||||
"""
|
||||
Create a counter (for the admins)
|
||||
"""
|
||||
"""Create a counter (for the admins)."""
|
||||
|
||||
model = Counter
|
||||
form_class = modelform_factory(
|
||||
@ -860,9 +833,7 @@ class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
|
||||
|
||||
|
||||
class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView):
|
||||
"""
|
||||
Delete a counter (for the admins)
|
||||
"""
|
||||
"""Delete a counter (for the admins)."""
|
||||
|
||||
model = Counter
|
||||
pk_url_kwarg = "counter_id"
|
||||
@ -875,9 +846,7 @@ class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView):
|
||||
|
||||
|
||||
class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
||||
"""
|
||||
A list view for the admins
|
||||
"""
|
||||
"""A list view for the admins."""
|
||||
|
||||
model = ProductType
|
||||
template_name = "counter/producttype_list.jinja"
|
||||
@ -885,9 +854,7 @@ class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
||||
|
||||
|
||||
class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
|
||||
"""
|
||||
A create view for the admins
|
||||
"""
|
||||
"""A create view for the admins."""
|
||||
|
||||
model = ProductType
|
||||
fields = ["name", "description", "comment", "icon", "priority"]
|
||||
@ -896,9 +863,7 @@ class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView
|
||||
|
||||
|
||||
class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
||||
"""
|
||||
An edit view for the admins
|
||||
"""
|
||||
"""An edit view for the admins."""
|
||||
|
||||
model = ProductType
|
||||
template_name = "core/edit.jinja"
|
||||
@ -908,9 +873,7 @@ class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
||||
|
||||
|
||||
class ProductArchivedListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
||||
"""
|
||||
A list view for the admins
|
||||
"""
|
||||
"""A list view for the admins."""
|
||||
|
||||
model = Product
|
||||
template_name = "counter/product_list.jinja"
|
||||
@ -920,9 +883,7 @@ class ProductArchivedListView(CounterAdminTabsMixin, CounterAdminMixin, ListView
|
||||
|
||||
|
||||
class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
||||
"""
|
||||
A list view for the admins
|
||||
"""
|
||||
"""A list view for the admins."""
|
||||
|
||||
model = Product
|
||||
template_name = "counter/product_list.jinja"
|
||||
@ -932,9 +893,7 @@ class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
||||
|
||||
|
||||
class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
|
||||
"""
|
||||
A create view for the admins
|
||||
"""
|
||||
"""A create view for the admins."""
|
||||
|
||||
model = Product
|
||||
form_class = ProductEditForm
|
||||
@ -943,9 +902,7 @@ class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
|
||||
|
||||
|
||||
class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
||||
"""
|
||||
An edit view for the admins
|
||||
"""
|
||||
"""An edit view for the admins."""
|
||||
|
||||
model = Product
|
||||
form_class = ProductEditForm
|
||||
@ -955,18 +912,14 @@ class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
||||
|
||||
|
||||
class RefillingDeleteView(DeleteView):
|
||||
"""
|
||||
Delete a refilling (for the admins)
|
||||
"""
|
||||
"""Delete a refilling (for the admins)."""
|
||||
|
||||
model = Refilling
|
||||
pk_url_kwarg = "refilling_id"
|
||||
template_name = "core/delete_confirm.jinja"
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
"""
|
||||
We have here a very particular right handling, we can't inherit from CanEditPropMixin
|
||||
"""
|
||||
"""We have here a very particular right handling, we can't inherit from CanEditPropMixin."""
|
||||
self.object = self.get_object()
|
||||
if (
|
||||
timezone.now() - self.object.date
|
||||
@ -990,18 +943,14 @@ class RefillingDeleteView(DeleteView):
|
||||
|
||||
|
||||
class SellingDeleteView(DeleteView):
|
||||
"""
|
||||
Delete a selling (for the admins)
|
||||
"""
|
||||
"""Delete a selling (for the admins)."""
|
||||
|
||||
model = Selling
|
||||
pk_url_kwarg = "selling_id"
|
||||
template_name = "core/delete_confirm.jinja"
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
"""
|
||||
We have here a very particular right handling, we can't inherit from CanEditPropMixin
|
||||
"""
|
||||
"""We have here a very particular right handling, we can't inherit from CanEditPropMixin."""
|
||||
self.object = self.get_object()
|
||||
if (
|
||||
timezone.now() - self.object.date
|
||||
@ -1028,9 +977,7 @@ class SellingDeleteView(DeleteView):
|
||||
|
||||
|
||||
class CashRegisterSummaryForm(forms.Form):
|
||||
"""
|
||||
Provide the cash summary form
|
||||
"""
|
||||
"""Provide the cash summary form."""
|
||||
|
||||
ten_cents = forms.IntegerField(label=_("10 cents"), required=False, min_value=0)
|
||||
twenty_cents = forms.IntegerField(label=_("20 cents"), required=False, min_value=0)
|
||||
@ -1238,9 +1185,7 @@ class CashRegisterSummaryForm(forms.Form):
|
||||
|
||||
|
||||
class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
"""
|
||||
Provide the last operations to allow barmen to delete them
|
||||
"""
|
||||
"""Provide the last operations to allow barmen to delete them."""
|
||||
|
||||
model = Counter
|
||||
pk_url_kwarg = "counter_id"
|
||||
@ -1248,9 +1193,7 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
current_tab = "last_ops"
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
"""
|
||||
We have here again a very particular right handling
|
||||
"""
|
||||
"""We have here again a very particular right handling."""
|
||||
self.object = self.get_object()
|
||||
if (
|
||||
self.object.get_barmen_list()
|
||||
@ -1267,7 +1210,7 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add form to the context"""
|
||||
"""Add form to the context."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
threshold = timezone.now() - timedelta(
|
||||
minutes=settings.SITH_LAST_OPERATIONS_LIMIT
|
||||
@ -1282,9 +1225,7 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
|
||||
|
||||
class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
"""
|
||||
Provide the cash summary form
|
||||
"""
|
||||
"""Provide the cash summary form."""
|
||||
|
||||
model = Counter
|
||||
pk_url_kwarg = "counter_id"
|
||||
@ -1292,9 +1233,7 @@ class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
current_tab = "cash_summary"
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
"""
|
||||
We have here again a very particular right handling
|
||||
"""
|
||||
"""We have here again a very particular right handling."""
|
||||
self.object = self.get_object()
|
||||
if (
|
||||
self.object.get_barmen_list()
|
||||
@ -1327,16 +1266,14 @@ class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
return reverse_lazy("counter:details", kwargs={"counter_id": self.object.id})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add form to the context"""
|
||||
"""Add form to the context."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["form"] = self.form
|
||||
return kwargs
|
||||
|
||||
|
||||
class CounterActivityView(DetailView):
|
||||
"""
|
||||
Show the bar activity
|
||||
"""
|
||||
"""Show the bar activity."""
|
||||
|
||||
model = Counter
|
||||
pk_url_kwarg = "counter_id"
|
||||
@ -1344,16 +1281,14 @@ class CounterActivityView(DetailView):
|
||||
|
||||
|
||||
class CounterStatView(DetailView, CounterAdminMixin):
|
||||
"""
|
||||
Show the bar stats
|
||||
"""
|
||||
"""Show the bar stats."""
|
||||
|
||||
model = Counter
|
||||
pk_url_kwarg = "counter_id"
|
||||
template_name = "counter/stats.jinja"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add stats to the context"""
|
||||
"""Add stats to the context."""
|
||||
counter: Counter = self.object
|
||||
semester_start = get_start_of_semester()
|
||||
office_hours = counter.get_top_barmen()
|
||||
@ -1386,7 +1321,7 @@ class CounterStatView(DetailView, CounterAdminMixin):
|
||||
|
||||
|
||||
class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
||||
"""Edit cash summaries"""
|
||||
"""Edit cash summaries."""
|
||||
|
||||
model = CashRegisterSummary
|
||||
template_name = "counter/cash_register_summary.jinja"
|
||||
@ -1400,7 +1335,7 @@ class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
||||
|
||||
|
||||
class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
||||
"""Display a list of cash summaries"""
|
||||
"""Display a list of cash summaries."""
|
||||
|
||||
model = CashRegisterSummary
|
||||
template_name = "counter/cash_summary_list.jinja"
|
||||
@ -1410,7 +1345,7 @@ class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
||||
paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add sums to the context"""
|
||||
"""Add sums to the context."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
form = CashSummaryFormBase(self.request.GET)
|
||||
kwargs["form"] = form
|
||||
@ -1461,7 +1396,7 @@ class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView):
|
||||
current_tab = "invoices_call"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add sums to the context"""
|
||||
"""Add sums to the context."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC")
|
||||
if "month" in self.request.GET:
|
||||
@ -1522,9 +1457,7 @@ class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView):
|
||||
|
||||
|
||||
class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
||||
"""
|
||||
A list view for the admins
|
||||
"""
|
||||
"""A list view for the admins."""
|
||||
|
||||
model = Eticket
|
||||
template_name = "counter/eticket_list.jinja"
|
||||
@ -1533,9 +1466,7 @@ class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
||||
|
||||
|
||||
class EticketCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
|
||||
"""
|
||||
Create an eticket
|
||||
"""
|
||||
"""Create an eticket."""
|
||||
|
||||
model = Eticket
|
||||
template_name = "core/create.jinja"
|
||||
@ -1544,9 +1475,7 @@ class EticketCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
|
||||
|
||||
|
||||
class EticketEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
||||
"""
|
||||
Edit an eticket
|
||||
"""
|
||||
"""Edit an eticket."""
|
||||
|
||||
model = Eticket
|
||||
template_name = "core/edit.jinja"
|
||||
@ -1556,9 +1485,7 @@ class EticketEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
||||
|
||||
|
||||
class EticketPDFView(CanViewMixin, DetailView):
|
||||
"""
|
||||
Display the PDF of an eticket
|
||||
"""
|
||||
"""Display the PDF of an eticket."""
|
||||
|
||||
model = Selling
|
||||
pk_url_kwarg = "selling_id"
|
||||
@ -1647,9 +1574,7 @@ class EticketPDFView(CanViewMixin, DetailView):
|
||||
|
||||
|
||||
class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
||||
"""
|
||||
List of refillings on a counter
|
||||
"""
|
||||
"""List of refillings on a counter."""
|
||||
|
||||
model = Refilling
|
||||
template_name = "counter/refilling_list.jinja"
|
||||
@ -1668,9 +1593,7 @@ class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListVie
|
||||
|
||||
|
||||
class StudentCardFormView(FormView):
|
||||
"""
|
||||
Add a new student card
|
||||
"""
|
||||
"""Add a new student card."""
|
||||
|
||||
form_class = StudentCardForm
|
||||
template_name = "core/create.jinja"
|
||||
|
@ -21,11 +21,10 @@
|
||||
|
||||
|
||||
class PaymentResultConverter:
|
||||
"""
|
||||
Converter used for url mapping of the ``eboutic.views.payment_result``
|
||||
view.
|
||||
"""Converter used for url mapping of the `eboutic.views.payment_result` view.
|
||||
|
||||
It's meant to build an url that can match
|
||||
either ``/eboutic/pay/success/`` or ``/eboutic/pay/failure/``
|
||||
either `/eboutic/pay/success/` or `/eboutic/pay/failure/`
|
||||
but nothing else.
|
||||
"""
|
||||
|
||||
|
@ -34,9 +34,8 @@ from eboutic.models import get_eboutic_products
|
||||
|
||||
|
||||
class BasketForm:
|
||||
"""
|
||||
Class intended to perform checks on the request sended to the server when
|
||||
the user submits his basket from /eboutic/
|
||||
"""Class intended to perform checks on the request sended to the server when
|
||||
the user submits his basket from /eboutic/.
|
||||
|
||||
Because it must check an unknown number of fields, coming from a cookie
|
||||
and needing some databases checks to be performed, inheriting from forms.Form
|
||||
@ -45,6 +44,7 @@ class BasketForm:
|
||||
However, it still tries to share some similarities with a standard django Form.
|
||||
|
||||
Example:
|
||||
-------
|
||||
::
|
||||
|
||||
def my_view(request):
|
||||
@ -62,6 +62,7 @@ class BasketForm:
|
||||
You can also use a little shortcut by directly calling `form.is_valid()`
|
||||
without calling `form.clean()`. In this case, the latter method shall be
|
||||
implicitly called.
|
||||
|
||||
"""
|
||||
|
||||
# check the json is an array containing non-nested objects.
|
||||
@ -85,8 +86,7 @@ class BasketForm:
|
||||
self.correct_cookie = []
|
||||
|
||||
def clean(self) -> None:
|
||||
"""
|
||||
Perform all the checks, but return nothing.
|
||||
"""Perform all the checks, but return nothing.
|
||||
To know if the form is valid, the `is_valid()` method must be used.
|
||||
|
||||
The form shall be considered as valid if it meets all the following conditions :
|
||||
@ -170,9 +170,9 @@ class BasketForm:
|
||||
# the form is invalid
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
"""
|
||||
return True if the form is correct else False.
|
||||
If the `clean()` method has not been called beforehand, call it
|
||||
"""Return True if the form is correct else False.
|
||||
|
||||
If the `clean()` method has not been called beforehand, call it.
|
||||
"""
|
||||
if self.error_messages == set() and self.correct_cookie == []:
|
||||
self.clean()
|
||||
|
@ -12,10 +12,10 @@
|
||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||
#
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import hmac
|
||||
import typing
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
|
||||
from dict2xml import dict2xml
|
||||
from django.conf import settings
|
||||
@ -29,7 +29,7 @@ from core.models import User
|
||||
from counter.models import BillingInfo, Counter, Customer, Product, Refilling, Selling
|
||||
|
||||
|
||||
def get_eboutic_products(user: User) -> List[Product]:
|
||||
def get_eboutic_products(user: User) -> list[Product]:
|
||||
products = (
|
||||
Counter.objects.get(type="EBOUTIC")
|
||||
.products.filter(product_type__isnull=False)
|
||||
@ -43,9 +43,7 @@ def get_eboutic_products(user: User) -> List[Product]:
|
||||
|
||||
|
||||
class Basket(models.Model):
|
||||
"""
|
||||
Basket is built when the user connects to an eboutic page
|
||||
"""
|
||||
"""Basket is built when the user connects to an eboutic page."""
|
||||
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
@ -60,8 +58,7 @@ class Basket(models.Model):
|
||||
return f"{self.user}'s basket ({self.items.all().count()} items)"
|
||||
|
||||
def add_product(self, p: Product, q: int = 1):
|
||||
"""
|
||||
Given p an object of the Product model and q an integer,
|
||||
"""Given p an object of the Product model and q an integer,
|
||||
add q items corresponding to this Product from the basket.
|
||||
|
||||
If this function is called with a product not in the basket, no error will be raised
|
||||
@ -81,8 +78,7 @@ class Basket(models.Model):
|
||||
item.save()
|
||||
|
||||
def del_product(self, p: Product, q: int = 1):
|
||||
"""
|
||||
Given p an object of the Product model and q an integer,
|
||||
"""Given p an object of the Product model and q an integer
|
||||
remove q items corresponding to this Product from the basket.
|
||||
|
||||
If this function is called with a product not in the basket, no error will be raised
|
||||
@ -98,9 +94,7 @@ class Basket(models.Model):
|
||||
item.save()
|
||||
|
||||
def clear(self) -> None:
|
||||
"""
|
||||
Remove all items from this basket without deleting the basket
|
||||
"""
|
||||
"""Remove all items from this basket without deleting the basket."""
|
||||
self.items.all().delete()
|
||||
|
||||
@cached_property
|
||||
@ -116,11 +110,8 @@ class Basket(models.Model):
|
||||
return float(total) if total is not None else 0
|
||||
|
||||
@classmethod
|
||||
def from_session(cls, session) -> typing.Union["Basket", None]:
|
||||
"""
|
||||
Given an HttpRequest django object, return the basket used in the current session
|
||||
if it exists else None
|
||||
"""
|
||||
def from_session(cls, session) -> Basket | None:
|
||||
"""The basket stored in the session object, if it exists."""
|
||||
if "basket_id" in session:
|
||||
try:
|
||||
return cls.objects.get(id=session["basket_id"])
|
||||
@ -129,23 +120,22 @@ class Basket(models.Model):
|
||||
return None
|
||||
|
||||
def generate_sales(self, counter, seller: User, payment_method: str):
|
||||
"""
|
||||
Generate a list of sold items corresponding to the items
|
||||
of this basket WITHOUT saving them NOR deleting the basket
|
||||
"""Generate a list of sold items corresponding to the items
|
||||
of this basket WITHOUT saving them NOR deleting the basket.
|
||||
|
||||
Example:
|
||||
::
|
||||
```python
|
||||
counter = Counter.objects.get(name="Eboutic")
|
||||
sales = basket.generate_sales(counter, "SITH_ACCOUNT")
|
||||
# here the basket is in the same state as before the method call
|
||||
|
||||
counter = Counter.objects.get(name="Eboutic")
|
||||
sales = basket.generate_sales(counter, "SITH_ACCOUNT")
|
||||
# here the basket is in the same state as before the method call
|
||||
|
||||
with transaction.atomic():
|
||||
for sale in sales:
|
||||
sale.save()
|
||||
basket.delete()
|
||||
# all the basket items are deleted by the on_delete=CASCADE relation
|
||||
# thus only the sales remain
|
||||
with transaction.atomic():
|
||||
for sale in sales:
|
||||
sale.save()
|
||||
basket.delete()
|
||||
# all the basket items are deleted by the on_delete=CASCADE relation
|
||||
# thus only the sales remain
|
||||
```
|
||||
"""
|
||||
# I must proceed with two distinct requests instead of
|
||||
# only one with a join because the AbstractBaseItem model has been
|
||||
@ -212,9 +202,7 @@ class Basket(models.Model):
|
||||
|
||||
|
||||
class Invoice(models.Model):
|
||||
"""
|
||||
Invoices are generated once the payment has been validated
|
||||
"""
|
||||
"""Invoices are generated once the payment has been validated."""
|
||||
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
@ -297,11 +285,12 @@ class BasketItem(AbstractBaseItem):
|
||||
|
||||
@classmethod
|
||||
def from_product(cls, product: Product, quantity: int):
|
||||
"""
|
||||
Create a BasketItem with the same characteristics as the
|
||||
product passed in parameters, with the specified quantity
|
||||
WARNING : the basket field is not filled, so you must set
|
||||
it yourself before saving the model
|
||||
"""Create a BasketItem with the same characteristics as the
|
||||
product passed in parameters, with the specified quantity.
|
||||
|
||||
Warnings:
|
||||
the basket field is not filled, so you must set
|
||||
it yourself before saving the model.
|
||||
"""
|
||||
return cls(
|
||||
product_id=product.id,
|
||||
|
@ -52,9 +52,9 @@ class EbouticTest(TestCase):
|
||||
cls.public = User.objects.get(username="public")
|
||||
|
||||
def get_busy_basket(self, user) -> Basket:
|
||||
"""
|
||||
Create and return a basket with 3 barbar and 1 cotis in it.
|
||||
Edit the client session to store the basket id in it
|
||||
"""Create and return a basket with 3 barbar and 1 cotis in it.
|
||||
|
||||
Edit the client session to store the basket id in it.
|
||||
"""
|
||||
session = self.client.session
|
||||
basket = Basket.objects.create(user=user)
|
||||
|
@ -43,8 +43,8 @@ from eboutic.models import Basket, Invoice, InvoiceItem, get_eboutic_products
|
||||
@login_required
|
||||
@require_GET
|
||||
def eboutic_main(request: HttpRequest) -> HttpResponse:
|
||||
"""
|
||||
Main view of the eboutic application.
|
||||
"""Main view of the eboutic application.
|
||||
|
||||
Return an Http response whose content is of type text/html.
|
||||
The latter represents the page from which a user can see
|
||||
the catalogue of products that he can buy and fill
|
||||
|
@ -7,9 +7,7 @@ from core.models import Group, User
|
||||
|
||||
|
||||
class Election(models.Model):
|
||||
"""
|
||||
This class allows to create a new election
|
||||
"""
|
||||
"""This class allows to create a new election."""
|
||||
|
||||
title = models.CharField(_("title"), max_length=255)
|
||||
description = models.TextField(_("description"), null=True, blank=True)
|
||||
@ -105,9 +103,7 @@ class Election(models.Model):
|
||||
|
||||
|
||||
class Role(OrderedModel):
|
||||
"""
|
||||
This class allows to create a new role avaliable for a candidature
|
||||
"""
|
||||
"""This class allows to create a new role avaliable for a candidature."""
|
||||
|
||||
election = models.ForeignKey(
|
||||
Election,
|
||||
@ -151,9 +147,7 @@ class Role(OrderedModel):
|
||||
|
||||
|
||||
class ElectionList(models.Model):
|
||||
"""
|
||||
To allow per list vote
|
||||
"""
|
||||
"""To allow per list vote."""
|
||||
|
||||
title = models.CharField(_("title"), max_length=255)
|
||||
election = models.ForeignKey(
|
||||
@ -176,9 +170,7 @@ class ElectionList(models.Model):
|
||||
|
||||
|
||||
class Candidature(models.Model):
|
||||
"""
|
||||
This class is a component of responsability
|
||||
"""
|
||||
"""This class is a component of responsability."""
|
||||
|
||||
role = models.ForeignKey(
|
||||
Role,
|
||||
@ -214,9 +206,7 @@ class Candidature(models.Model):
|
||||
|
||||
|
||||
class Vote(models.Model):
|
||||
"""
|
||||
This class allows to vote for candidates
|
||||
"""
|
||||
"""This class allows to vote for candidates."""
|
||||
|
||||
role = models.ForeignKey(
|
||||
Role, related_name="votes", verbose_name=_("role"), on_delete=models.CASCADE
|
||||
|
@ -19,10 +19,7 @@ from election.models import Candidature, Election, ElectionList, Role, Vote
|
||||
|
||||
|
||||
class LimitedCheckboxField(forms.ModelMultipleChoiceField):
|
||||
"""
|
||||
Used to replace ModelMultipleChoiceField but with
|
||||
automatic backend verification
|
||||
"""
|
||||
"""A `ModelMultipleChoiceField`, with a max limit of selectable inputs."""
|
||||
|
||||
def __init__(self, queryset, max_choice, **kwargs):
|
||||
self.max_choice = max_choice
|
||||
@ -45,7 +42,7 @@ class LimitedCheckboxField(forms.ModelMultipleChoiceField):
|
||||
|
||||
|
||||
class CandidateForm(forms.ModelForm):
|
||||
"""Form to candidate"""
|
||||
"""Form to candidate."""
|
||||
|
||||
class Meta:
|
||||
model = Candidature
|
||||
@ -91,7 +88,7 @@ class VoteForm(forms.Form):
|
||||
|
||||
|
||||
class RoleForm(forms.ModelForm):
|
||||
"""Form for creating a role"""
|
||||
"""Form for creating a role."""
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
@ -175,9 +172,7 @@ class ElectionForm(forms.ModelForm):
|
||||
|
||||
|
||||
class ElectionsListView(CanViewMixin, ListView):
|
||||
"""
|
||||
A list of all non archived elections visible
|
||||
"""
|
||||
"""A list of all non archived elections visible."""
|
||||
|
||||
model = Election
|
||||
ordering = ["-id"]
|
||||
@ -189,9 +184,7 @@ class ElectionsListView(CanViewMixin, ListView):
|
||||
|
||||
|
||||
class ElectionListArchivedView(CanViewMixin, ListView):
|
||||
"""
|
||||
A list of all archived elections visible
|
||||
"""
|
||||
"""A list of all archived elections visible."""
|
||||
|
||||
model = Election
|
||||
ordering = ["-id"]
|
||||
@ -203,9 +196,7 @@ class ElectionListArchivedView(CanViewMixin, ListView):
|
||||
|
||||
|
||||
class ElectionDetailView(CanViewMixin, DetailView):
|
||||
"""
|
||||
Details an election responsability by responsability
|
||||
"""
|
||||
"""Details an election responsability by responsability."""
|
||||
|
||||
model = Election
|
||||
template_name = "election/election_detail.jinja"
|
||||
@ -232,7 +223,7 @@ class ElectionDetailView(CanViewMixin, DetailView):
|
||||
return response
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add additionnal data to the template"""
|
||||
"""Add additionnal data to the template."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["election_form"] = VoteForm(self.object, self.request.user)
|
||||
kwargs["election_results"] = self.object.results
|
||||
@ -243,9 +234,7 @@ class ElectionDetailView(CanViewMixin, DetailView):
|
||||
|
||||
|
||||
class VoteFormView(CanCreateMixin, FormView):
|
||||
"""
|
||||
Alows users to vote
|
||||
"""
|
||||
"""Alows users to vote."""
|
||||
|
||||
form_class = VoteForm
|
||||
template_name = "election/election_detail.jinja"
|
||||
@ -278,9 +267,7 @@ class VoteFormView(CanCreateMixin, FormView):
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
Verify that the user is part in a vote group
|
||||
"""
|
||||
"""Verify that the user is part in a vote group."""
|
||||
data = form.clean()
|
||||
res = super(FormView, self).form_valid(form)
|
||||
for grp_id in self.election.vote_groups.values_list("pk", flat=True):
|
||||
@ -293,7 +280,7 @@ class VoteFormView(CanCreateMixin, FormView):
|
||||
return reverse_lazy("election:detail", kwargs={"election_id": self.election.id})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add additionnal data to the template"""
|
||||
"""Add additionnal data to the template."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["object"] = self.election
|
||||
kwargs["election"] = self.election
|
||||
@ -305,9 +292,7 @@ class VoteFormView(CanCreateMixin, FormView):
|
||||
|
||||
|
||||
class CandidatureCreateView(CanCreateMixin, CreateView):
|
||||
"""
|
||||
View dedicated to a cundidature creation
|
||||
"""
|
||||
"""View dedicated to a cundidature creation."""
|
||||
|
||||
form_class = CandidateForm
|
||||
model = Candidature
|
||||
@ -330,9 +315,7 @@ class CandidatureCreateView(CanCreateMixin, CreateView):
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
Verify that the selected user is in candidate group
|
||||
"""
|
||||
"""Verify that the selected user is in candidate group."""
|
||||
obj = form.instance
|
||||
obj.election = Election.objects.get(id=self.election.id)
|
||||
if (obj.election.can_candidate(obj.user)) and (
|
||||
@ -361,10 +344,7 @@ class ElectionCreateView(CanCreateMixin, CreateView):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
Allow every users that had passed the dispatch
|
||||
to create an election
|
||||
"""
|
||||
"""Allow every user that had passed the dispatch to create an election."""
|
||||
return super(CreateView, self).form_valid(form)
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
@ -388,9 +368,7 @@ class RoleCreateView(CanCreateMixin, CreateView):
|
||||
return init
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
Verify that the user can edit properly
|
||||
"""
|
||||
"""Verify that the user can edit properly."""
|
||||
obj: Role = form.instance
|
||||
user: User = self.request.user
|
||||
if obj.election:
|
||||
@ -432,9 +410,7 @@ class ElectionListCreateView(CanCreateMixin, CreateView):
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
Verify that the user can vote on this election
|
||||
"""
|
||||
"""Verify that the user can vote on this election."""
|
||||
obj: ElectionList = form.instance
|
||||
user: User = self.request.user
|
||||
if obj.election:
|
||||
|
@ -48,8 +48,7 @@ def get_default_view_group():
|
||||
|
||||
|
||||
class Forum(models.Model):
|
||||
"""
|
||||
The Forum class, made as a tree to allow nice tidy organization
|
||||
"""The Forum class, made as a tree to allow nice tidy organization.
|
||||
|
||||
owner_club allows club members to moderate there own topics
|
||||
edit_groups allows to put any group as a forum admin
|
||||
@ -157,7 +156,7 @@ class Forum(models.Model):
|
||||
c.apply_rights_recursively()
|
||||
|
||||
def copy_rights(self):
|
||||
"""Copy, if possible, the rights of the parent folder"""
|
||||
"""Copy, if possible, the rights of the parent folder."""
|
||||
if self.parent is not None:
|
||||
self.owner_club = self.parent.owner_club
|
||||
self.edit_groups.set(self.parent.edit_groups.all())
|
||||
@ -187,7 +186,7 @@ class Forum(models.Model):
|
||||
return False
|
||||
|
||||
def check_loop(self):
|
||||
"""Raise a validation error when a loop is found within the parent list"""
|
||||
"""Raise a validation error when a loop is found within the parent list."""
|
||||
objs = []
|
||||
cur = self
|
||||
while cur.parent is not None:
|
||||
@ -299,9 +298,7 @@ class ForumTopic(models.Model):
|
||||
|
||||
|
||||
class ForumMessage(models.Model):
|
||||
"""
|
||||
"A ForumMessage object represents a message in the forum" -- Cpt. Obvious
|
||||
"""
|
||||
"""A message in the forum (thx Cpt. Obvious.)."""
|
||||
|
||||
topic = models.ForeignKey(
|
||||
ForumTopic, related_name="messages", on_delete=models.CASCADE
|
||||
@ -425,7 +422,8 @@ class ForumMessageMeta(models.Model):
|
||||
|
||||
|
||||
class ForumUserInfo(models.Model):
|
||||
"""
|
||||
"""The forum infos of a user.
|
||||
|
||||
This currently stores only the last date a user clicked "Mark all as read".
|
||||
However, this can be extended with lot of user preferences dedicated to a
|
||||
user, such as the favourite topics, the signature, and so on...
|
||||
|
@ -118,11 +118,11 @@ class Command(BaseCommand):
|
||||
self.make_important_citizen(u)
|
||||
|
||||
def make_clubs(self):
|
||||
"""
|
||||
Create all the clubs (:class:`club.models.Club`)
|
||||
and store them in `self.clubs` for fast access later.
|
||||
"""Create all the clubs (:class:`club.models.Club`).
|
||||
|
||||
After creation, the clubs are stored in `self.clubs` for fast access later.
|
||||
Don't create the meta groups (:class:`core.models.MetaGroup`)
|
||||
nor the pages of the clubs (:class:`core.models.Page`)
|
||||
nor the pages of the clubs (:class:`core.models.Page`).
|
||||
"""
|
||||
self.clubs = []
|
||||
for i in range(self.NB_CLUBS):
|
||||
@ -132,8 +132,7 @@ class Command(BaseCommand):
|
||||
self.clubs = list(Club.objects.filter(unix_name__startswith="galaxy-").all())
|
||||
|
||||
def make_users(self):
|
||||
"""
|
||||
Create all the users and store them in `self.users` for fast access later.
|
||||
"""Create all the users and store them in `self.users` for fast access later.
|
||||
|
||||
Also create a subscription for all the generated users.
|
||||
"""
|
||||
@ -167,8 +166,7 @@ class Command(BaseCommand):
|
||||
Subscription.objects.bulk_create(subs)
|
||||
|
||||
def make_families(self):
|
||||
"""
|
||||
Generate the godfather/godchild relations for the users contained in :attr:`self.users`.
|
||||
"""Generate the godfather/godchild relations for the users contained in :attr:`self.users`.
|
||||
|
||||
The :meth:`make_users` method must have been called beforehand.
|
||||
|
||||
@ -194,8 +192,7 @@ class Command(BaseCommand):
|
||||
User.godfathers.through.objects.bulk_create(godfathers)
|
||||
|
||||
def make_club_memberships(self):
|
||||
"""
|
||||
Assign users to clubs and give them a role in a pseudo-random way.
|
||||
"""Assign users to clubs and give them a role in a pseudo-random way.
|
||||
|
||||
The :meth:`make_users` and :meth:`make_clubs` methods
|
||||
must have been called beforehand.
|
||||
@ -265,8 +262,7 @@ class Command(BaseCommand):
|
||||
Membership.objects.bulk_create(memberships)
|
||||
|
||||
def make_pictures(self):
|
||||
"""
|
||||
Create pictures for users to be tagged on later.
|
||||
"""Create pictures for users to be tagged on later.
|
||||
|
||||
The :meth:`make_users` method must have been called beforehand.
|
||||
"""
|
||||
@ -301,11 +297,10 @@ class Command(BaseCommand):
|
||||
self.picts = list(Picture.objects.filter(name__startswith="galaxy-").all())
|
||||
|
||||
def make_pictures_memberships(self):
|
||||
"""
|
||||
Assign users to pictures and make enough of them for our
|
||||
"""Assign users to pictures and make enough of them for our
|
||||
created users to be eligible for promotion as citizen.
|
||||
|
||||
See :meth:`galaxy.models.Galaxy.rule` for details on promotion to citizen.
|
||||
See `galaxy.models.Galaxy.rule` for details on promotion to citizen.
|
||||
"""
|
||||
self.pictures_tags = []
|
||||
|
||||
@ -363,9 +358,9 @@ class Command(BaseCommand):
|
||||
PeoplePictureRelation.objects.bulk_create(self.pictures_tags)
|
||||
|
||||
def make_important_citizen(self, uid: int):
|
||||
"""
|
||||
Make the user whose uid is given in parameter a more important citizen,
|
||||
thus triggering many more connections to others (lanes)
|
||||
"""Make the user whose uid is given in parameter a more important citizen.
|
||||
|
||||
This will trigger many more connections to others (lanes)
|
||||
and dragging him towards the center of the Galaxy.
|
||||
|
||||
This promotion is obtained by adding more family links
|
||||
@ -375,7 +370,8 @@ class Command(BaseCommand):
|
||||
also be tagged in more pictures, thus making them also
|
||||
more important.
|
||||
|
||||
:param uid: the id of the user to make more important
|
||||
Args:
|
||||
uid: the id of the user to make more important
|
||||
"""
|
||||
u1 = self.users[uid]
|
||||
u2 = self.users[uid - 100]
|
||||
|
@ -26,7 +26,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
import math
|
||||
import time
|
||||
from typing import List, NamedTuple, Optional, TypedDict, Union
|
||||
from typing import NamedTuple, TypedDict
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Case, Count, F, Q, Value, When
|
||||
@ -40,9 +40,9 @@ from sas.models import Picture
|
||||
|
||||
|
||||
class GalaxyStar(models.Model):
|
||||
"""
|
||||
Define a star (vertex -> user) in the galaxy graph,
|
||||
storing a reference to its owner citizen.
|
||||
"""Define a star (vertex -> user) in the galaxy graph.
|
||||
|
||||
Store a reference to its owner citizen.
|
||||
|
||||
Stars are linked to each others through the :class:`GalaxyLane` model.
|
||||
|
||||
@ -74,13 +74,14 @@ class GalaxyStar(models.Model):
|
||||
|
||||
|
||||
@property
|
||||
def current_star(self) -> Optional[GalaxyStar]:
|
||||
"""
|
||||
The star of this user in the :class:`Galaxy`.
|
||||
def current_star(self) -> GalaxyStar | None:
|
||||
"""The star of this user in the :class:`Galaxy`.
|
||||
|
||||
Only take into account the most recent active galaxy.
|
||||
|
||||
:return: The star of this user if there is an active Galaxy
|
||||
and this user is a citizen of it, else ``None``
|
||||
Returns:
|
||||
The star of this user if there is an active Galaxy
|
||||
and this user is a citizen of it, else `None`
|
||||
"""
|
||||
return self.stars.filter(galaxy=Galaxy.get_current_galaxy()).last()
|
||||
|
||||
@ -90,10 +91,9 @@ User.current_star = current_star
|
||||
|
||||
|
||||
class GalaxyLane(models.Model):
|
||||
"""
|
||||
Define a lane (edge -> link between galaxy citizen)
|
||||
in the galaxy map, storing a reference to both its
|
||||
ends and the distance it covers.
|
||||
"""Define a lane (edge -> link between galaxy citizen) in the galaxy map.
|
||||
|
||||
Store a reference to both its ends and the distance it covers.
|
||||
Score details between citizen owning the stars is also stored here.
|
||||
"""
|
||||
|
||||
@ -138,8 +138,8 @@ class StarDict(TypedDict):
|
||||
|
||||
|
||||
class GalaxyDict(TypedDict):
|
||||
nodes: List[StarDict]
|
||||
links: List
|
||||
nodes: list[StarDict]
|
||||
links: list
|
||||
|
||||
|
||||
class RelationScore(NamedTuple):
|
||||
@ -149,8 +149,8 @@ class RelationScore(NamedTuple):
|
||||
|
||||
|
||||
class Galaxy(models.Model):
|
||||
"""
|
||||
The Galaxy, a graph linking the active users between each others.
|
||||
"""The Galaxy, a graph linking the active users between each others.
|
||||
|
||||
The distance between two users is given by a relation score which takes
|
||||
into account a few parameter like the number of pictures they are both tagged on,
|
||||
the time during which they were in the same clubs and whether they are
|
||||
@ -204,8 +204,8 @@ class Galaxy(models.Model):
|
||||
|
||||
@classmethod
|
||||
def compute_user_score(cls, user: User) -> int:
|
||||
"""
|
||||
Compute an individual score for each citizen.
|
||||
"""Compute an individual score for each citizen.
|
||||
|
||||
It will later be used by the graph algorithm to push
|
||||
higher scores towards the center of the galaxy.
|
||||
|
||||
@ -231,10 +231,7 @@ class Galaxy(models.Model):
|
||||
|
||||
@classmethod
|
||||
def query_user_score(cls, user: User) -> int:
|
||||
"""
|
||||
Perform the db query to get the individual score
|
||||
of the given user in the galaxy.
|
||||
"""
|
||||
"""Get the individual score of the given user in the galaxy."""
|
||||
score_query = (
|
||||
User.objects.filter(id=user.id)
|
||||
.annotate(
|
||||
@ -262,9 +259,9 @@ class Galaxy(models.Model):
|
||||
|
||||
@classmethod
|
||||
def compute_users_score(cls, user1: User, user2: User) -> RelationScore:
|
||||
"""
|
||||
Compute the relationship scores of the two given users
|
||||
in the following fields :
|
||||
"""Compute the relationship scores of the two given users.
|
||||
|
||||
The computation is done with the following fields :
|
||||
|
||||
- family: if they have some godfather/godchild relation
|
||||
- pictures: in how many pictures are both tagged
|
||||
@ -277,11 +274,12 @@ class Galaxy(models.Model):
|
||||
|
||||
@classmethod
|
||||
def compute_users_family_score(cls, user1: User, user2: User) -> int:
|
||||
"""
|
||||
Compute the family score of the relation between the given users.
|
||||
"""Compute the family score of the relation between the given users.
|
||||
|
||||
This takes into account mutual godfathers.
|
||||
|
||||
:return: 366 if user1 is the godfather of user2 (or vice versa) else 0
|
||||
Returns:
|
||||
366 if user1 is the godfather of user2 (or vice versa) else 0
|
||||
"""
|
||||
link_count = User.objects.filter(
|
||||
Q(id=user1.id, godfathers=user2) | Q(id=user2.id, godfathers=user1)
|
||||
@ -294,14 +292,14 @@ class Galaxy(models.Model):
|
||||
|
||||
@classmethod
|
||||
def compute_users_pictures_score(cls, user1: User, user2: User) -> int:
|
||||
"""
|
||||
Compute the pictures score of the relation between the given users.
|
||||
"""Compute the pictures score of the relation between the given users.
|
||||
|
||||
The pictures score is obtained by counting the number
|
||||
of :class:`Picture` in which they have been both identified.
|
||||
This score is then multiplied by 2.
|
||||
|
||||
:return: The number of pictures both users have in common, times 2
|
||||
Returns:
|
||||
The number of pictures both users have in common, times 2
|
||||
"""
|
||||
picture_count = (
|
||||
Picture.objects.filter(people__user__in=(user1,))
|
||||
@ -316,8 +314,7 @@ class Galaxy(models.Model):
|
||||
|
||||
@classmethod
|
||||
def compute_users_clubs_score(cls, user1: User, user2: User) -> int:
|
||||
"""
|
||||
Compute the clubs score of the relation between the given users.
|
||||
"""Compute the clubs score of the relation between the given users.
|
||||
|
||||
The club score is obtained by counting the number of days
|
||||
during which the memberships (see :class:`club.models.Membership`)
|
||||
@ -328,7 +325,8 @@ class Galaxy(models.Model):
|
||||
31/12/2022 (also two years, but with an offset of one year), then their
|
||||
club score is 365.
|
||||
|
||||
:return: the number of days during which both users were in the same club
|
||||
Returns:
|
||||
the number of days during which both users were in the same club
|
||||
"""
|
||||
common_clubs = Club.objects.filter(members__in=user1.memberships.all()).filter(
|
||||
members__in=user2.memberships.all()
|
||||
@ -380,13 +378,13 @@ class Galaxy(models.Model):
|
||||
###################
|
||||
|
||||
@classmethod
|
||||
def scale_distance(cls, value: Union[int, float]) -> int:
|
||||
"""
|
||||
Given a numeric value, return a scaled value which can
|
||||
def scale_distance(cls, value: int | float) -> int:
|
||||
"""Given a numeric value, return a scaled value which can
|
||||
be used in the Galaxy's graphical interface to set the distance
|
||||
between two stars
|
||||
between two stars.
|
||||
|
||||
:return: the scaled value usable in the Galaxy's 3d graph
|
||||
Returns:
|
||||
the scaled value usable in the Galaxy's 3d graph
|
||||
"""
|
||||
# TODO: this will need adjustements with the real, typical data on Taiste
|
||||
if value == 0:
|
||||
@ -409,8 +407,8 @@ class Galaxy(models.Model):
|
||||
return int(value)
|
||||
|
||||
def rule(self, picture_count_threshold=10) -> None:
|
||||
"""
|
||||
Main function of the Galaxy.
|
||||
"""Main function of the Galaxy.
|
||||
|
||||
Iterate over all the rulable users to promote them to citizens.
|
||||
A citizen is a user who has a corresponding star in the Galaxy.
|
||||
Also build up the lanes, which are the links between the different citizen.
|
||||
@ -566,9 +564,7 @@ class Galaxy(models.Model):
|
||||
)
|
||||
|
||||
def make_state(self) -> None:
|
||||
"""
|
||||
Compute JSON structure to send to 3d-force-graph: https://github.com/vasturiano/3d-force-graph/
|
||||
"""
|
||||
"""Compute JSON structure to send to 3d-force-graph: https://github.com/vasturiano/3d-force-graph/."""
|
||||
self.logger.info(
|
||||
"Caching current Galaxy state for a quicker display of the Empire's power."
|
||||
)
|
||||
|
@ -46,9 +46,7 @@ class GalaxyTestModel(TestCase):
|
||||
cls.com = User.objects.get(username="comunity")
|
||||
|
||||
def test_user_self_score(self):
|
||||
"""
|
||||
Test that individual user scores are correct
|
||||
"""
|
||||
"""Test that individual user scores are correct."""
|
||||
with self.assertNumQueries(8):
|
||||
assert Galaxy.compute_user_score(self.root) == 9
|
||||
assert Galaxy.compute_user_score(self.skia) == 10
|
||||
@ -60,9 +58,8 @@ class GalaxyTestModel(TestCase):
|
||||
assert Galaxy.compute_user_score(self.com) == 1
|
||||
|
||||
def test_users_score(self):
|
||||
"""
|
||||
Test on the default dataset generated by the `populate` command
|
||||
that the relation scores are correct
|
||||
"""Test on the default dataset generated by the `populate` command
|
||||
that the relation scores are correct.
|
||||
"""
|
||||
expected_scores = {
|
||||
"krophil": {
|
||||
@ -138,8 +135,7 @@ class GalaxyTestModel(TestCase):
|
||||
self.assertDictEqual(expected_scores, computed_scores)
|
||||
|
||||
def test_rule(self):
|
||||
"""
|
||||
Test on the default dataset generated by the `populate` command
|
||||
"""Test on the default dataset generated by the `populate` command
|
||||
that the number of queries to rule the galaxy is stable.
|
||||
"""
|
||||
galaxy = Galaxy.objects.create()
|
||||
@ -151,18 +147,14 @@ class GalaxyTestModel(TestCase):
|
||||
class GalaxyTestView(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""
|
||||
Generate a plausible Galaxy once for every test
|
||||
"""
|
||||
"""Generate a plausible Galaxy once for every test."""
|
||||
call_command("generate_galaxy_test_data", "-v", "0")
|
||||
galaxy = Galaxy.objects.create()
|
||||
galaxy.rule(26) # We want a fast test
|
||||
cls.root = User.objects.get(username="root")
|
||||
|
||||
def test_page_is_citizen(self):
|
||||
"""
|
||||
Test that users can access the galaxy page of users who are citizens
|
||||
"""
|
||||
"""Test that users can access the galaxy page of users who are citizens."""
|
||||
self.client.force_login(self.root)
|
||||
user = User.objects.get(last_name="n°500")
|
||||
response = self.client.get(reverse("galaxy:user", args=[user.id]))
|
||||
@ -173,20 +165,19 @@ class GalaxyTestView(TestCase):
|
||||
)
|
||||
|
||||
def test_page_not_citizen(self):
|
||||
"""
|
||||
Test that trying to access the galaxy page of a user who is not
|
||||
citizens return a 404
|
||||
"""
|
||||
"""Test that trying to access the galaxy page of non-citizen users return a 404."""
|
||||
self.client.force_login(self.root)
|
||||
user = User.objects.get(last_name="n°1")
|
||||
response = self.client.get(reverse("galaxy:user", args=[user.id]))
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_full_galaxy_state(self):
|
||||
"""
|
||||
Test on the more complex dataset generated by the `generate_galaxy_test_data`
|
||||
command that the relation scores are correct, and that the view exposes the
|
||||
right data.
|
||||
"""Test with a more complete galaxy.
|
||||
|
||||
This time, the test is done on
|
||||
the more complex dataset generated by the `generate_galaxy_test_data`
|
||||
command that the relation scores are correct,
|
||||
and that the view exposes the right data.
|
||||
"""
|
||||
self.client.force_login(self.root)
|
||||
response = self.client.get(reverse("galaxy:data"))
|
||||
|
@ -44,9 +44,7 @@ class Launderette(models.Model):
|
||||
return reverse("launderette:launderette_list")
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
"""Method to see if that object can be edited by the given user."""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
launderette_club = Club.objects.filter(
|
||||
@ -108,9 +106,7 @@ class Machine(models.Model):
|
||||
)
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
"""Method to see if that object can be edited by the given user."""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
launderette_club = Club.objects.filter(
|
||||
@ -161,9 +157,7 @@ class Token(models.Model):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
"""Method to see if that object can be edited by the given user."""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
launderette_club = Club.objects.filter(
|
||||
|
@ -38,26 +38,26 @@ from launderette.models import Launderette, Machine, Slot, Token
|
||||
|
||||
|
||||
class LaunderetteMainView(TemplateView):
|
||||
"""Main presentation view"""
|
||||
"""Main presentation view."""
|
||||
|
||||
template_name = "launderette/launderette_main.jinja"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add page to the context"""
|
||||
"""Add page to the context."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["page"] = Page.objects.filter(name="launderette").first()
|
||||
return kwargs
|
||||
|
||||
|
||||
class LaunderetteBookMainView(CanViewMixin, ListView):
|
||||
"""Choose which launderette to book"""
|
||||
"""Choose which launderette to book."""
|
||||
|
||||
model = Launderette
|
||||
template_name = "launderette/launderette_book_choose.jinja"
|
||||
|
||||
|
||||
class LaunderetteBookView(CanViewMixin, DetailView):
|
||||
"""Display the launderette schedule"""
|
||||
"""Display the launderette schedule."""
|
||||
|
||||
model = Launderette
|
||||
pk_url_kwarg = "launderette_id"
|
||||
@ -133,7 +133,7 @@ class LaunderetteBookView(CanViewMixin, DetailView):
|
||||
currentDate += delta
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add page to the context"""
|
||||
"""Add page to the context."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["planning"] = OrderedDict()
|
||||
kwargs["slot_type"] = self.slot_type
|
||||
@ -166,7 +166,7 @@ class LaunderetteBookView(CanViewMixin, DetailView):
|
||||
|
||||
|
||||
class SlotDeleteView(CanEditPropMixin, DeleteView):
|
||||
"""Delete a slot"""
|
||||
"""Delete a slot."""
|
||||
|
||||
model = Slot
|
||||
pk_url_kwarg = "slot_id"
|
||||
@ -180,14 +180,14 @@ class SlotDeleteView(CanEditPropMixin, DeleteView):
|
||||
|
||||
|
||||
class LaunderetteListView(CanEditPropMixin, ListView):
|
||||
"""Choose which launderette to administer"""
|
||||
"""Choose which launderette to administer."""
|
||||
|
||||
model = Launderette
|
||||
template_name = "launderette/launderette_list.jinja"
|
||||
|
||||
|
||||
class LaunderetteEditView(CanEditPropMixin, UpdateView):
|
||||
"""Edit a launderette"""
|
||||
"""Edit a launderette."""
|
||||
|
||||
model = Launderette
|
||||
pk_url_kwarg = "launderette_id"
|
||||
@ -196,7 +196,7 @@ class LaunderetteEditView(CanEditPropMixin, UpdateView):
|
||||
|
||||
|
||||
class LaunderetteCreateView(CanCreateMixin, CreateView):
|
||||
"""Create a new launderette"""
|
||||
"""Create a new launderette."""
|
||||
|
||||
model = Launderette
|
||||
fields = ["name"]
|
||||
@ -275,7 +275,7 @@ class ManageTokenForm(forms.Form):
|
||||
|
||||
|
||||
class LaunderetteAdminView(CanEditPropMixin, BaseFormView, DetailView):
|
||||
"""The admin page of the launderette"""
|
||||
"""The admin page of the launderette."""
|
||||
|
||||
model = Launderette
|
||||
pk_url_kwarg = "launderette_id"
|
||||
@ -297,9 +297,7 @@ class LaunderetteAdminView(CanEditPropMixin, BaseFormView, DetailView):
|
||||
return self.form_invalid(form)
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
We handle here the redirection, passing the user id of the asked customer
|
||||
"""
|
||||
"""We handle here the redirection, passing the user id of the asked customer."""
|
||||
form.process(self.object)
|
||||
if form.is_valid():
|
||||
return super().form_valid(form)
|
||||
@ -307,9 +305,7 @@ class LaunderetteAdminView(CanEditPropMixin, BaseFormView, DetailView):
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""
|
||||
We handle here the login form for the barman
|
||||
"""
|
||||
"""We handle here the login form for the barman."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
if self.request.method == "GET":
|
||||
kwargs["form"] = self.get_form()
|
||||
@ -331,7 +327,7 @@ class GetLaunderetteUserForm(GetUserForm):
|
||||
|
||||
|
||||
class LaunderetteMainClickView(CanEditMixin, BaseFormView, DetailView):
|
||||
"""The click page of the launderette"""
|
||||
"""The click page of the launderette."""
|
||||
|
||||
model = Launderette
|
||||
pk_url_kwarg = "launderette_id"
|
||||
@ -347,16 +343,12 @@ class LaunderetteMainClickView(CanEditMixin, BaseFormView, DetailView):
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
We handle here the redirection, passing the user id of the asked customer
|
||||
"""
|
||||
"""We handle here the redirection, passing the user id of the asked customer."""
|
||||
self.kwargs["user_id"] = form.cleaned_data["user_id"]
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""
|
||||
We handle here the login form for the barman
|
||||
"""
|
||||
"""We handle here the login form for the barman."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["counter"] = self.object.counter
|
||||
kwargs["form"] = self.get_form()
|
||||
@ -417,7 +409,7 @@ class ClickTokenForm(forms.BaseForm):
|
||||
|
||||
|
||||
class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView):
|
||||
"""The click page of the launderette"""
|
||||
"""The click page of the launderette."""
|
||||
|
||||
model = Launderette
|
||||
pk_url_kwarg = "launderette_id"
|
||||
@ -465,14 +457,14 @@ class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView):
|
||||
return type("ClickForm", (ClickTokenForm,), kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Simple get view"""
|
||||
"""Simple get view."""
|
||||
self.customer = Customer.objects.filter(user__id=self.kwargs["user_id"]).first()
|
||||
self.subscriber = self.customer.user
|
||||
self.operator = request.user
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Handle the many possibilities of the post request"""
|
||||
"""Handle the many possibilities of the post request."""
|
||||
self.object = self.get_object()
|
||||
self.customer = Customer.objects.filter(user__id=self.kwargs["user_id"]).first()
|
||||
self.subscriber = self.customer.user
|
||||
@ -480,16 +472,12 @@ class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView):
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
We handle here the redirection, passing the user id of the asked customer
|
||||
"""
|
||||
"""We handle here the redirection, passing the user id of the asked customer."""
|
||||
self.request.session.update(form.last_basket)
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""
|
||||
We handle here the login form for the barman
|
||||
"""
|
||||
"""We handle here the login form for the barman."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
if "form" not in kwargs.keys():
|
||||
kwargs["form"] = self.get_form()
|
||||
@ -505,7 +493,7 @@ class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView):
|
||||
|
||||
|
||||
class MachineEditView(CanEditPropMixin, UpdateView):
|
||||
"""Edit a machine"""
|
||||
"""Edit a machine."""
|
||||
|
||||
model = Machine
|
||||
pk_url_kwarg = "machine_id"
|
||||
@ -514,7 +502,7 @@ class MachineEditView(CanEditPropMixin, UpdateView):
|
||||
|
||||
|
||||
class MachineDeleteView(CanEditPropMixin, DeleteView):
|
||||
"""Edit a machine"""
|
||||
"""Edit a machine."""
|
||||
|
||||
model = Machine
|
||||
pk_url_kwarg = "machine_id"
|
||||
@ -523,7 +511,7 @@ class MachineDeleteView(CanEditPropMixin, DeleteView):
|
||||
|
||||
|
||||
class MachineCreateView(CanCreateMixin, CreateView):
|
||||
"""Create a new machine"""
|
||||
"""Create a new machine."""
|
||||
|
||||
model = Machine
|
||||
fields = ["name", "launderette", "type"]
|
||||
|
@ -155,9 +155,7 @@ class SearchFormListView(FormerSubscriberMixin, SingleObjectMixin, ListView):
|
||||
|
||||
|
||||
class SearchFormView(FormerSubscriberMixin, FormView):
|
||||
"""
|
||||
Allows users to search inside the user list
|
||||
"""
|
||||
"""Allows users to search inside the user list."""
|
||||
|
||||
form_class = SearchForm
|
||||
|
||||
@ -200,9 +198,7 @@ class SearchQuickFormView(SearchFormView):
|
||||
|
||||
|
||||
class SearchClearFormView(FormerSubscriberMixin, View):
|
||||
"""
|
||||
Clear SearchFormView and redirect to it
|
||||
"""
|
||||
"""Clear SearchFormView and redirect to it."""
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
super().dispatch(request, *args, **kwargs)
|
||||
|
@ -30,9 +30,7 @@ from pedagogy.models import UV, UVComment, UVCommentReport
|
||||
|
||||
|
||||
class UVForm(forms.ModelForm):
|
||||
"""
|
||||
Form handeling creation and edit of an UV
|
||||
"""
|
||||
"""Form handeling creation and edit of an UV."""
|
||||
|
||||
class Meta:
|
||||
model = UV
|
||||
@ -85,9 +83,7 @@ class StarList(forms.NumberInput):
|
||||
|
||||
|
||||
class UVCommentForm(forms.ModelForm):
|
||||
"""
|
||||
Form handeling creation and edit of an UVComment
|
||||
"""
|
||||
"""Form handeling creation and edit of an UVComment."""
|
||||
|
||||
class Meta:
|
||||
model = UVComment
|
||||
@ -137,9 +133,7 @@ class UVCommentForm(forms.ModelForm):
|
||||
|
||||
|
||||
class UVCommentReportForm(forms.ModelForm):
|
||||
"""
|
||||
Form handeling creation and edit of an UVReport
|
||||
"""
|
||||
"""Form handeling creation and edit of an UVReport."""
|
||||
|
||||
class Meta:
|
||||
model = UVCommentReport
|
||||
@ -159,9 +153,7 @@ class UVCommentReportForm(forms.ModelForm):
|
||||
|
||||
|
||||
class UVCommentModerationForm(forms.Form):
|
||||
"""
|
||||
Form handeling bulk comment deletion
|
||||
"""
|
||||
"""Form handeling bulk comment deletion."""
|
||||
|
||||
accepted_reports = forms.ModelMultipleChoiceField(
|
||||
UVCommentReport.objects.all(),
|
||||
|
@ -36,9 +36,7 @@ from core.models import User
|
||||
|
||||
|
||||
class UV(models.Model):
|
||||
"""
|
||||
Contains infos about an UV (course)
|
||||
"""
|
||||
"""Contains infos about an UV (course)."""
|
||||
|
||||
code = models.CharField(
|
||||
_("code"),
|
||||
@ -148,15 +146,11 @@ class UV(models.Model):
|
||||
return reverse("pedagogy:uv_detail", kwargs={"uv_id": self.id})
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""
|
||||
Can be created by superuser, root or pedagogy admin user
|
||||
"""
|
||||
"""Can be created by superuser, root or pedagogy admin user."""
|
||||
return user.is_in_group(pk=settings.SITH_GROUP_PEDAGOGY_ADMIN_ID)
|
||||
|
||||
def can_be_viewed_by(self, user):
|
||||
"""
|
||||
Only visible by subscribers
|
||||
"""
|
||||
"""Only visible by subscribers."""
|
||||
return user.is_subscribed
|
||||
|
||||
def __grade_average_generic(self, field):
|
||||
@ -166,14 +160,13 @@ class UV(models.Model):
|
||||
|
||||
return int(sum(comments.values_list(field, flat=True)) / comments.count())
|
||||
|
||||
def has_user_already_commented(self, user):
|
||||
"""
|
||||
Help prevent multiples comments from the same user
|
||||
This function checks that no other comment has been posted by a specified user
|
||||
def has_user_already_commented(self, user: User) -> bool:
|
||||
"""Help prevent multiples comments from the same user.
|
||||
|
||||
:param user: core.models.User
|
||||
:return: if the user has already posted a comment on this UV
|
||||
:rtype: bool
|
||||
This function checks that no other comment has been posted by a specified user.
|
||||
|
||||
Returns:
|
||||
True if the user has already posted a comment on this UV, else False.
|
||||
"""
|
||||
return self.comments.filter(author=user).exists()
|
||||
|
||||
@ -199,9 +192,7 @@ class UV(models.Model):
|
||||
|
||||
|
||||
class UVComment(models.Model):
|
||||
"""
|
||||
A comment about an UV
|
||||
"""
|
||||
"""A comment about an UV."""
|
||||
|
||||
author = models.ForeignKey(
|
||||
User,
|
||||
@ -261,28 +252,30 @@ class UVComment(models.Model):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""
|
||||
Is owned by a pedagogy admin, a superuser or the author himself
|
||||
"""
|
||||
"""Is owned by a pedagogy admin, a superuser or the author himself."""
|
||||
return self.author == user or user.is_owner(self.uv)
|
||||
|
||||
@cached_property
|
||||
def is_reported(self):
|
||||
"""
|
||||
Return True if someone reported this UV
|
||||
"""
|
||||
"""Return True if someone reported this UV."""
|
||||
return self.reports.exists()
|
||||
|
||||
|
||||
# TODO : it seems that some views were meant to be implemented
|
||||
# to use this model.
|
||||
# However, it seems that the implementation finally didn't happen.
|
||||
# It should be discussed, when possible, of what to do with that :
|
||||
# - go on and finally implement the UV results features ?
|
||||
# - or fuck go back and remove this model ?
|
||||
class UVResult(models.Model):
|
||||
"""
|
||||
Results got to an UV
|
||||
"""Results got to an UV.
|
||||
|
||||
Views will be implemented after the first release
|
||||
Will list every UV done by an user
|
||||
Linked to user
|
||||
uv
|
||||
Contains a grade settings.SITH_PEDAGOGY_UV_RESULT_GRADE
|
||||
a semester (P/A)20xx
|
||||
a semester (P/A)20xx.
|
||||
"""
|
||||
|
||||
uv = models.ForeignKey(
|
||||
@ -308,9 +301,7 @@ class UVResult(models.Model):
|
||||
|
||||
|
||||
class UVCommentReport(models.Model):
|
||||
"""
|
||||
Report an inapropriate comment
|
||||
"""
|
||||
"""Report an inapropriate comment."""
|
||||
|
||||
comment = models.ForeignKey(
|
||||
UVComment,
|
||||
@ -334,9 +325,7 @@ class UVCommentReport(models.Model):
|
||||
return self.comment.uv
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""
|
||||
Can be created by a pedagogy admin, a superuser or a subscriber
|
||||
"""
|
||||
"""Can be created by a pedagogy admin, a superuser or a subscriber."""
|
||||
return user.is_subscribed or user.is_owner(self.comment.uv)
|
||||
|
||||
|
||||
@ -344,9 +333,9 @@ class UVCommentReport(models.Model):
|
||||
|
||||
|
||||
class UVSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Custom seralizer for UVs
|
||||
Allow adding more informations like absolute_url
|
||||
"""Custom seralizer for UVs.
|
||||
|
||||
Allow adding more informations like absolute_url.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
|
@ -29,9 +29,7 @@ from pedagogy.models import UV
|
||||
|
||||
|
||||
class IndexSignalProcessor(signals.BaseSignalProcessor):
|
||||
"""
|
||||
Auto update index on CRUD operations
|
||||
"""
|
||||
"""Auto update index on CRUD operations."""
|
||||
|
||||
def setup(self):
|
||||
# Listen only to the ``UV`` model.
|
||||
@ -45,9 +43,7 @@ class IndexSignalProcessor(signals.BaseSignalProcessor):
|
||||
|
||||
|
||||
class UVIndex(indexes.SearchIndex, indexes.Indexable):
|
||||
"""
|
||||
Indexer class for UVs
|
||||
"""
|
||||
"""Indexer class for UVs."""
|
||||
|
||||
text = BigCharFieldIndex(document=True, use_template=True)
|
||||
auto = indexes.EdgeNgramField(use_template=True)
|
||||
|
@ -32,9 +32,7 @@ from pedagogy.models import UV, UVComment, UVCommentReport
|
||||
|
||||
|
||||
def create_uv_template(user_id, code="IFC1", exclude_list=None):
|
||||
"""
|
||||
Factory to help UV creation/update in post requests
|
||||
"""
|
||||
"""Factory to help UV creation/update in post requests."""
|
||||
if exclude_list is None:
|
||||
exclude_list = []
|
||||
uv = {
|
||||
@ -79,9 +77,7 @@ def create_uv_template(user_id, code="IFC1", exclude_list=None):
|
||||
|
||||
|
||||
class UVCreation(TestCase):
|
||||
"""
|
||||
Test uv creation
|
||||
"""
|
||||
"""Test uv creation."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -291,9 +287,7 @@ class UVUpdateTest(TestCase):
|
||||
|
||||
|
||||
def create_uv_comment_template(user_id, uv_code="PA00", exclude_list=None):
|
||||
"""
|
||||
Factory to help UVComment creation/update in post requests
|
||||
"""
|
||||
"""Factory to help UVComment creation/update in post requests."""
|
||||
if exclude_list is None:
|
||||
exclude_list = []
|
||||
comment = {
|
||||
@ -312,9 +306,9 @@ def create_uv_comment_template(user_id, uv_code="PA00", exclude_list=None):
|
||||
|
||||
|
||||
class UVCommentCreationAndDisplay(TestCase):
|
||||
"""
|
||||
Test UVComment creation and it's display
|
||||
Display and creation are the same view
|
||||
"""Test UVComment creation and its display.
|
||||
|
||||
Display and creation are the same view.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@ -575,10 +569,7 @@ class UVCommentUpdateTest(TestCase):
|
||||
|
||||
|
||||
class UVSearchTest(TestCase):
|
||||
"""
|
||||
Test UV guide rights for view and API
|
||||
Test that the API is working well
|
||||
"""
|
||||
"""Test UV guide rights for view and API."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -751,10 +742,7 @@ class UVSearchTest(TestCase):
|
||||
|
||||
|
||||
class UVModerationFormTest(TestCase):
|
||||
"""
|
||||
Test moderation view
|
||||
Assert access rights and if the form works well
|
||||
"""
|
||||
"""Assert access rights and if the form works well."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -967,9 +955,9 @@ class UVModerationFormTest(TestCase):
|
||||
|
||||
|
||||
class UVCommentReportCreateTest(TestCase):
|
||||
"""
|
||||
Test report creation view view
|
||||
Assert access rights and if you can create with it
|
||||
"""Test report creation view.
|
||||
|
||||
Assert access rights and if you can create with it.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
|
@ -57,21 +57,15 @@ from pedagogy.models import UV, UVComment, UVCommentReport, UVSerializer
|
||||
|
||||
|
||||
class CanCreateUVFunctionMixin(View):
|
||||
"""
|
||||
Add the function can_create_uv(user) into the template
|
||||
"""
|
||||
"""Add the function can_create_uv(user) into the template."""
|
||||
|
||||
@staticmethod
|
||||
def can_create_uv(user):
|
||||
"""
|
||||
Creates a dummy instance of UV and test is_owner
|
||||
"""
|
||||
"""Creates a dummy instance of UV and test is_owner."""
|
||||
return user.is_owner(UV())
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""
|
||||
Pass the function to the template
|
||||
"""
|
||||
"""Pass the function to the template."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["can_create_uv"] = self.can_create_uv
|
||||
return kwargs
|
||||
@ -81,9 +75,9 @@ class CanCreateUVFunctionMixin(View):
|
||||
|
||||
|
||||
class UVDetailFormView(CanViewMixin, CanCreateUVFunctionMixin, DetailFormView):
|
||||
"""
|
||||
Dispaly every comment of an UV and detailed infos about it
|
||||
Allow to comment the UV
|
||||
"""Display every comment of an UV and detailed infos about it.
|
||||
|
||||
Allow to comment the UV.
|
||||
"""
|
||||
|
||||
model = UV
|
||||
@ -109,9 +103,7 @@ class UVDetailFormView(CanViewMixin, CanCreateUVFunctionMixin, DetailFormView):
|
||||
|
||||
|
||||
class UVCommentUpdateView(CanEditPropMixin, UpdateView):
|
||||
"""
|
||||
Allow edit of a given comment
|
||||
"""
|
||||
"""Allow edit of a given comment."""
|
||||
|
||||
model = UVComment
|
||||
form_class = UVCommentForm
|
||||
@ -132,9 +124,7 @@ class UVCommentUpdateView(CanEditPropMixin, UpdateView):
|
||||
|
||||
|
||||
class UVCommentDeleteView(CanEditPropMixin, DeleteView):
|
||||
"""
|
||||
Allow delete of a given comment
|
||||
"""
|
||||
"""Allow delete of a given comment."""
|
||||
|
||||
model = UVComment
|
||||
pk_url_kwarg = "comment_id"
|
||||
@ -145,9 +135,7 @@ class UVCommentDeleteView(CanEditPropMixin, DeleteView):
|
||||
|
||||
|
||||
class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView):
|
||||
"""
|
||||
UV guide main page
|
||||
"""
|
||||
"""UV guide main page."""
|
||||
|
||||
# This is very basic and is prone to changment
|
||||
|
||||
@ -208,9 +196,7 @@ class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView):
|
||||
|
||||
|
||||
class UVCommentReportCreateView(CanCreateMixin, CreateView):
|
||||
"""
|
||||
Create a new report for an inapropriate comment
|
||||
"""
|
||||
"""Create a new report for an inapropriate comment."""
|
||||
|
||||
model = UVCommentReport
|
||||
form_class = UVCommentReportForm
|
||||
@ -253,9 +239,7 @@ class UVCommentReportCreateView(CanCreateMixin, CreateView):
|
||||
|
||||
|
||||
class UVModerationFormView(FormView):
|
||||
"""
|
||||
Moderation interface (Privileged)
|
||||
"""
|
||||
"""Moderation interface (Privileged)."""
|
||||
|
||||
form_class = UVCommentModerationForm
|
||||
template_name = "pedagogy/moderation.jinja"
|
||||
@ -286,9 +270,7 @@ class UVModerationFormView(FormView):
|
||||
|
||||
|
||||
class UVCreateView(CanCreateMixin, CreateView):
|
||||
"""
|
||||
Add a new UV (Privileged)
|
||||
"""
|
||||
"""Add a new UV (Privileged)."""
|
||||
|
||||
model = UV
|
||||
form_class = UVForm
|
||||
@ -304,9 +286,7 @@ class UVCreateView(CanCreateMixin, CreateView):
|
||||
|
||||
|
||||
class UVDeleteView(CanEditPropMixin, DeleteView):
|
||||
"""
|
||||
Allow to delete an UV (Privileged)
|
||||
"""
|
||||
"""Allow to delete an UV (Privileged)."""
|
||||
|
||||
model = UV
|
||||
pk_url_kwarg = "uv_id"
|
||||
@ -317,9 +297,7 @@ class UVDeleteView(CanEditPropMixin, DeleteView):
|
||||
|
||||
|
||||
class UVUpdateView(CanEditPropMixin, UpdateView):
|
||||
"""
|
||||
Allow to edit an UV (Privilegied)
|
||||
"""
|
||||
"""Allow to edit an UV (Privilegied)."""
|
||||
|
||||
model = UV
|
||||
form_class = UVForm
|
||||
|
@ -100,6 +100,9 @@ ignore = [
|
||||
"DJ001", # null=True in CharField/TextField. this one would require a migration
|
||||
]
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
DJANGO_SETTINGS_MODULE = "sith.settings"
|
||||
python_files = ["tests.py", "test_*.py", "*_tests.py"]
|
||||
|
@ -29,9 +29,7 @@ from rootplace.views import delete_all_forum_user_messages
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Delete all forum messages from a user
|
||||
"""
|
||||
"""Delete all forum messages from a user."""
|
||||
|
||||
help = "Delete all user's forum message"
|
||||
|
||||
|
@ -38,8 +38,8 @@ from forum.models import ForumMessageMeta
|
||||
|
||||
|
||||
def __merge_subscriptions(u1: User, u2: User):
|
||||
"""
|
||||
Give all the subscriptions of the second user to first one
|
||||
"""Give all the subscriptions of the second user to first one.
|
||||
|
||||
If some subscriptions are still active, update their end date
|
||||
to increase the overall subscription time of the first user.
|
||||
|
||||
@ -87,8 +87,8 @@ def __merge_pictures(u1: User, u2: User) -> None:
|
||||
|
||||
|
||||
def merge_users(u1: User, u2: User) -> User:
|
||||
"""
|
||||
Merge u2 into u1
|
||||
"""Merge u2 into u1.
|
||||
|
||||
This means that u1 shall receive everything that belonged to u2 :
|
||||
|
||||
- pictures
|
||||
@ -134,11 +134,12 @@ def merge_users(u1: User, u2: User) -> User:
|
||||
|
||||
|
||||
def delete_all_forum_user_messages(user, moderator, *, verbose=False):
|
||||
"""
|
||||
Create a ForumMessageMeta that says a forum
|
||||
message is deleted on every forum message of an user
|
||||
user: the user to delete messages from
|
||||
moderator: the one marked as the moderator
|
||||
"""Soft delete all messages of a user.
|
||||
|
||||
Args:
|
||||
user: the user to delete messages from
|
||||
moderator: the one marked as the moderator.
|
||||
verbose: it True, print the deleted messages
|
||||
"""
|
||||
for message in user.forum_messages.all():
|
||||
if message.is_deleted():
|
||||
@ -184,10 +185,10 @@ class MergeUsersView(FormView):
|
||||
|
||||
|
||||
class DeleteAllForumUserMessagesView(FormView):
|
||||
"""
|
||||
Delete all forum messages from an user
|
||||
"""Delete all forum messages from an user.
|
||||
|
||||
Messages are soft deleted and are still visible from admins
|
||||
GUI frontend to the dedicated command
|
||||
GUI frontend to the dedicated command.
|
||||
"""
|
||||
|
||||
template_name = "rootplace/delete_user_messages.jinja"
|
||||
@ -209,9 +210,7 @@ class DeleteAllForumUserMessagesView(FormView):
|
||||
|
||||
|
||||
class OperationLogListView(ListView, CanEditPropMixin):
|
||||
"""
|
||||
List all logs
|
||||
"""
|
||||
"""List all logs."""
|
||||
|
||||
model = OperationLog
|
||||
template_name = "rootplace/logs.jinja"
|
||||
|
@ -221,10 +221,7 @@ def sas_notification_callback(notif):
|
||||
|
||||
|
||||
class PeoplePictureRelation(models.Model):
|
||||
"""
|
||||
The PeoplePictureRelation class makes the connection between User and Picture
|
||||
|
||||
"""
|
||||
"""The PeoplePictureRelation class makes the connection between User and Picture."""
|
||||
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
|
@ -22,8 +22,7 @@
|
||||
#
|
||||
#
|
||||
|
||||
"""
|
||||
Django settings for sith project.
|
||||
"""Django settings for sith project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 1.8.6.
|
||||
|
||||
|
16
sith/urls.py
16
sith/urls.py
@ -13,22 +13,6 @@
|
||||
#
|
||||
#
|
||||
|
||||
"""sith URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/1.8/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Add an import: from blog import urls as blog_urls
|
||||
2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls))
|
||||
"""
|
||||
|
||||
from ajax_select import urls as ajax_select_urls
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
|
@ -13,8 +13,7 @@
|
||||
#
|
||||
#
|
||||
|
||||
"""
|
||||
WSGI config for sith project.
|
||||
"""WSGI config for sith project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
|
@ -31,9 +31,7 @@ from counter.models import Counter, ProductType
|
||||
|
||||
|
||||
class Stock(models.Model):
|
||||
"""
|
||||
The Stock class, this one is used to know how many products are left for a specific counter
|
||||
"""
|
||||
"""The Stock class, this one is used to know how many products are left for a specific counter."""
|
||||
|
||||
name = models.CharField(_("name"), max_length=64)
|
||||
counter = models.OneToOneField(
|
||||
@ -54,9 +52,7 @@ class Stock(models.Model):
|
||||
|
||||
|
||||
class StockItem(models.Model):
|
||||
"""
|
||||
The StockItem class, element of the stock
|
||||
"""
|
||||
"""The StockItem class, element of the stock."""
|
||||
|
||||
name = models.CharField(_("name"), max_length=64)
|
||||
unit_quantity = models.IntegerField(
|
||||
@ -95,9 +91,7 @@ class StockItem(models.Model):
|
||||
|
||||
|
||||
class ShoppingList(models.Model):
|
||||
"""
|
||||
The ShoppingList class, used to make an history of the shopping lists
|
||||
"""
|
||||
"""The ShoppingList class, used to make an history of the shopping lists."""
|
||||
|
||||
date = models.DateTimeField(_("date"))
|
||||
name = models.CharField(_("name"), max_length=64)
|
||||
@ -118,7 +112,7 @@ class ShoppingList(models.Model):
|
||||
|
||||
|
||||
class ShoppingListItem(models.Model):
|
||||
""""""
|
||||
"""An Item on a shopping list."""
|
||||
|
||||
shopping_lists = models.ManyToManyField(
|
||||
ShoppingList,
|
||||
|
@ -41,9 +41,7 @@ from stock.models import ShoppingList, ShoppingListItem, Stock, StockItem
|
||||
|
||||
|
||||
class StockItemList(CounterAdminTabsMixin, CanCreateMixin, ListView):
|
||||
"""
|
||||
The stockitems list view for the counter owner
|
||||
"""
|
||||
"""The stockitems list view for the counter owner."""
|
||||
|
||||
model = Stock
|
||||
template_name = "stock/stock_item_list.jinja"
|
||||
@ -58,9 +56,7 @@ class StockItemList(CounterAdminTabsMixin, CanCreateMixin, ListView):
|
||||
|
||||
|
||||
class StockListView(CounterAdminTabsMixin, CanViewMixin, ListView):
|
||||
"""
|
||||
A list view for the admins
|
||||
"""
|
||||
"""A list view for the admins."""
|
||||
|
||||
model = Stock
|
||||
template_name = "stock/stock_list.jinja"
|
||||
@ -68,9 +64,7 @@ class StockListView(CounterAdminTabsMixin, CanViewMixin, ListView):
|
||||
|
||||
|
||||
class StockEditForm(forms.ModelForm):
|
||||
"""
|
||||
A form to change stock's characteristics
|
||||
"""
|
||||
"""A form to change stock's characteristics."""
|
||||
|
||||
class Meta:
|
||||
model = Stock
|
||||
@ -84,9 +78,7 @@ class StockEditForm(forms.ModelForm):
|
||||
|
||||
|
||||
class StockEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView):
|
||||
"""
|
||||
An edit view for the stock
|
||||
"""
|
||||
"""An edit view for the stock."""
|
||||
|
||||
model = Stock
|
||||
form_class = modelform_factory(Stock, fields=["name", "counter"])
|
||||
@ -96,9 +88,7 @@ class StockEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView):
|
||||
|
||||
|
||||
class StockItemEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView):
|
||||
"""
|
||||
An edit view for a stock item
|
||||
"""
|
||||
"""An edit view for a stock item."""
|
||||
|
||||
model = StockItem
|
||||
form_class = modelform_factory(
|
||||
@ -118,9 +108,7 @@ class StockItemEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView):
|
||||
|
||||
|
||||
class StockCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView):
|
||||
"""
|
||||
A create view for a new Stock
|
||||
"""
|
||||
"""A create view for a new Stock."""
|
||||
|
||||
model = Stock
|
||||
form_class = modelform_factory(Stock, fields=["name", "counter"])
|
||||
@ -137,9 +125,7 @@ class StockCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView):
|
||||
|
||||
|
||||
class StockItemCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView):
|
||||
"""
|
||||
A create view for a new StockItem
|
||||
"""
|
||||
"""A create view for a new StockItem."""
|
||||
|
||||
model = StockItem
|
||||
form_class = modelform_factory(
|
||||
@ -170,9 +156,7 @@ class StockItemCreateView(CounterAdminTabsMixin, CanCreateMixin, CreateView):
|
||||
|
||||
|
||||
class StockShoppingListView(CounterAdminTabsMixin, CanViewMixin, ListView):
|
||||
"""
|
||||
A list view for the people to know the item to buy
|
||||
"""
|
||||
"""A list view for the people to know the item to buy."""
|
||||
|
||||
model = Stock
|
||||
template_name = "stock/stock_shopping_list.jinja"
|
||||
@ -225,9 +209,7 @@ class StockItemQuantityForm(forms.BaseForm):
|
||||
class StockItemQuantityBaseFormView(
|
||||
CounterAdminTabsMixin, CanEditMixin, DetailView, BaseFormView
|
||||
):
|
||||
"""
|
||||
docstring for StockItemOutList
|
||||
"""
|
||||
"""docstring for StockItemOutList."""
|
||||
|
||||
model = StockItem
|
||||
template_name = "stock/shopping_list_quantity.jinja"
|
||||
@ -266,16 +248,12 @@ class StockItemQuantityBaseFormView(
|
||||
return type("StockItemQuantityForm", (StockItemQuantityForm,), kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Simple get view
|
||||
"""
|
||||
"""Simple get view."""
|
||||
self.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Handle the many possibilities of the post request
|
||||
"""
|
||||
"""Handle the many possibilities of the post request."""
|
||||
self.object = self.get_object()
|
||||
self.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first()
|
||||
return super().post(request, *args, **kwargs)
|
||||
@ -297,7 +275,7 @@ class StockItemQuantityBaseFormView(
|
||||
|
||||
|
||||
class StockShoppingListItemListView(CounterAdminTabsMixin, CanViewMixin, ListView):
|
||||
"""docstring for StockShoppingListItemListView"""
|
||||
"""docstring for StockShoppingListItemListView."""
|
||||
|
||||
model = ShoppingList
|
||||
template_name = "stock/shopping_list_items.jinja"
|
||||
@ -314,9 +292,7 @@ class StockShoppingListItemListView(CounterAdminTabsMixin, CanViewMixin, ListVie
|
||||
|
||||
|
||||
class StockShoppingListDeleteView(CounterAdminTabsMixin, CanEditMixin, DeleteView):
|
||||
"""
|
||||
Delete a ShoppingList (for the resonsible account)
|
||||
"""
|
||||
"""Delete a ShoppingList (for the resonsible account)."""
|
||||
|
||||
model = ShoppingList
|
||||
pk_url_kwarg = "shoppinglist_id"
|
||||
@ -330,9 +306,7 @@ class StockShoppingListDeleteView(CounterAdminTabsMixin, CanEditMixin, DeleteVie
|
||||
|
||||
|
||||
class StockShopppingListSetDone(CanEditMixin, DetailView):
|
||||
"""
|
||||
Set a ShoppingList as done
|
||||
"""
|
||||
"""Set a ShoppingList as done."""
|
||||
|
||||
model = ShoppingList
|
||||
pk_url_kwarg = "shoppinglist_id"
|
||||
@ -361,9 +335,7 @@ class StockShopppingListSetDone(CanEditMixin, DetailView):
|
||||
|
||||
|
||||
class StockShopppingListSetTodo(CanEditMixin, DetailView):
|
||||
"""
|
||||
Set a ShoppingList as done
|
||||
"""
|
||||
"""Set a ShoppingList as done."""
|
||||
|
||||
model = ShoppingList
|
||||
pk_url_kwarg = "shoppinglist_id"
|
||||
@ -415,9 +387,7 @@ class StockUpdateAfterShopppingForm(forms.BaseForm):
|
||||
class StockUpdateAfterShopppingBaseFormView(
|
||||
CounterAdminTabsMixin, CanEditMixin, DetailView, BaseFormView
|
||||
):
|
||||
"""
|
||||
docstring for StockUpdateAfterShopppingBaseFormView
|
||||
"""
|
||||
"""docstring for StockUpdateAfterShopppingBaseFormView."""
|
||||
|
||||
model = ShoppingList
|
||||
template_name = "stock/update_after_shopping.jinja"
|
||||
@ -453,9 +423,7 @@ class StockUpdateAfterShopppingBaseFormView(
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Handle the many possibilities of the post request
|
||||
"""
|
||||
"""Handle the many possibilities of the post request."""
|
||||
self.object = self.get_object()
|
||||
self.shoppinglist = ShoppingList.objects.filter(
|
||||
id=self.kwargs["shoppinglist_id"]
|
||||
@ -463,9 +431,7 @@ class StockUpdateAfterShopppingBaseFormView(
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
We handle here the redirection
|
||||
"""
|
||||
"""We handle here the redirection."""
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@ -484,9 +450,7 @@ class StockUpdateAfterShopppingBaseFormView(
|
||||
|
||||
|
||||
class StockTakeItemsForm(forms.BaseForm):
|
||||
"""
|
||||
docstring for StockTakeItemsFormView
|
||||
"""
|
||||
"""docstring for StockTakeItemsFormView."""
|
||||
|
||||
def clean(self):
|
||||
with transaction.atomic():
|
||||
@ -502,9 +466,7 @@ class StockTakeItemsForm(forms.BaseForm):
|
||||
class StockTakeItemsBaseFormView(
|
||||
CounterTabsMixin, CanEditMixin, DetailView, BaseFormView
|
||||
):
|
||||
"""
|
||||
docstring for StockTakeItemsBaseFormView
|
||||
"""
|
||||
"""docstring for StockTakeItemsBaseFormView."""
|
||||
|
||||
model = StockItem
|
||||
template_name = "stock/stock_take_items.jinja"
|
||||
@ -535,16 +497,11 @@ class StockTakeItemsBaseFormView(
|
||||
return type("StockTakeItemsForm", (StockTakeItemsForm,), kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Simple get view
|
||||
"""
|
||||
self.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Handle the many possibilities of the post request
|
||||
"""
|
||||
"""Handle the many possibilities of the post request."""
|
||||
self.object = self.get_object()
|
||||
self.stock = Stock.objects.filter(id=self.kwargs["stock_id"]).first()
|
||||
if self.stock.counter.type == "BAR" and not (
|
||||
|
@ -108,14 +108,15 @@ class Subscription(models.Model):
|
||||
|
||||
@staticmethod
|
||||
def compute_start(d: date = None, duration: int = 1, user: User = None) -> date:
|
||||
"""
|
||||
This function computes the start date of the subscription with respect to the given date (default is today),
|
||||
"""Computes the start date of the subscription.
|
||||
|
||||
The computation is done with respect to the given date (default is today)
|
||||
and the start date given in settings.SITH_SEMESTER_START_AUTUMN.
|
||||
It takes the nearest past start date.
|
||||
Exemples: with SITH_SEMESTER_START_AUTUMN = (8, 15)
|
||||
Today -> Start date
|
||||
2015-03-17 -> 2015-02-15
|
||||
2015-01-11 -> 2014-08-15
|
||||
2015-01-11 -> 2014-08-15.
|
||||
"""
|
||||
if not d:
|
||||
d = date.today()
|
||||
@ -129,14 +130,21 @@ class Subscription(models.Model):
|
||||
|
||||
@staticmethod
|
||||
def compute_end(duration: int, start: date = None, user: User = None) -> date:
|
||||
"""
|
||||
This function compute the end date of the subscription given a start date and a duration in number of semester
|
||||
Exemple:
|
||||
"""Compute the end date of the subscription.
|
||||
|
||||
Args:
|
||||
duration:
|
||||
the duration of the subscription, in semester
|
||||
(for example, 2 => 2 semesters => 1 year)
|
||||
start: The start date of the subscription
|
||||
user: the user which is (or will be) subscribed
|
||||
|
||||
Exemples:
|
||||
Start - Duration -> End date
|
||||
2015-09-18 - 1 -> 2016-03-18
|
||||
2015-09-18 - 2 -> 2016-09-18
|
||||
2015-09-18 - 3 -> 2017-03-18
|
||||
2015-09-18 - 4 -> 2017-09-18
|
||||
2015-09-18 - 4 -> 2017-09-18.
|
||||
"""
|
||||
if start is None:
|
||||
start = Subscription.compute_start(duration=duration, user=user)
|
||||
|
@ -45,8 +45,8 @@ class AvailableTrombiManager(models.Manager):
|
||||
|
||||
|
||||
class Trombi(models.Model):
|
||||
"""
|
||||
This is the main class, the Trombi itself.
|
||||
"""Main class of the trombi, the Trombi itself.
|
||||
|
||||
It contains the deadlines for the users, and the link to the club that makes
|
||||
its Trombi.
|
||||
"""
|
||||
@ -103,10 +103,10 @@ class Trombi(models.Model):
|
||||
|
||||
|
||||
class TrombiUser(models.Model):
|
||||
"""
|
||||
This class is only here to avoid cross references between the core, club,
|
||||
and trombi modules. It binds a User to a Trombi without needing to import
|
||||
Trombi into the core.
|
||||
"""Bound between a `User` and a `Trombi`.
|
||||
|
||||
This class is here to avoid cross-references between the core, club,
|
||||
and trombi modules.
|
||||
It also adds the pictures to the profile without needing all the security
|
||||
like the other SithFiles.
|
||||
"""
|
||||
@ -172,10 +172,7 @@ class TrombiUser(models.Model):
|
||||
|
||||
|
||||
class TrombiComment(models.Model):
|
||||
"""
|
||||
This represent a comment given by someone to someone else in the same Trombi
|
||||
instance.
|
||||
"""
|
||||
"""A comment given by someone to someone else in the same Trombi instance."""
|
||||
|
||||
author = models.ForeignKey(
|
||||
TrombiUser,
|
||||
@ -202,9 +199,7 @@ class TrombiComment(models.Model):
|
||||
|
||||
|
||||
class TrombiClubMembership(models.Model):
|
||||
"""
|
||||
This represent a membership to a club
|
||||
"""
|
||||
"""A membership in a club."""
|
||||
|
||||
user = models.ForeignKey(
|
||||
TrombiUser,
|
||||
|
@ -94,9 +94,7 @@ class TrombiTabsMixin(TabedViewMixin):
|
||||
|
||||
|
||||
class UserIsInATrombiMixin(View):
|
||||
"""
|
||||
This view check if the requested user has a trombi_user attribute
|
||||
"""
|
||||
"""Check if the requested user has a trombi_user attribute."""
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not hasattr(self.request.user, "trombi_user"):
|
||||
@ -118,18 +116,14 @@ class TrombiForm(forms.ModelForm):
|
||||
|
||||
|
||||
class TrombiCreateView(CanCreateMixin, CreateView):
|
||||
"""
|
||||
Create a trombi for a club
|
||||
"""
|
||||
"""Create a trombi for a club."""
|
||||
|
||||
model = Trombi
|
||||
form_class = TrombiForm
|
||||
template_name = "core/create.jinja"
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Affect club
|
||||
"""
|
||||
"""Affect club."""
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
club = get_object_or_404(Club, id=self.kwargs["club_id"])
|
||||
@ -304,9 +298,7 @@ class UserTrombiForm(forms.Form):
|
||||
class UserTrombiToolsView(
|
||||
QuickNotifMixin, TrombiTabsMixin, UserIsLoggedMixin, TemplateView
|
||||
):
|
||||
"""
|
||||
Display a user's trombi tools
|
||||
"""
|
||||
"""Display a user's trombi tools."""
|
||||
|
||||
template_name = "trombi/user_tools.jinja"
|
||||
current_tab = "tools"
|
||||
@ -466,9 +458,7 @@ class UserTrombiProfileView(TrombiTabsMixin, DetailView):
|
||||
|
||||
|
||||
class TrombiCommentFormView(LoginRequiredMixin, View):
|
||||
"""
|
||||
Create/edit a trombi comment
|
||||
"""
|
||||
"""Create/edit a trombi comment."""
|
||||
|
||||
model = TrombiComment
|
||||
fields = ["content"]
|
||||
|
Loading…
Reference in New Issue
Block a user