diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d478e690..2ebeca97 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 00000000..16adb95a --- /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 d7e1e9d9..b83682ec 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 481160ff..00000000 --- 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 bf818ec6..f27dc28d 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 254a41ba..9276441e 100644 --- a/accounting/models.py +++ b/accounting/models.py @@ -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( diff --git a/accounting/tests.py b/accounting/tests.py index 78a8c0ef..610a574d 100644 --- a/accounting/tests.py +++ b/accounting/tests.py @@ -28,7 +28,7 @@ from accounting.models import ( from core.models import User -class RefoundAccountTest(TestCase): +class TestRefoundAccount(TestCase): @classmethod def setUpTestData(cls): cls.skia = User.objects.get(username="skia") @@ -67,7 +67,7 @@ class RefoundAccountTest(TestCase): assert self.skia.customer.amount == 0 -class JournalTest(TestCase): +class TestJournal(TestCase): @classmethod def setUpTestData(cls): cls.journal = GeneralJournal.objects.get(id=1) @@ -91,7 +91,7 @@ class JournalTest(TestCase): assert "M\xc3\xa9thode de paiement" not in str(response_get.content) -class OperationTest(TestCase): +class TestOperation(TestCase): def setUp(self): self.tomorrow_formatted = (date.today() + timedelta(days=1)).strftime( "%d/%m/%Y" diff --git a/accounting/views.py b/accounting/views.py index 691bbbdc..85d1a4c7 100644 --- a/accounting/views.py +++ b/accounting/views.py @@ -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 diff --git a/api/__init__.py b/api/__init__.py deleted file mode 100644 index a098e7ba..00000000 --- a/api/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# -# Copyright 2023 © AE UTBM -# ae@utbm.fr / ae.info@utbm.fr -# -# This file is part of the website of the UTBM Student Association (AE UTBM), -# https://ae.utbm.fr. -# -# You can find the source code of the website at https://github.com/ae-utbm/sith3 -# -# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) -# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE -# OR WITHIN THE LOCAL FILE "LICENSE" -# -# diff --git a/api/admin.py b/api/admin.py deleted file mode 100644 index 1a02ff3a..00000000 --- a/api/admin.py +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright 2023 © AE UTBM -# ae@utbm.fr / ae.info@utbm.fr -# -# This file is part of the website of the UTBM Student Association (AE UTBM), -# https://ae.utbm.fr. -# -# You can find the source code of the website at https://github.com/ae-utbm/sith3 -# -# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) -# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE -# OR WITHIN THE LOCAL FILE "LICENSE" -# -# - -# Register your models here. diff --git a/api/models.py b/api/models.py deleted file mode 100644 index c6372d7f..00000000 --- a/api/models.py +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright 2023 © AE UTBM -# ae@utbm.fr / ae.info@utbm.fr -# -# This file is part of the website of the UTBM Student Association (AE UTBM), -# https://ae.utbm.fr. -# -# You can find the source code of the website at https://github.com/ae-utbm/sith3 -# -# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) -# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE -# OR WITHIN THE LOCAL FILE "LICENSE" -# -# - -# Create your models here. diff --git a/api/urls.py b/api/urls.py deleted file mode 100644 index 15bc6839..00000000 --- a/api/urls.py +++ /dev/null @@ -1,49 +0,0 @@ -# -# Copyright 2023 © AE UTBM -# ae@utbm.fr / ae.info@utbm.fr -# -# This file is part of the website of the UTBM Student Association (AE UTBM), -# https://ae.utbm.fr. -# -# You can find the source code of the website at https://github.com/ae-utbm/sith3 -# -# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) -# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE -# OR WITHIN THE LOCAL FILE "LICENSE" -# -# - -from django.urls import include, path, re_path -from rest_framework import routers - -from api.views import * - -# Router config -router = routers.DefaultRouter() -router.register(r"counter", CounterViewSet, basename="api_counter") -router.register(r"user", UserViewSet, basename="api_user") -router.register(r"club", ClubViewSet, basename="api_club") -router.register(r"group", GroupViewSet, basename="api_group") - -# Launderette -router.register( - r"launderette/place", LaunderettePlaceViewSet, basename="api_launderette_place" -) -router.register( - r"launderette/machine", - LaunderetteMachineViewSet, - basename="api_launderette_machine", -) -router.register( - r"launderette/token", LaunderetteTokenViewSet, basename="api_launderette_token" -) - -urlpatterns = [ - # API - re_path(r"^", include(router.urls)), - re_path(r"^login/", include("rest_framework.urls", namespace="rest_framework")), - re_path(r"^markdown$", RenderMarkdown, name="api_markdown"), - re_path(r"^mailings$", FetchMailingLists, name="mailings_fetch"), - re_path(r"^uv$", uv_endpoint, name="uv_endpoint"), - path("sas/", all_pictures_of_user_endpoint, name="all_pictures_of_user"), -] diff --git a/api/views/__init__.py b/api/views/__init__.py deleted file mode 100644 index d5ca6289..00000000 --- a/api/views/__init__.py +++ /dev/null @@ -1,72 +0,0 @@ -# -# Copyright 2023 © AE UTBM -# ae@utbm.fr / ae.info@utbm.fr -# -# This file is part of the website of the UTBM Student Association (AE UTBM), -# https://ae.utbm.fr. -# -# You can find the source code of the website at https://github.com/ae-utbm/sith3 -# -# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) -# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE -# OR WITHIN THE LOCAL FILE "LICENSE" -# -# - -from django.core.exceptions import PermissionDenied -from django.db.models.query import QuerySet -from rest_framework import viewsets -from rest_framework.decorators import action -from rest_framework.response import Response - -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 - """ - if isinstance(obj, QuerySet): - for o in obj: - if test(o, user) is False: - return False - return True - else: - return test(obj, user) - - -class ManageModelMixin: - @action(detail=True) - def id(self, request, pk=None): - """ - 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) - - -class RightModelViewSet(ManageModelMixin, viewsets.ModelViewSet): - def dispatch(self, request, *arg, **kwargs): - res = super().dispatch(request, *arg, **kwargs) - obj = self.queryset - user = self.request.user - try: - if request.method == "GET" and check_if(obj, user, can_view): - return res - if request.method != "GET" and check_if(obj, user, can_edit): - return res - except: - pass # To prevent bug with Anonymous user - raise PermissionDenied - - -from .api import * -from .club import * -from .counter import * -from .group import * -from .launderette import * -from .sas import * -from .user import * -from .uv import * diff --git a/api/views/api.py b/api/views/api.py deleted file mode 100644 index 6e3a056d..00000000 --- a/api/views/api.py +++ /dev/null @@ -1,33 +0,0 @@ -# -# Copyright 2023 © AE UTBM -# ae@utbm.fr / ae.info@utbm.fr -# -# This file is part of the website of the UTBM Student Association (AE UTBM), -# https://ae.utbm.fr. -# -# You can find the source code of the website at https://github.com/ae-utbm/sith3 -# -# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) -# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE -# OR WITHIN THE LOCAL FILE "LICENSE" -# -# - -from rest_framework.decorators import api_view, renderer_classes -from rest_framework.renderers import StaticHTMLRenderer -from rest_framework.response import Response - -from core.templatetags.renderer import markdown - - -@api_view(["POST"]) -@renderer_classes((StaticHTMLRenderer,)) -def RenderMarkdown(request): - """ - Render Markdown - """ - try: - data = markdown(request.POST["text"]) - except: - data = "Error" - return Response(data) diff --git a/api/views/club.py b/api/views/club.py deleted file mode 100644 index 2333fffb..00000000 --- a/api/views/club.py +++ /dev/null @@ -1,53 +0,0 @@ -# -# Copyright 2023 © AE UTBM -# ae@utbm.fr / ae.info@utbm.fr -# -# This file is part of the website of the UTBM Student Association (AE UTBM), -# https://ae.utbm.fr. -# -# You can find the source code of the website at https://github.com/ae-utbm/sith3 -# -# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) -# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE -# OR WITHIN THE LOCAL FILE "LICENSE" -# -# - -from django.conf import settings -from django.core.exceptions import PermissionDenied -from rest_framework import serializers -from rest_framework.decorators import api_view, renderer_classes -from rest_framework.renderers import StaticHTMLRenderer -from rest_framework.response import Response - -from api.views import RightModelViewSet -from club.models import Club, Mailing - - -class ClubSerializer(serializers.ModelSerializer): - class Meta: - model = Club - fields = ("id", "name", "unix_name", "address", "members") - - -class ClubViewSet(RightModelViewSet): - """ - Manage Clubs (api/v1/club/) - """ - - serializer_class = ClubSerializer - queryset = Club.objects.all() - - -@api_view(["GET"]) -@renderer_classes((StaticHTMLRenderer,)) -def FetchMailingLists(request): - key = request.GET.get("key", "") - if key != settings.SITH_MAILING_FETCH_KEY: - raise PermissionDenied - data = "" - for mailing in Mailing.objects.filter( - is_moderated=True, club__is_active=True - ).all(): - data += mailing.fetch_format() + "\n" - return Response(data) diff --git a/api/views/counter.py b/api/views/counter.py deleted file mode 100644 index a9fd64ce..00000000 --- a/api/views/counter.py +++ /dev/null @@ -1,50 +0,0 @@ -# -# Copyright 2023 © AE UTBM -# ae@utbm.fr / ae.info@utbm.fr -# -# This file is part of the website of the UTBM Student Association (AE UTBM), -# https://ae.utbm.fr. -# -# You can find the source code of the website at https://github.com/ae-utbm/sith3 -# -# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) -# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE -# OR WITHIN THE LOCAL FILE "LICENSE" -# -# - -from rest_framework import serializers -from rest_framework.decorators import action -from rest_framework.response import Response - -from api.views import RightModelViewSet -from counter.models import Counter - - -class CounterSerializer(serializers.ModelSerializer): - is_open = serializers.BooleanField(read_only=True) - barman_list = serializers.ListField( - child=serializers.IntegerField(), read_only=True - ) - - class Meta: - model = Counter - fields = ("id", "name", "type", "club", "products", "is_open", "barman_list") - - -class CounterViewSet(RightModelViewSet): - """ - 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/) - """ - 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 deleted file mode 100644 index c9183ed0..00000000 --- a/api/views/group.py +++ /dev/null @@ -1,33 +0,0 @@ -# -# Copyright 2023 © AE UTBM -# ae@utbm.fr / ae.info@utbm.fr -# -# This file is part of the website of the UTBM Student Association (AE UTBM), -# https://ae.utbm.fr. -# -# You can find the source code of the website at https://github.com/ae-utbm/sith3 -# -# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) -# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE -# OR WITHIN THE LOCAL FILE "LICENSE" -# -# - -from rest_framework import serializers - -from api.views import RightModelViewSet -from core.models import RealGroup - - -class GroupSerializer(serializers.ModelSerializer): - class Meta: - model = RealGroup - - -class GroupViewSet(RightModelViewSet): - """ - Manage Groups (api/v1/group/) - """ - - serializer_class = GroupSerializer - queryset = RealGroup.objects.all() diff --git a/api/views/launderette.py b/api/views/launderette.py deleted file mode 100644 index eae35a19..00000000 --- a/api/views/launderette.py +++ /dev/null @@ -1,126 +0,0 @@ -# -# Copyright 2023 © AE UTBM -# ae@utbm.fr / ae.info@utbm.fr -# -# This file is part of the website of the UTBM Student Association (AE UTBM), -# https://ae.utbm.fr. -# -# You can find the source code of the website at https://github.com/ae-utbm/sith3 -# -# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) -# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE -# OR WITHIN THE LOCAL FILE "LICENSE" -# -# - -from rest_framework import serializers -from rest_framework.decorators import action -from rest_framework.response import Response - -from api.views import RightModelViewSet -from launderette.models import Launderette, Machine, Token - - -class LaunderettePlaceSerializer(serializers.ModelSerializer): - machine_list = serializers.ListField( - child=serializers.IntegerField(), read_only=True - ) - token_list = serializers.ListField(child=serializers.IntegerField(), read_only=True) - - class Meta: - model = Launderette - fields = ( - "id", - "name", - "counter", - "machine_list", - "token_list", - "get_absolute_url", - ) - - -class LaunderetteMachineSerializer(serializers.ModelSerializer): - class Meta: - model = Machine - fields = ("id", "name", "type", "is_working", "launderette") - - -class LaunderetteTokenSerializer(serializers.ModelSerializer): - class Meta: - model = Token - fields = ( - "id", - "name", - "type", - "launderette", - "borrow_date", - "user", - "is_avaliable", - ) - - -class LaunderettePlaceViewSet(RightModelViewSet): - """ - Manage Launderette (api/v1/launderette/place/) - """ - - serializer_class = LaunderettePlaceSerializer - queryset = Launderette.objects.all() - - -class LaunderetteMachineViewSet(RightModelViewSet): - """ - 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/) - """ - - serializer_class = LaunderetteTokenSerializer - queryset = Token.objects.all() - - @action(detail=False) - def washing(self, request): - """ - 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) - """ - 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) - """ - self.queryset = self.queryset.filter( - borrow_date__isnull=True, user__isnull=True - ) - serializer = self.get_serializer(self.queryset, many=True) - return Response(serializer.data) - - @action(detail=False) - def unavaliable(self, request): - """ - Return all unavaliable tokens (api/v1/launderette/token/unavaliable) - """ - self.queryset = self.queryset.filter( - borrow_date__isnull=False, user__isnull=False - ) - serializer = self.get_serializer(self.queryset, many=True) - return Response(serializer.data) diff --git a/api/views/sas.py b/api/views/sas.py deleted file mode 100644 index 455edf09..00000000 --- a/api/views/sas.py +++ /dev/null @@ -1,43 +0,0 @@ -from typing import List - -from rest_framework.decorators import api_view, renderer_classes -from rest_framework.exceptions import PermissionDenied -from rest_framework.generics import get_object_or_404 -from rest_framework.renderers import JSONRenderer -from rest_framework.request import Request -from rest_framework.response import Response - -from core.models import User -from core.views import can_edit -from sas.models import Picture - - -def all_pictures_of_user(user: User) -> List[Picture]: - return [ - relation.picture - for relation in user.pictures.exclude(picture=None) - .order_by("-picture__parent__date", "id") - .select_related("picture__parent") - ] - - -@api_view(["GET"]) -@renderer_classes((JSONRenderer,)) -def all_pictures_of_user_endpoint(request: Request, user: int): - requested_user: User = get_object_or_404(User, pk=user) - if not can_edit(requested_user, request.user): - raise PermissionDenied - - return Response( - [ - { - "name": f"{picture.parent.name} - {picture.name}", - "date": picture.date, - "author": str(picture.owner), - "full_size_url": picture.get_download_url(), - "compressed_url": picture.get_download_compressed_url(), - "thumb_url": picture.get_download_thumb_url(), - } - for picture in all_pictures_of_user(requested_user) - ] - ) diff --git a/api/views/user.py b/api/views/user.py deleted file mode 100644 index d5aecd15..00000000 --- a/api/views/user.py +++ /dev/null @@ -1,58 +0,0 @@ -# -# Copyright 2023 © AE UTBM -# ae@utbm.fr / ae.info@utbm.fr -# -# This file is part of the website of the UTBM Student Association (AE UTBM), -# https://ae.utbm.fr. -# -# You can find the source code of the website at https://github.com/ae-utbm/sith3 -# -# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) -# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE -# OR WITHIN THE LOCAL FILE "LICENSE" -# -# - -import datetime - -from rest_framework import serializers -from rest_framework.decorators import action -from rest_framework.response import Response - -from api.views import RightModelViewSet -from core.models import User - - -class UserSerializer(serializers.ModelSerializer): - class Meta: - model = User - fields = ( - "id", - "first_name", - "last_name", - "email", - "date_of_birth", - "nick_name", - "is_active", - "date_joined", - ) - - -class UserViewSet(RightModelViewSet): - """ - Manage Users (api/v1/user/) - Only show active users - """ - - serializer_class = UserSerializer - queryset = User.objects.filter(is_active=True) - - @action(detail=False) - def birthday(self, request): - """ - 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) - return Response(serializer.data) diff --git a/api/views/uv.py b/api/views/uv.py deleted file mode 100644 index a83a8936..00000000 --- a/api/views/uv.py +++ /dev/null @@ -1,128 +0,0 @@ -import json -import urllib.request - -from django.conf import settings -from django.core.exceptions import PermissionDenied -from rest_framework import serializers -from rest_framework.decorators import api_view, renderer_classes -from rest_framework.renderers import JSONRenderer -from rest_framework.response import Response - -from pedagogy.views import CanCreateUVFunctionMixin - - -@api_view(["GET"]) -@renderer_classes((JSONRenderer,)) -def uv_endpoint(request): - if not CanCreateUVFunctionMixin.can_create_uv(request.user): - raise PermissionDenied - - params = request.query_params - if "year" not in params or "code" not in params: - raise serializers.ValidationError("Missing query parameter") - - short_uv, full_uv = find_uv("fr", params["year"], params["code"]) - if short_uv is None or full_uv is None: - return Response(status=204) - - 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 - information which are not in full_uv. - full_uv is the detailed representation of an UV. - """ - # query the UV list - uvs_url = settings.SITH_PEDAGOGY_UTBM_API + "/uvs/{}/{}".format(lang, year) - response = urllib.request.urlopen(uvs_url) - uvs = json.loads(response.read().decode("utf-8")) - - try: - # 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) - - # get detailed information about the UV - uv_url = settings.SITH_PEDAGOGY_UTBM_API + "/uv/{}/{}/{}/{}".format( - lang, year, code, short_uv["codeFormation"] - ) - response = urllib.request.urlopen(uv_url) - full_uv = json.loads(response.read().decode("utf-8")) - - 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. - """ - res = {} - - res["credit_type"] = short_uv["codeCategorie"] - - # probably wrong on a few UVs as we pick the first UV we find but - # availability depends on the formation - semesters = { - (True, True): "AUTUMN_AND_SPRING", - (True, False): "AUTUMN", - (False, True): "SPRING", - } - res["semester"] = semesters.get( - (short_uv["ouvertAutomne"], short_uv["ouvertPrintemps"]), "CLOSED" - ) - - langs = {"es": "SP", "en": "EN", "de": "DE"} - res["language"] = langs.get(full_uv["codeLangue"], "FR") - - if full_uv["departement"] == "Pôle Humanités": - res["department"] = "HUMA" - else: - departments = { - "AL": "IMSI", - "AE": "EE", - "GI": "GI", - "GC": "EE", - "GM": "MC", - "TC": "TC", - "GP": "IMSI", - "ED": "EDIM", - "AI": "GI", - "AM": "MC", - } - res["department"] = departments.get(full_uv["codeFormation"], "NA") - - res["credits"] = full_uv["creditsEcts"] - - activities = ("CM", "TD", "TP", "THE", "TE") - for activity in activities: - res["hours_{}".format(activity)] = 0 - for activity in full_uv["activites"]: - if activity["code"] in activities: - res["hours_{}".format(activity["code"])] += activity["nbh"] // 60 - - # wrong if the manager changes depending on the semester - semester = full_uv.get("automne", None) - if not semester: - semester = full_uv.get("printemps", {}) - res["manager"] = semester.get("responsable", "") - - res["title"] = full_uv["libelle"] - - descriptions = { - "objectives": "objectifs", - "program": "programme", - "skills": "acquisitionCompetences", - "key_concepts": "acquisitionNotions", - } - - for res_key, full_uv_key in descriptions.items(): - res[res_key] = full_uv[full_uv_key] - # if not found or the API did not return a string - if type(res[res_key]) != str: - res[res_key] = "" - - return res diff --git a/club/forms.py b/club/forms.py index ad3273c6..3a21fd6d 100644 --- a/club/forms.py +++ b/club/forms.py @@ -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"]: diff --git a/club/models.py b/club/models.py index e315f1d2..6baba7bd 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 @@ 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( @@ -501,16 +486,12 @@ class Mailing(models.Model): super().delete() def fetch_format(self): - resp = self.email + ": " - for sub in self.subscriptions.all(): - resp += sub.fetch_format() - return resp + destination = "".join(s.fetch_format() for s in self.subscriptions.all()) + return f"{self.email}: {destination}" 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 b893cb99..31b9562a 100644 --- a/club/tests.py +++ b/club/tests.py @@ -28,9 +28,9 @@ from core.models import AnonymousUser, User 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 +class TestClub(TestCase): + """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 : @@ -92,10 +92,9 @@ class ClubTest(TestCase): cache.clear() -class MembershipQuerySetTest(ClubTest): +class TestMembershipQuerySet(TestClub): 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) @@ -178,11 +170,9 @@ class MembershipQuerySetTest(ClubTest): assert cached_mem == "not_member" -class ClubModelTest(ClubTest): +class TestClubModel(TestClub): 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 '