mirror of
https://github.com/ae-utbm/sith.git
synced 2025-07-12 21:09:24 +00:00
Compare commits
5 Commits
trombi
...
feature/im
Author | SHA1 | Date | |
---|---|---|---|
46fa14ed12 | |||
18dffb0053 | |||
6e47d1471e | |||
b5146569e1 | |||
acde993352 |
20
.github/actions/setup_project/action.yml
vendored
20
.github/actions/setup_project/action.yml
vendored
@ -6,20 +6,26 @@ runs:
|
||||
- name: Install apt packages
|
||||
uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
with:
|
||||
packages: gettext
|
||||
packages: gettext libxapian-dev libgraphviz-dev
|
||||
version: 1.0 # increment to reset cache
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install gettext libxapian-dev libgraphviz-dev
|
||||
shell: bash
|
||||
|
||||
- name: Set up python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12"
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Load cached Poetry installation
|
||||
id: cached-poetry
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.local
|
||||
key: poetry-3 # increment to reset cache
|
||||
key: poetry-0 # increment to reset cache
|
||||
|
||||
- name: Install Poetry
|
||||
if: steps.cached-poetry.outputs.cache-hit != 'true'
|
||||
@ -39,11 +45,7 @@ runs:
|
||||
${{ runner.os }}-poetry-
|
||||
|
||||
- name: Install dependencies
|
||||
run: poetry install --with docs,tests
|
||||
shell: bash
|
||||
|
||||
- name: Install xapian
|
||||
run: poetry run ./manage.py install_xapian
|
||||
run: poetry install -E testing -E docs
|
||||
shell: bash
|
||||
|
||||
- name: Compile gettext messages
|
||||
|
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@ -8,7 +8,11 @@ updates:
|
||||
- package-ecosystem: "pip" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
interval: "daily"
|
||||
# Raise pull requests for version updates
|
||||
# to pip against the `develop` branch
|
||||
target-branch: "taiste"
|
||||
reviewers:
|
||||
- "ae-utbm/developpers-v3"
|
||||
commit-message:
|
||||
prefix: "[UPDATE] "
|
33
.github/workflows/ci.yml
vendored
33
.github/workflows/ci.yml
vendored
@ -1,38 +1,37 @@
|
||||
name: Sith CI
|
||||
name: Sith 3 CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, taiste]
|
||||
branches:
|
||||
- master
|
||||
- taiste
|
||||
pull_request:
|
||||
branches: [master, taiste]
|
||||
workflow_dispatch:
|
||||
branches:
|
||||
- master
|
||||
- taiste
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
name: Launch pre-commits checks (ruff)
|
||||
black:
|
||||
name: Black format
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: pre-commit/action@v3.0.1
|
||||
with:
|
||||
extra_args: --all-files
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Project
|
||||
uses: ./.github/actions/setup_project
|
||||
- run: poetry run black --check .
|
||||
|
||||
tests:
|
||||
name: Run tests and generate coverage report
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false # don't interrupt the other test processes
|
||||
matrix:
|
||||
pytest-mark: [slow, not slow]
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup_project
|
||||
- uses: ./.github/actions/setup_xapian
|
||||
- uses: ./.github/actions/compile_messages
|
||||
- name: Run tests
|
||||
run: poetry run coverage run -m pytest -m "${{ matrix.pytest-mark }}"
|
||||
run: poetry run coverage run ./manage.py test
|
||||
- name: Generate coverage report
|
||||
run: |
|
||||
poetry run coverage report
|
||||
|
21
.github/workflows/deploy.yml
vendored
21
.github/workflows/deploy.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: SSH Remote Commands
|
||||
uses: appleboy/ssh-action@v1.1.0
|
||||
uses: appleboy/ssh-action@dce9d565de8d876c11d93fa4fe677c0285a66d78
|
||||
with:
|
||||
# Proxy
|
||||
proxy_host : ${{secrets.PROXY_HOST}}
|
||||
@ -31,17 +31,16 @@ jobs:
|
||||
|
||||
script_stop: true
|
||||
|
||||
# See https://github.com/ae-utbm/sith/wiki/GitHub-Actions#deployment-action
|
||||
# See https://github.com/ae-utbm/sith3/wiki/GitHub-Actions#deployment-action
|
||||
script: |
|
||||
cd ${{secrets.SITH_PATH}}
|
||||
export PATH="/home/sith/.local/bin:$PATH"
|
||||
pushd ${{secrets.SITH_PATH}}
|
||||
|
||||
git fetch
|
||||
git reset --hard origin/master
|
||||
poetry install --with prod --without docs,tests
|
||||
npm install
|
||||
poetry run ./manage.py install_xapian
|
||||
git pull
|
||||
poetry install
|
||||
poetry run ./manage.py migrate
|
||||
poetry run ./manage.py collectstatic --clear --noinput
|
||||
echo "yes" | poetry run ./manage.py collectstatic
|
||||
poetry run ./manage.py compilestatic
|
||||
poetry run ./manage.py compilemessages
|
||||
|
||||
sudo systemctl restart uwsgi
|
||||
@ -52,10 +51,10 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
needs: deployment
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Sentry Release
|
||||
uses: getsentry/action-release@v1.7.0
|
||||
uses: getsentry/action-release@v1.2.0
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
|
21
.github/workflows/deploy_docs.yml
vendored
21
.github/workflows/deploy_docs.yml
vendored
@ -1,21 +0,0 @@
|
||||
name: deploy_docs
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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
|
40
.github/workflows/taiste.yml
vendored
40
.github/workflows/taiste.yml
vendored
@ -1,9 +1,8 @@
|
||||
name: Sith taiste
|
||||
name: Sith3 taiste
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [taiste]
|
||||
workflow_dispatch:
|
||||
branches: [ taiste ]
|
||||
|
||||
jobs:
|
||||
deployment:
|
||||
@ -13,7 +12,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: SSH Remote Commands
|
||||
uses: appleboy/ssh-action@v1.1.0
|
||||
uses: appleboy/ssh-action@dce9d565de8d876c11d93fa4fe677c0285a66d78
|
||||
with:
|
||||
# Proxy
|
||||
proxy_host : ${{secrets.PROXY_HOST}}
|
||||
@ -30,17 +29,34 @@ jobs:
|
||||
|
||||
script_stop: true
|
||||
|
||||
# See https://github.com/ae-utbm/sith/wiki/GitHub-Actions#deployment-action
|
||||
# See https://github.com/ae-utbm/sith3/wiki/GitHub-Actions#deployment-action
|
||||
script: |
|
||||
cd ${{secrets.SITH_PATH}}
|
||||
export PATH="$HOME/.poetry/bin:$PATH"
|
||||
pushd ${{secrets.SITH_PATH}}
|
||||
|
||||
git fetch
|
||||
git reset --hard origin/taiste
|
||||
poetry install --with prod --without docs,tests
|
||||
npm install
|
||||
poetry run ./manage.py install_xapian
|
||||
git pull
|
||||
poetry install
|
||||
poetry run ./manage.py migrate
|
||||
poetry run ./manage.py collectstatic --clear --noinput
|
||||
echo "yes" | poetry run ./manage.py collectstatic
|
||||
poetry run ./manage.py compilestatic
|
||||
poetry run ./manage.py compilemessages
|
||||
|
||||
sudo systemctl restart uwsgi
|
||||
|
||||
sentry:
|
||||
runs-on: ubuntu-latest
|
||||
environment: taiste
|
||||
timeout-minutes: 30
|
||||
needs: deployment
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Sentry Release
|
||||
uses: getsentry/action-release@v1.2.0
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
||||
with:
|
||||
environment: taiste
|
||||
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -1,4 +1,4 @@
|
||||
*.sqlite3
|
||||
db.sqlite3
|
||||
*.log
|
||||
*.pyc
|
||||
*.mo
|
||||
@ -17,7 +17,4 @@ sith/settings_custom.py
|
||||
sith/search_indexes/
|
||||
.coverage
|
||||
coverage_report/
|
||||
node_modules/
|
||||
|
||||
# compiled documentation
|
||||
site/
|
||||
doc/_build
|
||||
|
@ -1,26 +0,0 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.8.3
|
||||
hooks:
|
||||
- id: ruff # just check the code, and print the errors
|
||||
- id: ruff # actually fix the fixable errors, but print nothing
|
||||
args: ["--fix", "--silent"]
|
||||
# Run the formatter.
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/biomejs/pre-commit
|
||||
rev: "v0.1.0" # Use the sha / tag you want to point at
|
||||
hooks:
|
||||
- id: biome-check
|
||||
additional_dependencies: ["@biomejs/biome@1.9.3"]
|
||||
- repo: https://github.com/rtts/djhtml
|
||||
rev: 3.0.7
|
||||
hooks:
|
||||
- id: djhtml
|
||||
name: format templates
|
||||
entry: djhtml --tabwidth 2
|
||||
types: ["jinja"]
|
||||
- id: djcss
|
||||
name: format scss files
|
||||
entry: djcss --tabwidth 2
|
||||
types: ["scss"]
|
26
.readthedocs.yml
Normal file
26
.readthedocs.yml
Normal file
@ -0,0 +1,26 @@
|
||||
# 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
|
49
README.md
49
README.md
@ -1,21 +1,40 @@
|
||||
# Sith
|
||||
<p align="center">
|
||||
<a href="#">
|
||||
<img src="https://img.shields.io/badge/Code%20Style-Black-000000?style=for-the-badge">
|
||||
</a>
|
||||
<a href="#">
|
||||
<img src="https://img.shields.io/github/checks-status/ae-utbm/sith3/master?logo=github&style=for-the-badge&label=BUILD">
|
||||
</a>
|
||||
<a href="https://sith-ae.readthedocs.io/">
|
||||
<img src="https://img.shields.io/readthedocs/sith-ae?logo=readthedocs&style=for-the-badge">
|
||||
</a>
|
||||
<a href="https://discord.gg/XK9WfPsUFm">
|
||||
<img src="https://img.shields.io/discord/971448179075731476?label=Discord&logo=discord&style=for-the-badge">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
[](#)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](#)
|
||||
[](https://ae-utbm.github.io/sith)
|
||||
[](https://squidfunk.github.io/mkdocs-material/)
|
||||
[](https://biomejs.dev)
|
||||
[](https://discord.gg/xk9wfpsufm)
|
||||
<h3 align="center">This is the source code of the UTBM's student association available at https://ae.utbm.fr/.</h3>
|
||||
|
||||
### This is the source code of the UTBM's student association available at [https://ae.utbm.fr/](https://ae.utbm.fr/).
|
||||
<p align="justify">All documentation is in the <code>docs</code> 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.</p>
|
||||
|
||||
All documentation is in the `docs` directory and online at [https://ae-utbm.github.io/sith](https://ae-utbm.github.io/sith). 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.
|
||||
<h4>If you want to contribute, here's how we recommend to read the docs:</h4>
|
||||
|
||||
#### 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.
|
||||
<ul>
|
||||
<li>
|
||||
<p align="justify">
|
||||
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.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p align="justify">
|
||||
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.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p align="justify">
|
||||
Keep in mind that this documentation is thought to be read in order.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
> This project is licensed under GNU GPL, see the LICENSE file at the top of the repository for more details.
|
||||
|
@ -1,3 +1,4 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# Copyright 2023 © AE UTBM
|
||||
# ae@utbm.fr / ae.info@utbm.fr
|
||||
@ -5,10 +6,10 @@
|
||||
# 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/sith
|
||||
# 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/sith/master/LICENSE
|
||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||
#
|
||||
#
|
||||
|
@ -1,3 +1,4 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# Copyright 2023 © AE UTBM
|
||||
# ae@utbm.fr / ae.info@utbm.fr
|
||||
@ -5,26 +6,18 @@
|
||||
# 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/sith
|
||||
# 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/sith/master/LICENSE
|
||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||
#
|
||||
#
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from accounting.models import (
|
||||
AccountingType,
|
||||
BankAccount,
|
||||
ClubAccount,
|
||||
Company,
|
||||
GeneralJournal,
|
||||
Label,
|
||||
Operation,
|
||||
SimplifiedAccountingType,
|
||||
)
|
||||
from accounting.models import *
|
||||
|
||||
|
||||
admin.site.register(BankAccount)
|
||||
admin.site.register(ClubAccount)
|
||||
|
@ -1,23 +0,0 @@
|
||||
from typing import Annotated
|
||||
|
||||
from annotated_types import MinLen
|
||||
from ninja_extra import ControllerBase, api_controller, paginate, route
|
||||
from ninja_extra.pagination import PageNumberPaginationExtra
|
||||
from ninja_extra.schemas import PaginatedResponseSchema
|
||||
|
||||
from accounting.models import ClubAccount, Company
|
||||
from accounting.schemas import ClubAccountSchema, CompanySchema
|
||||
from core.api_permissions import CanAccessLookup
|
||||
|
||||
|
||||
@api_controller("/lookup", permissions=[CanAccessLookup])
|
||||
class AccountingController(ControllerBase):
|
||||
@route.get("/club-account", response=PaginatedResponseSchema[ClubAccountSchema])
|
||||
@paginate(PageNumberPaginationExtra, page_size=50)
|
||||
def search_club_account(self, search: Annotated[str, MinLen(1)]):
|
||||
return ClubAccount.objects.filter(name__icontains=search).values()
|
||||
|
||||
@route.get("/company", response=PaginatedResponseSchema[CompanySchema])
|
||||
@paginate(PageNumberPaginationExtra, page_size=50)
|
||||
def search_company(self, search: Annotated[str, MinLen(1)]):
|
||||
return Company.objects.filter(name__icontains=search).values()
|
@ -1,10 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import django.core.validators
|
||||
import accounting.models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -1,7 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -100,6 +101,6 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="operation", unique_together={("number", "journal")}
|
||||
name="operation", unique_together=set([("number", "journal")])
|
||||
),
|
||||
]
|
||||
|
@ -1,7 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import phonenumber_field.modelfields
|
||||
from django.db import migrations, models
|
||||
import phonenumber_field.modelfields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -1,7 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -45,6 +46,6 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="label", unique_together={("name", "club_account")}
|
||||
name="label", unique_together=set([("name", "club_account")])
|
||||
),
|
||||
]
|
||||
|
@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
@ -1,3 +1,4 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# Copyright 2023 © AE UTBM
|
||||
# ae@utbm.fr / ae.info@utbm.fr
|
||||
@ -5,56 +6,46 @@
|
||||
# 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/sith
|
||||
# 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/sith/master/LICENSE
|
||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||
#
|
||||
#
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import validators
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.template import defaultfilters
|
||||
from django.urls import reverse
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core import validators
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.template import defaultfilters
|
||||
|
||||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
|
||||
from decimal import Decimal
|
||||
from core.models import User, SithFile
|
||||
from club.models import Club
|
||||
from core.models import SithFile, User
|
||||
|
||||
|
||||
class CurrencyField(models.DecimalField):
|
||||
"""Custom database field used for currency."""
|
||||
"""
|
||||
This is a custom database field used for currency
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs["max_digits"] = 12
|
||||
kwargs["decimal_places"] = 2
|
||||
super().__init__(*args, **kwargs)
|
||||
super(CurrencyField, self).__init__(*args, **kwargs)
|
||||
|
||||
def to_python(self, value):
|
||||
try:
|
||||
return super().to_python(value).quantize(Decimal("0.01"))
|
||||
return super(CurrencyField, self).to_python(value).quantize(Decimal("0.01"))
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
|
||||
if settings.TESTING:
|
||||
from model_bakery import baker
|
||||
|
||||
baker.generators.add(
|
||||
CurrencyField,
|
||||
lambda: baker.random_gen.gen_decimal(max_digits=8, decimal_places=2),
|
||||
)
|
||||
else: # pragma: no cover
|
||||
# baker is only used in tests, so we don't need coverage for this part
|
||||
pass
|
||||
|
||||
|
||||
# Accounting classes
|
||||
|
||||
|
||||
@ -71,8 +62,31 @@ class Company(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("company")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
def is_owned_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("accounting:co_edit", kwargs={"co_id": self.id})
|
||||
@ -80,21 +94,8 @@ class Company(models.Model):
|
||||
def get_display_name(self):
|
||||
return self.name
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""Check if that object can be edited by the given user."""
|
||||
return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
|
||||
|
||||
def can_be_edited_by(self, user):
|
||||
"""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):
|
||||
"""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()
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class BankAccount(models.Model):
|
||||
@ -112,20 +113,24 @@ class BankAccount(models.Model):
|
||||
verbose_name = _("Bank account")
|
||||
ordering = ["club", "name"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("accounting:bank_details", kwargs={"b_account_id": self.id})
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""Check if that object can be edited by the given user."""
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
return True
|
||||
m = self.club.get_membership_for(user)
|
||||
return m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]
|
||||
if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("accounting:bank_details", kwargs={"b_account_id": self.id})
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class ClubAccount(models.Model):
|
||||
@ -147,33 +152,48 @@ class ClubAccount(models.Model):
|
||||
verbose_name = _("Club account")
|
||||
ordering = ["bank_account", "name"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
def is_owned_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_be_edited_by(self, user):
|
||||
"""
|
||||
Method to see 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
|
||||
"""
|
||||
m = self.club.get_membership_for(user)
|
||||
if m and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_open_journal(self):
|
||||
for j in self.journals.all():
|
||||
if not j.closed:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_open_journal(self):
|
||||
return self.journals.filter(closed=False).first()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("accounting:club_details", kwargs={"c_account_id": self.id})
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""Check if that object can be edited by the given user."""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
|
||||
|
||||
def can_be_edited_by(self, user):
|
||||
"""Check if that object can be edited by the given user."""
|
||||
m = self.club.get_membership_for(user)
|
||||
return m and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]
|
||||
|
||||
def can_be_viewed_by(self, user):
|
||||
"""Check if that object can be viewed by the given user."""
|
||||
m = self.club.get_membership_for(user)
|
||||
return m and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]
|
||||
|
||||
def has_open_journal(self):
|
||||
return self.journals.filter(closed=False).exists()
|
||||
|
||||
def get_open_journal(self):
|
||||
return self.journals.filter(closed=False).first()
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_display_name(self):
|
||||
return _("%(club_account)s on %(bank_account)s") % {
|
||||
@ -183,7 +203,9 @@ 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)
|
||||
@ -203,29 +225,37 @@ class GeneralJournal(models.Model):
|
||||
verbose_name = _("General journal")
|
||||
ordering = ["-start_date"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("accounting:journal_details", kwargs={"j_id": self.id})
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""Check if that object can be edited by the given user."""
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
return True
|
||||
return self.club_account.can_be_edited_by(user)
|
||||
if self.club_account.can_be_edited_by(user):
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_be_edited_by(self, user):
|
||||
"""Check if that object can be edited by the given user."""
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
return True
|
||||
return self.club_account.can_be_edited_by(user)
|
||||
if self.club_account.can_be_edited_by(user):
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_be_viewed_by(self, user):
|
||||
return self.club_account.can_be_viewed_by(user)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("accounting:journal_details", kwargs={"j_id": self.id})
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def update_amounts(self):
|
||||
self.amount = 0
|
||||
self.effective_amount = 0
|
||||
@ -242,7 +272,9 @@ 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(
|
||||
@ -325,18 +357,6 @@ class Operation(models.Model):
|
||||
unique_together = ("number", "journal")
|
||||
ordering = ["-number"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.amount} € | {self.date} | {self.accounting_type} | {self.done}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.number is None:
|
||||
self.number = self.journal.operations.count() + 1
|
||||
super().save(*args, **kwargs)
|
||||
self.journal.update_amounts()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("accounting:journal_details", kwargs={"j_id": self.journal.id})
|
||||
|
||||
def __getattribute__(self, attr):
|
||||
if attr == "target":
|
||||
return self.get_target()
|
||||
@ -344,7 +364,7 @@ class Operation(models.Model):
|
||||
return object.__getattribute__(self, attr)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
super(Operation, self).clean()
|
||||
if self.date is None:
|
||||
raise ValidationError(_("The date must be set."))
|
||||
elif self.date < self.journal.start_date:
|
||||
@ -390,8 +410,16 @@ class Operation(models.Model):
|
||||
tar = Company.objects.filter(id=self.target_id).first()
|
||||
return tar
|
||||
|
||||
def save(self):
|
||||
if self.number is None:
|
||||
self.number = self.journal.operations.count() + 1
|
||||
super(Operation, self).save()
|
||||
self.journal.update_amounts()
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""Check if that object can be edited by the given user."""
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
@ -399,22 +427,40 @@ class Operation(models.Model):
|
||||
if self.journal.closed:
|
||||
return False
|
||||
m = self.journal.club_account.club.get_membership_for(user)
|
||||
return m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]
|
||||
if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_be_edited_by(self, user):
|
||||
"""Check if that object can be edited by the given user."""
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
return True
|
||||
if self.journal.closed:
|
||||
return False
|
||||
m = self.journal.club_account.club.get_membership_for(user)
|
||||
return m is not None and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]
|
||||
if m is not None and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("accounting:journal_details", kwargs={"j_id": self.journal.id})
|
||||
|
||||
def __str__(self):
|
||||
return "%d € | %s | %s | %s" % (
|
||||
self.amount,
|
||||
self.date,
|
||||
self.accounting_type,
|
||||
self.done,
|
||||
)
|
||||
|
||||
|
||||
class AccountingType(models.Model):
|
||||
"""Accounting types.
|
||||
"""
|
||||
Class describing the accounting types.
|
||||
|
||||
Those are numbers used in accounting to classify operations
|
||||
Thoses are numbers used in accounting to classify operations
|
||||
"""
|
||||
|
||||
code = models.CharField(
|
||||
@ -441,21 +487,27 @@ class AccountingType(models.Model):
|
||||
verbose_name = _("accounting type")
|
||||
ordering = ["movement_type", "code"]
|
||||
|
||||
def __str__(self):
|
||||
return self.code + " - " + self.get_movement_type_display() + " - " + self.label
|
||||
def is_owned_by(self, user):
|
||||
"""
|
||||
Method to see if that object can be edited by the given user
|
||||
"""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("accounting:type_list")
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""Check if that object can be edited by the given user."""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
|
||||
def __str__(self):
|
||||
return self.code + " - " + self.get_movement_type_display() + " - " + self.label
|
||||
|
||||
|
||||
class SimplifiedAccountingType(models.Model):
|
||||
"""Simplified version of `AccountingType`."""
|
||||
"""
|
||||
Class describing the simplified accounting types.
|
||||
"""
|
||||
|
||||
label = models.CharField(_("label"), max_length=128)
|
||||
accounting_type = models.ForeignKey(
|
||||
@ -469,15 +521,6 @@ class SimplifiedAccountingType(models.Model):
|
||||
verbose_name = _("simplified type")
|
||||
ordering = ["accounting_type__movement_type", "accounting_type__code"]
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"{self.get_movement_type_display()} "
|
||||
f"- {self.accounting_type.code} - {self.label}"
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("accounting:simple_type_list")
|
||||
|
||||
@property
|
||||
def movement_type(self):
|
||||
return self.accounting_type.movement_type
|
||||
@ -485,9 +528,21 @@ class SimplifiedAccountingType(models.Model):
|
||||
def get_movement_type_display(self):
|
||||
return self.accounting_type.get_movement_type_display()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("accounting:simple_type_list")
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
self.get_movement_type_display()
|
||||
+ " - "
|
||||
+ self.accounting_type.code
|
||||
+ " - "
|
||||
+ self.label
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
|
@ -1,15 +0,0 @@
|
||||
from ninja import ModelSchema
|
||||
|
||||
from accounting.models import ClubAccount, Company
|
||||
|
||||
|
||||
class ClubAccountSchema(ModelSchema):
|
||||
class Meta:
|
||||
model = ClubAccount
|
||||
fields = ["id", "name"]
|
||||
|
||||
|
||||
class CompanySchema(ModelSchema):
|
||||
class Meta:
|
||||
model = Company
|
||||
fields = ["id", "name"]
|
@ -1,60 +0,0 @@
|
||||
import { AjaxSelect } from "#core:core/components/ajax-select-base";
|
||||
import { registerComponent } from "#core:utils/web-components";
|
||||
import type { TomOption } from "tom-select/dist/types/types";
|
||||
import type { escape_html } from "tom-select/dist/types/utils";
|
||||
import {
|
||||
type ClubAccountSchema,
|
||||
type CompanySchema,
|
||||
accountingSearchClubAccount,
|
||||
accountingSearchCompany,
|
||||
} from "#openapi";
|
||||
|
||||
@registerComponent("club-account-ajax-select")
|
||||
export class ClubAccountAjaxSelect extends AjaxSelect {
|
||||
protected valueField = "id";
|
||||
protected labelField = "name";
|
||||
protected searchField = ["code", "name"];
|
||||
|
||||
protected async search(query: string): Promise<TomOption[]> {
|
||||
const resp = await accountingSearchClubAccount({ query: { search: query } });
|
||||
if (resp.data) {
|
||||
return resp.data.results;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
protected renderOption(item: ClubAccountSchema, sanitize: typeof escape_html) {
|
||||
return `<div class="select-item">
|
||||
<span class="select-item-text">${sanitize(item.name)}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected renderItem(item: ClubAccountSchema, sanitize: typeof escape_html) {
|
||||
return `<span>${sanitize(item.name)}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
@registerComponent("company-ajax-select")
|
||||
export class CompanyAjaxSelect extends AjaxSelect {
|
||||
protected valueField = "id";
|
||||
protected labelField = "name";
|
||||
protected searchField = ["code", "name"];
|
||||
|
||||
protected async search(query: string): Promise<TomOption[]> {
|
||||
const resp = await accountingSearchCompany({ query: { search: query } });
|
||||
if (resp.data) {
|
||||
return resp.data.results;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
protected renderOption(item: CompanySchema, sanitize: typeof escape_html) {
|
||||
return `<div class="select-item">
|
||||
<span class="select-item-text">${sanitize(item.name)}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected renderItem(item: CompanySchema, sanitize: typeof escape_html) {
|
||||
return `<span>${sanitize(item.name)}</span>`;
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}Accounting type list{% endtrans %}
|
||||
{% trans %}Accounting type list{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}Bank account: {% endtrans %}{{ object.name }}
|
||||
{% trans %}Bank account: {% endtrans %}{{ object.name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}Bank account list{% endtrans %}
|
||||
{% trans %}Bank account list{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}Club account:{% endtrans %} {{ object.name }}
|
||||
{% trans %}Club account:{% endtrans %} {{ object.name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}Company list{% endtrans %}
|
||||
{% trans %}Company list{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@ -10,9 +10,9 @@
|
||||
or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
|
||||
%}
|
||||
<p><a href="{{ url('accounting:co_new') }}">{% trans %}Create new company{% endtrans %}</a></p>
|
||||
{% endif %}
|
||||
<br/>
|
||||
<table>
|
||||
{% endif %}
|
||||
<br/>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{% trans %}Companies{% endtrans %}</td>
|
||||
@ -25,6 +25,6 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}General journal:{% endtrans %} {{ object.name }}
|
||||
{% trans %}General journal:{% endtrans %} {{ object.name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@ -95,9 +95,9 @@
|
||||
</td>
|
||||
<td><a href="{{ url('accounting:op_pdf', op_id=o.id) }}">{% trans %}Generate{% endtrans %}</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -1,12 +1,12 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}General journal:{% endtrans %} {{ object.name }}
|
||||
{% trans %}General journal:{% endtrans %} {{ object.name }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div id="accounting">
|
||||
<div id="accounting">
|
||||
<h3>{% trans %}Accounting statement: {% endtrans %} {{ object.name }}</h3>
|
||||
|
||||
<table>
|
||||
@ -29,5 +29,5 @@
|
||||
|
||||
<p><strong>{% trans %}Amount: {% endtrans %}</strong>{{ "%.2f" % object.amount }} €</p>
|
||||
<p><strong>{% trans %}Effective amount: {% endtrans %}</strong>{{ "%.2f" %object.effective_amount }} €</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -1,11 +1,11 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}General journal:{% endtrans %} {{ object.name }}
|
||||
{% trans %}General journal:{% endtrans %} {{ object.name }}
|
||||
{% endblock %}
|
||||
|
||||
{% macro display_tables(dict) %}
|
||||
<div id="accounting">
|
||||
<div id="accounting">
|
||||
<h6>{% trans %}Credit{% endtrans %}</h6>
|
||||
<table>
|
||||
<thead>
|
||||
@ -43,9 +43,9 @@
|
||||
</tbody>
|
||||
</table>
|
||||
{% trans %}Total: {% endtrans %}{{ "%.2f" % dict['DEBIT_sum'] }}
|
||||
{% endmacro %}
|
||||
{% endmacro %}
|
||||
|
||||
{% block content %}
|
||||
{% block content %}
|
||||
<h3>{% trans %}Statement by nature: {% endtrans %} {{ object.name }}</h3>
|
||||
|
||||
{% for k,v in statement.items() %}
|
||||
@ -53,5 +53,5 @@
|
||||
{{ display_tables(v) }}
|
||||
<hr>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -1,12 +1,12 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}General journal:{% endtrans %} {{ object.name }}
|
||||
{% trans %}General journal:{% endtrans %} {{ object.name }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div id="accounting">
|
||||
<div id="accounting">
|
||||
<h3>{% trans %}Statement by person: {% endtrans %} {{ object.name }}</h3>
|
||||
|
||||
<h4>{% trans %}Credit{% endtrans %}</h4>
|
||||
@ -64,5 +64,5 @@
|
||||
</table>
|
||||
|
||||
<p>Total : {{ "%.2f" % total_debit }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}Label list{% endtrans %}
|
||||
{% trans %}Label list{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -1,11 +1,11 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}Edit operation{% endtrans %}
|
||||
{% trans %}Edit operation{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="accounting">
|
||||
<div id="accounting">
|
||||
<p>
|
||||
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
|
||||
<a href="{{ url('accounting:bank_details', b_account_id=object.club_account.bank_account.id) }}">{{object.club_account.bank_account }}</a> >
|
||||
@ -54,17 +54,17 @@
|
||||
{% endif %}
|
||||
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
{% block script %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
$( function() {
|
||||
var target_type = $('#id_target_type');
|
||||
var user = $('user-ajax-select');
|
||||
var club = $('club-ajax-select');
|
||||
var club_account = $('club-account-ajax-select');
|
||||
var company = $('company-ajax-select');
|
||||
var user = $('#id_user_wrapper');
|
||||
var club = $('#id_club_wrapper');
|
||||
var club_account = $('#id_club_account_wrapper');
|
||||
var company = $('#id_company_wrapper');
|
||||
var other = $('#id_target_label');
|
||||
var need_link = $('#id_need_link_full');
|
||||
function update_targets () {
|
||||
@ -117,7 +117,7 @@
|
||||
target_type.change(update_targets);
|
||||
} );
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}Refound account{% endtrans %}
|
||||
{% trans %}Refound account{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}Simplified type list{% endtrans %}
|
||||
{% trans %}Simplified type list{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -1,3 +1,4 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# Copyright 2023 © AE UTBM
|
||||
# ae@utbm.fr / ae.info@utbm.fr
|
||||
@ -5,93 +6,98 @@
|
||||
# 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/sith
|
||||
# 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/sith/master/LICENSE
|
||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||
#
|
||||
#
|
||||
|
||||
from datetime import date, timedelta
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.core.management import call_command
|
||||
from datetime import date, timedelta
|
||||
|
||||
from core.models import User
|
||||
from accounting.models import (
|
||||
AccountingType,
|
||||
GeneralJournal,
|
||||
Label,
|
||||
Operation,
|
||||
Label,
|
||||
AccountingType,
|
||||
SimplifiedAccountingType,
|
||||
)
|
||||
from core.models import User
|
||||
|
||||
|
||||
class TestRefoundAccount(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.skia = User.objects.get(username="skia")
|
||||
class RefoundAccountTest(TestCase):
|
||||
def setUp(self):
|
||||
self.skia = User.objects.filter(username="skia").first()
|
||||
# reffil skia's account
|
||||
cls.skia.customer.amount = 800
|
||||
cls.skia.customer.save()
|
||||
cls.refound_account_url = reverse("accounting:refound_account")
|
||||
self.skia.customer.amount = 800
|
||||
self.skia.customer.save()
|
||||
|
||||
def test_permission_denied(self):
|
||||
self.client.force_login(User.objects.get(username="guy"))
|
||||
self.client.login(username="guy", password="plop")
|
||||
response_post = self.client.post(
|
||||
self.refound_account_url, {"user": self.skia.id}
|
||||
reverse("accounting:refound_account"), {"user": self.skia.id}
|
||||
)
|
||||
response_get = self.client.get(self.refound_account_url)
|
||||
assert response_get.status_code == 403
|
||||
assert response_post.status_code == 403
|
||||
response_get = self.client.get(reverse("accounting:refound_account"))
|
||||
self.assertTrue(response_get.status_code == 403)
|
||||
self.assertTrue(response_post.status_code == 403)
|
||||
|
||||
def test_root_granteed(self):
|
||||
self.client.force_login(User.objects.get(username="root"))
|
||||
response = self.client.post(self.refound_account_url, {"user": self.skia.id})
|
||||
self.assertRedirects(response, self.refound_account_url)
|
||||
self.skia.refresh_from_db()
|
||||
response = self.client.get(self.refound_account_url)
|
||||
assert response.status_code == 200
|
||||
assert '<form action="" method="post">' in str(response.content)
|
||||
assert self.skia.customer.amount == 0
|
||||
self.client.login(username="root", password="plop")
|
||||
response_post = self.client.post(
|
||||
reverse("accounting:refound_account"), {"user": self.skia.id}
|
||||
)
|
||||
self.skia = User.objects.filter(username="skia").first()
|
||||
response_get = self.client.get(reverse("accounting:refound_account"))
|
||||
self.assertFalse(response_get.status_code == 403)
|
||||
self.assertTrue('<form action="" method="post">' in str(response_get.content))
|
||||
self.assertFalse(response_post.status_code == 403)
|
||||
self.assertTrue(self.skia.customer.amount == 0)
|
||||
|
||||
def test_comptable_granteed(self):
|
||||
self.client.force_login(User.objects.get(username="comptable"))
|
||||
response = self.client.post(self.refound_account_url, {"user": self.skia.id})
|
||||
self.assertRedirects(response, self.refound_account_url)
|
||||
self.skia.refresh_from_db()
|
||||
response = self.client.get(self.refound_account_url)
|
||||
assert response.status_code == 200
|
||||
assert '<form action="" method="post">' in str(response.content)
|
||||
assert self.skia.customer.amount == 0
|
||||
self.client.login(username="comptable", password="plop")
|
||||
response_post = self.client.post(
|
||||
reverse("accounting:refound_account"), {"user": self.skia.id}
|
||||
)
|
||||
self.skia = User.objects.filter(username="skia").first()
|
||||
response_get = self.client.get(reverse("accounting:refound_account"))
|
||||
self.assertFalse(response_get.status_code == 403)
|
||||
self.assertTrue('<form action="" method="post">' in str(response_get.content))
|
||||
self.assertFalse(response_post.status_code == 403)
|
||||
self.assertTrue(self.skia.customer.amount == 0)
|
||||
|
||||
|
||||
class TestJournal(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.journal = GeneralJournal.objects.get(id=1)
|
||||
class JournalTest(TestCase):
|
||||
def setUp(self):
|
||||
self.journal = GeneralJournal.objects.filter(id=1).first()
|
||||
|
||||
def test_permission_granted(self):
|
||||
self.client.force_login(User.objects.get(username="comptable"))
|
||||
self.client.login(username="comptable", password="plop")
|
||||
response_get = self.client.get(
|
||||
reverse("accounting:journal_details", args=[self.journal.id])
|
||||
)
|
||||
|
||||
assert response_get.status_code == 200
|
||||
assert "<td>M\\xc3\\xa9thode de paiement</td>" in str(response_get.content)
|
||||
self.assertTrue(response_get.status_code == 200)
|
||||
self.assertTrue(
|
||||
"<td>M\\xc3\\xa9thode de paiement</td>" in str(response_get.content)
|
||||
)
|
||||
|
||||
def test_permission_not_granted(self):
|
||||
self.client.force_login(User.objects.get(username="skia"))
|
||||
self.client.login(username="skia", password="plop")
|
||||
response_get = self.client.get(
|
||||
reverse("accounting:journal_details", args=[self.journal.id])
|
||||
)
|
||||
|
||||
assert response_get.status_code == 403
|
||||
assert "<td>M\xc3\xa9thode de paiement</td>" not in str(response_get.content)
|
||||
self.assertTrue(response_get.status_code == 403)
|
||||
self.assertFalse(
|
||||
"<td>M\xc3\xa9thode de paiement</td>" in str(response_get.content)
|
||||
)
|
||||
|
||||
|
||||
class TestOperation(TestCase):
|
||||
class OperationTest(TestCase):
|
||||
def setUp(self):
|
||||
self.tomorrow_formatted = (date.today() + timedelta(days=1)).strftime(
|
||||
"%d/%m/%Y"
|
||||
@ -102,8 +108,9 @@ class TestOperation(TestCase):
|
||||
code="443", label="Ce code n'existe pas", movement_type="CREDIT"
|
||||
)
|
||||
at.save()
|
||||
label = Label.objects.create(club_account=self.journal.club_account, name="bob")
|
||||
self.client.force_login(User.objects.get(username="comptable"))
|
||||
l = Label(club_account=self.journal.club_account, name="bob")
|
||||
l.save()
|
||||
self.client.login(username="comptable", password="plop")
|
||||
self.op1 = Operation(
|
||||
journal=self.journal,
|
||||
date=date.today(),
|
||||
@ -111,7 +118,7 @@ class TestOperation(TestCase):
|
||||
remark="Test bilan",
|
||||
mode="CASH",
|
||||
done=True,
|
||||
label=label,
|
||||
label=l,
|
||||
accounting_type=at,
|
||||
target_type="USER",
|
||||
target_id=self.skia.id,
|
||||
@ -124,7 +131,7 @@ class TestOperation(TestCase):
|
||||
remark="Test bilan",
|
||||
mode="CASH",
|
||||
done=True,
|
||||
label=label,
|
||||
label=l,
|
||||
accounting_type=at,
|
||||
target_type="USER",
|
||||
target_id=self.skia.id,
|
||||
@ -132,7 +139,8 @@ class TestOperation(TestCase):
|
||||
self.op2.save()
|
||||
|
||||
def test_new_operation(self):
|
||||
at = AccountingType.objects.get(code="604")
|
||||
self.client.login(username="comptable", password="plop")
|
||||
at = AccountingType.objects.filter(code="604").first()
|
||||
response = self.client.post(
|
||||
reverse("accounting:op_new", args=[self.journal.id]),
|
||||
{
|
||||
@ -164,7 +172,8 @@ class TestOperation(TestCase):
|
||||
self.assertTrue("<td>Le fantome de la nuit</td>" in str(response_get.content))
|
||||
|
||||
def test_bad_new_operation(self):
|
||||
AccountingType.objects.get(code="604")
|
||||
self.client.login(username="comptable", password="plop")
|
||||
AccountingType.objects.filter(code="604").first()
|
||||
response = self.client.post(
|
||||
reverse("accounting:op_new", args=[self.journal.id]),
|
||||
{
|
||||
@ -190,7 +199,7 @@ class TestOperation(TestCase):
|
||||
)
|
||||
|
||||
def test_new_operation_not_authorized(self):
|
||||
self.client.force_login(self.skia)
|
||||
self.client.login(username="skia", password="plop")
|
||||
at = AccountingType.objects.filter(code="604").first()
|
||||
response = self.client.post(
|
||||
reverse("accounting:op_new", args=[self.journal.id]),
|
||||
@ -217,6 +226,7 @@ class TestOperation(TestCase):
|
||||
)
|
||||
|
||||
def test__operation_simple_accounting(self):
|
||||
self.client.login(username="comptable", password="plop")
|
||||
sat = SimplifiedAccountingType.objects.all().first()
|
||||
response = self.client.post(
|
||||
reverse("accounting:op_new", args=[self.journal.id]),
|
||||
@ -253,37 +263,47 @@ class TestOperation(TestCase):
|
||||
)
|
||||
|
||||
def test_nature_statement(self):
|
||||
self.client.login(username="comptable", password="plop")
|
||||
response = self.client.get(
|
||||
reverse("accounting:journal_nature_statement", args=[self.journal.id])
|
||||
)
|
||||
self.assertContains(response, "bob (Troll Penché) : 3.00", status_code=200)
|
||||
|
||||
def test_person_statement(self):
|
||||
self.client.login(username="comptable", password="plop")
|
||||
response = self.client.get(
|
||||
reverse("accounting:journal_person_statement", args=[self.journal.id])
|
||||
)
|
||||
self.assertContains(response, "Total : 5575.72", status_code=200)
|
||||
self.assertContains(response, "Total : 71.42")
|
||||
content = response.content.decode()
|
||||
self.assertInHTML(
|
||||
"""<td><a href="/user/1/">S' Kia</a></td><td>3.00</td>""", content
|
||||
self.assertContains(
|
||||
response,
|
||||
"""
|
||||
<td><a href="/user/1/">S' Kia</a></td>
|
||||
|
||||
<td>3.00</td>""",
|
||||
)
|
||||
self.assertInHTML(
|
||||
"""<td><a href="/user/1/">S' Kia</a></td><td>823.00</td>""", content
|
||||
self.assertContains(
|
||||
response,
|
||||
"""
|
||||
<td><a href="/user/1/">S' Kia</a></td>
|
||||
|
||||
<td>823.00</td>""",
|
||||
)
|
||||
|
||||
def test_accounting_statement(self):
|
||||
self.client.login(username="comptable", password="plop")
|
||||
response = self.client.get(
|
||||
reverse("accounting:journal_accounting_statement", args=[self.journal.id])
|
||||
)
|
||||
assert response.status_code == 200
|
||||
self.assertInHTML(
|
||||
self.assertContains(
|
||||
response,
|
||||
"""
|
||||
<tr>
|
||||
<td>443 - Crédit - Ce code n'existe pas</td>
|
||||
<td>3.00</td>
|
||||
</tr>""",
|
||||
response.content.decode(),
|
||||
status_code=200,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
|
@ -1,3 +1,4 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# Copyright 2023 © AE UTBM
|
||||
# ae@utbm.fr / ae.info@utbm.fr
|
||||
@ -5,51 +6,17 @@
|
||||
# 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/sith
|
||||
# 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/sith/master/LICENSE
|
||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||
#
|
||||
#
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from accounting.views import (
|
||||
AccountingTypeCreateView,
|
||||
AccountingTypeEditView,
|
||||
AccountingTypeListView,
|
||||
BankAccountCreateView,
|
||||
BankAccountDeleteView,
|
||||
BankAccountDetailView,
|
||||
BankAccountEditView,
|
||||
BankAccountListView,
|
||||
ClubAccountCreateView,
|
||||
ClubAccountDeleteView,
|
||||
ClubAccountDetailView,
|
||||
ClubAccountEditView,
|
||||
CompanyCreateView,
|
||||
CompanyEditView,
|
||||
CompanyListView,
|
||||
JournalAccountingStatementView,
|
||||
JournalCreateView,
|
||||
JournalDeleteView,
|
||||
JournalDetailView,
|
||||
JournalEditView,
|
||||
JournalNatureStatementView,
|
||||
JournalPersonStatementView,
|
||||
LabelCreateView,
|
||||
LabelDeleteView,
|
||||
LabelEditView,
|
||||
LabelListView,
|
||||
OperationCreateView,
|
||||
OperationEditView,
|
||||
OperationPDFView,
|
||||
RefoundAccountView,
|
||||
SimplifiedAccountingTypeCreateView,
|
||||
SimplifiedAccountingTypeEditView,
|
||||
SimplifiedAccountingTypeListView,
|
||||
)
|
||||
from accounting.views import *
|
||||
|
||||
urlpatterns = [
|
||||
# Accounting types
|
||||
|
@ -1,3 +1,4 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# Copyright 2023 © AE UTBM
|
||||
# ae@utbm.fr / ae.info@utbm.fr
|
||||
@ -5,62 +6,57 @@
|
||||
# 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/sith
|
||||
# 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/sith/master/LICENSE
|
||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||
#
|
||||
#
|
||||
|
||||
import collections
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.views.generic import ListView, DetailView
|
||||
from django.views.generic.edit import UpdateView, CreateView, DeleteView, FormView
|
||||
from django.urls import reverse_lazy, reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.forms.models import modelform_factory
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.forms import HiddenInput
|
||||
from django.db import transaction
|
||||
from django.db.models import Sum
|
||||
from django.forms import HiddenInput
|
||||
from django.forms.models import modelform_factory
|
||||
from django.conf import settings
|
||||
from django import forms
|
||||
from django.http import HttpResponse
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import DetailView, ListView
|
||||
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
|
||||
import collections
|
||||
|
||||
from ajax_select.fields import AutoCompleteSelectField
|
||||
|
||||
from accounting.models import (
|
||||
AccountingType,
|
||||
BankAccount,
|
||||
ClubAccount,
|
||||
Company,
|
||||
GeneralJournal,
|
||||
Label,
|
||||
Operation,
|
||||
SimplifiedAccountingType,
|
||||
)
|
||||
from accounting.widgets.select import (
|
||||
AutoCompleteSelectClubAccount,
|
||||
AutoCompleteSelectCompany,
|
||||
)
|
||||
from club.models import Club
|
||||
from club.widgets.select import AutoCompleteSelectClub
|
||||
from core.models import User
|
||||
from core.views import (
|
||||
CanCreateMixin,
|
||||
CanViewMixin,
|
||||
CanEditMixin,
|
||||
CanEditPropMixin,
|
||||
CanViewMixin,
|
||||
CanCreateMixin,
|
||||
TabedViewMixin,
|
||||
)
|
||||
from core.views.forms import SelectDate, SelectFile
|
||||
from core.views.widgets.select import AutoCompleteSelectUser
|
||||
from counter.models import Counter, Product, Selling
|
||||
from core.views.forms import SelectFile, SelectDate
|
||||
from accounting.models import (
|
||||
BankAccount,
|
||||
ClubAccount,
|
||||
GeneralJournal,
|
||||
Operation,
|
||||
AccountingType,
|
||||
Company,
|
||||
SimplifiedAccountingType,
|
||||
Label,
|
||||
)
|
||||
from counter.models import Counter, Selling, Product
|
||||
|
||||
# Main accounting view
|
||||
|
||||
|
||||
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"
|
||||
@ -71,14 +67,18 @@ 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"
|
||||
@ -87,7 +87,9 @@ 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"]
|
||||
@ -98,14 +100,18 @@ 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"
|
||||
@ -114,7 +120,9 @@ 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"]
|
||||
@ -125,7 +133,9 @@ 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"
|
||||
@ -134,7 +144,9 @@ 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"
|
||||
@ -142,7 +154,9 @@ 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"]
|
||||
@ -152,7 +166,9 @@ 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"
|
||||
@ -164,7 +180,9 @@ 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"
|
||||
@ -173,7 +191,9 @@ 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"
|
||||
@ -181,15 +201,17 @@ 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"]
|
||||
template_name = "core/create.jinja"
|
||||
|
||||
def get_initial(self):
|
||||
ret = super().get_initial()
|
||||
if "parent" in self.request.GET:
|
||||
ret = super(ClubAccountCreateView, self).get_initial()
|
||||
if "parent" in self.request.GET.keys():
|
||||
obj = BankAccount.objects.filter(id=int(self.request.GET["parent"])).first()
|
||||
if obj is not None:
|
||||
ret["bank_account"] = obj.id
|
||||
@ -199,7 +221,9 @@ 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"
|
||||
@ -259,7 +283,9 @@ class JournalTabsMixin(TabedViewMixin):
|
||||
|
||||
|
||||
class JournalCreateView(CanCreateMixin, CreateView):
|
||||
"""Create a general journal."""
|
||||
"""
|
||||
Create a general journal
|
||||
"""
|
||||
|
||||
model = GeneralJournal
|
||||
form_class = modelform_factory(
|
||||
@ -270,8 +296,8 @@ class JournalCreateView(CanCreateMixin, CreateView):
|
||||
template_name = "core/create.jinja"
|
||||
|
||||
def get_initial(self):
|
||||
ret = super().get_initial()
|
||||
if "parent" in self.request.GET:
|
||||
ret = super(JournalCreateView, self).get_initial()
|
||||
if "parent" in self.request.GET.keys():
|
||||
obj = ClubAccount.objects.filter(id=int(self.request.GET["parent"])).first()
|
||||
if obj is not None:
|
||||
ret["club_account"] = obj.id
|
||||
@ -279,7 +305,9 @@ 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"
|
||||
@ -288,7 +316,9 @@ class JournalDetailView(JournalTabsMixin, CanViewMixin, DetailView):
|
||||
|
||||
|
||||
class JournalEditView(CanEditMixin, UpdateView):
|
||||
"""Update a general journal."""
|
||||
"""
|
||||
Update a general journal
|
||||
"""
|
||||
|
||||
model = GeneralJournal
|
||||
pk_url_kwarg = "j_id"
|
||||
@ -297,7 +327,9 @@ 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"
|
||||
@ -307,7 +339,7 @@ class JournalDeleteView(CanEditPropMixin, DeleteView):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
if self.object.operations.count() == 0:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
return super(JournalDeleteView, self).dispatch(request, *args, **kwargs)
|
||||
else:
|
||||
raise PermissionDenied
|
||||
|
||||
@ -341,30 +373,12 @@ class OperationForm(forms.ModelForm):
|
||||
"invoice": SelectFile,
|
||||
}
|
||||
|
||||
user = forms.ModelChoiceField(
|
||||
help_text=None,
|
||||
required=False,
|
||||
widget=AutoCompleteSelectUser,
|
||||
queryset=User.objects.all(),
|
||||
)
|
||||
club_account = forms.ModelChoiceField(
|
||||
help_text=None,
|
||||
required=False,
|
||||
widget=AutoCompleteSelectClubAccount,
|
||||
queryset=ClubAccount.objects.all(),
|
||||
)
|
||||
club = forms.ModelChoiceField(
|
||||
help_text=None,
|
||||
required=False,
|
||||
widget=AutoCompleteSelectClub,
|
||||
queryset=Club.objects.all(),
|
||||
)
|
||||
company = forms.ModelChoiceField(
|
||||
help_text=None,
|
||||
required=False,
|
||||
widget=AutoCompleteSelectCompany,
|
||||
queryset=Company.objects.all(),
|
||||
user = AutoCompleteSelectField("users", help_text=None, required=False)
|
||||
club_account = AutoCompleteSelectField(
|
||||
"club_accounts", help_text=None, required=False
|
||||
)
|
||||
club = AutoCompleteSelectField("clubs", help_text=None, required=False)
|
||||
company = AutoCompleteSelectField("companies", help_text=None, required=False)
|
||||
need_link = forms.BooleanField(
|
||||
label=_("Link this operation to the target account"),
|
||||
required=False,
|
||||
@ -373,7 +387,7 @@ class OperationForm(forms.ModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
club_account = kwargs.pop("club_account", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
super(OperationForm, self).__init__(*args, **kwargs)
|
||||
if club_account:
|
||||
self.fields["label"].queryset = club_account.labels.order_by("name").all()
|
||||
if self.instance.target_type == "USER":
|
||||
@ -386,8 +400,8 @@ class OperationForm(forms.ModelForm):
|
||||
self.fields["company"].initial = self.instance.target_id
|
||||
|
||||
def clean(self):
|
||||
self.cleaned_data = super().clean()
|
||||
if "target_type" in self.cleaned_data:
|
||||
self.cleaned_data = super(OperationForm, self).clean()
|
||||
if "target_type" in self.cleaned_data.keys():
|
||||
if (
|
||||
self.cleaned_data.get("user") is None
|
||||
and self.cleaned_data.get("club") is None
|
||||
@ -416,7 +430,7 @@ class OperationForm(forms.ModelForm):
|
||||
return self.cleaned_data
|
||||
|
||||
def save(self):
|
||||
ret = super().save()
|
||||
ret = super(OperationForm, self).save()
|
||||
if (
|
||||
self.instance.target_type == "ACCOUNT"
|
||||
and not self.instance.linked_operation
|
||||
@ -454,7 +468,9 @@ class OperationForm(forms.ModelForm):
|
||||
|
||||
|
||||
class OperationCreateView(CanCreateMixin, CreateView):
|
||||
"""Create an operation."""
|
||||
"""
|
||||
Create an operation
|
||||
"""
|
||||
|
||||
model = Operation
|
||||
form_class = OperationForm
|
||||
@ -466,21 +482,23 @@ class OperationCreateView(CanCreateMixin, CreateView):
|
||||
return self.form_class(club_account=ca, **self.get_form_kwargs())
|
||||
|
||||
def get_initial(self):
|
||||
ret = super().get_initial()
|
||||
ret = super(OperationCreateView, self).get_initial()
|
||||
if self.journal is not None:
|
||||
ret["journal"] = self.journal.id
|
||||
return ret
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add journal to the context."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
"""Add journal to the context"""
|
||||
kwargs = super(OperationCreateView, self).get_context_data(**kwargs)
|
||||
if self.journal:
|
||||
kwargs["object"] = self.journal
|
||||
return kwargs
|
||||
|
||||
|
||||
class OperationEditView(CanEditMixin, UpdateView):
|
||||
"""An edit view, working as detail for the moment."""
|
||||
"""
|
||||
An edit view, working as detail for the moment
|
||||
"""
|
||||
|
||||
model = Operation
|
||||
pk_url_kwarg = "op_id"
|
||||
@ -488,27 +506,29 @@ class OperationEditView(CanEditMixin, UpdateView):
|
||||
template_name = "accounting/operation_edit.jinja"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add journal to the context."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
"""Add journal to the context"""
|
||||
kwargs = super(OperationEditView, self).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"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.units import cm
|
||||
from reportlab.platypus import Table, TableStyle
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.lib.units import cm
|
||||
from reportlab.lib.utils import ImageReader
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.platypus import Table, TableStyle
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
|
||||
pdfmetrics.registerFont(TTFont("DejaVu", "DejaVuSerif.ttf"))
|
||||
|
||||
@ -579,7 +599,7 @@ class OperationPDFView(CanViewMixin, DetailView):
|
||||
payment_mode = ""
|
||||
for m in settings.SITH_ACCOUNTING_PAYMENT_METHOD:
|
||||
if m[0] == mode:
|
||||
payment_mode += "[\u00d7]"
|
||||
payment_mode += "[\u00D7]"
|
||||
else:
|
||||
payment_mode += "[ ]"
|
||||
payment_mode += " %s\n" % (m[1])
|
||||
@ -647,7 +667,9 @@ 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"
|
||||
@ -658,17 +680,19 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
||||
ret = collections.OrderedDict()
|
||||
statement = collections.OrderedDict()
|
||||
total_sum = 0
|
||||
for sat in [
|
||||
None,
|
||||
*list(SimplifiedAccountingType.objects.order_by("label")),
|
||||
]:
|
||||
amount = queryset.filter(
|
||||
for sat in [None] + list(
|
||||
SimplifiedAccountingType.objects.order_by("label").all()
|
||||
):
|
||||
sum = queryset.filter(
|
||||
accounting_type__movement_type=movement_type, simpleaccounting_type=sat
|
||||
).aggregate(amount_sum=Sum("amount"))["amount_sum"]
|
||||
label = sat.label if sat is not None else ""
|
||||
if amount:
|
||||
total_sum += amount
|
||||
statement[label] = amount
|
||||
if sat:
|
||||
sat = sat.label
|
||||
else:
|
||||
sat = ""
|
||||
if sum:
|
||||
total_sum += sum
|
||||
statement[sat] = sum
|
||||
ret[movement_type] = statement
|
||||
ret[movement_type + "_sum"] = total_sum
|
||||
return ret
|
||||
@ -691,23 +715,28 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
||||
self.statement(self.object.operations.filter(label=None).all(), "DEBIT")
|
||||
)
|
||||
statement[_("No label operations")] = no_label_statement
|
||||
for label in labels:
|
||||
for l in labels:
|
||||
l_stmt = collections.OrderedDict()
|
||||
journals = self.object.operations.filter(label=label).all()
|
||||
l_stmt.update(self.statement(journals, "CREDIT"))
|
||||
l_stmt.update(self.statement(journals, "DEBIT"))
|
||||
statement[label] = l_stmt
|
||||
l_stmt.update(
|
||||
self.statement(self.object.operations.filter(label=l).all(), "CREDIT")
|
||||
)
|
||||
l_stmt.update(
|
||||
self.statement(self.object.operations.filter(label=l).all(), "DEBIT")
|
||||
)
|
||||
statement[l] = l_stmt
|
||||
return statement
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add infos to the context."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
"""Add infos to the context"""
|
||||
kwargs = super(JournalNatureStatementView, self).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"
|
||||
@ -737,8 +766,8 @@ class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
||||
return sum(self.statement(movement_type).values())
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add journal to the context."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
"""Add journal to the context"""
|
||||
kwargs = super(JournalPersonStatementView, self).get_context_data(**kwargs)
|
||||
kwargs["credit_statement"] = self.statement("CREDIT")
|
||||
kwargs["debit_statement"] = self.statement("DEBIT")
|
||||
kwargs["total_credit"] = self.total("CREDIT")
|
||||
@ -747,7 +776,9 @@ 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"
|
||||
@ -765,8 +796,8 @@ class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView)
|
||||
return statement
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add journal to the context."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
"""Add journal to the context"""
|
||||
kwargs = super(JournalAccountingStatementView, self).get_context_data(**kwargs)
|
||||
kwargs["statement"] = self.statement()
|
||||
return kwargs
|
||||
|
||||
@ -780,7 +811,9 @@ class CompanyListView(CanViewMixin, ListView):
|
||||
|
||||
|
||||
class CompanyCreateView(CanCreateMixin, CreateView):
|
||||
"""Create a company."""
|
||||
"""
|
||||
Create a company
|
||||
"""
|
||||
|
||||
model = Company
|
||||
fields = ["name"]
|
||||
@ -789,7 +822,9 @@ class CompanyCreateView(CanCreateMixin, CreateView):
|
||||
|
||||
|
||||
class CompanyEditView(CanCreateMixin, UpdateView):
|
||||
"""Edit a company."""
|
||||
"""
|
||||
Edit a company
|
||||
"""
|
||||
|
||||
model = Company
|
||||
pk_url_kwarg = "co_id"
|
||||
@ -817,8 +852,8 @@ class LabelCreateView(
|
||||
template_name = "core/create.jinja"
|
||||
|
||||
def get_initial(self):
|
||||
ret = super().get_initial()
|
||||
if "parent" in self.request.GET:
|
||||
ret = super(LabelCreateView, self).get_initial()
|
||||
if "parent" in self.request.GET.keys():
|
||||
obj = ClubAccount.objects.filter(id=int(self.request.GET["parent"])).first()
|
||||
if obj is not None:
|
||||
ret["club_account"] = obj.id
|
||||
@ -842,17 +877,15 @@ class LabelDeleteView(CanEditMixin, DeleteView):
|
||||
|
||||
|
||||
class CloseCustomerAccountForm(forms.Form):
|
||||
user = forms.ModelChoiceField(
|
||||
label=_("Refound this account"),
|
||||
help_text=None,
|
||||
required=True,
|
||||
widget=AutoCompleteSelectUser,
|
||||
queryset=User.objects.all(),
|
||||
user = AutoCompleteSelectField(
|
||||
"users", label=_("Refound this account"), help_text=None, required=True
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
@ -864,19 +897,19 @@ class RefoundAccountView(FormView):
|
||||
raise PermissionDenied
|
||||
|
||||
def dispatch(self, request, *arg, **kwargs):
|
||||
res = super().dispatch(request, *arg, **kwargs)
|
||||
res = super(RefoundAccountView, self).dispatch(request, *arg, **kwargs)
|
||||
if self.permission(request.user):
|
||||
return res
|
||||
|
||||
def post(self, request, *arg, **kwargs):
|
||||
self.operator = request.user
|
||||
if self.permission(request.user):
|
||||
return super().post(self, request, *arg, **kwargs)
|
||||
return super(RefoundAccountView, self).post(self, request, *arg, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
self.customer = form.cleaned_data["user"]
|
||||
self.create_selling()
|
||||
return super().form_valid(form)
|
||||
return super(RefoundAccountView, self).form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("accounting:refound_account")
|
||||
|
@ -1,39 +0,0 @@
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
from accounting.models import ClubAccount, Company
|
||||
from accounting.schemas import ClubAccountSchema, CompanySchema
|
||||
from core.views.widgets.select import AutoCompleteSelect, AutoCompleteSelectMultiple
|
||||
|
||||
_js = ["bundled/accounting/components/ajax-select-index.ts"]
|
||||
|
||||
|
||||
class AutoCompleteSelectClubAccount(AutoCompleteSelect):
|
||||
component_name = "club-account-ajax-select"
|
||||
model = ClubAccount
|
||||
adapter = TypeAdapter(list[ClubAccountSchema])
|
||||
|
||||
js = _js
|
||||
|
||||
|
||||
class AutoCompleteSelectMultipleClubAccount(AutoCompleteSelectMultiple):
|
||||
component_name = "club-account-ajax-select"
|
||||
model = ClubAccount
|
||||
adapter = TypeAdapter(list[ClubAccountSchema])
|
||||
|
||||
js = _js
|
||||
|
||||
|
||||
class AutoCompleteSelectCompany(AutoCompleteSelect):
|
||||
component_name = "company-ajax-select"
|
||||
model = Company
|
||||
adapter = TypeAdapter(list[CompanySchema])
|
||||
|
||||
js = _js
|
||||
|
||||
|
||||
class AutoCompleteSelectMultipleCompany(AutoCompleteSelectMultiple):
|
||||
component_name = "company-ajax-select"
|
||||
model = Company
|
||||
adapter = TypeAdapter(list[CompanySchema])
|
||||
|
||||
js = _js
|
@ -1,10 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from antispam.models import ToxicDomain
|
||||
|
||||
|
||||
@admin.register(ToxicDomain)
|
||||
class ToxicDomainAdmin(admin.ModelAdmin):
|
||||
list_display = ("domain", "is_externally_managed", "created")
|
||||
search_fields = ("domain", "is_externally_managed", "created")
|
||||
list_filter = ("is_externally_managed",)
|
@ -1,7 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AntispamConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
verbose_name = "antispam"
|
||||
name = "antispam"
|
@ -1,18 +0,0 @@
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
from django.core.validators import EmailValidator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from antispam.models import ToxicDomain
|
||||
|
||||
|
||||
class AntiSpamEmailField(forms.EmailField):
|
||||
"""An email field that email addresses with a known toxic domain."""
|
||||
|
||||
def run_validators(self, value: str):
|
||||
super().run_validators(value)
|
||||
# Domain part should exist since email validation is guaranteed to run first
|
||||
domain = re.search(EmailValidator.domain_regex, value)
|
||||
if ToxicDomain.objects.filter(domain=domain[0]).exists():
|
||||
raise forms.ValidationError(_("Email domain is not allowed."))
|
@ -1,69 +0,0 @@
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.core.management import BaseCommand
|
||||
from django.db.models import Max
|
||||
from django.utils import timezone
|
||||
|
||||
from antispam.models import ToxicDomain
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Update blocked ips/mails database"""
|
||||
|
||||
help = "Update blocked ips/mails database"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--force", action="store_true", help="Force re-creation even if up to date"
|
||||
)
|
||||
|
||||
def _should_update(self, *, force: bool = False) -> bool:
|
||||
if force:
|
||||
return True
|
||||
oldest = ToxicDomain.objects.filter(is_externally_managed=True).aggregate(
|
||||
res=Max("created")
|
||||
)["res"]
|
||||
return not (oldest and timezone.now() < (oldest + timezone.timedelta(days=1)))
|
||||
|
||||
def _download_domains(self, providers: list[str]) -> set[str]:
|
||||
domains = set()
|
||||
for provider in providers:
|
||||
res = requests.get(provider)
|
||||
if not res.ok:
|
||||
self.stderr.write(
|
||||
f"Source {provider} responded with code {res.status_code}"
|
||||
)
|
||||
continue
|
||||
domains |= set(res.content.decode().splitlines())
|
||||
return domains
|
||||
|
||||
def _update_domains(self, domains: set[str]):
|
||||
# Cleanup database
|
||||
ToxicDomain.objects.filter(is_externally_managed=True).delete()
|
||||
|
||||
# Create database
|
||||
ToxicDomain.objects.bulk_create(
|
||||
[
|
||||
ToxicDomain(domain=domain, is_externally_managed=True)
|
||||
for domain in domains
|
||||
],
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
self.stdout.write("Domain database updated")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if not self._should_update(force=options["force"]):
|
||||
self.stdout.write("Domain database is up to date")
|
||||
return
|
||||
self.stdout.write("Updating domain database")
|
||||
|
||||
domains = self._download_domains(settings.TOXIC_DOMAINS_PROVIDERS)
|
||||
|
||||
if not domains:
|
||||
self.stderr.write(
|
||||
"No domains could be fetched from settings.TOXIC_DOMAINS_PROVIDERS. "
|
||||
"Please, have a look at your settings."
|
||||
)
|
||||
return
|
||||
|
||||
self._update_domains(domains)
|
@ -1,35 +0,0 @@
|
||||
# Generated by Django 4.2.14 on 2024-08-03 23:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ToxicDomain",
|
||||
fields=[
|
||||
(
|
||||
"domain",
|
||||
models.URLField(
|
||||
max_length=253,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="domain",
|
||||
),
|
||||
),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"is_externally_managed",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="True if kept up-to-date using external toxic domain providers, else False",
|
||||
verbose_name="is externally managed",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
@ -1,19 +0,0 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class ToxicDomain(models.Model):
|
||||
"""Domain marked as spam in public databases"""
|
||||
|
||||
domain = models.URLField(_("domain"), max_length=253, primary_key=True)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
is_externally_managed = models.BooleanField(
|
||||
_("is externally managed"),
|
||||
default=False,
|
||||
help_text=_(
|
||||
"True if kept up-to-date using external toxic domain providers, else False"
|
||||
),
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.domain
|
15
api/__init__.py
Normal file
15
api/__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# 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"
|
||||
#
|
||||
#
|
19
api/admin.py
Normal file
19
api/admin.py
Normal file
@ -0,0 +1,19 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# 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.contrib import admin
|
||||
|
||||
# Register your models here.
|
19
api/models.py
Normal file
19
api/models.py
Normal file
@ -0,0 +1,19 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# 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.db import models
|
||||
|
||||
# Create your models here.
|
19
api/tests.py
Normal file
19
api/tests.py
Normal file
@ -0,0 +1,19 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# 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.test import TestCase
|
||||
|
||||
# Create your tests here.
|
50
api/urls.py
Normal file
50
api/urls.py
Normal file
@ -0,0 +1,50 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# 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 re_path, path, include
|
||||
|
||||
from api.views import *
|
||||
from rest_framework import routers
|
||||
|
||||
# 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/<int:user>", all_pictures_of_user_endpoint, name="all_pictures_of_user"),
|
||||
]
|
73
api/views/__init__.py
Normal file
73
api/views/__init__.py
Normal file
@ -0,0 +1,73 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# 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.response import Response
|
||||
from rest_framework import viewsets
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from rest_framework.decorators import action
|
||||
from django.db.models.query import QuerySet
|
||||
|
||||
from core.views import can_view, can_edit
|
||||
|
||||
|
||||
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(RightModelViewSet, self).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 .counter import *
|
||||
from .user import *
|
||||
from .club import *
|
||||
from .group import *
|
||||
from .launderette import *
|
||||
from .uv import *
|
||||
from .sas import *
|
34
api/views/api.py
Normal file
34
api/views/api.py
Normal file
@ -0,0 +1,34 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# 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.response import Response
|
||||
from rest_framework.decorators import api_view, renderer_classes
|
||||
from rest_framework.renderers import StaticHTMLRenderer
|
||||
|
||||
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)
|
56
api/views/club.py
Normal file
56
api/views/club.py
Normal file
@ -0,0 +1,56 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# 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.response import Response
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import api_view, renderer_classes
|
||||
from rest_framework.renderers import StaticHTMLRenderer
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
from club.models import Club, Mailing
|
||||
|
||||
from api.views import RightModelViewSet
|
||||
|
||||
|
||||
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)
|
52
api/views/counter.py
Normal file
52
api/views/counter.py
Normal file
@ -0,0 +1,52 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# 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.response import Response
|
||||
from rest_framework.decorators import action
|
||||
|
||||
from counter.models import Counter
|
||||
|
||||
from api.views import RightModelViewSet
|
||||
|
||||
|
||||
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)
|
35
api/views/group.py
Normal file
35
api/views/group.py
Normal file
@ -0,0 +1,35 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# 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 core.models import RealGroup
|
||||
|
||||
from api.views import RightModelViewSet
|
||||
|
||||
|
||||
class GroupSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RealGroup
|
||||
|
||||
|
||||
class GroupViewSet(RightModelViewSet):
|
||||
"""
|
||||
Manage Groups (api/v1/group/)
|
||||
"""
|
||||
|
||||
serializer_class = GroupSerializer
|
||||
queryset = RealGroup.objects.all()
|
128
api/views/launderette.py
Normal file
128
api/views/launderette.py
Normal file
@ -0,0 +1,128 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# 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.response import Response
|
||||
from rest_framework.decorators import action
|
||||
|
||||
from launderette.models import Launderette, Machine, Token
|
||||
|
||||
from api.views import RightModelViewSet
|
||||
|
||||
|
||||
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)
|
42
api/views/sas.py
Normal file
42
api/views/sas.py
Normal file
@ -0,0 +1,42 @@
|
||||
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.views import can_edit
|
||||
from core.models import User
|
||||
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)
|
||||
]
|
||||
)
|
60
api/views/user.py
Normal file
60
api/views/user.py
Normal file
@ -0,0 +1,60 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# 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.response import Response
|
||||
from rest_framework.decorators import action
|
||||
|
||||
from core.models import User
|
||||
|
||||
from api.views import RightModelViewSet
|
||||
|
||||
|
||||
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)
|
127
api/views/uv.py
Normal file
127
api/views/uv.py
Normal file
@ -0,0 +1,127 @@
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.decorators import api_view, renderer_classes
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.conf import settings
|
||||
from rest_framework import serializers
|
||||
import urllib.request
|
||||
import json
|
||||
|
||||
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
|
29
biome.json
29
biome.json
@ -1,29 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"ignore": ["*.min.*", "staticfiles/generated"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"lineWidth": 88
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"all": true
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"globals": ["Alpine", "$", "jQuery", "gettext", "interpolate"]
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# Copyright 2023 © AE UTBM
|
||||
# ae@utbm.fr / ae.info@utbm.fr
|
||||
@ -5,10 +6,10 @@
|
||||
# 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/sith
|
||||
# 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/sith/master/LICENSE
|
||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||
#
|
||||
#
|
||||
|
@ -1,3 +1,4 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# Copyright 2023 © AE UTBM
|
||||
# ae@utbm.fr / ae.info@utbm.fr
|
||||
@ -5,13 +6,14 @@
|
||||
# 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/sith
|
||||
# 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/sith/master/LICENSE
|
||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||
#
|
||||
#
|
||||
from ajax_select import make_ajax_form
|
||||
from django.contrib import admin
|
||||
|
||||
from club.models import Club, Membership
|
||||
@ -31,4 +33,4 @@ class MembershipAdmin(admin.ModelAdmin):
|
||||
"user__last_name",
|
||||
"club__name",
|
||||
)
|
||||
autocomplete_fields = ("user",)
|
||||
form = make_ajax_form(Membership, {"user": "users"})
|
||||
|
22
club/api.py
22
club/api.py
@ -1,22 +0,0 @@
|
||||
from typing import Annotated
|
||||
|
||||
from annotated_types import MinLen
|
||||
from ninja_extra import ControllerBase, api_controller, paginate, route
|
||||
from ninja_extra.pagination import PageNumberPaginationExtra
|
||||
from ninja_extra.schemas import PaginatedResponseSchema
|
||||
|
||||
from club.models import Club
|
||||
from club.schemas import ClubSchema
|
||||
from core.api_permissions import CanAccessLookup
|
||||
|
||||
|
||||
@api_controller("/club")
|
||||
class ClubController(ControllerBase):
|
||||
@route.get(
|
||||
"/search",
|
||||
response=PaginatedResponseSchema[ClubSchema],
|
||||
permissions=[CanAccessLookup],
|
||||
)
|
||||
@paginate(PageNumberPaginationExtra, page_size=50)
|
||||
def search_club(self, search: Annotated[str, MinLen(1)]):
|
||||
return Club.objects.filter(name__icontains=search).values()
|
@ -1,3 +1,4 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# Copyright 2016,2017
|
||||
# - Skia <skia@libskia.so>
|
||||
@ -22,15 +23,18 @@
|
||||
#
|
||||
#
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from club.models import Club, Mailing, MailingSubscription, Membership
|
||||
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
|
||||
|
||||
from club.models import Mailing, MailingSubscription, Club, Membership
|
||||
|
||||
from core.models import User
|
||||
from core.views.forms import SelectDate, SelectDateTime
|
||||
from core.views.widgets.select import AutoCompleteSelectMultipleUser
|
||||
from counter.models import Counter
|
||||
from core.views.forms import TzAwareDateTimeField
|
||||
|
||||
|
||||
class ClubEditForm(forms.ModelForm):
|
||||
@ -39,27 +43,28 @@ class ClubEditForm(forms.ModelForm):
|
||||
fields = ["address", "logo", "short_description"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
super(ClubEditForm, self).__init__(*args, **kwargs)
|
||||
self.fields["short_description"].widget = forms.Textarea()
|
||||
|
||||
|
||||
class MailingForm(forms.Form):
|
||||
"""Form handling mailing lists right."""
|
||||
"""
|
||||
Form handling mailing lists right
|
||||
"""
|
||||
|
||||
ACTION_NEW_MAILING = 1
|
||||
ACTION_NEW_SUBSCRIPTION = 2
|
||||
ACTION_REMOVE_SUBSCRIPTION = 3
|
||||
|
||||
subscription_users = forms.ModelMultipleChoiceField(
|
||||
subscription_users = AutoCompleteSelectMultipleField(
|
||||
"users",
|
||||
label=_("Users to add"),
|
||||
help_text=_("Search users to add (one or more)."),
|
||||
required=False,
|
||||
widget=AutoCompleteSelectMultipleUser,
|
||||
queryset=User.objects.all(),
|
||||
)
|
||||
|
||||
def __init__(self, club_id, user_id, mailings, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
super(MailingForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields["action"] = forms.TypedChoiceField(
|
||||
choices=(
|
||||
@ -104,15 +109,24 @@ 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."""
|
||||
cleaned_data = super().clean()
|
||||
"""
|
||||
Convert given users into real users and check their validity
|
||||
"""
|
||||
cleaned_data = super(MailingForm, self).clean()
|
||||
users = []
|
||||
for user in cleaned_data["subscription_users"]:
|
||||
user = User.objects.filter(id=user).first()
|
||||
if not user:
|
||||
raise forms.ValidationError(
|
||||
_("One of the selected users doesn't exist"), code="invalid"
|
||||
)
|
||||
if not user.email:
|
||||
raise forms.ValidationError(
|
||||
_("One of the selected users doesn't have an email address"),
|
||||
@ -122,9 +136,9 @@ class MailingForm(forms.Form):
|
||||
return users
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
cleaned_data = super(MailingForm, self).clean()
|
||||
|
||||
if "action" not in cleaned_data:
|
||||
if not "action" in cleaned_data:
|
||||
# If there is no action provided, we can stop here
|
||||
raise forms.ValidationError(_("An action is required"), code="invalid")
|
||||
|
||||
@ -145,19 +159,15 @@ class MailingForm(forms.Form):
|
||||
|
||||
|
||||
class SellingsForm(forms.Form):
|
||||
begin_date = forms.DateTimeField(
|
||||
label=_("Begin date"), widget=SelectDateTime, required=False
|
||||
)
|
||||
end_date = forms.DateTimeField(
|
||||
label=_("End date"), widget=SelectDateTime, required=False
|
||||
)
|
||||
begin_date = TzAwareDateTimeField(label=_("Begin date"), required=False)
|
||||
end_date = TzAwareDateTimeField(label=_("End date"), required=False)
|
||||
|
||||
counters = forms.ModelMultipleChoiceField(
|
||||
Counter.objects.order_by("name").all(), label=_("Counter"), required=False
|
||||
)
|
||||
|
||||
def __init__(self, club, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
super(SellingsForm, self).__init__(*args, **kwargs)
|
||||
self.fields["products"] = forms.ModelMultipleChoiceField(
|
||||
club.products.order_by("name").filter(archived=False).all(),
|
||||
label=_("Products"),
|
||||
@ -171,17 +181,18 @@ 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"
|
||||
|
||||
users = forms.ModelMultipleChoiceField(
|
||||
users = AutoCompleteSelectMultipleField(
|
||||
"users",
|
||||
label=_("Users to add"),
|
||||
help_text=_("Search users to add (one or more)."),
|
||||
required=False,
|
||||
widget=AutoCompleteSelectMultipleUser,
|
||||
queryset=User.objects.all(),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -193,7 +204,7 @@ class ClubMemberForm(forms.Form):
|
||||
self.club.members.filter(end_date=None).order_by("-role").all()
|
||||
)
|
||||
self.request_user_membership = self.club.get_membership_for(self.request_user)
|
||||
super().__init__(*args, **kwargs)
|
||||
super(ClubMemberForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Using a ModelForm binds too much the form with the model and we don't want that
|
||||
# We want the view to process the model creation since they are multiple users
|
||||
@ -229,13 +240,18 @@ 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.
|
||||
"""
|
||||
cleaned_data = super().clean()
|
||||
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(ClubMemberForm, self).clean()
|
||||
users = []
|
||||
for user in cleaned_data["users"]:
|
||||
for user_id in cleaned_data["users"]:
|
||||
user = User.objects.filter(id=user_id).first()
|
||||
if not user:
|
||||
raise forms.ValidationError(
|
||||
_("One of the selected users doesn't exist"), code="invalid"
|
||||
)
|
||||
if not user.is_subscribed:
|
||||
raise forms.ValidationError(
|
||||
_("User must be subscriber to take part to a club"), code="invalid"
|
||||
@ -248,8 +264,10 @@ class ClubMemberForm(forms.Form):
|
||||
return users
|
||||
|
||||
def clean(self):
|
||||
"""Check user rights for adding an user."""
|
||||
cleaned_data = super().clean()
|
||||
"""
|
||||
Check user rights for adding an user
|
||||
"""
|
||||
cleaned_data = super(ClubMemberForm, self).clean()
|
||||
|
||||
if "start_date" in cleaned_data and not cleaned_data["start_date"]:
|
||||
# Drop start_date if allowed to edition but not specified
|
||||
|
@ -1,8 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -1,8 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
from django.conf import settings
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
@ -1,8 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
from django.conf import settings
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -1,7 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -1,7 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
@ -1,11 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.conf import settings
|
||||
import re
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -109,6 +109,6 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="mailingsubscription",
|
||||
unique_together={("user", "email", "mailing")},
|
||||
unique_together=set([("user", "email", "mailing")]),
|
||||
),
|
||||
]
|
||||
|
@ -1,10 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
from club.models import Club
|
||||
from core.operations import PsqlRunOnly
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def generate_club_pages(apps, schema_editor):
|
||||
|
@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
@ -1,9 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import club.models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -15,7 +15,7 @@ class Migration(migrations.Migration):
|
||||
name="owner_group",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
default=club.models.get_default_owner_group,
|
||||
default=club.models.Club.get_default_owner_group,
|
||||
related_name="owned_club",
|
||||
to="core.Group",
|
||||
),
|
||||
|
257
club/models.py
257
club/models.py
@ -1,3 +1,4 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# Copyright 2016,2017
|
||||
# - Skia <skia@libskia.so>
|
||||
@ -21,35 +22,31 @@
|
||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#
|
||||
#
|
||||
from __future__ import annotations
|
||||
from typing import Optional
|
||||
|
||||
from typing import Self
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import validators
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.validators import RegexValidator, validate_email
|
||||
from django.db import models, transaction
|
||||
from django.db import models
|
||||
from django.core import validators
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||
from django.db import transaction
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.core.validators import RegexValidator, validate_email
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import localdate
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import Group, MetaGroup, Notification, Page, RealGroup, SithFile, User
|
||||
from core.models import User, MetaGroup, Group, SithFile, RealGroup, Notification, Page
|
||||
|
||||
# Create your models here.
|
||||
|
||||
|
||||
# This function prevents generating migration upon settings change
|
||||
def get_default_owner_group():
|
||||
return settings.SITH_GROUP_ROOT_ID
|
||||
|
||||
|
||||
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)
|
||||
@ -80,6 +77,10 @@ class Club(models.Model):
|
||||
)
|
||||
address = models.CharField(_("address"), max_length=254)
|
||||
|
||||
# This function prevents generating migration upon settings change
|
||||
def get_default_owner_group():
|
||||
return settings.SITH_GROUP_ROOT_ID
|
||||
|
||||
owner_group = models.ForeignKey(
|
||||
Group,
|
||||
related_name="owned_club",
|
||||
@ -107,34 +108,6 @@ class Club(models.Model):
|
||||
class Meta:
|
||||
ordering = ["name", "unix_name"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@transaction.atomic()
|
||||
def save(self, *args, **kwargs):
|
||||
old = Club.objects.filter(id=self.id).first()
|
||||
creation = old is None
|
||||
if not creation and old.unix_name != self.unix_name:
|
||||
self._change_unixname(self.unix_name)
|
||||
super().save(*args, **kwargs)
|
||||
if creation:
|
||||
board = MetaGroup(name=self.unix_name + settings.SITH_BOARD_SUFFIX)
|
||||
board.save()
|
||||
member = MetaGroup(name=self.unix_name + settings.SITH_MEMBER_SUFFIX)
|
||||
member.save()
|
||||
subscribers = Group.objects.filter(
|
||||
name=settings.SITH_MAIN_MEMBERS_GROUP
|
||||
).first()
|
||||
self.make_home()
|
||||
self.home.edit_groups.set([board])
|
||||
self.home.view_groups.set([member, subscribers])
|
||||
self.home.save()
|
||||
self.make_page()
|
||||
cache.set(f"sith_club_{self.unix_name}", self)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("club:club_view", kwargs={"club_id": self.id})
|
||||
|
||||
@cached_property
|
||||
def president(self):
|
||||
return self.members.filter(
|
||||
@ -142,7 +115,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:
|
||||
@ -213,18 +186,48 @@ class Club(models.Model):
|
||||
self.page.parent = self.parent.page
|
||||
self.page.save(force_lock=True)
|
||||
|
||||
@transaction.atomic()
|
||||
def save(self, *args, **kwargs):
|
||||
old = Club.objects.filter(id=self.id).first()
|
||||
creation = old is None
|
||||
if not creation and old.unix_name != self.unix_name:
|
||||
self._change_unixname(self.unix_name)
|
||||
super(Club, self).save(*args, **kwargs)
|
||||
if creation:
|
||||
board = MetaGroup(name=self.unix_name + settings.SITH_BOARD_SUFFIX)
|
||||
board.save()
|
||||
member = MetaGroup(name=self.unix_name + settings.SITH_MEMBER_SUFFIX)
|
||||
member.save()
|
||||
subscribers = Group.objects.filter(
|
||||
name=settings.SITH_MAIN_MEMBERS_GROUP
|
||||
).first()
|
||||
self.make_home()
|
||||
self.home.edit_groups.set([board])
|
||||
self.home.view_groups.set([member, subscribers])
|
||||
self.home.save()
|
||||
self.make_page()
|
||||
cache.set(f"sith_club_{self.unix_name}", self)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
# Invalidate the cache of this club and of its memberships
|
||||
for membership in self.members.ongoing().select_related("user"):
|
||||
cache.delete(f"membership_{self.id}_{membership.user.id}")
|
||||
cache.delete(f"sith_club_{self.unix_name}")
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("club:club_view", kwargs={"club_id": self.id})
|
||||
|
||||
def get_display_name(self):
|
||||
return self.name
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""Method to see if that object can be super edited by the given user."""
|
||||
"""
|
||||
Method to see if that object can be super edited by the given user
|
||||
"""
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
return user.is_board_member
|
||||
@ -233,20 +236,23 @@ 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) -> Membership | None:
|
||||
"""Return the current membership the given user.
|
||||
|
||||
Note:
|
||||
def get_membership_for(self, user: User) -> Optional["Membership"]:
|
||||
"""
|
||||
Return the current membership the given user.
|
||||
The result is cached.
|
||||
"""
|
||||
if user.is_anonymous:
|
||||
@ -268,12 +274,16 @@ class Club(models.Model):
|
||||
|
||||
|
||||
class MembershipQuerySet(models.QuerySet):
|
||||
def ongoing(self) -> Self:
|
||||
"""Filter all memberships which are not finished yet."""
|
||||
return self.filter(Q(end_date=None) | Q(end_date__gt=localdate()))
|
||||
def ongoing(self) -> "MembershipQuerySet":
|
||||
"""
|
||||
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) -> Self:
|
||||
"""Filter all memberships where the user is/was in the board.
|
||||
def board(self) -> "MembershipQuerySet":
|
||||
"""
|
||||
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.
|
||||
@ -281,12 +291,13 @@ class MembershipQuerySet(models.QuerySet):
|
||||
If you want to get the users who are currently in the board,
|
||||
mind combining this with the :meth:`ongoing` queryset method
|
||||
"""
|
||||
# noinspection PyTypeChecker
|
||||
return self.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE)
|
||||
|
||||
def update(self, **kwargs):
|
||||
"""Refresh the cache for the elements of the queryset.
|
||||
|
||||
Besides that, does the same job as a regular update method.
|
||||
"""
|
||||
Work just like the default Django's update() method,
|
||||
but add a cache refresh for the elements of the queryset.
|
||||
|
||||
Be aware that this adds a db query to retrieve the updated objects
|
||||
"""
|
||||
@ -306,7 +317,8 @@ 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.
|
||||
|
||||
@ -322,7 +334,8 @@ 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
|
||||
@ -363,11 +376,36 @@ class Membership(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"{self.club.name} - {self.user.username} "
|
||||
f"- {settings.SITH_CLUB_ROLES[self.role]} "
|
||||
f"- {str(_('past member')) if self.end_date is not None else ''}"
|
||||
self.club.name
|
||||
+ " - "
|
||||
+ self.user.username
|
||||
+ " - "
|
||||
+ str(settings.SITH_CLUB_ROLES[self.role])
|
||||
+ str(" - " + str(_("past member")) if self.end_date is not None else "")
|
||||
)
|
||||
|
||||
def is_owned_by(self, 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
|
||||
"""
|
||||
if user.is_root or user.is_board_member:
|
||||
return True
|
||||
membership = self.club.get_membership_for(user)
|
||||
if membership is not None and membership.role >= self.role:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("club:club_members", kwargs={"club_id": self.club_id})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
if self.end_date is None:
|
||||
@ -375,32 +413,15 @@ class Membership(models.Model):
|
||||
else:
|
||||
cache.set(f"membership_{self.club_id}_{self.user_id}", "not_member")
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("club:club_members", kwargs={"club_id": self.club_id})
|
||||
|
||||
def is_owned_by(self, user):
|
||||
"""Method to see if that object can be super edited by the given user."""
|
||||
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."""
|
||||
if user.is_root or user.is_board_member:
|
||||
return True
|
||||
membership = self.club.get_membership_for(user)
|
||||
return membership is not None and membership.role >= self.role
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
cache.delete(f"membership_{self.club_id}_{self.user_id}")
|
||||
|
||||
|
||||
class Mailing(models.Model):
|
||||
"""A Mailing list for a club.
|
||||
|
||||
Warning:
|
||||
Remember that mailing lists should be validated by UTBM.
|
||||
"""
|
||||
This class correspond to a mailing list
|
||||
Remember that mailing lists should be validated by UTBM
|
||||
"""
|
||||
|
||||
club = models.ForeignKey(
|
||||
@ -433,26 +454,6 @@ class Mailing(models.Model):
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "%s - %s" % (self.club, self.email_full)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.is_moderated:
|
||||
for user in (
|
||||
RealGroup.objects.filter(id=settings.SITH_GROUP_COM_ADMIN_ID)
|
||||
.first()
|
||||
.users.all()
|
||||
):
|
||||
if not user.notifications.filter(
|
||||
type="MAILING_MODERATION", viewed=False
|
||||
).exists():
|
||||
Notification(
|
||||
user=user,
|
||||
url=reverse("com:mailing_admin"),
|
||||
type="MAILING_MODERATION",
|
||||
).save(*args, **kwargs)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
if Mailing.objects.filter(email=self.email).exists():
|
||||
raise ValidationError(_("This mailing list already exists."))
|
||||
@ -460,7 +461,7 @@ class Mailing(models.Model):
|
||||
self.is_moderated = True
|
||||
else:
|
||||
self.moderator = None
|
||||
super().clean()
|
||||
super(Mailing, self).clean()
|
||||
|
||||
@property
|
||||
def email_full(self):
|
||||
@ -482,15 +483,39 @@ class Mailing(models.Model):
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
self.subscriptions.all().delete()
|
||||
super().delete()
|
||||
super(Mailing, self).delete()
|
||||
|
||||
def fetch_format(self):
|
||||
destination = "".join(s.fetch_format() for s in self.subscriptions.all())
|
||||
return f"{self.email}: {destination}"
|
||||
resp = self.email + ": "
|
||||
for sub in self.subscriptions.all():
|
||||
resp += sub.fetch_format()
|
||||
return resp
|
||||
|
||||
def save(self):
|
||||
if not self.is_moderated:
|
||||
for user in (
|
||||
RealGroup.objects.filter(id=settings.SITH_GROUP_COM_ADMIN_ID)
|
||||
.first()
|
||||
.users.all()
|
||||
):
|
||||
if not user.notifications.filter(
|
||||
type="MAILING_MODERATION", viewed=False
|
||||
).exists():
|
||||
Notification(
|
||||
user=user,
|
||||
url=reverse("com:mailing_admin"),
|
||||
type="MAILING_MODERATION",
|
||||
).save()
|
||||
super(Mailing, self).save()
|
||||
|
||||
def __str__(self):
|
||||
return "%s - %s" % (self.club, self.email_full)
|
||||
|
||||
|
||||
class MailingSubscription(models.Model):
|
||||
"""Link between user and mailing list."""
|
||||
"""
|
||||
This class makes the link between user and mailing list
|
||||
"""
|
||||
|
||||
mailing = models.ForeignKey(
|
||||
Mailing,
|
||||
@ -513,9 +538,6 @@ class MailingSubscription(models.Model):
|
||||
class Meta:
|
||||
unique_together = (("user", "email", "mailing"),)
|
||||
|
||||
def __str__(self):
|
||||
return "(%s) - %s : %s" % (self.mailing, self.get_username, self.email)
|
||||
|
||||
def clean(self):
|
||||
if not self.user and not self.email:
|
||||
raise ValidationError(_("At least user or email is required"))
|
||||
@ -530,7 +552,7 @@ class MailingSubscription(models.Model):
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
super().clean()
|
||||
super(MailingSubscription, self).clean()
|
||||
|
||||
def is_owned_by(self, user):
|
||||
if user.is_anonymous:
|
||||
@ -558,3 +580,6 @@ class MailingSubscription(models.Model):
|
||||
|
||||
def fetch_format(self):
|
||||
return self.get_email + " "
|
||||
|
||||
def __str__(self):
|
||||
return "(%s) - %s : %s" % (self.mailing, self.get_username, self.email)
|
||||
|
@ -1,9 +0,0 @@
|
||||
from ninja import ModelSchema
|
||||
|
||||
from club.models import Club
|
||||
|
||||
|
||||
class ClubSchema(ModelSchema):
|
||||
class Meta:
|
||||
model = Club
|
||||
fields = ["id", "name"]
|
@ -1,30 +0,0 @@
|
||||
import { AjaxSelect } from "#core:core/components/ajax-select-base";
|
||||
import { registerComponent } from "#core:utils/web-components";
|
||||
import type { TomOption } from "tom-select/dist/types/types";
|
||||
import type { escape_html } from "tom-select/dist/types/utils";
|
||||
import { type ClubSchema, clubSearchClub } from "#openapi";
|
||||
|
||||
@registerComponent("club-ajax-select")
|
||||
export class ClubAjaxSelect extends AjaxSelect {
|
||||
protected valueField = "id";
|
||||
protected labelField = "name";
|
||||
protected searchField = ["code", "name"];
|
||||
|
||||
protected async search(query: string): Promise<TomOption[]> {
|
||||
const resp = await clubSearchClub({ query: { search: query } });
|
||||
if (resp.data) {
|
||||
return resp.data.results;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
protected renderOption(item: ClubSchema, sanitize: typeof escape_html) {
|
||||
return `<div class="select-item">
|
||||
<span class="select-item-text">${sanitize(item.name)}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected renderItem(item: ClubSchema, sanitize: typeof escape_html) {
|
||||
return `<span>${sanitize(item.name)}</span>`;
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}Club list{% endtrans %}
|
||||
{% trans %}Club list{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% macro display_club(club) -%}
|
||||
|
@ -1,48 +1,20 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
{% from 'core/macros.jinja' import user_profile_link %}
|
||||
|
||||
{# This page uses a custom macro instead of the core `paginate_jinja` and `paginate_alpine`
|
||||
because it works with a somewhat dynamic form,
|
||||
but was written before Alpine was introduced in the project.
|
||||
TODO : rewrite the pagination used in this template an Alpine one
|
||||
#}
|
||||
{% macro paginate(page_obj, paginator, js_action) %}
|
||||
{% set js = js_action|default('') %}
|
||||
{% if page_obj.has_previous() or page_obj.has_next() %}
|
||||
{% if page_obj.has_previous() %}
|
||||
<a {% if js %} type="submit" onclick="{{ js }}" {% endif %} href="?page={{ page_obj.previous_page_number() }}">{% trans %}Previous{% endtrans %}</a>
|
||||
{% else %}
|
||||
<span class="disabled">{% trans %}Previous{% endtrans %}</span>
|
||||
{% endif %}
|
||||
{% for i in paginator.page_range %}
|
||||
{% if page_obj.number == i %}
|
||||
<span class="active">{{ i }} <span class="sr-only">({% trans %}current{% endtrans %})</span></span>
|
||||
{% else %}
|
||||
<a {% if js %} type="submit" onclick="{{ js }}" {% endif %} href="?page={{ i }}">{{ i }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if page_obj.has_next() %}
|
||||
<a {% if js %} type="submit" onclick="{{ js }}" {% endif %} href="?page={{ page_obj.next_page_number() }}">{% trans %}Next{% endtrans %}</a>
|
||||
{% else %}
|
||||
<span class="disabled">{% trans %}Next{% endtrans %}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{% from 'core/macros.jinja' import user_profile_link, paginate %}
|
||||
|
||||
{% block content %}
|
||||
<h3>{% trans %}Sales{% endtrans %}</h3>
|
||||
<form id="form" action="?page=1" method="post">
|
||||
<h3>{% trans %}Sellings{% endtrans %}</h3>
|
||||
<form id="form" action="?page=1" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
<p><input type="submit" value="{% trans %}Show{% endtrans %}" /></p>
|
||||
<p><input type="submit" value="{% trans %}Download as cvs{% endtrans %}" formaction="{{ url('club:sellings_csv', club_id=object.id) }}"/></p>
|
||||
</form>
|
||||
<p>
|
||||
{% trans %}Quantity: {% endtrans %}{{ total_quantity }} {% trans %}units{% endtrans %}<br/>
|
||||
{% trans %}Total: {% endtrans %}{{ total }} €<br/>
|
||||
{% trans %}Benefit: {% endtrans %}{{ benefit }} €
|
||||
</p>
|
||||
<table>
|
||||
</form>
|
||||
<p>
|
||||
{% trans %}Quantity: {% endtrans %}{{ total_quantity }} {% trans %}units{% endtrans %}<br/>
|
||||
{% trans %}Total: {% endtrans %}{{ total }} €<br/>
|
||||
{% trans %}Benefit: {% endtrans %}{{ benefit }} €
|
||||
</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{% trans %}Date{% endtrans %}</td>
|
||||
@ -80,15 +52,15 @@ TODO : rewrite the pagination used in this template an Alpine one
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<script type="text/javascript">
|
||||
</table>
|
||||
<script type="text/javascript">
|
||||
function formPagination(link){
|
||||
$("form").attr("action", link.href);
|
||||
link.href = "javascript:void(0)"; // block link action
|
||||
$("form").submit();
|
||||
}
|
||||
</script>
|
||||
{{ paginate(paginated_result, paginator, "formPagination(this)") }}
|
||||
</script>
|
||||
{{ paginate(paginated_result, paginator, "formPagination(this)") }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
|
||||
{% block content %}
|
||||
<h3>{% trans %}Club tools{% endtrans %}</h3>
|
||||
<div>
|
||||
<h3>{% trans %}Club tools{% endtrans %}</h3>
|
||||
<div>
|
||||
<h4>{% trans %}Communication:{% endtrans %}</h4>
|
||||
<ul>
|
||||
<li> <a href="{{ url('com:news_new') }}?club={{ object.id }}">{% trans %}Create a news{% endtrans %}</a></li>
|
||||
@ -30,7 +30,7 @@
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% if object.club_account.exists() %}
|
||||
<h4>{% trans %}Accounting: {% endtrans %}</h4>
|
||||
<h4>{% trans %}Accouting: {% endtrans %}</h4>
|
||||
<ul>
|
||||
{% for ca in object.club_account.all() %}
|
||||
<li><a href="{{ url('accounting:club_details', c_account_id=ca.id) }}">{{ ca.get_display_name() }}</a></li>
|
||||
@ -40,7 +40,7 @@
|
||||
{% if object.unix_name == settings.SITH_LAUNDERETTE_MANAGER['unix_name'] %}
|
||||
<li><a href="{{ url('launderette:launderette_list') }}">{% trans %}Manage launderettes{% endtrans %}</a></li>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
{% from 'core/macros.jinja' import select_all_checkbox %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}Mailing lists{% endtrans %}
|
||||
{% trans %}Mailing lists{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -2,7 +2,7 @@
|
||||
{% from 'core/macros_pages.jinja' import page_edit_form %}
|
||||
|
||||
{% block content %}
|
||||
{{ page_edit_form(page, form, url('club:club_edit_page', club_id=page.club.id), csrf_token) }}
|
||||
{{ page_edit_form(page, form, url('club:club_edit_page', club_id=page.club.id), csrf_token) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}Club stats{% endtrans %}
|
||||
{% trans %}Club stats{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
669
club/tests.py
669
club/tests.py
File diff suppressed because it is too large
Load Diff
52
club/urls.py
52
club/urls.py
@ -1,3 +1,4 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# Copyright 2016,2017
|
||||
# - Skia <skia@libskia.so>
|
||||
@ -24,32 +25,7 @@
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from club.views import (
|
||||
ClubCreateView,
|
||||
ClubEditPropView,
|
||||
ClubEditView,
|
||||
ClubListView,
|
||||
ClubMailingView,
|
||||
ClubMembersView,
|
||||
ClubOldMembersView,
|
||||
ClubPageEditView,
|
||||
ClubPageHistView,
|
||||
ClubRevView,
|
||||
ClubSellingCSVView,
|
||||
ClubSellingView,
|
||||
ClubStatView,
|
||||
ClubToolsView,
|
||||
ClubView,
|
||||
MailingAutoGenerationView,
|
||||
MailingDeleteView,
|
||||
MailingSubscriptionDeleteView,
|
||||
MembershipDeleteView,
|
||||
MembershipSetOldView,
|
||||
PosterCreateView,
|
||||
PosterDeleteView,
|
||||
PosterEditView,
|
||||
PosterListView,
|
||||
)
|
||||
from club.views import *
|
||||
|
||||
urlpatterns = [
|
||||
path("", ClubListView.as_view(), name="club_list"),
|
||||
@ -57,20 +33,32 @@ urlpatterns = [
|
||||
path("stats/", ClubStatView.as_view(), name="club_stats"),
|
||||
path("<int:club_id>/", ClubView.as_view(), name="club_view"),
|
||||
path(
|
||||
"<int:club_id>/rev/<int:rev_id>/", ClubRevView.as_view(), name="club_view_rev"
|
||||
"<int:club_id>/rev/<int:rev_id>/",
|
||||
ClubRevView.as_view(),
|
||||
name="club_view_rev",
|
||||
),
|
||||
path("<int:club_id>/hist/", ClubPageHistView.as_view(), name="club_hist"),
|
||||
path("<int:club_id>/edit/", ClubEditView.as_view(), name="club_edit"),
|
||||
path("<int:club_id>/edit/page/", ClubPageEditView.as_view(), name="club_edit_page"),
|
||||
path(
|
||||
"<int:club_id>/edit/page/",
|
||||
ClubPageEditView.as_view(),
|
||||
name="club_edit_page",
|
||||
),
|
||||
path("<int:club_id>/members/", ClubMembersView.as_view(), name="club_members"),
|
||||
path(
|
||||
"<int:club_id>/elderlies/",
|
||||
ClubOldMembersView.as_view(),
|
||||
name="club_old_members",
|
||||
),
|
||||
path("<int:club_id>/sellings/", ClubSellingView.as_view(), name="club_sellings"),
|
||||
path(
|
||||
"<int:club_id>/sellings/csv/", ClubSellingCSVView.as_view(), name="sellings_csv"
|
||||
"<int:club_id>/sellings/",
|
||||
ClubSellingView.as_view(),
|
||||
name="club_sellings",
|
||||
),
|
||||
path(
|
||||
"<int:club_id>/sellings/csv/",
|
||||
ClubSellingCSVView.as_view(),
|
||||
name="sellings_csv",
|
||||
),
|
||||
path("<int:club_id>/prop/", ClubEditPropView.as_view(), name="club_prop"),
|
||||
path("<int:club_id>/tools/", ClubToolsView.as_view(), name="tools"),
|
||||
@ -102,7 +90,9 @@ urlpatterns = [
|
||||
),
|
||||
path("<int:club_id>/poster/", PosterListView.as_view(), name="poster_list"),
|
||||
path(
|
||||
"<int:club_id>/poster/create/", PosterCreateView.as_view(), name="poster_create"
|
||||
"<int:club_id>/poster/create/",
|
||||
PosterCreateView.as_view(),
|
||||
name="poster_create",
|
||||
),
|
||||
path(
|
||||
"<int:club_id>/poster/<int:poster_id>/edit/",
|
||||
|
204
club/views.py
204
club/views.py
@ -1,3 +1,4 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# Copyright 2016,2017
|
||||
# - Skia <skia@libskia.so>
|
||||
@ -25,43 +26,51 @@
|
||||
import csv
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import NON_FIELD_ERRORS, PermissionDenied, ValidationError
|
||||
from django.core.paginator import InvalidPage, Paginator
|
||||
from django.db.models import Sum
|
||||
from django import forms
|
||||
from django.views.generic import ListView, DetailView, TemplateView, View
|
||||
from django.views.generic.edit import DeleteView
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from django.views.generic.edit import UpdateView, CreateView
|
||||
from django.http import (
|
||||
Http404,
|
||||
HttpResponseRedirect,
|
||||
HttpResponse,
|
||||
Http404,
|
||||
StreamingHttpResponse,
|
||||
)
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _t
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import DetailView, ListView, TemplateView, View
|
||||
from django.views.generic.edit import CreateView, DeleteView, UpdateView
|
||||
from django.utils.translation import gettext as _t
|
||||
from django.core.exceptions import PermissionDenied, ValidationError, NON_FIELD_ERRORS
|
||||
from django.core.paginator import Paginator, InvalidPage
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.db.models import Sum
|
||||
|
||||
|
||||
from club.forms import ClubEditForm, ClubMemberForm, MailingForm, SellingsForm
|
||||
from club.models import Club, Mailing, MailingSubscription, Membership
|
||||
from com.views import (
|
||||
PosterCreateBaseView,
|
||||
PosterDeleteBaseView,
|
||||
PosterEditBaseView,
|
||||
PosterListBaseView,
|
||||
)
|
||||
from core.models import PageRev
|
||||
from core.views import (
|
||||
CanCreateMixin,
|
||||
CanViewMixin,
|
||||
CanEditMixin,
|
||||
CanEditPropMixin,
|
||||
CanViewMixin,
|
||||
DetailFormView,
|
||||
PageEditViewBase,
|
||||
TabedViewMixin,
|
||||
UserIsRootMixin,
|
||||
TabedViewMixin,
|
||||
PageEditViewBase,
|
||||
DetailFormView,
|
||||
)
|
||||
from core.models import PageRev
|
||||
|
||||
from counter.models import Selling
|
||||
|
||||
from com.views import (
|
||||
PosterListBaseView,
|
||||
PosterCreateBaseView,
|
||||
PosterEditBaseView,
|
||||
PosterDeleteBaseView,
|
||||
)
|
||||
|
||||
from club.models import Club, Membership, Mailing, MailingSubscription
|
||||
from club.forms import MailingForm, ClubEditForm, ClubMemberForm, SellingsForm
|
||||
|
||||
|
||||
class ClubTabsMixin(TabedViewMixin):
|
||||
def get_tabs_title(self):
|
||||
@ -175,14 +184,18 @@ class ClubTabsMixin(TabedViewMixin):
|
||||
|
||||
|
||||
class ClubListView(ListView):
|
||||
"""List the Clubs."""
|
||||
"""
|
||||
List the Clubs
|
||||
"""
|
||||
|
||||
model = Club
|
||||
template_name = "club/club_list.jinja"
|
||||
|
||||
|
||||
class ClubView(ClubTabsMixin, DetailView):
|
||||
"""Front page of a Club."""
|
||||
"""
|
||||
Front page of a Club
|
||||
"""
|
||||
|
||||
model = Club
|
||||
pk_url_kwarg = "club_id"
|
||||
@ -190,22 +203,24 @@ class ClubView(ClubTabsMixin, DetailView):
|
||||
current_tab = "infos"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs = super(ClubView, self).get_context_data(**kwargs)
|
||||
if self.object.page and self.object.page.revisions.exists():
|
||||
kwargs["page_revision"] = self.object.page.revisions.last().content
|
||||
return kwargs
|
||||
|
||||
|
||||
class ClubRevView(ClubView):
|
||||
"""Display a specific page revision."""
|
||||
"""
|
||||
Display a specific page revision
|
||||
"""
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
self.revision = get_object_or_404(PageRev, pk=kwargs["rev_id"], page__club=obj)
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
return super(ClubRevView, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs = super(ClubRevView, self).get_context_data(**kwargs)
|
||||
kwargs["page_revision"] = self.revision.content
|
||||
return kwargs
|
||||
|
||||
@ -218,7 +233,7 @@ class ClubPageEditView(ClubTabsMixin, PageEditViewBase):
|
||||
self.club = get_object_or_404(Club, pk=kwargs["club_id"])
|
||||
if not self.club.page:
|
||||
raise Http404
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
return super(ClubPageEditView, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_object(self):
|
||||
self.page = self.club.page
|
||||
@ -229,7 +244,9 @@ class ClubPageEditView(ClubTabsMixin, PageEditViewBase):
|
||||
|
||||
|
||||
class ClubPageHistView(ClubTabsMixin, CanViewMixin, DetailView):
|
||||
"""Modification hostory of the page."""
|
||||
"""
|
||||
Modification hostory of the page
|
||||
"""
|
||||
|
||||
model = Club
|
||||
pk_url_kwarg = "club_id"
|
||||
@ -238,7 +255,9 @@ class ClubPageHistView(ClubTabsMixin, CanViewMixin, DetailView):
|
||||
|
||||
|
||||
class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView):
|
||||
"""Tools page of a Club."""
|
||||
"""
|
||||
Tools page of a Club
|
||||
"""
|
||||
|
||||
model = Club
|
||||
pk_url_kwarg = "club_id"
|
||||
@ -247,7 +266,9 @@ class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView):
|
||||
|
||||
|
||||
class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
|
||||
"""View of a club's members."""
|
||||
"""
|
||||
View of a club's members
|
||||
"""
|
||||
|
||||
model = Club
|
||||
pk_url_kwarg = "club_id"
|
||||
@ -256,20 +277,22 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
|
||||
current_tab = "members"
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs = super(ClubMembersView, self).get_form_kwargs()
|
||||
kwargs["request_user"] = self.request.user
|
||||
kwargs["club"] = self.get_object()
|
||||
kwargs["club_members"] = self.members
|
||||
return kwargs
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
kwargs = super().get_context_data(*args, **kwargs)
|
||||
kwargs = super(ClubMembersView, self).get_context_data(*args, **kwargs)
|
||||
kwargs["members"] = self.members
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
"""Check user rights."""
|
||||
resp = super().form_valid(form)
|
||||
"""
|
||||
Check user rights
|
||||
"""
|
||||
resp = super(ClubMembersView, self).form_valid(form)
|
||||
|
||||
data = form.clean()
|
||||
users = data.pop("users", [])
|
||||
@ -284,7 +307,7 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.members = self.get_object().members.ongoing().order_by("-role")
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
return super(ClubMembersView, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
return reverse_lazy(
|
||||
@ -293,7 +316,9 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
|
||||
|
||||
|
||||
class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView):
|
||||
"""Old members of a club."""
|
||||
"""
|
||||
Old members of a club
|
||||
"""
|
||||
|
||||
model = Club
|
||||
pk_url_kwarg = "club_id"
|
||||
@ -302,7 +327,9 @@ class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView):
|
||||
|
||||
|
||||
class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
||||
"""Sellings of a club."""
|
||||
"""
|
||||
Sellings of a club
|
||||
"""
|
||||
|
||||
model = Club
|
||||
pk_url_kwarg = "club_id"
|
||||
@ -314,12 +341,12 @@ class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
self.asked_page = int(request.GET.get("page", 1))
|
||||
except ValueError as e:
|
||||
raise Http404 from e
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
except ValueError:
|
||||
raise Http404
|
||||
return super(ClubSellingView, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs = super(ClubSellingView, self).get_form_kwargs()
|
||||
kwargs["club"] = self.object
|
||||
return kwargs
|
||||
|
||||
@ -327,7 +354,7 @@ class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs = super(ClubSellingView, self).get_context_data(**kwargs)
|
||||
qs = Selling.objects.filter(club=self.object)
|
||||
|
||||
kwargs["result"] = qs[:0]
|
||||
@ -371,17 +398,19 @@ class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
||||
kwargs["paginator"] = Paginator(kwargs["result"], self.paginate_by)
|
||||
try:
|
||||
kwargs["paginated_result"] = kwargs["paginator"].page(self.asked_page)
|
||||
except InvalidPage as e:
|
||||
raise Http404 from e
|
||||
except InvalidPage:
|
||||
raise Http404
|
||||
|
||||
return kwargs
|
||||
|
||||
|
||||
class ClubSellingCSVView(ClubSellingView):
|
||||
"""Generate sellings in csv for a given period."""
|
||||
"""
|
||||
Generate sellings in csv for a given period
|
||||
"""
|
||||
|
||||
class StreamWriter:
|
||||
"""Implements a file-like interface for streaming the CSV."""
|
||||
"""Implements a file-like interface for streaming the CSV"""
|
||||
|
||||
def write(self, value):
|
||||
"""Write the value by returning it, instead of storing in a buffer."""
|
||||
@ -397,8 +426,7 @@ class ClubSellingCSVView(ClubSellingView):
|
||||
row.append(selling.customer.user.get_display_name())
|
||||
else:
|
||||
row.append("")
|
||||
row = [
|
||||
*row,
|
||||
row = row + [
|
||||
selling.label,
|
||||
selling.quantity,
|
||||
selling.quantity * selling.unit_price,
|
||||
@ -409,7 +437,7 @@ class ClubSellingCSVView(ClubSellingView):
|
||||
row.append(selling.product.purchase_price)
|
||||
row.append(selling.product.selling_price - selling.product.purchase_price)
|
||||
else:
|
||||
row = [*row, "", "", ""]
|
||||
row = row + ["", "", ""]
|
||||
return row
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
@ -456,7 +484,9 @@ class ClubSellingCSVView(ClubSellingView):
|
||||
|
||||
|
||||
class ClubEditView(ClubTabsMixin, CanEditMixin, UpdateView):
|
||||
"""Edit a Club's main informations (for the club's members)."""
|
||||
"""
|
||||
Edit a Club's main informations (for the club's members)
|
||||
"""
|
||||
|
||||
model = Club
|
||||
pk_url_kwarg = "club_id"
|
||||
@ -466,7 +496,9 @@ class ClubEditView(ClubTabsMixin, CanEditMixin, UpdateView):
|
||||
|
||||
|
||||
class ClubEditPropView(ClubTabsMixin, CanEditPropMixin, UpdateView):
|
||||
"""Edit the properties of a Club object (for the Sith admins)."""
|
||||
"""
|
||||
Edit the properties of a Club object (for the Sith admins)
|
||||
"""
|
||||
|
||||
model = Club
|
||||
pk_url_kwarg = "club_id"
|
||||
@ -476,7 +508,9 @@ class ClubEditPropView(ClubTabsMixin, CanEditPropMixin, UpdateView):
|
||||
|
||||
|
||||
class ClubCreateView(CanCreateMixin, CreateView):
|
||||
"""Create a club (for the Sith admin)."""
|
||||
"""
|
||||
Create a club (for the Sith admin)
|
||||
"""
|
||||
|
||||
model = Club
|
||||
pk_url_kwarg = "club_id"
|
||||
@ -485,7 +519,9 @@ class ClubCreateView(CanCreateMixin, CreateView):
|
||||
|
||||
|
||||
class MembershipSetOldView(CanEditMixin, DetailView):
|
||||
"""Set a membership as beeing old."""
|
||||
"""
|
||||
Set a membership as beeing old
|
||||
"""
|
||||
|
||||
model = Membership
|
||||
pk_url_kwarg = "membership_id"
|
||||
@ -514,7 +550,9 @@ class MembershipSetOldView(CanEditMixin, DetailView):
|
||||
|
||||
|
||||
class MembershipDeleteView(UserIsRootMixin, DeleteView):
|
||||
"""Delete a membership (for admins only)."""
|
||||
"""
|
||||
Delete a membership (for admins only)
|
||||
"""
|
||||
|
||||
model = Membership
|
||||
pk_url_kwarg = "membership_id"
|
||||
@ -528,13 +566,15 @@ class ClubStatView(TemplateView):
|
||||
template_name = "club/stats.jinja"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs = super(ClubStatView, self).get_context_data(**kwargs)
|
||||
kwargs["club_list"] = Club.objects.all()
|
||||
return kwargs
|
||||
|
||||
|
||||
class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
||||
"""A list of mailing for a given club."""
|
||||
"""
|
||||
A list of mailing for a given club
|
||||
"""
|
||||
|
||||
model = Club
|
||||
form_class = MailingForm
|
||||
@ -543,7 +583,7 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
||||
current_tab = "mailing"
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs = super(ClubMailingView, self).get_form_kwargs()
|
||||
kwargs["club_id"] = self.get_object().id
|
||||
kwargs["user_id"] = self.request.user.id
|
||||
kwargs["mailings"] = self.mailings
|
||||
@ -551,10 +591,10 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.mailings = Mailing.objects.filter(club_id=self.get_object().id).all()
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
return super(ClubMailingView, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs = super(ClubMailingView, self).get_context_data(**kwargs)
|
||||
kwargs["club"] = self.get_object()
|
||||
kwargs["user"] = self.request.user
|
||||
kwargs["mailings"] = self.mailings
|
||||
@ -571,8 +611,10 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
||||
}
|
||||
return kwargs
|
||||
|
||||
def add_new_mailing(self, cleaned_data) -> ValidationError | None:
|
||||
"""Create a new mailing list from the form."""
|
||||
def add_new_mailing(self, cleaned_data) -> ValidationError:
|
||||
"""
|
||||
Create a new mailing list from the form
|
||||
"""
|
||||
mailing = Mailing(
|
||||
club=self.get_object(),
|
||||
email=cleaned_data["mailing_email"],
|
||||
@ -586,8 +628,10 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
||||
mailing.save()
|
||||
return None
|
||||
|
||||
def add_new_subscription(self, cleaned_data) -> ValidationError | None:
|
||||
"""Add mailing subscriptions for each user given and/or for the specified email in form."""
|
||||
def add_new_subscription(self, cleaned_data) -> ValidationError:
|
||||
"""
|
||||
Add mailing subscriptions for each user given and/or for the specified email in form
|
||||
"""
|
||||
users_to_save = []
|
||||
|
||||
for user in cleaned_data["subscription_users"]:
|
||||
@ -621,16 +665,20 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
||||
return None
|
||||
|
||||
def remove_subscription(self, cleaned_data):
|
||||
"""Remove specified users from a mailing list."""
|
||||
"""
|
||||
Remove specified users from a mailing list
|
||||
"""
|
||||
fields = [
|
||||
val for key, val in cleaned_data.items() if key.startswith("removal_")
|
||||
cleaned_data[key]
|
||||
for key in cleaned_data.keys()
|
||||
if key.startswith("removal_")
|
||||
]
|
||||
for field in fields:
|
||||
for sub in field:
|
||||
sub.delete()
|
||||
|
||||
def form_valid(self, form):
|
||||
resp = super().form_valid(form)
|
||||
resp = super(ClubMailingView, self).form_valid(form)
|
||||
|
||||
cleaned_data = form.clean()
|
||||
error = None
|
||||
@ -662,7 +710,7 @@ class MailingDeleteView(CanEditMixin, DeleteView):
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.club_id = self.get_object().club.id
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
return super(MailingDeleteView, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
if self.redirect_page:
|
||||
@ -678,7 +726,9 @@ class MailingSubscriptionDeleteView(CanEditMixin, DeleteView):
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.club_id = self.get_object().mailing.club.id
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
return super(MailingSubscriptionDeleteView, self).dispatch(
|
||||
request, *args, **kwargs
|
||||
)
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
return reverse_lazy("club:mailing", kwargs={"club_id": self.club_id})
|
||||
@ -689,7 +739,7 @@ class MailingAutoGenerationView(View):
|
||||
self.mailing = get_object_or_404(Mailing, pk=kwargs["mailing_id"])
|
||||
if not request.user.can_edit(self.mailing):
|
||||
raise PermissionDenied
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
return super(MailingAutoGenerationView, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
club = self.mailing.club
|
||||
@ -703,25 +753,25 @@ class MailingAutoGenerationView(View):
|
||||
|
||||
|
||||
class PosterListView(ClubTabsMixin, PosterListBaseView, CanViewMixin):
|
||||
"""List communication posters."""
|
||||
"""List communication posters"""
|
||||
|
||||
def get_object(self):
|
||||
return self.club
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs = super(PosterListView, self).get_context_data(**kwargs)
|
||||
kwargs["app"] = "club"
|
||||
kwargs["club"] = self.club
|
||||
return kwargs
|
||||
|
||||
|
||||
class PosterCreateView(PosterCreateBaseView, CanCreateMixin):
|
||||
"""Create communication poster."""
|
||||
"""Create communication poster"""
|
||||
|
||||
pk_url_kwarg = "club_id"
|
||||
|
||||
def get_object(self):
|
||||
obj = super().get_object()
|
||||
obj = super(PosterCreateView, self).get_object()
|
||||
if not obj:
|
||||
return self.club
|
||||
return obj
|
||||
@ -731,19 +781,19 @@ class PosterCreateView(PosterCreateBaseView, CanCreateMixin):
|
||||
|
||||
|
||||
class PosterEditView(ClubTabsMixin, PosterEditBaseView, CanEditMixin):
|
||||
"""Edit communication poster."""
|
||||
"""Edit communication poster"""
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs = super(PosterEditView, self).get_context_data(**kwargs)
|
||||
kwargs["app"] = "club"
|
||||
return kwargs
|
||||
|
||||
|
||||
class PosterDeleteView(PosterDeleteBaseView, ClubTabsMixin, CanEditMixin):
|
||||
"""Delete communication poster."""
|
||||
"""Delete communication poster"""
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id})
|
||||
|
@ -1,23 +0,0 @@
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
from club.models import Club
|
||||
from club.schemas import ClubSchema
|
||||
from core.views.widgets.select import AutoCompleteSelect, AutoCompleteSelectMultiple
|
||||
|
||||
_js = ["bundled/club/components/ajax-select-index.ts"]
|
||||
|
||||
|
||||
class AutoCompleteSelectClub(AutoCompleteSelect):
|
||||
component_name = "club-ajax-select"
|
||||
model = Club
|
||||
adapter = TypeAdapter(list[ClubSchema])
|
||||
|
||||
js = _js
|
||||
|
||||
|
||||
class AutoCompleteSelectMultipleClub(AutoCompleteSelectMultiple):
|
||||
component_name = "club-ajax-select"
|
||||
model = Club
|
||||
adapter = TypeAdapter(list[ClubSchema])
|
||||
|
||||
js = _js
|
@ -1,3 +1,4 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# Copyright 2023 © AE UTBM
|
||||
# ae@utbm.fr / ae.info@utbm.fr
|
||||
@ -5,10 +6,10 @@
|
||||
# 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/sith
|
||||
# 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/sith/master/LICENSE
|
||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||
#
|
||||
#
|
||||
|
18
com/admin.py
18
com/admin.py
@ -1,3 +1,4 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# Copyright 2023 © AE UTBM
|
||||
# ae@utbm.fr / ae.info@utbm.fr
|
||||
@ -5,30 +6,37 @@
|
||||
# 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/sith
|
||||
# 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/sith/master/LICENSE
|
||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||
#
|
||||
#
|
||||
from ajax_select import make_ajax_form
|
||||
from django.contrib import admin
|
||||
from haystack.admin import SearchModelAdmin
|
||||
|
||||
from com.models import News, Poster, Screen, Sith, Weekmail
|
||||
from com.models import *
|
||||
|
||||
|
||||
@admin.register(News)
|
||||
class NewsAdmin(SearchModelAdmin):
|
||||
list_display = ("title", "type", "club", "author")
|
||||
search_fields = ("title", "summary", "content")
|
||||
autocomplete_fields = ("author", "moderator")
|
||||
form = make_ajax_form(
|
||||
News,
|
||||
{
|
||||
"author": "users",
|
||||
"moderator": "users",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@admin.register(Poster)
|
||||
class PosterAdmin(SearchModelAdmin):
|
||||
list_display = ("name", "club", "date_begin", "date_end", "moderator")
|
||||
autocomplete_fields = ("moderator",)
|
||||
form = make_ajax_form(Poster, {"moderator": "users"})
|
||||
|
||||
|
||||
@admin.register(Weekmail)
|
||||
|
@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
@ -1,8 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
from django.conf import settings
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -1,8 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
from django.conf import settings
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -1,9 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.23 on 2019-08-18 17:00
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
119
com/models.py
119
com/models.py
@ -1,3 +1,4 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# Copyright 2016,2017
|
||||
# - Skia <skia@libskia.so>
|
||||
@ -22,36 +23,40 @@
|
||||
#
|
||||
#
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.shortcuts import render
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import render
|
||||
from django.templatetags.static import static
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils import timezone
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
from django.templatetags.static import static
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from core import utils
|
||||
from core.models import User, Preferences, RealGroup, Notification, SithFile
|
||||
from club.models import Club
|
||||
from core.models import Notification, Preferences, RealGroup, User
|
||||
|
||||
|
||||
class Sith(models.Model):
|
||||
"""A one instance class storing all the modifiable infos."""
|
||||
"""A one instance class storing all the modifiable infos"""
|
||||
|
||||
alert_msg = models.TextField(_("alert message"), default="", blank=True)
|
||||
info_msg = models.TextField(_("info message"), default="", blank=True)
|
||||
weekmail_destinations = models.TextField(_("weekmail destinations"), default="")
|
||||
|
||||
def __str__(self):
|
||||
return "⛩ Sith ⛩"
|
||||
version = utils.get_git_revision_short_hash()
|
||||
|
||||
def is_owned_by(self, user):
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
return user.is_com_admin
|
||||
|
||||
def __str__(self):
|
||||
return "⛩ Sith ⛩"
|
||||
|
||||
|
||||
NEWS_TYPES = [
|
||||
("NOTICE", _("Notice")),
|
||||
@ -62,7 +67,7 @@ NEWS_TYPES = [
|
||||
|
||||
|
||||
class News(models.Model):
|
||||
"""The news class."""
|
||||
"""The news class"""
|
||||
|
||||
title = models.CharField(_("title"), max_length=64)
|
||||
summary = models.TextField(_("summary"))
|
||||
@ -88,11 +93,28 @@ class News(models.Model):
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
def is_owned_by(self, user):
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
return user.is_com_admin or user == self.author
|
||||
|
||||
def can_be_edited_by(self, user):
|
||||
return user.is_com_admin
|
||||
|
||||
def can_be_viewed_by(self, user):
|
||||
return self.is_moderated or user.is_com_admin
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("com:news_detail", kwargs={"news_id": self.id})
|
||||
|
||||
def get_full_url(self):
|
||||
return "https://%s%s" % (settings.SITH_URL, self.get_absolute_url())
|
||||
|
||||
def __str__(self):
|
||||
return "%s: %s" % (self.type, self.title)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
super(News, self).save(*args, **kwargs)
|
||||
for u in (
|
||||
RealGroup.objects.filter(id=settings.SITH_GROUP_COM_ADMIN_ID)
|
||||
.first()
|
||||
@ -105,23 +127,6 @@ class News(models.Model):
|
||||
param="1",
|
||||
).save()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("com:news_detail", kwargs={"news_id": self.id})
|
||||
|
||||
def get_full_url(self):
|
||||
return "https://%s%s" % (settings.SITH_URL, self.get_absolute_url())
|
||||
|
||||
def is_owned_by(self, user):
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
return user.is_com_admin or user == self.author
|
||||
|
||||
def can_be_edited_by(self, user):
|
||||
return user.is_com_admin
|
||||
|
||||
def can_be_viewed_by(self, user):
|
||||
return self.is_moderated or user.is_com_admin
|
||||
|
||||
|
||||
def news_notification_callback(notif):
|
||||
count = (
|
||||
@ -141,7 +146,8 @@ def news_notification_callback(notif):
|
||||
|
||||
|
||||
class NewsDate(models.Model):
|
||||
"""A date class, useful for weekly events, or for events that just have no date.
|
||||
"""
|
||||
A date class, useful for weekly events, or for events that just have no date
|
||||
|
||||
This class allows more flexibilty managing the dates related to a news, particularly when this news is weekly, since
|
||||
we don't have to make copies
|
||||
@ -161,7 +167,8 @@ class NewsDate(models.Model):
|
||||
|
||||
|
||||
class Weekmail(models.Model):
|
||||
"""The weekmail class.
|
||||
"""
|
||||
The weekmail class
|
||||
|
||||
:ivar title: Title of the weekmail
|
||||
:ivar intro: Introduction of the weekmail
|
||||
@ -181,12 +188,9 @@ class Weekmail(models.Model):
|
||||
class Meta:
|
||||
ordering = ["-id"]
|
||||
|
||||
def __str__(self):
|
||||
return f"Weekmail {self.id} (sent: {self.sent}) - {self.title}"
|
||||
|
||||
def send(self):
|
||||
"""Send the weekmail to all users with the receive weekmail option opt-in.
|
||||
|
||||
"""
|
||||
Send the weekmail to all users with the receive weekmail option opt-in.
|
||||
Also send the weekmail to the mailing list in settings.SITH_COM_EMAIL.
|
||||
"""
|
||||
dest = [
|
||||
@ -210,27 +214,38 @@ class Weekmail(models.Model):
|
||||
Weekmail().save()
|
||||
|
||||
def render_text(self):
|
||||
"""Renders a pure text version of the mail for readers without HTML support."""
|
||||
"""
|
||||
Renders a pure text version of the mail for readers without HTML support.
|
||||
"""
|
||||
return render(
|
||||
None, "com/weekmail_renderer_text.jinja", context={"weekmail": self}
|
||||
).content.decode("utf-8")
|
||||
|
||||
def render_html(self):
|
||||
"""Renders an HTML version of the mail with images and fancy CSS."""
|
||||
"""
|
||||
Renders an HTML version of the mail with images and fancy CSS.
|
||||
"""
|
||||
return render(
|
||||
None, "com/weekmail_renderer_html.jinja", context={"weekmail": self}
|
||||
).content.decode("utf-8")
|
||||
|
||||
def get_banner(self):
|
||||
"""Return an absolute link to the banner."""
|
||||
"""
|
||||
Return an absolute link to the banner.
|
||||
"""
|
||||
return (
|
||||
"http://" + settings.SITH_URL + static("com/img/weekmail_bannerV2P22.png")
|
||||
)
|
||||
|
||||
def get_footer(self):
|
||||
"""Return an absolute link to the footer."""
|
||||
"""
|
||||
Return an absolute link to the footer.
|
||||
"""
|
||||
return "http://" + settings.SITH_URL + static("com/img/weekmail_footerP22.png")
|
||||
|
||||
def __str__(self):
|
||||
return "Weekmail %s (sent: %s) - %s" % (self.id, self.sent, self.title)
|
||||
|
||||
def is_owned_by(self, user):
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
@ -261,21 +276,18 @@ class WeekmailArticle(models.Model):
|
||||
)
|
||||
rank = models.IntegerField(_("rank"), default=-1)
|
||||
|
||||
def __str__(self):
|
||||
return "%s - %s (%s)" % (self.title, self.author, self.club)
|
||||
|
||||
def is_owned_by(self, user):
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
return user.is_com_admin
|
||||
|
||||
def __str__(self):
|
||||
return "%s - %s (%s)" % (self.title, self.author, self.club)
|
||||
|
||||
|
||||
class Screen(models.Model):
|
||||
name = models.CharField(_("name"), max_length=128)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def active_posters(self):
|
||||
now = timezone.now()
|
||||
return self.posters.filter(is_moderated=True, date_begin__lte=now).filter(
|
||||
@ -287,6 +299,9 @@ class Screen(models.Model):
|
||||
return False
|
||||
return user.is_com_admin
|
||||
|
||||
def __str__(self):
|
||||
return "%s" % (self.name)
|
||||
|
||||
|
||||
class Poster(models.Model):
|
||||
name = models.CharField(
|
||||
@ -316,9 +331,6 @@ class Poster(models.Model):
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.is_moderated:
|
||||
for u in (
|
||||
@ -331,7 +343,7 @@ class Poster(models.Model):
|
||||
url=reverse("com:poster_moderate_list"),
|
||||
type="POSTER_MODERATION",
|
||||
).save()
|
||||
return super().save(*args, **kwargs)
|
||||
return super(Poster, self).save(*args, **kwargs)
|
||||
|
||||
def clean(self, *args, **kwargs):
|
||||
if self.date_end and self.date_begin > self.date_end:
|
||||
@ -351,3 +363,6 @@ class Poster(models.Model):
|
||||
@property
|
||||
def page(self):
|
||||
return self.club.page
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
@ -1,23 +0,0 @@
|
||||
$(document).ready(() => {
|
||||
$("#poster_list #view").click(() => {
|
||||
$("#view").removeClass("active");
|
||||
});
|
||||
|
||||
$("#poster_list .poster .image").click((e) => {
|
||||
let el = $(e.target);
|
||||
if (el.hasClass("image")) {
|
||||
el = el.find("img");
|
||||
}
|
||||
$("#poster_list #view #placeholder").html(el.clone());
|
||||
|
||||
$("#view").addClass("active");
|
||||
});
|
||||
|
||||
$(document).keyup((e) => {
|
||||
if (e.keyCode === 27) {
|
||||
// escape key maps to keycode `27`
|
||||
e.preventDefault();
|
||||
$("#view").removeClass("active");
|
||||
}
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user