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/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/views/__init__.py b/api/views/__init__.py index d5ca6289..5017806b 100644 --- a/api/views/__init__.py +++ b/api/views/__init__.py @@ -23,9 +23,9 @@ from core.views import can_edit, can_view def check_if(obj, user, test): - """ - Detect if it's a single object or a queryset - aply a given test on individual object and return global permission + """Detect if it's a single object or a queryset. + + Apply a given test on individual object and return global permission. """ if isinstance(obj, QuerySet): for o in obj: @@ -39,9 +39,7 @@ def check_if(obj, user, test): class ManageModelMixin: @action(detail=True) def id(self, request, pk=None): - """ - Get by id (api/v1/router/{pk}/id/) - """ + """Get by id (api/v1/router/{pk}/id/).""" self.queryset = get_object_or_404(self.queryset.filter(id=pk)) serializer = self.get_serializer(self.queryset) return Response(serializer.data) diff --git a/api/views/api.py b/api/views/api.py index 6e3a056d..9de0a87e 100644 --- a/api/views/api.py +++ b/api/views/api.py @@ -23,9 +23,7 @@ from core.templatetags.renderer import markdown @api_view(["POST"]) @renderer_classes((StaticHTMLRenderer,)) def RenderMarkdown(request): - """ - Render Markdown - """ + """Render Markdown.""" try: data = markdown(request.POST["text"]) except: diff --git a/api/views/club.py b/api/views/club.py index 2333fffb..6c4c5b0b 100644 --- a/api/views/club.py +++ b/api/views/club.py @@ -31,9 +31,7 @@ class ClubSerializer(serializers.ModelSerializer): class ClubViewSet(RightModelViewSet): - """ - Manage Clubs (api/v1/club/) - """ + """Manage Clubs (api/v1/club/).""" serializer_class = ClubSerializer queryset = Club.objects.all() diff --git a/api/views/counter.py b/api/views/counter.py index a9fd64ce..3fbaa931 100644 --- a/api/views/counter.py +++ b/api/views/counter.py @@ -33,18 +33,14 @@ class CounterSerializer(serializers.ModelSerializer): class CounterViewSet(RightModelViewSet): - """ - Manage Counters (api/v1/counter/) - """ + """Manage Counters (api/v1/counter/).""" serializer_class = CounterSerializer queryset = Counter.objects.all() @action(detail=False) def bar(self, request): - """ - Return all bars (api/v1/counter/bar/) - """ + """Return all bars (api/v1/counter/bar/).""" self.queryset = self.queryset.filter(type="BAR") serializer = self.get_serializer(self.queryset, many=True) return Response(serializer.data) diff --git a/api/views/group.py b/api/views/group.py index c9183ed0..da37dbbc 100644 --- a/api/views/group.py +++ b/api/views/group.py @@ -25,9 +25,7 @@ class GroupSerializer(serializers.ModelSerializer): class GroupViewSet(RightModelViewSet): - """ - Manage Groups (api/v1/group/) - """ + """Manage Groups (api/v1/group/).""" serializer_class = GroupSerializer queryset = RealGroup.objects.all() diff --git a/api/views/launderette.py b/api/views/launderette.py index eae35a19..cb88d80c 100644 --- a/api/views/launderette.py +++ b/api/views/launderette.py @@ -60,54 +60,42 @@ class LaunderetteTokenSerializer(serializers.ModelSerializer): class LaunderettePlaceViewSet(RightModelViewSet): - """ - Manage Launderette (api/v1/launderette/place/) - """ + """Manage Launderette (api/v1/launderette/place/).""" serializer_class = LaunderettePlaceSerializer queryset = Launderette.objects.all() class LaunderetteMachineViewSet(RightModelViewSet): - """ - Manage Washing Machines (api/v1/launderette/machine/) - """ + """Manage Washing Machines (api/v1/launderette/machine/).""" serializer_class = LaunderetteMachineSerializer queryset = Machine.objects.all() class LaunderetteTokenViewSet(RightModelViewSet): - """ - Manage Launderette's tokens (api/v1/launderette/token/) - """ + """Manage Launderette's tokens (api/v1/launderette/token/).""" serializer_class = LaunderetteTokenSerializer queryset = Token.objects.all() @action(detail=False) def washing(self, request): - """ - Return all washing tokens (api/v1/launderette/token/washing) - """ + """Return all washing tokens (api/v1/launderette/token/washing).""" self.queryset = self.queryset.filter(type="WASHING") serializer = self.get_serializer(self.queryset, many=True) return Response(serializer.data) @action(detail=False) def drying(self, request): - """ - Return all drying tokens (api/v1/launderette/token/drying) - """ + """Return all drying tokens (api/v1/launderette/token/drying).""" self.queryset = self.queryset.filter(type="DRYING") serializer = self.get_serializer(self.queryset, many=True) return Response(serializer.data) @action(detail=False) def avaliable(self, request): - """ - Return all avaliable tokens (api/v1/launderette/token/avaliable) - """ + """Return all avaliable tokens (api/v1/launderette/token/avaliable).""" self.queryset = self.queryset.filter( borrow_date__isnull=True, user__isnull=True ) @@ -116,9 +104,7 @@ class LaunderetteTokenViewSet(RightModelViewSet): @action(detail=False) def unavaliable(self, request): - """ - Return all unavaliable tokens (api/v1/launderette/token/unavaliable) - """ + """Return all unavaliable tokens (api/v1/launderette/token/unavaliable).""" self.queryset = self.queryset.filter( borrow_date__isnull=False, user__isnull=False ) diff --git a/api/views/user.py b/api/views/user.py index d5aecd15..84078ce2 100644 --- a/api/views/user.py +++ b/api/views/user.py @@ -39,9 +39,9 @@ class UserSerializer(serializers.ModelSerializer): class UserViewSet(RightModelViewSet): - """ - Manage Users (api/v1/user/) - Only show active users + """Manage Users (api/v1/user/). + + Only show active users. """ serializer_class = UserSerializer @@ -49,9 +49,7 @@ class UserViewSet(RightModelViewSet): @action(detail=False) def birthday(self, request): - """ - Return all users born today (api/v1/user/birstdays) - """ + """Return all users born today (api/v1/user/birstdays).""" date = datetime.datetime.today() self.queryset = self.queryset.filter(date_of_birth=date) serializer = self.get_serializer(self.queryset, many=True) diff --git a/api/views/uv.py b/api/views/uv.py index a83a8936..09292d8e 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 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..0ed51389 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( @@ -508,9 +493,7 @@ class Mailing(models.Model): class MailingSubscription(models.Model): - """ - This class makes the link between user and mailing list - """ + """Link between user and mailing list.""" mailing = models.ForeignKey( Mailing, diff --git a/club/tests.py b/club/tests.py index b893cb99..af8aafde 100644 --- a/club/tests.py +++ b/club/tests.py @@ -29,8 +29,8 @@ from sith.settings import SITH_BAR_MANAGER, SITH_MAIN_CLUB_ID class ClubTest(TestCase): - """ - Set up data for test cases related to clubs and membership + """Set up data for test cases related to clubs and membership. + The generated dataset is the one created by the populate command, plus the following modifications : @@ -94,8 +94,7 @@ class ClubTest(TestCase): class MembershipQuerySetTest(ClubTest): def test_ongoing(self): - """ - Test that the ongoing queryset method returns the memberships that + """Test that the ongoing queryset method returns the memberships that are not ended. """ current_members = list(self.club.members.ongoing().order_by("id")) @@ -108,9 +107,8 @@ class MembershipQuerySetTest(ClubTest): assert current_members == expected def test_board(self): - """ - Test that the board queryset method returns the memberships - of user in the club board + """Test that the board queryset method returns the memberships + of user in the club board. """ board_members = list(self.club.members.board().order_by("id")) expected = [ @@ -123,9 +121,8 @@ class MembershipQuerySetTest(ClubTest): assert board_members == expected def test_ongoing_board(self): - """ - Test that combining ongoing and board returns users - who are currently board members of the club + """Test that combining ongoing and board returns users + who are currently board members of the club. """ members = list(self.club.members.ongoing().board().order_by("id")) expected = [ @@ -136,9 +133,7 @@ class MembershipQuerySetTest(ClubTest): assert members == expected def test_update_invalidate_cache(self): - """ - Test that the `update` queryset method properly invalidate cache - """ + """Test that the `update` queryset method properly invalidate cache.""" mem_skia = self.skia.memberships.get(club=self.club) cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia) self.skia.memberships.update(end_date=localtime(now()).date()) @@ -157,10 +152,7 @@ class MembershipQuerySetTest(ClubTest): assert new_mem.role == 5 def test_delete_invalidate_cache(self): - """ - Test that the `delete` queryset properly invalidate cache - """ - + """Test that the `delete` queryset properly invalidate cache.""" mem_skia = self.skia.memberships.get(club=self.club) mem_comptable = self.comptable.memberships.get(club=self.club) cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia) @@ -180,9 +172,7 @@ class MembershipQuerySetTest(ClubTest): class ClubModelTest(ClubTest): def assert_membership_started_today(self, user: User, role: int): - """ - Assert that the given membership is active and started today - """ + """Assert that the given membership is active and started today.""" membership = user.memberships.ongoing().filter(club=self.club).first() assert membership is not None assert localtime(now()).date() == membership.start_date @@ -195,17 +185,14 @@ class ClubModelTest(ClubTest): assert user.is_in_group(name=board_group) def assert_membership_ended_today(self, user: User): - """ - Assert that the given user have a membership which ended today - """ + """Assert that the given user have a membership which ended today.""" today = localtime(now()).date() assert user.memberships.filter(club=self.club, end_date=today).exists() assert self.club.get_membership_for(user) is None def test_access_unauthorized(self): - """ - Test that users who never subscribed and anonymous users - cannot see the page + """Test that users who never subscribed and anonymous users + cannot see the page. """ response = self.client.post(self.members_url) assert response.status_code == 403 @@ -215,8 +202,7 @@ class ClubModelTest(ClubTest): assert response.status_code == 403 def test_display(self): - """ - Test that a GET request return a page where the requested + """Test that a GET request return a page where the requested information are displayed. """ self.client.force_login(self.skia) @@ -251,9 +237,7 @@ class ClubModelTest(ClubTest): self.assertInHTML(expected_html, response.content.decode()) def test_root_add_one_club_member(self): - """ - Test that root users can add members to clubs, one at a time - """ + """Test that root users can add members to clubs, one at a time.""" self.client.force_login(self.root) response = self.client.post( self.members_url, @@ -264,9 +248,7 @@ class ClubModelTest(ClubTest): self.assert_membership_started_today(self.subscriber, role=3) def test_root_add_multiple_club_member(self): - """ - Test that root users can add multiple members at once to clubs - """ + """Test that root users can add multiple members at once to clubs.""" self.client.force_login(self.root) response = self.client.post( self.members_url, @@ -281,8 +263,7 @@ class ClubModelTest(ClubTest): self.assert_membership_started_today(self.krophil, role=3) def test_add_unauthorized_members(self): - """ - Test that users who are not currently subscribed + """Test that users who are not currently subscribed cannot be members of clubs. """ self.client.force_login(self.root) @@ -302,9 +283,8 @@ class ClubModelTest(ClubTest): assert '