diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d478e6906..2ebeca97d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -37,7 +37,7 @@ jobs: pushd ${{secrets.SITH_PATH}} git pull - poetry install + poetry install --with prod --without docs,tests poetry run ./manage.py install_xapian poetry run ./manage.py migrate echo "yes" | poetry run ./manage.py collectstatic diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml new file mode 100644 index 000000000..16adb95a6 --- /dev/null +++ b/.github/workflows/deploy_docs.yml @@ -0,0 +1,21 @@ +name: deploy_docs +on: + push: + branches: + - master +permissions: + contents: write +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: ./.github/actions/setup_project + - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV + - uses: actions/cache@v3 + with: + key: mkdocs-material-${{ env.cache_id }} + path: .cache + restore-keys: | + mkdocs-material- + - run: poetry run mkdocs gh-deploy --force \ No newline at end of file diff --git a/.github/workflows/taiste.yml b/.github/workflows/taiste.yml index d7e1e9d9c..b83682ecf 100644 --- a/.github/workflows/taiste.yml +++ b/.github/workflows/taiste.yml @@ -36,7 +36,7 @@ jobs: pushd ${{secrets.SITH_PATH}} git pull - poetry install + poetry install --with prod --without docs,tests poetry run ./manage.py install_xapian poetry run ./manage.py migrate echo "yes" | poetry run ./manage.py collectstatic diff --git a/.readthedocs.yml b/.readthedocs.yml deleted file mode 100644 index 481160ffd..000000000 --- a/.readthedocs.yml +++ /dev/null @@ -1,26 +0,0 @@ -# Read the Docs configuration file -# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details - -# Required -version: 2 - -# Allow installing xapian-bindings in pip -build: - apt_packages: - - libxapian-dev - -# Build documentation in the doc/ directory with Sphinx -sphinx: - configuration: doc/conf.py - -# Optionally build your docs in additional formats such as PDF and ePub -formats: all - -# Optionally set the version of Python and requirements required to build your docs -python: - version: "3.8" - install: - - method: pip - path: . - extra_requirements: - - docs diff --git a/README.md b/README.md index bf818ec63..f27dc28da 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,20 @@ -

- - - - - - - - - - - - -

+# Sith -

This is the source code of the UTBM's student association available at https://ae.utbm.fr/.

+[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](#) +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) +[![CI status](https://github.com/ae-utbm/sith3/actions/workflows/ci.yml/badge.svg)](#) +[![Docs status](https://github.com/ae-utbm/sith3/actions/workflows/deploy_docs.yml/badge.svg)](https://ae-utbm.github.io/sith3) +[![Built with Material for MkDocs](https://img.shields.io/badge/Material_for_MkDocs-526CFE?style=default&logo=MaterialForMkDocs&logoColor=white)](https://squidfunk.github.io/mkdocs-material/) +[![discord](https://img.shields.io/discord/971448179075731476?label=discord&logo=discord&style=default)](https://discord.gg/xk9wfpsufm) -

All documentation is in the docs directory and online at https://sith-ae.readthedocs.io/. This documentation is written in French because it targets a French audience and it's too much work to maintain two versions. The code and code comments are strictly written in English.

+### This is the source code of the UTBM's student association available at [https://ae.utbm.fr/](https://ae.utbm.fr/). -

If you want to contribute, here's how we recommend to read the docs:

+All documentation is in the `docs` directory and online at [https://ae-utbm.github.io/sith3](https://ae-utbm.github.io/sith3). This documentation is written in French because it targets a French audience and it's too much work to maintain two versions. The code and code comments are strictly written in English. - +#### If you want to contribute, here's how we recommend to read the docs: + +* First, it's advised to read the about part of the project to understand the goals and the mindset of the current and previous maintainers and know what to expect to learn. +* If in the first part you realize that you need more background about what we use, we provide some links to tutorials and documentation at the end of our documentation. Feel free to use it and complete it with what you found helpful. +* Keep in mind that this documentation is thought to be read in order. > This project is licensed under GNU GPL, see the LICENSE file at the top of the repository for more details. diff --git a/accounting/models.py b/accounting/models.py index 254a41ba6..9276441e4 100644 --- a/accounting/models.py +++ b/accounting/models.py @@ -29,9 +29,7 @@ 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 @@ def get_display_name(self): 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 @@ def get_absolute_url(self): 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 @@ def get_absolute_url(self): 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 @@ def is_owned_by(self, user): 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 @@ def get_display_name(self): 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 @@ def get_absolute_url(self): 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 @@ def is_owned_by(self, user): 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 @@ def update_amounts(self): 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 @@ def get_target(self): 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 @@ def is_owned_by(self, user): 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 @@ def can_be_edited_by(self, user): 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 @@ def get_absolute_url(self): 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 @@ def is_owned_by(self, user): 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 @@ def get_movement_type_display(self): 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( diff --git a/accounting/views.py b/accounting/views.py index 691bbbdc0..85d1a4c72 100644 --- a/accounting/views.py +++ b/accounting/views.py @@ -53,9 +53,7 @@ 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 @@ def get_initial(self): 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 @@ def get_list_of_tabs(self): class JournalCreateView(CanCreateMixin, CreateView): - """ - Create a general journal - """ + """Create a general journal.""" model = GeneralJournal form_class = modelform_factory( @@ -304,9 +272,7 @@ def get_initial(self): 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 @@ def save(self): class OperationCreateView(CanCreateMixin, CreateView): - """ - Create an operation - """ + """Create an operation.""" model = Operation form_class = OperationForm @@ -487,7 +447,7 @@ def get_initial(self): 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 @@ def get_context_data(self, **kwargs): 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 @@ def get(self, request, *args, **kwargs): 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 @@ def big_statement(self): 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 @@ def total(self, movement_type): 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 @@ def get_context_data(self, **kwargs): 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 @@ def statement(self): 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 diff --git a/api/views/__init__.py b/api/views/__init__.py index d5ca62897..5017806b4 100644 --- a/api/views/__init__.py +++ b/api/views/__init__.py @@ -23,9 +23,9 @@ 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) diff --git a/api/views/api.py b/api/views/api.py index 6e3a056d2..9de0a87e2 100644 --- a/api/views/api.py +++ b/api/views/api.py @@ -23,9 +23,7 @@ @api_view(["POST"]) @renderer_classes((StaticHTMLRenderer,)) def RenderMarkdown(request): - """ - Render Markdown - """ + """Render Markdown.""" try: data = markdown(request.POST["text"]) except: diff --git a/api/views/club.py b/api/views/club.py index 2333fffb6..6c4c5b0bd 100644 --- a/api/views/club.py +++ b/api/views/club.py @@ -31,9 +31,7 @@ class Meta: class ClubViewSet(RightModelViewSet): - """ - Manage Clubs (api/v1/club/) - """ + """Manage Clubs (api/v1/club/).""" serializer_class = ClubSerializer queryset = Club.objects.all() diff --git a/api/views/counter.py b/api/views/counter.py index a9fd64ce3..3fbaa9313 100644 --- a/api/views/counter.py +++ b/api/views/counter.py @@ -33,18 +33,14 @@ class Meta: 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) diff --git a/api/views/group.py b/api/views/group.py index c9183ed01..da37dbbc3 100644 --- a/api/views/group.py +++ b/api/views/group.py @@ -25,9 +25,7 @@ class Meta: class GroupViewSet(RightModelViewSet): - """ - Manage Groups (api/v1/group/) - """ + """Manage Groups (api/v1/group/).""" serializer_class = GroupSerializer queryset = RealGroup.objects.all() diff --git a/api/views/launderette.py b/api/views/launderette.py index eae35a190..cb88d80c9 100644 --- a/api/views/launderette.py +++ b/api/views/launderette.py @@ -60,54 +60,42 @@ class Meta: 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 @@ def avaliable(self, request): @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 ) diff --git a/api/views/user.py b/api/views/user.py index d5aecd15f..84078ce28 100644 --- a/api/views/user.py +++ b/api/views/user.py @@ -39,9 +39,9 @@ class Meta: 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) diff --git a/api/views/uv.py b/api/views/uv.py index a83a89365..09292d8ef 100644 --- a/api/views/uv.py +++ b/api/views/uv.py @@ -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"] diff --git a/club/forms.py b/club/forms.py index ad3273c69..3a21fd6dd 100644 --- a/club/forms.py +++ b/club/forms.py @@ -44,9 +44,7 @@ def __init__(self, *args, **kwargs): 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 @@ def __init__(self, club_id, user_id, mailings, *args, **kwargs): ) 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 @@ def __init__(self, club, *args, **kwargs): 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 @@ def __init__(self, *args, **kwargs): 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 @@ def clean_users(self): 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"]: diff --git a/club/models.py b/club/models.py index e315f1d28..0ed513892 100644 --- a/club/models.py +++ b/club/models.py @@ -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 @@ def president(self): ).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 @@ def get_display_name(self): 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 @@ def get_full_logo_url(self): 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 @@ def has_rights_in_club(self, user): 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 @@ def board(self) -> "MembershipQuerySet": 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 @@ def update(self, **kwargs): ) 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 @@ def delete(self): 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 @@ def get_absolute_url(self): 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 @@ def delete(self, *args, **kwargs): 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 @@ def fetch_format(self): class MailingSubscription(models.Model): - """ - This class makes the link between user and mailing list - """ + """Link between user and mailing list.""" mailing = models.ForeignKey( Mailing, diff --git a/club/tests.py b/club/tests.py index b893cb993..af8aafde2 100644 --- a/club/tests.py +++ b/club/tests.py @@ -29,8 +29,8 @@ 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 @@ def setUp(self): 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 @@ def test_ongoing(self): 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 @@ def test_board(self): 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 @@ def test_ongoing_board(self): 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 @@ def test_update_invalidate_cache(self): 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 @@ def test_delete_invalidate_cache(self): 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 @@ def assert_membership_started_today(self, user: User, role: int): 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 @@ def test_access_unauthorized(self): 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 @@ def test_display(self): 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 @@ def test_root_add_one_club_member(self): 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 @@ def test_root_add_multiple_club_member(self): 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 @@ def test_add_unauthorized_members(self): assert '