Compare commits
254 Commits
3501703c15
...
feature/ea
Author | SHA1 | Date | |
---|---|---|---|
858199e476 | |||
f6ecbd899d | |||
e868946fd7 | |||
254044c36b | |||
c695d6f7a0 | |||
feef855f01 | |||
b3a48ca5af | |||
f3a52d094e | |||
2901bd919f | |||
0396a5bf2b | |||
b48ad16f04 | |||
7cc6250860 | |||
ae2e4b518d | |||
e9b9f3a62b | |||
3321669726 | |||
21fc85670e | |||
18a5ad6541 | |||
71c5456225 | |||
50e04164a2 | |||
3b1d71f317 | |||
65c2689578 | |||
b45673f04a | |||
cb6e037f5e | |||
5e6d60bb3a | |||
64f8d9bad3 | |||
05b86e1f7a | |||
700fed860d | |||
820bf6279b | |||
b97ce81dd2 | |||
f4dfd8f99c | |||
29139bf360 | |||
4f9c2724f5 | |||
7a914f5e94 | |||
121d04e1d5 | |||
fc6cdba8e2 | |||
7f39ead159 | |||
1da82ac2dd | |||
f2dcc39c14 | |||
705dc56153 | |||
02047b62d7 | |||
895d4b33a6 | |||
142cb3316e | |||
997fcc9fff | |||
ec65ca11d6 | |||
0198027544 | |||
69e0550d4f | |||
9a1a5635e2 | |||
863f9ff77e | |||
4146c4c5cb | |||
b3ad5c5df9 | |||
9388e2dc88 | |||
56dec9eaa1 | |||
596126f4f4 | |||
8646b2c8f7 | |||
c81bb1fb90 | |||
d17a52a8d6 | |||
55e0eecc0b | |||
496adc17ea | |||
ab43d7d2df | |||
13f0bfe546 | |||
83a384145b | |||
8a923761a5 | |||
6e4a99eba3 | |||
0470aa185e | |||
273371db8b | |||
ed3aa0c328 | |||
acfff6b103 | |||
ada4579193 | |||
3a17c3079e | |||
26e46de8e1 | |||
111bcc8e60 | |||
cdaa204ba2 | |||
e85511fcb9 | |||
35c120a29f | |||
7c4c1bc387 | |||
6e77edcf67 | |||
effed9c760 | |||
0e5c8b53b0 | |||
47a332445c | |||
c904b2d827 | |||
f56263d6bd | |||
0c2494cb34 | |||
9e5743a64c | |||
b5241ec75e | |||
4f00224f0d | |||
320a896610 | |||
08924c5e05 | |||
98bfc308a7 | |||
dee24fbc9c | |||
2556427c7d | |||
a2b35e5bba | |||
3e8f1acb96 | |||
85788977fe | |||
066ca5bada | |||
41369f738e | |||
67377b3cbf | |||
ac3d668655 | |||
c57b15e159 | |||
66efb8012e | |||
cad0c0dadb | |||
b32c90ed5d | |||
4d361dc67b | |||
2b170d91f7 | |||
9e074d6ca6 | |||
b655b2695b | |||
366aeed2ba | |||
454ae5f9e3 | |||
b811114425 | |||
712e7c8939 | |||
713cd92141 | |||
4154b499b1 | |||
253f204225 | |||
7241f3eb1d | |||
2422f60898 | |||
ba6599fa56 | |||
f2666f6fb0 | |||
b33839191d | |||
ee3e375dde | |||
5b0f7ca21b | |||
f581d91730 | |||
bbf362691b | |||
15e2c8c7b3 | |||
f838127730 | |||
d4c0bb3b0e | |||
b81aee3f1c | |||
c6caf5dbce | |||
7acc59f2cd | |||
757ff7ead7 | |||
bc2fe16b74 | |||
35363d9ee7 | |||
52106db6fd | |||
c4b1829e78 | |||
489a9378c5 | |||
28ae109b32 | |||
e7a6a94ff2 | |||
234556a172 | |||
e4ddceabea | |||
05dd3ad642 | |||
6c5db61a97 | |||
a0e4e9e8e3 | |||
c66df77d4a | |||
cfb6b34630 | |||
d8fd0adf47 | |||
928ae13a8a | |||
c2e0ea70e4 | |||
782ce24895 | |||
b630742fd4 | |||
b20df930a2 | |||
d60a96fc5c | |||
05b0a0ab2f | |||
9eb137e503 | |||
7d797009bb | |||
3c1818f229 | |||
d8b69e9b45 | |||
9177c9d4c2 | |||
5195352975 | |||
deb8f865df | |||
5b2c70e4fb | |||
f66db0859e | |||
b6488d1d00 | |||
6a4ac336ad | |||
7ac6dcf8a0 | |||
c6a3677cc5 | |||
707459acd6 | |||
6390c3320e | |||
b8aabc466c | |||
c66e4232b9 | |||
336450d43f | |||
7e66aadd6f | |||
bf2b796936 | |||
85623f48a9 | |||
4fbee9c3de | |||
bfa3b45547 | |||
677a9da469 | |||
1f7752d457 | |||
89979dbf61 | |||
8d1abb8f33 | |||
2df3494c3b | |||
39bb490257 | |||
7a7aad0503 | |||
b157a3fa90 | |||
1b688a8aa5 | |||
e8978cc065 | |||
7fd68e4825 | |||
4119eefe37 | |||
aafc2e6e96 | |||
2cbe6fa11c | |||
eec7bcf296 | |||
6c45de34a4 | |||
61a40c47d2 | |||
007157e2e8 | |||
49a0ade315 | |||
782cd9a45a | |||
6382e631b6 | |||
12493cffca | |||
a38ab57ddf | |||
30091ef69c | |||
1a483bfa2c | |||
1a091951e8 | |||
bfb66b352a | |||
be26e3df7f | |||
cb3307509d | |||
a3158253a7 | |||
406380e4f1 | |||
efb70652af | |||
05256bb99a | |||
64d0cc2fa8 | |||
f5d7267ba7 | |||
24c0a21cc1 | |||
6a352d642b | |||
48ae1f7c1c | |||
aaf1adaaa1 | |||
f34f5fe693
|
|||
f485178422 | |||
797ca0f926 | |||
390a4b0064 | |||
94b029dc9c
|
|||
45d5728c3e | |||
6eabbaf209 | |||
03fdd0b947 | |||
fb8faacddc
|
|||
7ee4557ab5 | |||
5accdbccbb | |||
7fb26f9e45 | |||
26a07f722d | |||
9176a03a8a | |||
4a1bfc366d
|
|||
ebee8c34e1
|
|||
4ecad1c73b
|
|||
d1b3a4d3f6
|
|||
40832bb3bf | |||
4a78157f9a
|
|||
bf5fc8750d
|
|||
274a7b7137
|
|||
8dd2c02d3e
|
|||
a73f5cb270
|
|||
7d40e11144
|
|||
af48553e35
|
|||
ad8bcc7282 | |||
22a44415e4 | |||
6a153719f9
|
|||
5c8fa1b9e7
|
|||
d82679e3d7 | |||
9cb432a082 | |||
869d29d4a4 | |||
c3d2e64a43
|
|||
e1770ec52c
|
|||
1256744f1b
|
|||
77dddbc581
|
|||
bfa4000365 | |||
50c2f8164d | |||
5c30de5f22
|
|||
1c03ce621f
|
|||
e634cda318
|
15
.envrc
@ -1 +1,14 @@
|
|||||||
source ./env/bin/activate
|
if [[ ! -f pyproject.toml ]]; then
|
||||||
|
log_error 'No pyproject.toml found. Use `poetry new` or `poetry init` to create one first.'
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
local VENV=$(poetry env list --full-path | cut -d' ' -f1)
|
||||||
|
if [[ -z $VENV || ! -d $VENV/bin ]]; then
|
||||||
|
log_error 'No poetry virtual environment found. Use `poetry install` to create one first.'
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
export VIRTUAL_ENV=$VENV
|
||||||
|
export POETRY_ACTIVE=1
|
||||||
|
PATH_add "$VENV/bin"
|
13
.github/auto_assign.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Set to true to add reviewers to pull requests
|
||||||
|
addReviewers: true
|
||||||
|
|
||||||
|
# Set to true to add assignees to pull requests
|
||||||
|
addAssignees: author
|
||||||
|
|
||||||
|
# A list of team reviewers to be added to pull requests (GitHub team slug)
|
||||||
|
reviewers:
|
||||||
|
- ae-utbm/sith-3-developers
|
||||||
|
|
||||||
|
# Number of reviewers has no impact on GitHub teams
|
||||||
|
# Set 0 to add all the reviewers (default: 0)
|
||||||
|
numberOfReviewers: 0
|
64
.github/workflows/deploy.yml
vendored
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
name: Deploy to production
|
||||||
|
concurrency: production
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deployment:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: production
|
||||||
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: SSH Remote Commands
|
||||||
|
uses: appleboy/ssh-action@dce9d565de8d876c11d93fa4fe677c0285a66d78
|
||||||
|
with:
|
||||||
|
# Proxy
|
||||||
|
proxy_host : ${{secrets.PROXY_HOST}}
|
||||||
|
proxy_port : ${{secrets.PROXY_PORT}}
|
||||||
|
proxy_username : ${{secrets.PROXY_USER}}
|
||||||
|
proxy_passphrase: ${{secrets.PROXY_PASSPHRASE}}
|
||||||
|
proxy_key: ${{secrets.PROXY_KEY}}
|
||||||
|
|
||||||
|
# Serveur web
|
||||||
|
host: ${{secrets.HOST}}
|
||||||
|
port : ${{secrets.PORT}}
|
||||||
|
username : ${{secrets.USER}}
|
||||||
|
key: ${{secrets.KEY}}
|
||||||
|
|
||||||
|
script_stop: true
|
||||||
|
|
||||||
|
# See https://github.com/ae-utbm/sith3/wiki/GitHub-Actions#deployment-action
|
||||||
|
script: |
|
||||||
|
export PATH="$HOME/.poetry/bin:$PATH"
|
||||||
|
pushd ${{secrets.SITH_PATH}}
|
||||||
|
|
||||||
|
git pull
|
||||||
|
poetry update
|
||||||
|
poetry run ./manage.py migrate
|
||||||
|
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: production
|
||||||
|
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: production
|
83
.github/workflows/unittests.yml
vendored
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
name: Sith3 CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
unittests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# Skip unit testing if no diff on .py files
|
||||||
|
- name: Check file diff
|
||||||
|
uses: technote-space/get-diff-action@v6
|
||||||
|
id: git-diff
|
||||||
|
with:
|
||||||
|
PATTERNS: |
|
||||||
|
**/*.py
|
||||||
|
|
||||||
|
- name: Set up python
|
||||||
|
if: steps.git-diff.outputs.diff
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.8'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
if: steps.git-diff.outputs.diff
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install gettext libxapian-dev libgraphviz-dev
|
||||||
|
|
||||||
|
- name: Install poetry
|
||||||
|
if: steps.git-diff.outputs.diff
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install poetry
|
||||||
|
|
||||||
|
- name: Checking pyproject.toml syntax
|
||||||
|
if: steps.git-diff.outputs.diff
|
||||||
|
run: poetry check
|
||||||
|
|
||||||
|
- name: Install project
|
||||||
|
if: steps.git-diff.outputs.diff
|
||||||
|
run: poetry install -E testing
|
||||||
|
|
||||||
|
- name: Setup xapian index
|
||||||
|
if: steps.git-diff.outputs.diff
|
||||||
|
run: |
|
||||||
|
mkdir -p /dev/shm/search_indexes
|
||||||
|
ln -s /dev/shm/search_indexes sith/search_indexes
|
||||||
|
|
||||||
|
- name: Setup project
|
||||||
|
if: steps.git-diff.outputs.diff
|
||||||
|
run: poetry run ./manage.py compilemessages
|
||||||
|
|
||||||
|
- name: Launch tests and generate coverage report
|
||||||
|
if: steps.git-diff.outputs.diff
|
||||||
|
run: |
|
||||||
|
poetry run coverage run ./manage.py test
|
||||||
|
poetry run coverage report
|
||||||
|
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Set up python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.8'
|
||||||
|
|
||||||
|
- name: Install black
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install black==22.6.0
|
||||||
|
|
||||||
|
- name: Check linting
|
||||||
|
run: black --check .
|
2
.gitignore
vendored
@ -4,6 +4,8 @@ db.sqlite3
|
|||||||
*.mo
|
*.mo
|
||||||
*__pycache__*
|
*__pycache__*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
pyrightconfig.json
|
||||||
|
dist/
|
||||||
.vscode/
|
.vscode/
|
||||||
env/
|
env/
|
||||||
doc/html
|
doc/html
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
stages:
|
|
||||||
- test
|
|
||||||
|
|
||||||
test:
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
- apt-get update
|
|
||||||
- apt-get install -y gettext python3-xapian libgraphviz-dev
|
|
||||||
- pushd /usr/lib/python3/dist-packages/xapian && ln -s _xapian* _xapian.so && popd
|
|
||||||
- export PYTHONPATH="/usr/lib/python3/dist-packages:$PYTHONPATH"
|
|
||||||
- python -c 'import xapian' # Fail immediately if there is a problem with xapian
|
|
||||||
- pip install -r requirements.txt
|
|
||||||
- pip install coverage
|
|
||||||
- ./manage.py compilemessages
|
|
||||||
- coverage run ./manage.py test
|
|
||||||
- coverage html
|
|
||||||
- coverage report
|
|
||||||
- cd doc
|
|
||||||
- make html # Make documentation
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- coverage_report/
|
|
||||||
|
|
||||||
black:
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
- pip install black
|
|
||||||
- black --check .
|
|
18
.mailmap
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
Code <gregoire.duvauchelle@utbm.fr>
|
||||||
|
Cyl <labetowiez@aol.fr>
|
||||||
|
Juste <maaxleblanc@gmail.com>
|
||||||
|
Krophil <pierre.brunet@krophil.fr>
|
||||||
|
Lo-J <renaudg779@gmail.com>
|
||||||
|
Nabos <gnikwo@hotmail.com>
|
||||||
|
Och <francescowitz68@gmail.com>
|
||||||
|
Partoo <joqaste@gmail.com>
|
||||||
|
Skia <skia@hya.sk> <lordbanana25@mailoo.org>
|
||||||
|
Skia <skia@hya.sk> <skia@libskia.so>
|
||||||
|
Sli <klmp200@klmp200.net> <antoine@bartuccio.fr>
|
||||||
|
Soldat <ryan-68@live.fr>
|
||||||
|
Terre <jbaptiste.lenglet+git@gmail.com>
|
||||||
|
Vial <robin.trioux@utbm.fr>
|
||||||
|
Zar <antoine.charmeau@utbm.fr> <antoine.charmeau@laposte.net>
|
||||||
|
root <root@localhost.localdomain>
|
||||||
|
tleb <tleb@openmailbox.org> <theo.lebrun@live.fr>
|
||||||
|
tleb <tleb@openmailbox.org> <theo.lebrun@utbm.fr>
|
@ -4,6 +4,11 @@
|
|||||||
# Required
|
# Required
|
||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
|
# Allow installing xapian-bindings in pip
|
||||||
|
build:
|
||||||
|
apt_packages:
|
||||||
|
- libxapian-dev
|
||||||
|
|
||||||
# Build documentation in the doc/ directory with Sphinx
|
# Build documentation in the doc/ directory with Sphinx
|
||||||
sphinx:
|
sphinx:
|
||||||
configuration: doc/conf.py
|
configuration: doc/conf.py
|
||||||
@ -13,6 +18,9 @@ formats: all
|
|||||||
|
|
||||||
# Optionally set the version of Python and requirements required to build your docs
|
# Optionally set the version of Python and requirements required to build your docs
|
||||||
python:
|
python:
|
||||||
version: 3.6
|
version: 3.8
|
||||||
install:
|
install:
|
||||||
- requirements: requirements.txt
|
- method: pip
|
||||||
|
path: .
|
||||||
|
extra_requirements:
|
||||||
|
- docs
|
||||||
|
41
README.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<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/889796155523874847?label=Discord&logo=discord&style=for-the-badge">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 align="center">This is the source code of the UTBM's student association available at https://ae.utbm.fr/.</h3>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<h4>If you want to contribute, here's how we recommend to read the docs:</h4>
|
||||||
|
|
||||||
|
<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 licenced under GNU GPL, see the LICENSE file at the top of the repository for more details.
|
||||||
|
|
37
README.rst
@ -1,37 +0,0 @@
|
|||||||
.. image:: https://ae-dev.utbm.fr/ae/Sith/badges/master/pipeline.svg
|
|
||||||
:target: https://ae-dev.utbm.fr/ae/Sith/commits/master
|
|
||||||
:alt: pipeline status
|
|
||||||
|
|
||||||
.. image:: https://readthedocs.org/projects/sith-ae/badge/?version=latest
|
|
||||||
:target: https://sith-ae.readthedocs.io/?badge=latest
|
|
||||||
:alt: documentation Status
|
|
||||||
|
|
||||||
.. image:: https://ae-dev.utbm.fr/ae/Sith/badges/master/coverage.svg
|
|
||||||
:target: https://ae-dev.utbm.fr/ae/Sith/commits/master
|
|
||||||
:alt: coverage report
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
|
||||||
:target: https://github.com/ambv/black
|
|
||||||
:alt: code style: black
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/badge/zulip-join_chat-brightgreen.svg
|
|
||||||
:target: https://ae-dev.zulipchat.com
|
|
||||||
:alt: project chat
|
|
||||||
|
|
||||||
This is the source code of the UTBM's student association available at https://ae.utbm.fr/.
|
|
||||||
|
|
||||||
All documentation is in the ``docs`` directory and online at https://sith-ae.readthedocs.io/. This documentation is written in French because it targets a French audience and it's too much work to maintain two versions. The code and code comments are strictly written in English.
|
|
||||||
|
|
||||||
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 find 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.
|
|
||||||
|
|
||||||
To join our team :
|
|
||||||
|
|
||||||
* Send a mail at mailto:ae.utbm.fr
|
|
||||||
* Join our group chat at https://ae-dev.zulipchat.com
|
|
||||||
* See and join our Trello at https://trello.com/b/YQOaF33m/site-ae.
|
|
||||||
|
|
||||||
This project is licenced under GNU GPL, see the LICENSE file at the top of the repository for more details.
|
|
@ -27,7 +27,7 @@ from django.core.exceptions import ValidationError
|
|||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.template import defaultfilters
|
from django.template import defaultfilters
|
||||||
|
|
||||||
from phonenumber_field.modelfields import PhoneNumberField
|
from phonenumber_field.modelfields import PhoneNumberField
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from datetime import date
|
from datetime import date, timedelta
|
||||||
|
|
||||||
from core.models import User
|
from core.models import User
|
||||||
from accounting.models import (
|
from accounting.models import (
|
||||||
@ -110,6 +110,9 @@ class JournalTest(TestCase):
|
|||||||
class OperationTest(TestCase):
|
class OperationTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
call_command("populate")
|
call_command("populate")
|
||||||
|
self.tomorrow_formatted = (date.today() + timedelta(days=1)).strftime(
|
||||||
|
"%d/%m/%Y"
|
||||||
|
)
|
||||||
self.journal = GeneralJournal.objects.filter(id=1).first()
|
self.journal = GeneralJournal.objects.filter(id=1).first()
|
||||||
self.skia = User.objects.filter(username="skia").first()
|
self.skia = User.objects.filter(username="skia").first()
|
||||||
at = AccountingType(
|
at = AccountingType(
|
||||||
@ -158,7 +161,7 @@ class OperationTest(TestCase):
|
|||||||
"target_type": "OTHER",
|
"target_type": "OTHER",
|
||||||
"target_id": "",
|
"target_id": "",
|
||||||
"target_label": "Le fantome de la nuit",
|
"target_label": "Le fantome de la nuit",
|
||||||
"date": "04/12/2020",
|
"date": self.tomorrow_formatted,
|
||||||
"mode": "CASH",
|
"mode": "CASH",
|
||||||
"cheque_number": "",
|
"cheque_number": "",
|
||||||
"invoice": "",
|
"invoice": "",
|
||||||
@ -191,7 +194,7 @@ class OperationTest(TestCase):
|
|||||||
"target_type": "OTHER",
|
"target_type": "OTHER",
|
||||||
"target_id": "",
|
"target_id": "",
|
||||||
"target_label": "Le fantome de la nuit",
|
"target_label": "Le fantome de la nuit",
|
||||||
"date": "04/12/2020",
|
"date": self.tomorrow_formatted,
|
||||||
"mode": "CASH",
|
"mode": "CASH",
|
||||||
"cheque_number": "",
|
"cheque_number": "",
|
||||||
"invoice": "",
|
"invoice": "",
|
||||||
@ -218,7 +221,7 @@ class OperationTest(TestCase):
|
|||||||
"target_type": "OTHER",
|
"target_type": "OTHER",
|
||||||
"target_id": "",
|
"target_id": "",
|
||||||
"target_label": "Le fantome du jour",
|
"target_label": "Le fantome du jour",
|
||||||
"date": "04/12/2020",
|
"date": self.tomorrow_formatted,
|
||||||
"mode": "CASH",
|
"mode": "CASH",
|
||||||
"cheque_number": "",
|
"cheque_number": "",
|
||||||
"invoice": "",
|
"invoice": "",
|
||||||
@ -245,7 +248,7 @@ class OperationTest(TestCase):
|
|||||||
"target_type": "OTHER",
|
"target_type": "OTHER",
|
||||||
"target_id": "",
|
"target_id": "",
|
||||||
"target_label": "Le fantome de l'aurore",
|
"target_label": "Le fantome de l'aurore",
|
||||||
"date": "04/12/2020",
|
"date": self.tomorrow_formatted,
|
||||||
"mode": "CASH",
|
"mode": "CASH",
|
||||||
"cheque_number": "",
|
"cheque_number": "",
|
||||||
"invoice": "",
|
"invoice": "",
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
from django.views.generic import ListView, DetailView
|
from django.views.generic import ListView, DetailView
|
||||||
from django.views.generic.edit import UpdateView, CreateView, DeleteView, FormView
|
from django.views.generic.edit import UpdateView, CreateView, DeleteView, FormView
|
||||||
from django.urls import reverse_lazy, reverse
|
from django.urls import reverse_lazy, reverse
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.forms.models import modelform_factory
|
from django.forms.models import modelform_factory
|
||||||
from django.core.exceptions import PermissionDenied, ValidationError
|
from django.core.exceptions import PermissionDenied, ValidationError
|
||||||
from django.forms import HiddenInput
|
from django.forms import HiddenInput
|
||||||
@ -496,7 +496,7 @@ class OperationCreateView(CanCreateMixin, CreateView):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add journal to the context """
|
"""Add journal to the context"""
|
||||||
kwargs = super(OperationCreateView, self).get_context_data(**kwargs)
|
kwargs = super(OperationCreateView, self).get_context_data(**kwargs)
|
||||||
if self.journal:
|
if self.journal:
|
||||||
kwargs["object"] = self.journal
|
kwargs["object"] = self.journal
|
||||||
@ -514,7 +514,7 @@ class OperationEditView(CanEditMixin, UpdateView):
|
|||||||
template_name = "accounting/operation_edit.jinja"
|
template_name = "accounting/operation_edit.jinja"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add journal to the context """
|
"""Add journal to the context"""
|
||||||
kwargs = super(OperationEditView, self).get_context_data(**kwargs)
|
kwargs = super(OperationEditView, self).get_context_data(**kwargs)
|
||||||
kwargs["object"] = self.object.journal
|
kwargs["object"] = self.object.journal
|
||||||
return kwargs
|
return kwargs
|
||||||
@ -735,7 +735,7 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
|||||||
return statement
|
return statement
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add infos to the context """
|
"""Add infos to the context"""
|
||||||
kwargs = super(JournalNatureStatementView, self).get_context_data(**kwargs)
|
kwargs = super(JournalNatureStatementView, self).get_context_data(**kwargs)
|
||||||
kwargs["statement"] = self.big_statement()
|
kwargs["statement"] = self.big_statement()
|
||||||
return kwargs
|
return kwargs
|
||||||
@ -774,7 +774,7 @@ class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
|||||||
return sum(self.statement(movement_type).values())
|
return sum(self.statement(movement_type).values())
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add journal to the context """
|
"""Add journal to the context"""
|
||||||
kwargs = super(JournalPersonStatementView, self).get_context_data(**kwargs)
|
kwargs = super(JournalPersonStatementView, self).get_context_data(**kwargs)
|
||||||
kwargs["credit_statement"] = self.statement("CREDIT")
|
kwargs["credit_statement"] = self.statement("CREDIT")
|
||||||
kwargs["debit_statement"] = self.statement("DEBIT")
|
kwargs["debit_statement"] = self.statement("DEBIT")
|
||||||
@ -804,7 +804,7 @@ class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView)
|
|||||||
return statement
|
return statement
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add journal to the context """
|
"""Add journal to the context"""
|
||||||
kwargs = super(JournalAccountingStatementView, self).get_context_data(**kwargs)
|
kwargs = super(JournalAccountingStatementView, self).get_context_data(**kwargs)
|
||||||
kwargs["statement"] = self.statement()
|
kwargs["statement"] = self.statement()
|
||||||
return kwargs
|
return kwargs
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
from django.urls import re_path, include
|
from django.urls import re_path, path, include
|
||||||
|
|
||||||
from api.views import *
|
from api.views import *
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
@ -54,4 +54,5 @@ urlpatterns = [
|
|||||||
re_path(r"^markdown$", RenderMarkdown, name="api_markdown"),
|
re_path(r"^markdown$", RenderMarkdown, name="api_markdown"),
|
||||||
re_path(r"^mailings$", FetchMailingLists, name="mailings_fetch"),
|
re_path(r"^mailings$", FetchMailingLists, name="mailings_fetch"),
|
||||||
re_path(r"^uv$", uv_endpoint, name="uv_endpoint"),
|
re_path(r"^uv$", uv_endpoint, name="uv_endpoint"),
|
||||||
|
path("sas/<int:user>", all_pictures_of_user_endpoint, name="all_pictures_of_user"),
|
||||||
]
|
]
|
||||||
|
@ -33,8 +33,8 @@ from core.views import can_view, can_edit
|
|||||||
|
|
||||||
def check_if(obj, user, test):
|
def check_if(obj, user, test):
|
||||||
"""
|
"""
|
||||||
Detect if it's a single object or a queryset
|
Detect if it's a single object or a queryset
|
||||||
aply a given test on individual object and return global permission
|
aply a given test on individual object and return global permission
|
||||||
"""
|
"""
|
||||||
if isinstance(obj, QuerySet):
|
if isinstance(obj, QuerySet):
|
||||||
for o in obj:
|
for o in obj:
|
||||||
@ -49,7 +49,7 @@ class ManageModelMixin:
|
|||||||
@action(detail=True)
|
@action(detail=True)
|
||||||
def id(self, request, pk=None):
|
def id(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
Get by id (api/v1/router/{pk}/id/)
|
Get by id (api/v1/router/{pk}/id/)
|
||||||
"""
|
"""
|
||||||
self.queryset = get_object_or_404(self.queryset.filter(id=pk))
|
self.queryset = get_object_or_404(self.queryset.filter(id=pk))
|
||||||
serializer = self.get_serializer(self.queryset)
|
serializer = self.get_serializer(self.queryset)
|
||||||
@ -78,3 +78,4 @@ from .club import *
|
|||||||
from .group import *
|
from .group import *
|
||||||
from .launderette import *
|
from .launderette import *
|
||||||
from .uv import *
|
from .uv import *
|
||||||
|
from .sas import *
|
||||||
|
@ -33,7 +33,7 @@ from core.templatetags.renderer import markdown
|
|||||||
@renderer_classes((StaticHTMLRenderer,))
|
@renderer_classes((StaticHTMLRenderer,))
|
||||||
def RenderMarkdown(request):
|
def RenderMarkdown(request):
|
||||||
"""
|
"""
|
||||||
Render Markdown
|
Render Markdown
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = markdown(request.POST["text"])
|
data = markdown(request.POST["text"])
|
||||||
|
@ -43,7 +43,7 @@ class ClubSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class ClubViewSet(RightModelViewSet):
|
class ClubViewSet(RightModelViewSet):
|
||||||
"""
|
"""
|
||||||
Manage Clubs (api/v1/club/)
|
Manage Clubs (api/v1/club/)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serializer_class = ClubSerializer
|
serializer_class = ClubSerializer
|
||||||
|
@ -45,7 +45,7 @@ class CounterSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class CounterViewSet(RightModelViewSet):
|
class CounterViewSet(RightModelViewSet):
|
||||||
"""
|
"""
|
||||||
Manage Counters (api/v1/counter/)
|
Manage Counters (api/v1/counter/)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serializer_class = CounterSerializer
|
serializer_class = CounterSerializer
|
||||||
@ -54,7 +54,7 @@ class CounterViewSet(RightModelViewSet):
|
|||||||
@action(detail=False)
|
@action(detail=False)
|
||||||
def bar(self, request):
|
def bar(self, request):
|
||||||
"""
|
"""
|
||||||
Return all bars (api/v1/counter/bar/)
|
Return all bars (api/v1/counter/bar/)
|
||||||
"""
|
"""
|
||||||
self.queryset = self.queryset.filter(type="BAR")
|
self.queryset = self.queryset.filter(type="BAR")
|
||||||
serializer = self.get_serializer(self.queryset, many=True)
|
serializer = self.get_serializer(self.queryset, many=True)
|
||||||
|
@ -36,7 +36,7 @@ class GroupSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class GroupViewSet(RightModelViewSet):
|
class GroupViewSet(RightModelViewSet):
|
||||||
"""
|
"""
|
||||||
Manage Groups (api/v1/group/)
|
Manage Groups (api/v1/group/)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serializer_class = GroupSerializer
|
serializer_class = GroupSerializer
|
||||||
|
@ -72,7 +72,7 @@ class LaunderetteTokenSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class LaunderettePlaceViewSet(RightModelViewSet):
|
class LaunderettePlaceViewSet(RightModelViewSet):
|
||||||
"""
|
"""
|
||||||
Manage Launderette (api/v1/launderette/place/)
|
Manage Launderette (api/v1/launderette/place/)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serializer_class = LaunderettePlaceSerializer
|
serializer_class = LaunderettePlaceSerializer
|
||||||
@ -81,7 +81,7 @@ class LaunderettePlaceViewSet(RightModelViewSet):
|
|||||||
|
|
||||||
class LaunderetteMachineViewSet(RightModelViewSet):
|
class LaunderetteMachineViewSet(RightModelViewSet):
|
||||||
"""
|
"""
|
||||||
Manage Washing Machines (api/v1/launderette/machine/)
|
Manage Washing Machines (api/v1/launderette/machine/)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serializer_class = LaunderetteMachineSerializer
|
serializer_class = LaunderetteMachineSerializer
|
||||||
@ -90,7 +90,7 @@ class LaunderetteMachineViewSet(RightModelViewSet):
|
|||||||
|
|
||||||
class LaunderetteTokenViewSet(RightModelViewSet):
|
class LaunderetteTokenViewSet(RightModelViewSet):
|
||||||
"""
|
"""
|
||||||
Manage Launderette's tokens (api/v1/launderette/token/)
|
Manage Launderette's tokens (api/v1/launderette/token/)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serializer_class = LaunderetteTokenSerializer
|
serializer_class = LaunderetteTokenSerializer
|
||||||
@ -99,7 +99,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
|
|||||||
@action(detail=False)
|
@action(detail=False)
|
||||||
def washing(self, request):
|
def washing(self, request):
|
||||||
"""
|
"""
|
||||||
Return all washing tokens (api/v1/launderette/token/washing)
|
Return all washing tokens (api/v1/launderette/token/washing)
|
||||||
"""
|
"""
|
||||||
self.queryset = self.queryset.filter(type="WASHING")
|
self.queryset = self.queryset.filter(type="WASHING")
|
||||||
serializer = self.get_serializer(self.queryset, many=True)
|
serializer = self.get_serializer(self.queryset, many=True)
|
||||||
@ -108,7 +108,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
|
|||||||
@action(detail=False)
|
@action(detail=False)
|
||||||
def drying(self, request):
|
def drying(self, request):
|
||||||
"""
|
"""
|
||||||
Return all drying tokens (api/v1/launderette/token/drying)
|
Return all drying tokens (api/v1/launderette/token/drying)
|
||||||
"""
|
"""
|
||||||
self.queryset = self.queryset.filter(type="DRYING")
|
self.queryset = self.queryset.filter(type="DRYING")
|
||||||
serializer = self.get_serializer(self.queryset, many=True)
|
serializer = self.get_serializer(self.queryset, many=True)
|
||||||
@ -117,7 +117,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
|
|||||||
@action(detail=False)
|
@action(detail=False)
|
||||||
def avaliable(self, request):
|
def avaliable(self, request):
|
||||||
"""
|
"""
|
||||||
Return all avaliable tokens (api/v1/launderette/token/avaliable)
|
Return all avaliable tokens (api/v1/launderette/token/avaliable)
|
||||||
"""
|
"""
|
||||||
self.queryset = self.queryset.filter(
|
self.queryset = self.queryset.filter(
|
||||||
borrow_date__isnull=True, user__isnull=True
|
borrow_date__isnull=True, user__isnull=True
|
||||||
@ -128,7 +128,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
|
|||||||
@action(detail=False)
|
@action(detail=False)
|
||||||
def unavaliable(self, request):
|
def unavaliable(self, request):
|
||||||
"""
|
"""
|
||||||
Return all unavaliable tokens (api/v1/launderette/token/unavaliable)
|
Return all unavaliable tokens (api/v1/launderette/token/unavaliable)
|
||||||
"""
|
"""
|
||||||
self.queryset = self.queryset.filter(
|
self.queryset = self.queryset.filter(
|
||||||
borrow_date__isnull=False, user__isnull=False
|
borrow_date__isnull=False, user__isnull=False
|
||||||
|
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)
|
||||||
|
]
|
||||||
|
)
|
@ -50,8 +50,8 @@ class UserSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class UserViewSet(RightModelViewSet):
|
class UserViewSet(RightModelViewSet):
|
||||||
"""
|
"""
|
||||||
Manage Users (api/v1/user/)
|
Manage Users (api/v1/user/)
|
||||||
Only show active users
|
Only show active users
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
@ -60,7 +60,7 @@ class UserViewSet(RightModelViewSet):
|
|||||||
@action(detail=False)
|
@action(detail=False)
|
||||||
def birthday(self, request):
|
def birthday(self, request):
|
||||||
"""
|
"""
|
||||||
Return all users born today (api/v1/user/birstdays)
|
Return all users born today (api/v1/user/birstdays)
|
||||||
"""
|
"""
|
||||||
date = datetime.datetime.today()
|
date = datetime.datetime.today()
|
||||||
self.queryset = self.queryset.filter(date_of_birth=date)
|
self.queryset = self.queryset.filter(date_of_birth=date)
|
||||||
|
@ -29,10 +29,10 @@ def uv_endpoint(request):
|
|||||||
|
|
||||||
def find_uv(lang, year, code):
|
def find_uv(lang, year, code):
|
||||||
"""
|
"""
|
||||||
Uses the UTBM API to find an UV.
|
Uses the UTBM API to find an UV.
|
||||||
short_uv is the UV entry in the UV list. It is returned as it contains
|
short_uv is the UV entry in the UV list. It is returned as it contains
|
||||||
information which are not in full_uv.
|
information which are not in full_uv.
|
||||||
full_uv is the detailed representation of an UV.
|
full_uv is the detailed representation of an UV.
|
||||||
"""
|
"""
|
||||||
# query the UV list
|
# query the UV list
|
||||||
uvs_url = settings.SITH_PEDAGOGY_UTBM_API + "/uvs/{}/{}".format(lang, year)
|
uvs_url = settings.SITH_PEDAGOGY_UTBM_API + "/uvs/{}/{}".format(lang, year)
|
||||||
@ -57,7 +57,7 @@ def find_uv(lang, year, code):
|
|||||||
|
|
||||||
def make_clean_uv(short_uv, full_uv):
|
def make_clean_uv(short_uv, full_uv):
|
||||||
"""
|
"""
|
||||||
Cleans the data up so that it corresponds to our data representation.
|
Cleans the data up so that it corresponds to our data representation.
|
||||||
"""
|
"""
|
||||||
res = {}
|
res = {}
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
|
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
|
||||||
|
|
||||||
@ -34,6 +34,7 @@ from club.models import Mailing, MailingSubscription, Club, Membership
|
|||||||
from core.models import User
|
from core.models import User
|
||||||
from core.views.forms import SelectDate, SelectDateTime
|
from core.views.forms import SelectDate, SelectDateTime
|
||||||
from counter.models import Counter
|
from counter.models import Counter
|
||||||
|
from core.views.forms import TzAwareDateTimeField
|
||||||
|
|
||||||
|
|
||||||
class ClubEditForm(forms.ModelForm):
|
class ClubEditForm(forms.ModelForm):
|
||||||
@ -157,23 +158,28 @@ class MailingForm(forms.Form):
|
|||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
class SellingsFormBase(forms.Form):
|
class SellingsForm(forms.Form):
|
||||||
begin_date = forms.DateTimeField(
|
begin_date = TzAwareDateTimeField(label=_("Begin date"), required=False)
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
end_date = TzAwareDateTimeField(label=_("End date"), required=False)
|
||||||
label=_("Begin date"),
|
|
||||||
required=False,
|
counters = forms.ModelMultipleChoiceField(
|
||||||
widget=SelectDateTime,
|
|
||||||
)
|
|
||||||
end_date = forms.DateTimeField(
|
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
|
||||||
label=_("End date"),
|
|
||||||
required=False,
|
|
||||||
widget=SelectDateTime,
|
|
||||||
)
|
|
||||||
counter = forms.ModelChoiceField(
|
|
||||||
Counter.objects.order_by("name").all(), label=_("Counter"), required=False
|
Counter.objects.order_by("name").all(), label=_("Counter"), required=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __init__(self, club, *args, **kwargs):
|
||||||
|
|
||||||
|
super(SellingsForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields["products"] = forms.ModelMultipleChoiceField(
|
||||||
|
club.products.order_by("name").filter(archived=False).all(),
|
||||||
|
label=_("Products"),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
self.fields["archived_products"] = forms.ModelMultipleChoiceField(
|
||||||
|
club.products.order_by("name").filter(archived=True).all(),
|
||||||
|
label=_("Archived products"),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ClubMemberForm(forms.Form):
|
class ClubMemberForm(forms.Form):
|
||||||
"""
|
"""
|
||||||
@ -238,8 +244,8 @@ class ClubMemberForm(forms.Form):
|
|||||||
|
|
||||||
def clean_users(self):
|
def clean_users(self):
|
||||||
"""
|
"""
|
||||||
Check that the user is not trying to add an user already in the club
|
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
|
Also check that the user is valid and has a valid subscription
|
||||||
"""
|
"""
|
||||||
cleaned_data = super(ClubMemberForm, self).clean()
|
cleaned_data = super(ClubMemberForm, self).clean()
|
||||||
users = []
|
users = []
|
||||||
@ -262,7 +268,7 @@ class ClubMemberForm(forms.Form):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""
|
"""
|
||||||
Check user rights for adding an user
|
Check user rights for adding an user
|
||||||
"""
|
"""
|
||||||
cleaned_data = super(ClubMemberForm, self).clean()
|
cleaned_data = super(ClubMemberForm, self).clean()
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
{% from 'core/macros.jinja' import user_profile_link %}
|
{% from 'core/macros.jinja' import user_profile_link, paginate %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h3>{% trans %}Sellings{% endtrans %}</h3>
|
<h3>{% trans %}Sellings{% endtrans %}</h3>
|
||||||
<form action="" method="get">
|
<form id="form" action="?page=1" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form }}
|
{{ form }}
|
||||||
<p><input type="submit" value="{% trans %}Show{% endtrans %}" /></p>
|
<p><input type="submit" value="{% trans %}Show{% endtrans %}" /></p>
|
||||||
@ -28,7 +28,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for s in result %}
|
{% for s in paginated_result %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ s.date|localtime|date(DATETIME_FORMAT) }} {{ s.date|localtime|time(DATETIME_FORMAT) }}</td>
|
<td>{{ s.date|localtime|date(DATETIME_FORMAT) }} {{ s.date|localtime|time(DATETIME_FORMAT) }}</td>
|
||||||
<td>{{ s.counter }}</td>
|
<td>{{ s.counter }}</td>
|
||||||
@ -53,6 +53,14 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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)") }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for widget in form_mailing_removal.subwidgets %}
|
{% for widget in form_mailing_removal.subwidgets %}
|
||||||
{% set user = ms[widget.data.value][0] %}
|
{% set user = ms[widget.data.value.value][0] %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ user.get_username }}</td>
|
<td>{{ user.get_username }}</td>
|
||||||
<td>{{ user.get_email }}</td>
|
<td>{{ user.get_email }}</td>
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import timezone, html
|
from django.utils import timezone, html
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
|
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
|
||||||
@ -161,10 +161,10 @@ class ClubTest(TestCase):
|
|||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id})
|
reverse("club:club_members", kwargs={"club_id": self.bdf.id})
|
||||||
)
|
)
|
||||||
self.assertTrue(response.status_code == 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTrue(
|
self.assertIn(
|
||||||
"""Richard Batsbak</a></td>\\n <td>Vice-Pr\\xc3\\xa9sident</td>"""
|
"""Richard Batsbak</a></td>\n <td>Vice-Président⸱e</td>""",
|
||||||
in str(response.content)
|
response.content.decode(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_create_add_user_to_club_from_richard_fail(self):
|
def test_create_add_user_to_club_from_richard_fail(self):
|
||||||
@ -180,7 +180,7 @@ class ClubTest(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertTrue(response.status_code == 200)
|
self.assertTrue(response.status_code == 200)
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
"<li>Vous n'avez pas la permission de faire cela</li>"
|
"<li>Vous n'avez pas la permission de faire cela</li>"
|
||||||
in str(response.content)
|
in str(response.content)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -369,14 +369,15 @@ class ClubTest(TestCase):
|
|||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id})
|
reverse("club:club_members", kwargs={"club_id": self.bdf.id})
|
||||||
)
|
)
|
||||||
self.assertTrue(response.status_code == 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
content = str(response.content)
|
content = response.content.decode()
|
||||||
self.assertTrue(
|
self.assertIn(
|
||||||
"Richard Batsbak</a></td>\\n <td>Curieux</td>" in content
|
"Richard Batsbak</a></td>\n <td>Curieux⸱euse</td>",
|
||||||
|
content,
|
||||||
)
|
)
|
||||||
self.assertTrue(
|
self.assertIn(
|
||||||
"S' Kia</a></td>\\n <td>Responsable info</td>"
|
"S' Kia</a></td>\n <td>Responsable info</td>",
|
||||||
in content
|
content,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -91,6 +91,11 @@ urlpatterns = [
|
|||||||
MembershipSetOldView.as_view(),
|
MembershipSetOldView.as_view(),
|
||||||
name="membership_set_old",
|
name="membership_set_old",
|
||||||
),
|
),
|
||||||
|
re_path(
|
||||||
|
r"^membership/(?P<membership_id>[0-9]+)/delete$",
|
||||||
|
MembershipDeleteView.as_view(),
|
||||||
|
name="membership_delete",
|
||||||
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^(?P<club_id>[0-9]+)/poster$", PosterListView.as_view(), name="poster_list"
|
r"^(?P<club_id>[0-9]+)/poster$", PosterListView.as_view(), name="poster_list"
|
||||||
),
|
),
|
||||||
|
184
club/views.py
@ -23,6 +23,7 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
|
import csv
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django import forms
|
from django import forms
|
||||||
@ -30,19 +31,28 @@ from django.views.generic import ListView, DetailView, TemplateView, View
|
|||||||
from django.views.generic.edit import DeleteView
|
from django.views.generic.edit import DeleteView
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
from django.views.generic.edit import UpdateView, CreateView
|
from django.views.generic.edit import UpdateView, CreateView
|
||||||
from django.http import HttpResponseRedirect, HttpResponse, Http404
|
from django.http import (
|
||||||
|
HttpResponseRedirect,
|
||||||
|
HttpResponse,
|
||||||
|
Http404,
|
||||||
|
StreamingHttpResponse,
|
||||||
|
)
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.utils.translation import ugettext as _t
|
from django.utils.translation import gettext as _t
|
||||||
from django.core.exceptions import PermissionDenied, ValidationError, NON_FIELD_ERRORS
|
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.shortcuts import get_object_or_404, redirect
|
||||||
|
from django.db.models import Sum
|
||||||
|
|
||||||
|
|
||||||
from core.views import (
|
from core.views import (
|
||||||
CanCreateMixin,
|
CanCreateMixin,
|
||||||
CanViewMixin,
|
CanViewMixin,
|
||||||
CanEditMixin,
|
CanEditMixin,
|
||||||
CanEditPropMixin,
|
CanEditPropMixin,
|
||||||
|
UserIsRootMixin,
|
||||||
TabedViewMixin,
|
TabedViewMixin,
|
||||||
PageEditViewBase,
|
PageEditViewBase,
|
||||||
DetailFormView,
|
DetailFormView,
|
||||||
@ -59,7 +69,7 @@ from com.views import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from club.models import Club, Membership, Mailing, MailingSubscription
|
from club.models import Club, Membership, Mailing, MailingSubscription
|
||||||
from club.forms import MailingForm, ClubEditForm, ClubMemberForm, SellingsFormBase
|
from club.forms import MailingForm, ClubEditForm, ClubMemberForm, SellingsForm
|
||||||
|
|
||||||
|
|
||||||
class ClubTabsMixin(TabedViewMixin):
|
class ClubTabsMixin(TabedViewMixin):
|
||||||
@ -280,7 +290,7 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
|
|||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
"""
|
"""
|
||||||
Check user rights
|
Check user rights
|
||||||
"""
|
"""
|
||||||
resp = super(ClubMembersView, self).form_valid(form)
|
resp = super(ClubMembersView, self).form_valid(form)
|
||||||
|
|
||||||
@ -318,7 +328,7 @@ class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView):
|
|||||||
current_tab = "elderlies"
|
current_tab = "elderlies"
|
||||||
|
|
||||||
|
|
||||||
class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailView):
|
class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
||||||
"""
|
"""
|
||||||
Sellings of a club
|
Sellings of a club
|
||||||
"""
|
"""
|
||||||
@ -327,21 +337,35 @@ class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailView):
|
|||||||
pk_url_kwarg = "club_id"
|
pk_url_kwarg = "club_id"
|
||||||
template_name = "club/club_sellings.jinja"
|
template_name = "club/club_sellings.jinja"
|
||||||
current_tab = "sellings"
|
current_tab = "sellings"
|
||||||
|
form_class = SellingsForm
|
||||||
|
paginate_by = 70
|
||||||
|
|
||||||
def get_form_class(self):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
kwargs = {
|
try:
|
||||||
"product": forms.ModelChoiceField(
|
self.asked_page = int(request.GET.get("page", 1))
|
||||||
self.object.products.order_by("name").all(),
|
except ValueError:
|
||||||
label=_("Product"),
|
raise Http404
|
||||||
required=False,
|
return super(ClubSellingView, self).dispatch(request, *args, **kwargs)
|
||||||
)
|
|
||||||
}
|
def get_form_kwargs(self):
|
||||||
return type("SellingsForm", (SellingsFormBase,), kwargs)
|
kwargs = super(ClubSellingView, self).get_form_kwargs()
|
||||||
|
kwargs["club"] = self.object
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
return self.get(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs = super(ClubSellingView, self).get_context_data(**kwargs)
|
kwargs = super(ClubSellingView, self).get_context_data(**kwargs)
|
||||||
form = self.get_form_class()(self.request.GET)
|
|
||||||
qs = Selling.objects.filter(club=self.object)
|
qs = Selling.objects.filter(club=self.object)
|
||||||
|
|
||||||
|
kwargs["result"] = qs[:0]
|
||||||
|
kwargs["paginated_result"] = kwargs["result"]
|
||||||
|
kwargs["total"] = 0
|
||||||
|
kwargs["total_quantity"] = 0
|
||||||
|
kwargs["benefit"] = 0
|
||||||
|
|
||||||
|
form = self.get_form()
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
if not len([v for v in form.cleaned_data.values() if v is not None]):
|
if not len([v for v in form.cleaned_data.values() if v is not None]):
|
||||||
qs = Selling.objects.filter(id=-1)
|
qs = Selling.objects.filter(id=-1)
|
||||||
@ -349,19 +373,36 @@ class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailView):
|
|||||||
qs = qs.filter(date__gte=form.cleaned_data["begin_date"])
|
qs = qs.filter(date__gte=form.cleaned_data["begin_date"])
|
||||||
if form.cleaned_data["end_date"]:
|
if form.cleaned_data["end_date"]:
|
||||||
qs = qs.filter(date__lte=form.cleaned_data["end_date"])
|
qs = qs.filter(date__lte=form.cleaned_data["end_date"])
|
||||||
if form.cleaned_data["counter"]:
|
|
||||||
qs = qs.filter(counter=form.cleaned_data["counter"])
|
if form.cleaned_data["counters"]:
|
||||||
if form.cleaned_data["product"]:
|
qs = qs.filter(counter__in=form.cleaned_data["counters"])
|
||||||
qs = qs.filter(product__id=form.cleaned_data["product"].id)
|
|
||||||
|
selected_products = []
|
||||||
|
if form.cleaned_data["products"]:
|
||||||
|
selected_products.extend(form.cleaned_data["products"])
|
||||||
|
if form.cleaned_data["archived_products"]:
|
||||||
|
selected_products.extend(form.cleaned_data["archived_products"])
|
||||||
|
|
||||||
|
if len(selected_products) > 0:
|
||||||
|
qs = qs.filter(product__in=selected_products)
|
||||||
|
|
||||||
kwargs["result"] = qs.all().order_by("-id")
|
kwargs["result"] = qs.all().order_by("-id")
|
||||||
kwargs["total"] = sum([s.quantity * s.unit_price for s in qs.all()])
|
kwargs["total"] = sum([s.quantity * s.unit_price for s in kwargs["result"]])
|
||||||
kwargs["total_quantity"] = sum([s.quantity for s in qs.all()])
|
total_quantity = qs.all().aggregate(Sum("quantity"))
|
||||||
kwargs["benefit"] = kwargs["total"] - sum(
|
if total_quantity["quantity__sum"]:
|
||||||
[s.product.purchase_price for s in qs.exclude(product=None)]
|
kwargs["total_quantity"] = total_quantity["quantity__sum"]
|
||||||
|
benefit = (
|
||||||
|
qs.exclude(product=None).all().aggregate(Sum("product__purchase_price"))
|
||||||
)
|
)
|
||||||
else:
|
if benefit["product__purchase_price__sum"]:
|
||||||
kwargs["result"] = qs[:0]
|
kwargs["benefit"] = benefit["product__purchase_price__sum"]
|
||||||
kwargs["form"] = form
|
|
||||||
|
kwargs["paginator"] = Paginator(kwargs["result"], self.paginate_by)
|
||||||
|
try:
|
||||||
|
kwargs["paginated_result"] = kwargs["paginator"].page(self.asked_page)
|
||||||
|
except InvalidPage:
|
||||||
|
raise Http404
|
||||||
|
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
@ -370,16 +411,46 @@ class ClubSellingCSVView(ClubSellingView):
|
|||||||
Generate sellings in csv for a given period
|
Generate sellings in csv for a given period
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
class StreamWriter:
|
||||||
import 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."""
|
||||||
|
return value
|
||||||
|
|
||||||
|
def write_selling(self, selling):
|
||||||
|
row = [selling.date, selling.counter]
|
||||||
|
if selling.seller:
|
||||||
|
row.append(selling.seller.get_display_name())
|
||||||
|
else:
|
||||||
|
row.append("")
|
||||||
|
if selling.customer:
|
||||||
|
row.append(selling.customer.user.get_display_name())
|
||||||
|
else:
|
||||||
|
row.append("")
|
||||||
|
row = row + [
|
||||||
|
selling.label,
|
||||||
|
selling.quantity,
|
||||||
|
selling.quantity * selling.unit_price,
|
||||||
|
selling.get_payment_method_display(),
|
||||||
|
]
|
||||||
|
if selling.product:
|
||||||
|
row.append(selling.product.selling_price)
|
||||||
|
row.append(selling.product.purchase_price)
|
||||||
|
row.append(selling.product.selling_price - selling.product.purchase_price)
|
||||||
|
else:
|
||||||
|
row = row + ["", "", ""]
|
||||||
|
return row
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
|
||||||
response = HttpResponse(content_type="text/csv")
|
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
name = _("Sellings") + "_" + self.object.name + ".csv"
|
|
||||||
response["Content-Disposition"] = "filename=" + name
|
|
||||||
kwargs = self.get_context_data(**kwargs)
|
kwargs = self.get_context_data(**kwargs)
|
||||||
|
|
||||||
|
# Use the StreamWriter class instead of request for streaming
|
||||||
|
pseudo_buffer = self.StreamWriter()
|
||||||
writer = csv.writer(
|
writer = csv.writer(
|
||||||
response, delimiter=";", lineterminator="\n", quoting=csv.QUOTE_ALL
|
pseudo_buffer, delimiter=";", lineterminator="\n", quoting=csv.QUOTE_ALL
|
||||||
)
|
)
|
||||||
|
|
||||||
writer.writerow([_t("Quantity"), kwargs["total_quantity"]])
|
writer.writerow([_t("Quantity"), kwargs["total_quantity"]])
|
||||||
@ -400,29 +471,17 @@ class ClubSellingCSVView(ClubSellingView):
|
|||||||
_t("Benefit"),
|
_t("Benefit"),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
for o in kwargs["result"]:
|
|
||||||
row = [o.date, o.counter]
|
# Stream response
|
||||||
if o.seller:
|
response = StreamingHttpResponse(
|
||||||
row.append(o.seller.get_display_name())
|
(
|
||||||
else:
|
writer.writerow(self.write_selling(selling))
|
||||||
row.append("")
|
for selling in kwargs["result"]
|
||||||
if o.customer:
|
),
|
||||||
row.append(o.customer.user.get_display_name())
|
content_type="text/csv",
|
||||||
else:
|
)
|
||||||
row.append("")
|
name = _("Sellings") + "_" + self.object.name + ".csv"
|
||||||
row = row + [
|
response["Content-Disposition"] = "filename=" + name
|
||||||
o.label,
|
|
||||||
o.quantity,
|
|
||||||
o.quantity * o.unit_price,
|
|
||||||
o.get_payment_method_display(),
|
|
||||||
]
|
|
||||||
if o.product:
|
|
||||||
row.append(o.product.selling_price)
|
|
||||||
row.append(o.product.purchase_price)
|
|
||||||
row.append(o.product.selling_price - o.product.purchase_price)
|
|
||||||
else:
|
|
||||||
row = row + ["", "", ""]
|
|
||||||
writer.writerow(row)
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@ -493,6 +552,19 @@ class MembershipSetOldView(CanEditMixin, DetailView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MembershipDeleteView(UserIsRootMixin, DeleteView):
|
||||||
|
"""
|
||||||
|
Delete a membership (for admins only)
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = Membership
|
||||||
|
pk_url_kwarg = "membership_id"
|
||||||
|
template_name = "core/delete_confirm.jinja"
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse_lazy("core:user_clubs", kwargs={"user_id": self.object.user.id})
|
||||||
|
|
||||||
|
|
||||||
class ClubStatView(TemplateView):
|
class ClubStatView(TemplateView):
|
||||||
template_name = "club/stats.jinja"
|
template_name = "club/stats.jinja"
|
||||||
|
|
||||||
|
@ -26,11 +26,11 @@
|
|||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.staticfiles.templatetags.staticfiles import static
|
from django.templatetags.static import static
|
||||||
from django.core.mail import EmailMultiAlternatives
|
from django.core.mail import EmailMultiAlternatives
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
@ -61,6 +61,11 @@ NEWS_TYPES = [
|
|||||||
("CALL", _("Call")),
|
("CALL", _("Call")),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
WEEKMAIL_TYPE = [
|
||||||
|
("WEEKMAIL", _("Weekmail")),
|
||||||
|
("INVITATION", _("Invitation")),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class News(models.Model):
|
class News(models.Model):
|
||||||
"""The news class"""
|
"""The news class"""
|
||||||
@ -178,6 +183,9 @@ class Weekmail(models.Model):
|
|||||||
protip = models.TextField(_("protip"), blank=True)
|
protip = models.TextField(_("protip"), blank=True)
|
||||||
conclusion = models.TextField(_("conclusion"), blank=True)
|
conclusion = models.TextField(_("conclusion"), blank=True)
|
||||||
sent = models.BooleanField(_("sent"), default=False)
|
sent = models.BooleanField(_("sent"), default=False)
|
||||||
|
type = models.CharField(
|
||||||
|
_("type"), max_length=16, choices=WEEKMAIL_TYPE, default="WEEKMAIL"
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["-id"]
|
ordering = ["-id"]
|
||||||
@ -215,6 +223,17 @@ class Weekmail(models.Model):
|
|||||||
None, "com/weekmail_renderer_text.jinja", context={"weekmail": self}
|
None, "com/weekmail_renderer_text.jinja", context={"weekmail": self}
|
||||||
).content.decode("utf-8")
|
).content.decode("utf-8")
|
||||||
|
|
||||||
|
def switch_type(self):
|
||||||
|
"""
|
||||||
|
Switch the type of weekmail we are sending :
|
||||||
|
- a simple weekmail
|
||||||
|
- or an invitation
|
||||||
|
"""
|
||||||
|
if self.type == "INVITATION":
|
||||||
|
self.type = "WEEKMAIL"
|
||||||
|
else:
|
||||||
|
self.type = "INVITATION"
|
||||||
|
|
||||||
def render_html(self):
|
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.
|
||||||
@ -227,13 +246,20 @@ class Weekmail(models.Model):
|
|||||||
"""
|
"""
|
||||||
Return an absolute link to the banner.
|
Return an absolute link to the banner.
|
||||||
"""
|
"""
|
||||||
return "http://" + settings.SITH_URL + static("com/img/weekmail_bannerA19.jpg")
|
if self.type == "INVITATION":
|
||||||
|
return (
|
||||||
|
"http://" + settings.SITH_URL + static("com/img/invitation_bannerP22.png")
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
"http://" + settings.SITH_URL + static("com/img/weekmail_bannerV2P22.png")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_footer(self):
|
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_footerA19.jpg")
|
return "http://" + settings.SITH_URL + static("com/img/weekmail_footerP22.png")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Weekmail %s (sent: %s) - %s" % (self.id, self.sent, self.title)
|
return "Weekmail %s (sent: %s) - %s" % (self.id, self.sent, self.title)
|
||||||
|
@ -6,152 +6,150 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{% if user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %}
|
||||||
|
<div id="news_admin">
|
||||||
|
<a href="{{ url('com:news_admin_list') }}">{% trans %}Administrate news{% endtrans %}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div id="news">
|
<div id="news">
|
||||||
{% if user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %}
|
|
||||||
<div id="news_admin">
|
|
||||||
<a href="{{ url('com:news_admin_list') }}">{% trans %}Administrate news{% endtrans %}</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div id="right_column" class="news_column">
|
|
||||||
<div id="agenda">
|
|
||||||
<div id="agenda_title">{% trans %}Agenda{% endtrans %}</div>
|
|
||||||
<div id="agenda_content">
|
|
||||||
{% for d in NewsDate.objects.filter(end_date__gte=timezone.now(),
|
|
||||||
news__is_moderated=True, news__type__in=["WEEKLY",
|
|
||||||
"EVENT"]).order_by('start_date', 'end_date') %}
|
|
||||||
<div class="agenda_item">
|
|
||||||
<div class="agenda_date">
|
|
||||||
<strong>{{ d.start_date|localtime|date('D d M Y') }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="agenda_time">
|
|
||||||
<span>{{ d.start_date|localtime|time(DATETIME_FORMAT) }}</span> -
|
|
||||||
<span>{{ d.end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong><a href="{{ url('com:news_detail', news_id=d.news.id) }}">{{ d.news.title }}</a></strong>
|
|
||||||
<a href="{{ d.news.club.get_absolute_url() }}">{{ d.news.club }}</a>
|
|
||||||
</div>
|
|
||||||
<div class="agenda_item_content">{{ d.news.summary|markdown }}</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="birthdays">
|
|
||||||
<div id="birthdays_title">{% trans %}Birthdays{% endtrans %}</div>
|
|
||||||
<div id="birthdays_content">
|
|
||||||
{% if user.is_subscribed %}
|
|
||||||
{# Cache request for 1 hour #}
|
|
||||||
{% cache 3600 birthdays %}
|
|
||||||
<ul class="birthdays_year">
|
|
||||||
{% for d in birthdays.dates('date_of_birth', 'year', 'DESC') %}
|
|
||||||
<li>
|
|
||||||
{% trans age=timezone.now().year - d.year %}{{ age }} year old{% endtrans %}
|
|
||||||
<ul>
|
|
||||||
{% for u in birthdays.filter(date_of_birth__year=d.year) %}
|
|
||||||
<li><a href="{{ u.get_absolute_url() }}">{{ u.get_short_name() }}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endcache %}
|
|
||||||
{% else %}
|
|
||||||
<p>{% trans %}You need an up to date subscription to access this content{% endtrans %}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div id="left_column" class="news_column">
|
<div id="left_column" class="news_column">
|
||||||
|
|
||||||
{% for news in object_list.filter(type="NOTICE") %}
|
{% for news in object_list.filter(type="NOTICE") %}
|
||||||
<section class="news_notice">
|
<section class="news_notice">
|
||||||
<h4><a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
|
<h4><a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
|
||||||
<div class="news_content">{{ news.summary|markdown }}</div>
|
<div class="news_content">{{ news.summary|markdown }}</div>
|
||||||
</section>
|
</section>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% for news in object_list.filter(dates__start_date__lte=timezone.now(),
|
{% for news in object_list.filter(dates__start_date__lte=timezone.now(), dates__end_date__gte=timezone.now(), type="CALL") %}
|
||||||
dates__end_date__gte=timezone.now(), type="CALL") %}
|
<section class="news_call">
|
||||||
<section class="news_call">
|
<h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
|
||||||
<h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
|
<div class="news_date">
|
||||||
<div class="news_date">
|
<span>{{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }}
|
||||||
<span>{{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }}
|
{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
|
||||||
{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
|
<span>{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }}
|
||||||
<span>{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }}
|
{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
||||||
{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
</div>
|
||||||
</div>
|
<div class="news_content">{{ news.summary|markdown }}</div>
|
||||||
<div class="news_content">{{ news.summary|markdown }}</div>
|
</section>
|
||||||
</section>
|
{% endfor %}
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% set events_dates = NewsDate.objects.filter(end_date__gte=timezone.now(), start_date__lte=timezone.now()+timedelta(days=5),
|
{% set events_dates = NewsDate.objects.filter(end_date__gte=timezone.now(), start_date__lte=timezone.now()+timedelta(days=5), news__type="EVENT", news__is_moderated=True).datetimes('start_date', 'day') %}
|
||||||
news__type="EVENT", news__is_moderated=True).datetimes('start_date', 'day') %}
|
<h3>{% trans %}Events today and the next few days{% endtrans %}</h3>
|
||||||
<h3>{% trans %}Events today and the next few days{% endtrans %}</h3>
|
{% if events_dates %}
|
||||||
{% if events_dates %}
|
{% for d in events_dates %}
|
||||||
{% for d in events_dates %}
|
<div class="news_events_group">
|
||||||
<div class="news_events_group">
|
<div class="news_events_group_date">
|
||||||
<div class="news_events_group_date">
|
<div>
|
||||||
<div>
|
<div>{{ d|localtime|date('D') }}</div>
|
||||||
<div>{{ d|localtime|date('D') }}</div>
|
<div class="day">{{ d|localtime|date('d') }}</div>
|
||||||
<div class="day">{{ d|localtime|date('d') }}</div>
|
<div>{{ d|localtime|date('b') }}</div>
|
||||||
<div>{{ d|localtime|date('b') }}</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="news_events_group_items">
|
||||||
<div class="news_events_group_items">
|
{% for news in object_list.filter(dates__start_date__gte=d,
|
||||||
{% for news in object_list.filter(dates__start_date__gte=d,
|
dates__start_date__lte=d+timedelta(days=1),
|
||||||
dates__start_date__lte=d+timedelta(days=1),
|
type="EVENT").exclude(dates__end_date__lt=timezone.now())
|
||||||
type="EVENT").exclude(dates__end_date__lt=timezone.now())
|
.order_by('dates__start_date') %}
|
||||||
.order_by('dates__start_date') %}
|
<section class="news_event">
|
||||||
<section class="news_event">
|
<div class="club_logo">
|
||||||
<div class="club_logo">
|
{% if news.club.logo %}
|
||||||
{% if news.club.logo %}
|
<img src="{{ news.club.logo.url }}" alt="{{ news.club }}" />
|
||||||
<img src="{{ news.club.logo.url }}" alt="{{ news.club }}" />
|
{% else %}
|
||||||
{% else %}
|
<img src="{{ static("com/img/news.png") }}" alt="{{ news.club }}" />
|
||||||
<img src="{{ static("com/img/news.png") }}" alt="{{ news.club }}" />
|
{% endif %}
|
||||||
{% endif %}
|
</div>
|
||||||
</div>
|
<h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
|
||||||
<h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
|
<div><a href="{{ news.club.get_absolute_url() }}">{{ news.club }}</a></div>
|
||||||
<div><a href="{{ news.club.get_absolute_url() }}">{{ news.club }}</a></div>
|
<div class="news_date">
|
||||||
<div class="news_date">
|
<span>{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
|
||||||
<span>{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
|
<span>{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
||||||
<span>{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
</div>
|
||||||
</div>
|
<div class="news_content">{{ news.summary|markdown }}
|
||||||
<div class="news_content">{{ news.summary|markdown }}
|
<div class="button_bar">
|
||||||
<div class="button_bar">
|
{{ fb_quick(news) }}
|
||||||
{{ fb_quick(news) }}
|
{{ tweet_quick(news) }}
|
||||||
{{ tweet_quick(news) }}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
{% endfor %}
|
||||||
{% endfor %}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% endfor %}
|
||||||
{% endfor %}
|
{% else %}
|
||||||
{% else %}
|
<div class="news_empty">
|
||||||
<div class="news_empty">
|
<em>{% trans %}Nothing to come...{% endtrans %}</em>
|
||||||
<em>{% trans %}Nothing to come...{% endtrans %}</em>
|
</div>
|
||||||
</div>
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% set coming_soon = object_list.filter(dates__start_date__gte=timezone.now()+timedelta(days=5),
|
{% set coming_soon = object_list.filter(dates__start_date__gte=timezone.now()+timedelta(days=5),
|
||||||
type="EVENT").order_by('dates__start_date') %}
|
type="EVENT").order_by('dates__start_date') %}
|
||||||
{% if coming_soon %}
|
{% if coming_soon %}
|
||||||
<h3>{% trans %}Coming soon... don't miss!{% endtrans %}</h3>
|
<h3>{% trans %}Coming soon... don't miss!{% endtrans %}</h3>
|
||||||
{% for news in coming_soon %}
|
{% for news in coming_soon %}
|
||||||
<section class="news_coming_soon">
|
<section class="news_coming_soon">
|
||||||
<a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a>
|
<a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a>
|
||||||
<span class="news_date">{{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }}
|
<span class="news_date">{{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }}
|
||||||
{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }} -
|
{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }} -
|
||||||
{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }}
|
{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }}
|
||||||
{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
||||||
</section>
|
</section>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="right_column" class="news_column">
|
||||||
|
<div id="agenda">
|
||||||
|
<div id="agenda_title">{% trans %}Agenda{% endtrans %}</div>
|
||||||
|
<div id="agenda_content">
|
||||||
|
{% for d in NewsDate.objects.filter(end_date__gte=timezone.now(),
|
||||||
|
news__is_moderated=True, news__type__in=["WEEKLY",
|
||||||
|
"EVENT"]).order_by('start_date', 'end_date') %}
|
||||||
|
<div class="agenda_item">
|
||||||
|
<div class="agenda_date">
|
||||||
|
<strong>{{ d.start_date|localtime|date('D d M Y') }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="agenda_time">
|
||||||
|
<span>{{ d.start_date|localtime|time(DATETIME_FORMAT) }}</span> -
|
||||||
|
<span>{{ d.end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong><a href="{{ url('com:news_detail', news_id=d.news.id) }}">{{ d.news.title }}</a></strong>
|
||||||
|
<a href="{{ d.news.club.get_absolute_url() }}">{{ d.news.club }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="agenda_item_content">{{ d.news.summary|markdown }}</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="birthdays">
|
||||||
|
<div id="birthdays_title">{% trans %}Birthdays{% endtrans %}</div>
|
||||||
|
<div id="birthdays_content">
|
||||||
|
{% if user.is_subscribed %}
|
||||||
|
{# Cache request for 1 hour #}
|
||||||
|
{% cache 3600 "birthdays" %}
|
||||||
|
<ul class="birthdays_year">
|
||||||
|
{% for d in birthdays.dates('date_of_birth', 'year', 'DESC') %}
|
||||||
|
<li>
|
||||||
|
{% trans age=timezone.now().year - d.year %}{{ age }} year old{% endtrans %}
|
||||||
|
<ul>
|
||||||
|
{% for u in birthdays.filter(date_of_birth__year=d.year) %}
|
||||||
|
<li><a href="{{ u.get_absolute_url() }}">{{ u.get_short_name() }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endcache %}
|
||||||
|
{% else %}
|
||||||
|
<p>{% trans %}You need an up to date subscription to access this content{% endtrans %}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -36,8 +36,8 @@
|
|||||||
<div class="name">{{ poster.name }}</div>
|
<div class="name">{{ poster.name }}</div>
|
||||||
<div class="image"><img src="{{ poster.file.url }}"></img></div>
|
<div class="image"><img src="{{ poster.file.url }}"></img></div>
|
||||||
<div class="dates">
|
<div class="dates">
|
||||||
<div class="begin">{{ poster.date_begin | date("d/M/Y H:m") }}</div>
|
<div class="begin">{{ poster.date_begin | localtime | date("d/M/Y H:m") }}</div>
|
||||||
<div class="end">{{ poster.date_end | date("d/M/Y H:m") }}</div>
|
<div class="end">{{ poster.date_end | localtime | date("d/M/Y H:m") }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% if app == "com" %}
|
{% if app == "com" %}
|
||||||
<a class="edit" href="{{ url(app + ":poster_edit", poster.id) }}">{% trans %}Edit{% endtrans %}</a>
|
<a class="edit" href="{{ url(app + ":poster_edit", poster.id) }}">{% trans %}Edit{% endtrans %}</a>
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
<p><a href="{{ url('com:weekmail_preview') }}">{% trans %}Preview{% endtrans %}</a></p>
|
<p><a href="{{ url('com:weekmail_preview') }}">{% trans %}Preview{% endtrans %}</a></p>
|
||||||
<p><a href="{{ url('com:weekmail_preview') }}?send=true">{% trans %}Send{% endtrans %}</a></p>
|
<p><a href="{{ url('com:weekmail_preview') }}?send=true">{% trans %}Send{% endtrans %}</a></p>
|
||||||
<p><a href="{{ url('com:weekmail_article') }}">{% trans %}New article{% endtrans %}</a></p>
|
<p><a href="{{ url('com:weekmail_article') }}">{% trans %}New article{% endtrans %}</a></p>
|
||||||
|
<p><a href="{{ url('com:weekmail') }}" onclick="{{weekmail.switch_type()}}">{% trans %}Switch invitation/weekmail{% endtrans %}</a></p>
|
||||||
<h4>{% trans %}Articles in no weekmail yet{% endtrans %}</h4>
|
<h4>{% trans %}Articles in no weekmail yet{% endtrans %}</h4>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -7,15 +7,33 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<a href="{{ url('com:weekmail') }}">{% trans %}Back{% endtrans %}</a>
|
<a href="{{ url('com:weekmail') }}">{% trans %}Back{% endtrans %}</a>
|
||||||
{% if request.GET['send'] %}
|
{% if bad_recipients %}
|
||||||
<p>{% trans %}Are you sure you want to send this weekmail?{% endtrans %}</p>
|
<p>
|
||||||
{% if request.LANGUAGE_CODE != settings.LANGUAGE_CODE[:2] %}
|
<span class="important">
|
||||||
<p><strong>{% trans %}Warning: you are sending the weekmail in another language than the default one!{% endtrans %}</strong></p>
|
{% trans %}The following recipients were refused by the SMTP:{% endtrans %}
|
||||||
{% endif %}
|
</span>
|
||||||
<form method="post" action="">
|
<ul>
|
||||||
{% csrf_token %}
|
{% for r in bad_recipients.keys() %}
|
||||||
<button type="submit" name="send" value="validate">{% trans %}Send{% endtrans %}</button>
|
<li>{{ r }}</li>
|
||||||
</form>
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form method="post" action="">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" name="send" value="clean">{% trans %}Clean subscribers{% endtrans %}</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
{% if request.GET['send'] %}
|
||||||
|
<p>{% trans %}Are you sure you want to send this weekmail?{% endtrans %}</p>
|
||||||
|
{% if request.LANGUAGE_CODE != settings.LANGUAGE_CODE[:2] %}
|
||||||
|
<p><strong>{% trans %}Warning: you are sending the weekmail in another language than the default one!{% endtrans %}</strong></p>
|
||||||
|
{% endif %}
|
||||||
|
<form method="post" action="">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" name="send" value="validate">{% trans %}Send{% endtrans %}</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<hr>
|
<hr>
|
||||||
{{ weekmail_rendered|safe }}
|
{{ weekmail_rendered|safe }}
|
||||||
|
25
com/tests.py
@ -27,7 +27,7 @@ from django.conf import settings
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.utils import html
|
from django.utils import html
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
|
||||||
from core.models import User, RealGroup
|
from core.models import User, RealGroup
|
||||||
@ -40,8 +40,8 @@ class ComAlertTest(TestCase):
|
|||||||
def test_page_is_working(self):
|
def test_page_is_working(self):
|
||||||
self.client.login(username="comunity", password="plop")
|
self.client.login(username="comunity", password="plop")
|
||||||
response = self.client.get(reverse("com:alert_edit"))
|
response = self.client.get(reverse("com:alert_edit"))
|
||||||
self.assertNotEquals(response.status_code, 500)
|
self.assertNotEqual(response.status_code, 500)
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
class ComInfoTest(TestCase):
|
class ComInfoTest(TestCase):
|
||||||
@ -51,8 +51,8 @@ class ComInfoTest(TestCase):
|
|||||||
def test_page_is_working(self):
|
def test_page_is_working(self):
|
||||||
self.client.login(username="comunity", password="plop")
|
self.client.login(username="comunity", password="plop")
|
||||||
response = self.client.get(reverse("com:info_edit"))
|
response = self.client.get(reverse("com:info_edit"))
|
||||||
self.assertNotEquals(response.status_code, 500)
|
self.assertNotEqual(response.status_code, 500)
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
class ComTest(TestCase):
|
class ComTest(TestCase):
|
||||||
@ -79,9 +79,11 @@ class ComTest(TestCase):
|
|||||||
)
|
)
|
||||||
r = self.client.get(reverse("core:index"))
|
r = self.client.get(reverse("core:index"))
|
||||||
self.assertTrue(r.status_code == 200)
|
self.assertTrue(r.status_code == 200)
|
||||||
self.assertTrue(
|
self.assertContains(
|
||||||
"""<div id="alert_box">\\n <div class="markdown"><h3>ALERTE!</h3>\\n<p><strong>Caaaataaaapuuuulte!!!!</strong></p>"""
|
r,
|
||||||
in str(r.content)
|
"""<div id="alert_box">
|
||||||
|
<div class="markdown"><h3>ALERTE!</h3>
|
||||||
|
<p><strong>Caaaataaaapuuuulte!!!!</strong></p>""",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_info_msg(self):
|
def test_info_msg(self):
|
||||||
@ -95,9 +97,10 @@ class ComTest(TestCase):
|
|||||||
)
|
)
|
||||||
r = self.client.get(reverse("core:index"))
|
r = self.client.get(reverse("core:index"))
|
||||||
self.assertTrue(r.status_code == 200)
|
self.assertTrue(r.status_code == 200)
|
||||||
self.assertTrue(
|
self.assertContains(
|
||||||
"""<div id="info_box">\\n <div class="markdown"><h3>INFO: <strong>Caaaataaaapuuuulte!!!!</strong></h3>"""
|
r,
|
||||||
in str(r.content)
|
"""<div id="info_box">
|
||||||
|
<div class="markdown"><h3>INFO: <strong>Caaaataaaapuuuulte!!!!</strong></h3>""",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_birthday_non_subscribed_user(self):
|
def test_birthday_non_subscribed_user(self):
|
||||||
|
66
com/views.py
@ -28,7 +28,7 @@ from django.http import HttpResponseRedirect
|
|||||||
from django.views.generic import ListView, DetailView, View
|
from django.views.generic import ListView, DetailView, View
|
||||||
from django.views.generic.edit import UpdateView, CreateView, DeleteView
|
from django.views.generic.edit import UpdateView, CreateView, DeleteView
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -39,6 +39,7 @@ from django.core.exceptions import PermissionDenied
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from smtplib import SMTPRecipientsRefused
|
||||||
|
|
||||||
from com.models import Sith, News, NewsDate, Weekmail, WeekmailArticle, Screen, Poster
|
from com.models import Sith, News, NewsDate, Weekmail, WeekmailArticle, Screen, Poster
|
||||||
from core.views import (
|
from core.views import (
|
||||||
@ -52,6 +53,7 @@ from core.views import (
|
|||||||
from core.views.forms import SelectDateTime, MarkdownInput
|
from core.views.forms import SelectDateTime, MarkdownInput
|
||||||
from core.models import Notification, RealGroup, User
|
from core.models import Notification, RealGroup, User
|
||||||
from club.models import Club, Mailing
|
from club.models import Club, Mailing
|
||||||
|
from core.views.forms import TzAwareDateTimeField
|
||||||
|
|
||||||
|
|
||||||
# Sith object
|
# Sith object
|
||||||
@ -72,20 +74,14 @@ class PosterForm(forms.ModelForm):
|
|||||||
"display_time",
|
"display_time",
|
||||||
]
|
]
|
||||||
widgets = {"screens": forms.CheckboxSelectMultiple}
|
widgets = {"screens": forms.CheckboxSelectMultiple}
|
||||||
|
help_texts = {"file": _("Format: 16:9 | Resolution: 1920x1080")}
|
||||||
|
|
||||||
date_begin = forms.DateTimeField(
|
date_begin = TzAwareDateTimeField(
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
|
||||||
label=_("Start date"),
|
label=_("Start date"),
|
||||||
widget=SelectDateTime,
|
|
||||||
required=True,
|
required=True,
|
||||||
initial=timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
|
initial=timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
)
|
)
|
||||||
date_end = forms.DateTimeField(
|
date_end = TzAwareDateTimeField(label=_("End date"), required=False)
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
|
||||||
label=_("End date"),
|
|
||||||
widget=SelectDateTime,
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.user = kwargs.pop("user", None)
|
self.user = kwargs.pop("user", None)
|
||||||
@ -199,24 +195,10 @@ class NewsForm(forms.ModelForm):
|
|||||||
"content": MarkdownInput,
|
"content": MarkdownInput,
|
||||||
}
|
}
|
||||||
|
|
||||||
start_date = forms.DateTimeField(
|
start_date = TzAwareDateTimeField(label=_("Start date"), required=False)
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
end_date = TzAwareDateTimeField(label=_("End date"), required=False)
|
||||||
label=_("Start date"),
|
until = TzAwareDateTimeField(label=_("Until"), required=False)
|
||||||
widget=SelectDateTime,
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
end_date = forms.DateTimeField(
|
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
|
||||||
label=_("End date"),
|
|
||||||
widget=SelectDateTime,
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
until = forms.DateTimeField(
|
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
|
||||||
label=_("Until"),
|
|
||||||
widget=SelectDateTime,
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
automoderation = forms.BooleanField(label=_("Automoderation"), required=False)
|
automoderation = forms.BooleanField(label=_("Automoderation"), required=False)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
@ -433,23 +415,36 @@ class NewsDetailView(CanViewMixin, DetailView):
|
|||||||
# Weekmail
|
# Weekmail
|
||||||
|
|
||||||
|
|
||||||
class WeekmailPreviewView(ComTabsMixin, CanEditPropMixin, DetailView):
|
class WeekmailPreviewView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, DetailView):
|
||||||
model = Weekmail
|
model = Weekmail
|
||||||
template_name = "com/weekmail_preview.jinja"
|
template_name = "com/weekmail_preview.jinja"
|
||||||
success_url = reverse_lazy("com:weekmail")
|
success_url = reverse_lazy("com:weekmail")
|
||||||
current_tab = "weekmail"
|
current_tab = "weekmail"
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
self.bad_recipients = []
|
||||||
|
return super(WeekmailPreviewView, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
try:
|
if request.POST["send"] == "validate":
|
||||||
if request.POST["send"] == "validate":
|
try:
|
||||||
self.object.send()
|
self.object.send()
|
||||||
return HttpResponseRedirect(
|
return HttpResponseRedirect(
|
||||||
reverse("com:weekmail") + "?qn_weekmail_send_success"
|
reverse("com:weekmail") + "?qn_weekmail_send_success"
|
||||||
)
|
)
|
||||||
except:
|
except SMTPRecipientsRefused as e:
|
||||||
pass
|
self.bad_recipients = e.recipients
|
||||||
return super(WeekmailEditView, self).get(request, *args, **kwargs)
|
elif request.POST["send"] == "clean":
|
||||||
|
try:
|
||||||
|
self.object.send() # This should fail
|
||||||
|
except SMTPRecipientsRefused as e:
|
||||||
|
users = User.objects.filter(email__in=e.recipients.keys())
|
||||||
|
for u in users:
|
||||||
|
u.preferences.receive_weekmail = False
|
||||||
|
u.preferences.save()
|
||||||
|
self.quick_notif_list += ["qn_success"]
|
||||||
|
return super(WeekmailPreviewView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
def get_object(self, queryset=None):
|
||||||
return self.model.objects.filter(sent=False).order_by("-id").first()
|
return self.model.objects.filter(sent=False).order_by("-id").first()
|
||||||
@ -458,6 +453,7 @@ class WeekmailPreviewView(ComTabsMixin, CanEditPropMixin, DetailView):
|
|||||||
"""Add rendered weekmail"""
|
"""Add rendered weekmail"""
|
||||||
kwargs = super(WeekmailPreviewView, self).get_context_data(**kwargs)
|
kwargs = super(WeekmailPreviewView, self).get_context_data(**kwargs)
|
||||||
kwargs["weekmail_rendered"] = self.object.render_html()
|
kwargs["weekmail_rendered"] = self.object.render_html()
|
||||||
|
kwargs["bad_recipients"] = self.bad_recipients
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
@ -534,7 +530,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
|
|||||||
return super(WeekmailEditView, self).get(request, *args, **kwargs)
|
return super(WeekmailEditView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add orphan articles """
|
"""Add orphan articles"""
|
||||||
kwargs = super(WeekmailEditView, self).get_context_data(**kwargs)
|
kwargs = super(WeekmailEditView, self).get_context_data(**kwargs)
|
||||||
kwargs["orphans"] = WeekmailArticle.objects.filter(weekmail=None)
|
kwargs["orphans"] = WeekmailArticle.objects.filter(weekmail=None)
|
||||||
return kwargs
|
return kwargs
|
||||||
|
@ -21,5 +21,3 @@
|
|||||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
default_app_config = "core.apps.SithConfig"
|
|
||||||
|
107
core/management/commands/check_front.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import re
|
||||||
|
from subprocess import PIPE, Popen, TimeoutExpired
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
# see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
|
||||||
|
# added "v?"
|
||||||
|
semver_regex = re.compile(
|
||||||
|
"""^v?(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Checks the front dependencies are up to date."
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
deps = settings.SITH_FRONT_DEP_VERSIONS
|
||||||
|
|
||||||
|
processes = dict(
|
||||||
|
(url, create_process(url))
|
||||||
|
for url in deps.keys()
|
||||||
|
if parse_semver(deps[url]) is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
for url, process in processes.items():
|
||||||
|
try:
|
||||||
|
stdout, stderr = process.communicate(timeout=15)
|
||||||
|
except TimeoutExpired:
|
||||||
|
process.kill()
|
||||||
|
self.stderr.write(self.style.WARNING("{}: timeout".format(url)))
|
||||||
|
continue
|
||||||
|
# error, notice, warning
|
||||||
|
|
||||||
|
stdout = stdout.decode("utf-8")
|
||||||
|
stderr = stderr.decode("utf-8")
|
||||||
|
|
||||||
|
if stderr != "":
|
||||||
|
self.stderr.write(self.style.WARNING(stderr.strip()))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# get all tags, parse them as semvers and find the biggest
|
||||||
|
tags = list_tags(stdout)
|
||||||
|
tags = map(parse_semver, tags)
|
||||||
|
tags = filter(lambda tag: tag is not None, tags)
|
||||||
|
latest_version = max(tags)
|
||||||
|
|
||||||
|
# cannot fail as those which fail are filtered in the processes dict creation
|
||||||
|
current_version = parse_semver(deps[url])
|
||||||
|
assert current_version is not None
|
||||||
|
|
||||||
|
if latest_version == current_version:
|
||||||
|
msg = "{}: {}".format(url, semver_to_s(current_version))
|
||||||
|
self.stdout.write(self.style.SUCCESS(msg))
|
||||||
|
else:
|
||||||
|
msg = "{}: {} < {}".format(
|
||||||
|
url, semver_to_s(current_version), semver_to_s(latest_version)
|
||||||
|
)
|
||||||
|
self.stdout.write(self.style.ERROR(msg))
|
||||||
|
|
||||||
|
|
||||||
|
def create_process(url):
|
||||||
|
"""Spawn a "git ls-remote --tags" child process."""
|
||||||
|
return Popen(["git", "ls-remote", "--tags", url], stdout=PIPE, stderr=PIPE)
|
||||||
|
|
||||||
|
|
||||||
|
def list_tags(s):
|
||||||
|
"""Parses "git ls-remote --tags" output. Takes a string."""
|
||||||
|
tag_prefix = "refs/tags/"
|
||||||
|
|
||||||
|
for line in s.strip().split("\n"):
|
||||||
|
# an example line could be:
|
||||||
|
# "1f41e2293f9c3c1962d2d97afa666207b98a222a\trefs/tags/foo"
|
||||||
|
parts = line.split("\t")
|
||||||
|
|
||||||
|
# check we have a commit ID (SHA-1 hash) and a tag name
|
||||||
|
assert len(parts) == 2
|
||||||
|
assert len(parts[0]) == 40
|
||||||
|
assert parts[1].startswith(tag_prefix)
|
||||||
|
|
||||||
|
# avoid duplicates (a peeled tag will appear twice: as "name" and as "name^{}")
|
||||||
|
if not parts[1].endswith("^{}"):
|
||||||
|
yield parts[1][len(tag_prefix) :]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_semver(s):
|
||||||
|
"""
|
||||||
|
Turns a semver string into a 3-tuple or None if the parsing failed, it is a
|
||||||
|
prerelease or it has build metadata.
|
||||||
|
|
||||||
|
See https://semver.org
|
||||||
|
"""
|
||||||
|
m = semver_regex.match(s)
|
||||||
|
|
||||||
|
if (
|
||||||
|
m is None
|
||||||
|
or m.group("prerelease") is not None
|
||||||
|
or m.group("buildmetadata") is not None
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return (int(m.group("major")), int(m.group("minor")), int(m.group("patch")))
|
||||||
|
|
||||||
|
|
||||||
|
def semver_to_s(t):
|
||||||
|
"""Expects a 3-tuple with ints and turns it into a string of type "1.2.3"."""
|
||||||
|
return "{}.{}.{}".format(t[0], t[1], t[2])
|
@ -31,7 +31,7 @@ from django.conf import settings
|
|||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
"""
|
"""
|
||||||
Compiles scss in static folder for production
|
Compiles scss in static folder for production
|
||||||
"""
|
"""
|
||||||
|
|
||||||
help = "Compile scss files from static folder"
|
help = "Compile scss files from static folder"
|
||||||
|
@ -25,13 +25,12 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import signal
|
||||||
|
|
||||||
from http.server import test, CGIHTTPRequestHandler
|
from http.server import test, CGIHTTPRequestHandler
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.utils import autoreload
|
||||||
# TODO Django 2.2 : implement autoreload following
|
|
||||||
# https://stackoverflow.com/questions/42907285/django-autoreload-add-watched-file
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
@ -45,15 +44,15 @@ class Command(BaseCommand):
|
|||||||
"addrport", nargs="?", help="Optional port number, or ipaddr:port"
|
"addrport", nargs="?", help="Optional port number, or ipaddr:port"
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, *args, **kwargs):
|
def build_documentation(self):
|
||||||
os.chdir("doc")
|
os.chdir(os.path.join(self.project_dir, "doc"))
|
||||||
err = os.system("make html")
|
err = os.system("make html")
|
||||||
|
|
||||||
if err != 0:
|
if err != 0:
|
||||||
self.stdout.write("A build error occured, exiting")
|
self.stdout.write("A build error occured")
|
||||||
sys.exit(err)
|
|
||||||
|
|
||||||
os.chdir("_build/html")
|
def start_server(self, **kwargs):
|
||||||
|
os.chdir(os.path.join(self.project_dir, "doc", "_build/html"))
|
||||||
addr = self.default_addr
|
addr = self.default_addr
|
||||||
port = self.default_port
|
port = self.default_port
|
||||||
if kwargs["addrport"]:
|
if kwargs["addrport"]:
|
||||||
@ -69,3 +68,25 @@ class Command(BaseCommand):
|
|||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
test(HandlerClass=CGIHTTPRequestHandler, port=int(port), bind=addr)
|
test(HandlerClass=CGIHTTPRequestHandler, port=int(port), bind=addr)
|
||||||
|
|
||||||
|
def build_and_start_server(self, **kwargs):
|
||||||
|
self.build_documentation()
|
||||||
|
self.start_server(**kwargs)
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
self.project_dir = os.getcwd()
|
||||||
|
|
||||||
|
signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
|
||||||
|
try:
|
||||||
|
if os.environ.get(autoreload.DJANGO_AUTORELOAD_ENV) == "true":
|
||||||
|
reloader = autoreload.get_reloader()
|
||||||
|
reloader.watch_dir(os.path.join(self.project_dir, "doc"), "**/*.rst")
|
||||||
|
autoreload.logger.info(
|
||||||
|
"Watching for file changes with %s", reloader.__class__.__name__
|
||||||
|
)
|
||||||
|
autoreload.start_django(reloader, self.build_and_start_server, **kwargs)
|
||||||
|
else:
|
||||||
|
exit_code = autoreload.restart_with_reloader()
|
||||||
|
sys.exit(exit_code)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
@ -611,6 +611,7 @@ Welcome to the wiki page!
|
|||||||
mde.products.add(cons)
|
mde.products.add(cons)
|
||||||
mde.products.add(dcons)
|
mde.products.add(dcons)
|
||||||
mde.sellers.add(skia)
|
mde.sellers.add(skia)
|
||||||
|
|
||||||
mde.save()
|
mde.save()
|
||||||
|
|
||||||
eboutic = Counter.objects.filter(name="Eboutic").first()
|
eboutic = Counter.objects.filter(name="Eboutic").first()
|
||||||
@ -935,6 +936,7 @@ Welcome to the wiki page!
|
|||||||
# Add barman to counter
|
# Add barman to counter
|
||||||
c = Counter.objects.get(id=2)
|
c = Counter.objects.get(id=2)
|
||||||
c.sellers.add(User.objects.get(pk=krophil.pk))
|
c.sellers.add(User.objects.get(pk=krophil.pk))
|
||||||
|
mde.sellers.add(sli)
|
||||||
c.save()
|
c.save()
|
||||||
|
|
||||||
# Create an election
|
# Create an election
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
|
import threading
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.functional import SimpleLazyObject
|
from django.utils.functional import SimpleLazyObject
|
||||||
from django.contrib.auth import get_user
|
from django.contrib.auth import get_user
|
||||||
@ -49,8 +50,31 @@ class AuthenticationMiddleware(DjangoAuthenticationMiddleware):
|
|||||||
def process_request(self, request):
|
def process_request(self, request):
|
||||||
assert hasattr(request, "session"), (
|
assert hasattr(request, "session"), (
|
||||||
"The Django authentication middleware requires session middleware "
|
"The Django authentication middleware requires session middleware "
|
||||||
"to be installed. Edit your MIDDLEWARE_CLASSES setting to insert "
|
"to be installed. Edit your MIDDLEWARE setting to insert "
|
||||||
"'django.contrib.sessions.middleware.SessionMiddleware' before "
|
"'django.contrib.sessions.middleware.SessionMiddleware' before "
|
||||||
"'account.middleware.AuthenticationMiddleware'."
|
"'account.middleware.AuthenticationMiddleware'."
|
||||||
)
|
)
|
||||||
request.user = SimpleLazyObject(lambda: get_cached_user(request))
|
request.user = SimpleLazyObject(lambda: get_cached_user(request))
|
||||||
|
|
||||||
|
|
||||||
|
_threadlocal = threading.local()
|
||||||
|
|
||||||
|
|
||||||
|
def get_signal_request():
|
||||||
|
"""
|
||||||
|
!!! Do not use if your operation is asynchronus !!!
|
||||||
|
Allow to access current request in signals
|
||||||
|
This is a hack that looks into the thread
|
||||||
|
Mainly used for log purpose
|
||||||
|
"""
|
||||||
|
|
||||||
|
return getattr(_threadlocal, "request", None)
|
||||||
|
|
||||||
|
|
||||||
|
class SignalRequestMiddleware:
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
setattr(_threadlocal, "request", request)
|
||||||
|
return self.get_response(request)
|
||||||
|
51
core/migrations/0034_operationlog.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Generated by Django 2.2.6 on 2019-11-14 15:10
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("core", "0033_auto_20191006_0049"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="OperationLog",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("date", models.DateTimeField(auto_now_add=True, verbose_name="date")),
|
||||||
|
("label", models.CharField(max_length=255, verbose_name="label")),
|
||||||
|
(
|
||||||
|
"operation_type",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("SELLING_DELETION", "Selling deletion"),
|
||||||
|
("REFILLING_DELETION", "Refilling deletion"),
|
||||||
|
],
|
||||||
|
max_length=40,
|
||||||
|
verbose_name="operation type",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"operator",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="logs",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
24
core/migrations/0035_auto_20200216_1743.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 2.2.10 on 2020-02-16 16:43
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("core", "0034_operationlog"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="sex",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[("MAN", "Man"), ("WOMAN", "Woman")],
|
||||||
|
max_length=10,
|
||||||
|
null=True,
|
||||||
|
verbose_name="sex",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
28
core/migrations/0036_auto_20211001_0248.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 2.2.24 on 2021-10-01 00:48
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("core", "0035_auto_20200216_1743")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="pronouns",
|
||||||
|
field=models.CharField(default="", max_length=64, verbose_name="pronouns"),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="sex",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[("MAN", "Man"), ("WOMAN", "Woman"), ("OTHER", "Other")],
|
||||||
|
max_length=10,
|
||||||
|
null=True,
|
||||||
|
verbose_name="sex",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
18
core/migrations/0037_auto_20211105_1708.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2.24 on 2021-11-05 16:08
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("core", "0036_auto_20211001_0248")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="pronouns",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, default="", max_length=64, verbose_name="pronouns"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
@ -34,7 +34,7 @@ from django.contrib.auth.models import (
|
|||||||
GroupManager as AuthGroupManager,
|
GroupManager as AuthGroupManager,
|
||||||
AnonymousUser as AuthAnonymousUser,
|
AnonymousUser as AuthAnonymousUser,
|
||||||
)
|
)
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.core.exceptions import ValidationError, PermissionDenied
|
from django.core.exceptions import ValidationError, PermissionDenied
|
||||||
@ -65,11 +65,19 @@ class MetaGroupManager(AuthGroupManager):
|
|||||||
|
|
||||||
|
|
||||||
class Group(AuthGroup):
|
class Group(AuthGroup):
|
||||||
|
"""
|
||||||
|
Implement both RealGroups and Meta groups
|
||||||
|
|
||||||
|
Groups are sorted by their is_meta property
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: If False, this is a RealGroup
|
||||||
is_meta = models.BooleanField(
|
is_meta = models.BooleanField(
|
||||||
_("meta group status"),
|
_("meta group status"),
|
||||||
default=False,
|
default=False,
|
||||||
help_text=_("Whether a group is a meta group or not"),
|
help_text=_("Whether a group is a meta group or not"),
|
||||||
)
|
)
|
||||||
|
#: Description of the group
|
||||||
description = models.CharField(_("description"), max_length=60)
|
description = models.CharField(_("description"), max_length=60)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -83,6 +91,15 @@ class Group(AuthGroup):
|
|||||||
|
|
||||||
|
|
||||||
class MetaGroup(Group):
|
class MetaGroup(Group):
|
||||||
|
"""
|
||||||
|
MetaGroups are dynamically created groups.
|
||||||
|
Generaly used with clubs where creating a club creates two groups:
|
||||||
|
|
||||||
|
* club-SITH_BOARD_SUFFIX
|
||||||
|
* club-SITH_MEMBER_SUFFIX
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: Assign a manager in a way that MetaGroup.objects only return groups with is_meta=False
|
||||||
objects = MetaGroupManager()
|
objects = MetaGroupManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -94,6 +111,12 @@ class MetaGroup(Group):
|
|||||||
|
|
||||||
|
|
||||||
class RealGroup(Group):
|
class RealGroup(Group):
|
||||||
|
"""
|
||||||
|
RealGroups are created by the developer.
|
||||||
|
Most of the time they match a number in settings to be easily used for permissions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: Assign a manager in a way that MetaGroup.objects only return groups with is_meta=True
|
||||||
objects = RealGroupManager()
|
objects = RealGroupManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -202,9 +225,11 @@ class User(AbstractBaseUser):
|
|||||||
sex = models.CharField(
|
sex = models.CharField(
|
||||||
_("sex"),
|
_("sex"),
|
||||||
max_length=10,
|
max_length=10,
|
||||||
choices=[("MAN", _("Man")), ("WOMAN", _("Woman"))],
|
null=True,
|
||||||
default="MAN",
|
blank=True,
|
||||||
|
choices=[("MAN", _("Man")), ("WOMAN", _("Woman")), ("OTHER", _("Other"))],
|
||||||
)
|
)
|
||||||
|
pronouns = models.CharField(_("pronouns"), max_length=64, blank=True, default="")
|
||||||
tshirt_size = models.CharField(
|
tshirt_size = models.CharField(
|
||||||
_("tshirt size"),
|
_("tshirt size"),
|
||||||
max_length=5,
|
max_length=5,
|
||||||
@ -389,6 +414,20 @@ class User(AbstractBaseUser):
|
|||||||
.has_rights_in_club(self)
|
.has_rights_in_club(self)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def can_read_subscription_history(self):
|
||||||
|
if self.is_root or self.is_board_member:
|
||||||
|
return True
|
||||||
|
|
||||||
|
from club.models import Club
|
||||||
|
|
||||||
|
for club in Club.objects.filter(
|
||||||
|
id__in=settings.SITH_CAN_READ_SUBSCRIPTION_HISTORY
|
||||||
|
).all():
|
||||||
|
if club.has_rights_in_club(self):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def can_create_subscription(self):
|
def can_create_subscription(self):
|
||||||
from club.models import Club
|
from club.models import Club
|
||||||
@ -666,6 +705,10 @@ class AnonymousUser(AuthAnonymousUser):
|
|||||||
def can_create_subscription(self):
|
def can_create_subscription(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_read_subscription_history(self):
|
||||||
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def was_subscribed(self):
|
def was_subscribed(self):
|
||||||
return False
|
return False
|
||||||
@ -1454,3 +1497,24 @@ class Gift(models.Model):
|
|||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
return user.is_board_member or user.is_root
|
return user.is_board_member or user.is_root
|
||||||
|
|
||||||
|
|
||||||
|
class OperationLog(models.Model):
|
||||||
|
"""
|
||||||
|
General purpose log object to register operations
|
||||||
|
"""
|
||||||
|
|
||||||
|
date = models.DateTimeField(_("date"), auto_now_add=True)
|
||||||
|
label = models.CharField(_("label"), max_length=255)
|
||||||
|
operator = models.ForeignKey(
|
||||||
|
User, related_name="logs", on_delete=models.SET_NULL, null=True
|
||||||
|
)
|
||||||
|
operation_type = models.CharField(
|
||||||
|
_("operation type"), max_length=40, choices=settings.SITH_LOG_OPERATION_TYPE
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_owned_by(self, user):
|
||||||
|
return user.is_root
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s - %s - %s" % (self.operation_type, self.label, self.operator)
|
||||||
|
@ -33,16 +33,16 @@ from django.db import connection, migrations
|
|||||||
|
|
||||||
class PsqlRunOnly(migrations.RunSQL):
|
class PsqlRunOnly(migrations.RunSQL):
|
||||||
"""
|
"""
|
||||||
This is an SQL runner that will launch the given command only if
|
This is an SQL runner that will launch the given command only if
|
||||||
the used DBMS is PostgreSQL.
|
the used DBMS is PostgreSQL.
|
||||||
It may be useful to run Postgres' specific SQL, or to take actions
|
It may be useful to run Postgres' specific SQL, or to take actions
|
||||||
that would be non-senses with backends other than Postgre, such
|
that would be non-senses with backends other than Postgre, such
|
||||||
as disabling particular constraints that would prevent the migration
|
as disabling particular constraints that would prevent the migration
|
||||||
to run successfully.
|
to run successfully.
|
||||||
|
|
||||||
See `club/migrations/0010_auto_20170912_2028.py` as an example.
|
See `club/migrations/0010_auto_20170912_2028.py` as an example.
|
||||||
Some explanations can be found here too:
|
Some explanations can be found here too:
|
||||||
https://stackoverflow.com/questions/28429933/django-migrations-using-runpython-to-commit-changes
|
https://stackoverflow.com/questions/28429933/django-migrations-using-runpython-to-commit-changes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _run_sql(self, schema_editor, sqls):
|
def _run_sql(self, schema_editor, sqls):
|
||||||
|
@ -25,9 +25,9 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sass
|
import sass
|
||||||
|
from urllib.parse import urljoin
|
||||||
from django.utils.encoding import force_bytes, iri_to_uri
|
from django.utils.encoding import force_bytes, iri_to_uri
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.utils.six.moves.urllib.parse import urljoin
|
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from core.scss.storage import ScssFileStorage, find_file
|
from core.scss.storage import ScssFileStorage, find_file
|
||||||
@ -35,9 +35,9 @@ from core.scss.storage import ScssFileStorage, find_file
|
|||||||
|
|
||||||
class ScssProcessor(object):
|
class ScssProcessor(object):
|
||||||
"""
|
"""
|
||||||
If DEBUG mode enabled : compile the scss file
|
If DEBUG mode enabled : compile the scss file
|
||||||
Else : give the path of the corresponding css supposed to already be compiled
|
Else : give the path of the corresponding css supposed to already be compiled
|
||||||
Don't forget to use compilestatics to compile scss for production
|
Don't forget to use compilestatics to compile scss for production
|
||||||
"""
|
"""
|
||||||
|
|
||||||
prefix = iri_to_uri(getattr(settings, "STATIC_URL", "/static/"))
|
prefix = iri_to_uri(getattr(settings, "STATIC_URL", "/static/"))
|
||||||
|
@ -34,6 +34,7 @@ from forum.models import ForumMessage, ForumMessageMeta
|
|||||||
class UserIndex(indexes.SearchIndex, indexes.Indexable):
|
class UserIndex(indexes.SearchIndex, indexes.Indexable):
|
||||||
text = indexes.CharField(document=True, use_template=True)
|
text = indexes.CharField(document=True, use_template=True)
|
||||||
auto = indexes.EdgeNgramField(use_template=True)
|
auto = indexes.EdgeNgramField(use_template=True)
|
||||||
|
last_update = indexes.DateTimeField(model_attr="last_update")
|
||||||
|
|
||||||
def get_model(self):
|
def get_model(self):
|
||||||
return User
|
return User
|
||||||
@ -45,6 +46,9 @@ class UserIndex(indexes.SearchIndex, indexes.Indexable):
|
|||||||
def get_updated_field(self):
|
def get_updated_field(self):
|
||||||
return "last_update"
|
return "last_update"
|
||||||
|
|
||||||
|
def prepare_auto(self, obj):
|
||||||
|
return self.prepared_data["auto"].strip()[:245]
|
||||||
|
|
||||||
|
|
||||||
class IndexSignalProcessor(signals.BaseSignalProcessor):
|
class IndexSignalProcessor(signals.BaseSignalProcessor):
|
||||||
def setup(self):
|
def setup(self):
|
||||||
|
BIN
core/static/com/img/invitation_bannerP22.png
Normal file
After Width: | Height: | Size: 273 KiB |
BIN
core/static/com/img/weekmail_bannerV1P22.png
Normal file
After Width: | Height: | Size: 275 KiB |
BIN
core/static/com/img/weekmail_bannerV2P22.png
Normal file
After Width: | Height: | Size: 275 KiB |
BIN
core/static/com/img/weekmail_footerP22.png
Normal file
After Width: | Height: | Size: 242 KiB |
BIN
core/static/core/img/promo_20.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
core/static/core/img/promo_21.png
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
core/static/core/img/promo_22.png
Normal file
After Width: | Height: | Size: 55 KiB |
1
core/static/core/js/vue.global.prod.js
Normal file
@ -28,7 +28,7 @@ $twitblue: hsl(206, 82%, 63%);
|
|||||||
|
|
||||||
$shadow-color: rgb(223, 223, 223);
|
$shadow-color: rgb(223, 223, 223);
|
||||||
|
|
||||||
$background-bouton-color: hsl(0, 0%, 90%);
|
$background-button-color: hsl(0, 0%, 95%);
|
||||||
|
|
||||||
/*--------------------------MEDIA QUERY HELPERS------------------------*/
|
/*--------------------------MEDIA QUERY HELPERS------------------------*/
|
||||||
$small-devices: 576px;
|
$small-devices: 576px;
|
||||||
@ -47,10 +47,11 @@ body {
|
|||||||
input[type=button], input[type=submit], input[type=reset],input[type=file] {
|
input[type=button], input[type=submit], input[type=reset],input[type=file] {
|
||||||
border: none;
|
border: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: $background-bouton-color;
|
background-color: $background-button-color;
|
||||||
padding: 10px;
|
padding: 0.4em;
|
||||||
|
margin: 0.1em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 16px;
|
font-size: 1.2em;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: $shadow-color 0px 0px 1px;
|
box-shadow: $shadow-color 0px 0px 1px;
|
||||||
@ -62,9 +63,10 @@ input[type=button], input[type=submit], input[type=reset],input[type=file] {
|
|||||||
button{
|
button{
|
||||||
border: none;
|
border: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: $background-bouton-color;
|
background-color: $background-button-color;
|
||||||
padding: 10px;
|
padding: 0.4em;
|
||||||
font-size: 14px;
|
margin: 0.1em;
|
||||||
|
font-size: 1.18em;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
box-shadow: $shadow-color 0px 0px 1px;
|
box-shadow: $shadow-color 0px 0px 1px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -75,24 +77,26 @@ button{
|
|||||||
input,textarea[type=text],[type=number]{
|
input,textarea[type=text],[type=number]{
|
||||||
border: none;
|
border: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: $background-bouton-color;
|
background-color: $background-button-color;
|
||||||
padding: 7px;
|
padding: 0.4em;
|
||||||
font-size: 16px;
|
margin: 0.1em;
|
||||||
|
font-size: 1.2em;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
max-width: 95%;
|
||||||
}
|
}
|
||||||
textarea{
|
textarea{
|
||||||
border: none;
|
border: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: $background-bouton-color;
|
background-color: $background-button-color;
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
font-size: 16px;
|
font-size: 1.2em;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
select{
|
select{
|
||||||
border: none;
|
border: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 15px;
|
font-size: 1.2em;
|
||||||
background-color: $background-bouton-color;
|
background-color: $background-button-color;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -130,9 +134,10 @@ a {
|
|||||||
|
|
||||||
#header_language_chooser {
|
#header_language_chooser {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.2em;
|
top: 2em;
|
||||||
right: 0.5em;
|
left: 0.5em;
|
||||||
width: 3%;
|
width: 3%;
|
||||||
|
min-width: 2.2em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
input {
|
input {
|
||||||
display: block;
|
display: block;
|
||||||
@ -157,9 +162,6 @@ header {
|
|||||||
border-radius: 0px 0px 10px 10px;
|
border-radius: 0px 0px 10px 10px;
|
||||||
|
|
||||||
#header_logo {
|
#header_logo {
|
||||||
display: inline-block;
|
|
||||||
flex: none;
|
|
||||||
background-size: 100% 100%;
|
|
||||||
background-color: $white-color;
|
background-color: $white-color;
|
||||||
padding: 0.2em;
|
padding: 0.2em;
|
||||||
border-radius: 0px 0px 0px 9px;
|
border-radius: 0px 0px 0px 9px;
|
||||||
@ -169,11 +171,19 @@ header {
|
|||||||
margin: 0px;
|
margin: 0px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 70%;
|
||||||
|
max-height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#header_connect_links {
|
#header_connect_links {
|
||||||
margin: 0.6em 0.6em 0em auto;
|
margin: 0.6em 0.6em 0em auto;
|
||||||
|
padding: 0.2em;
|
||||||
color: $white-color;
|
color: $white-color;
|
||||||
form {
|
form {
|
||||||
display: inline;
|
display: inline;
|
||||||
@ -190,8 +200,9 @@ header {
|
|||||||
#header_bar {
|
#header_bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: auto;
|
flex: auto;
|
||||||
|
flex-wrap: wrap;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
margin: 0em 1em;
|
margin: 0em 1em;
|
||||||
@ -203,7 +214,6 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#header_bars_infos {
|
#header_bars_infos {
|
||||||
width: 35ch;
|
|
||||||
flex: initial;
|
flex: initial;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
margin: 0.2em 0.2em;
|
margin: 0.2em 0.2em;
|
||||||
@ -213,12 +223,15 @@ header {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
flex: auto;
|
flex: auto;
|
||||||
margin: 0.8em 0em;
|
margin: 0.8em 0em;
|
||||||
|
input {
|
||||||
|
width: 14ch;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#header_user_links {
|
#header_user_links {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 120ch;
|
|
||||||
flex: initial;
|
flex: initial;
|
||||||
|
flex-wrap: wrap;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
margin: 0em;
|
margin: 0em;
|
||||||
div {
|
div {
|
||||||
@ -287,42 +300,34 @@ header {
|
|||||||
|
|
||||||
#info_boxes {
|
#info_boxes {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
margin: 1em auto;
|
margin: 1em auto;
|
||||||
p {
|
|
||||||
margin: 0px;
|
|
||||||
padding: 7px;
|
|
||||||
}
|
|
||||||
#alert_box, #info_box {
|
#alert_box, #info_box {
|
||||||
font-size: 14px;
|
flex: 49%;
|
||||||
display: inline-block;
|
font-size: 0.9em;
|
||||||
flex: auto;
|
margin: 0.2em;
|
||||||
padding: 2px;
|
border-radius: 0.6em;
|
||||||
margin: 0.2em 1.5%;
|
.markdown {
|
||||||
min-width: 10%;
|
margin: 0.5em;
|
||||||
max-width: 46%;
|
}
|
||||||
min-height: 20px;
|
|
||||||
&:before {
|
&:before {
|
||||||
float: left;
|
font-family: FontAwesome;
|
||||||
|
font-size: 4em;
|
||||||
|
float: right;
|
||||||
margin: 0.2em;
|
margin: 0.2em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#info_box {
|
#info_box {
|
||||||
border-radius: 10px;
|
|
||||||
background: $primary-neutral-light-color;
|
background: $primary-neutral-light-color;
|
||||||
&:before {
|
&:before {
|
||||||
font-family: FontAwesome;
|
|
||||||
font-size: 4em;
|
|
||||||
content: "\f05a";
|
content: "\f05a";
|
||||||
color: hsl(210, 100%, 56%);
|
color: hsl(210, 100%, 56%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#alert_box {
|
#alert_box {
|
||||||
border-radius: 10px;
|
|
||||||
background: $second-color;
|
background: $second-color;
|
||||||
&:before {
|
&:before {
|
||||||
font-family: FontAwesome;
|
|
||||||
font-size: 4em;
|
|
||||||
content: "\f06a";
|
content: "\f06a";
|
||||||
color: $white-color;
|
color: $white-color;
|
||||||
}
|
}
|
||||||
@ -345,12 +350,12 @@ header {
|
|||||||
a {
|
a {
|
||||||
flex: auto;
|
flex: auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px;
|
padding: 1.5em;
|
||||||
color: $white-color;
|
color: $white-color;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: $secondary-neutral-color;
|
background: $secondary-neutral-color;
|
||||||
color: $white-color;
|
color: $white-color;
|
||||||
@ -458,6 +463,8 @@ header {
|
|||||||
|
|
||||||
/*---------------------------------NEWS--------------------------------*/
|
/*---------------------------------NEWS--------------------------------*/
|
||||||
#news {
|
#news {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
.news_column {
|
.news_column {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
@ -467,11 +474,13 @@ header {
|
|||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
#right_column {
|
#right_column {
|
||||||
width: 20%;
|
flex: 20%;
|
||||||
float: right;
|
float: right;
|
||||||
|
margin: 0.2em;
|
||||||
}
|
}
|
||||||
#left_column {
|
#left_column {
|
||||||
width: 79%;
|
flex: 79%;
|
||||||
|
margin: 0.2em;
|
||||||
h3 {
|
h3 {
|
||||||
background: $second-color;
|
background: $second-color;
|
||||||
box-shadow: $shadow-color 1px 1px 1px;
|
box-shadow: $shadow-color 1px 1px 1px;
|
||||||
@ -484,6 +493,11 @@ header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@media screen and (max-width: $small-devices){
|
||||||
|
#left_column, #right_column {
|
||||||
|
flex: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* AGENDA/BIRTHDAYS */
|
/* AGENDA/BIRTHDAYS */
|
||||||
#agenda,#birthdays {
|
#agenda,#birthdays {
|
||||||
@ -691,6 +705,12 @@ header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $small-devices){
|
||||||
|
#page {
|
||||||
|
width: 98%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#news_details {
|
#news_details {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
@ -723,7 +743,7 @@ header {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 16px;
|
font-size: 1.2em;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
float: right;
|
float: right;
|
||||||
display: block;
|
display: block;
|
||||||
@ -1111,33 +1131,36 @@ u, .underline {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
#basket {
|
|
||||||
width: 40%;
|
|
||||||
background: $primary-neutral-light-color;
|
|
||||||
float: right;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#products {
|
|
||||||
width: 90%;
|
|
||||||
margin: 0px auto;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bar_ui {
|
#bar_ui {
|
||||||
float: left;
|
padding: 0.4em;
|
||||||
min-width: 57%;
|
display: flex;
|
||||||
}
|
flex-wrap: wrap;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
|
||||||
#user_info_container {}
|
#products {
|
||||||
|
flex-basis: 100%;
|
||||||
|
margin: 0.2em;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
#user_info {
|
#click_form {
|
||||||
float: right;
|
flex: auto;
|
||||||
padding: 5px;
|
margin: 0.2em;
|
||||||
width: 40%;
|
}
|
||||||
margin: 0px auto;
|
|
||||||
background: $secondary-neutral-light-color;
|
#user_info {
|
||||||
|
flex: auto;
|
||||||
|
padding: 0.5em;
|
||||||
|
margin: 0.2em;
|
||||||
|
height: 100%;
|
||||||
|
background: $secondary-neutral-light-color;
|
||||||
|
img {
|
||||||
|
max-width: 70%;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*-----------------------------USER PROFILE----------------------------*/
|
/*-----------------------------USER PROFILE----------------------------*/
|
||||||
@ -1169,6 +1192,7 @@ u, .underline {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
img {
|
img {
|
||||||
|
width: 5em;
|
||||||
margin: 0.5em;
|
margin: 0.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1212,6 +1236,11 @@ u, .underline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@media screen and (max-width: $small-devices){
|
||||||
|
#user_profile_infos, #user_profile_pictures {
|
||||||
|
flex-basis: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1412,6 +1441,7 @@ textarea {
|
|||||||
.search_bar {
|
.search_bar {
|
||||||
margin: 10px 0px;
|
margin: 10px 0px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
height: 20p;
|
height: 20p;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@ -1551,6 +1581,7 @@ footer {
|
|||||||
color: $white-color;
|
color: $white-color;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
background-color: $primary-neutral-dark-color;
|
background-color: $primary-neutral-dark-color;
|
||||||
box-shadow: $shadow-color 0px 0px 15px;
|
box-shadow: $shadow-color 0px 0px 15px;
|
||||||
a {
|
a {
|
||||||
@ -2181,4 +2212,4 @@ $pedagogy-white-text: #f0f0f0;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
281
core/static/election/election.scss
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
$padding: 1.5rem;
|
||||||
|
$padding_smaller: .5rem;
|
||||||
|
$gap: .25rem;
|
||||||
|
$border: .01rem solid black;
|
||||||
|
$min_col_width: 100px;
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: red !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: $gap;
|
||||||
|
|
||||||
|
> input,
|
||||||
|
> label {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.election_vote {
|
||||||
|
overflow-x: scroll !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.election_table {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
>.lists {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
>tr {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
>.column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: $padding;
|
||||||
|
border: $border;
|
||||||
|
border-collapse: collapse;
|
||||||
|
position: relative;
|
||||||
|
min-width: $min_col_width;
|
||||||
|
|
||||||
|
>a{
|
||||||
|
margin-left: $padding;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 25%;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: absolute;
|
||||||
|
right: $gap;
|
||||||
|
top: $gap;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
>.role {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
>tr {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
background-color: lightgrey;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: lightgrey;
|
||||||
|
}
|
||||||
|
|
||||||
|
>.role_title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 0;
|
||||||
|
padding: $padding;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
|
||||||
|
>.role_text {
|
||||||
|
>h4 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
>p {
|
||||||
|
margin-top: .5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
>.role_buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: $gap;
|
||||||
|
|
||||||
|
> button,
|
||||||
|
> button > i,
|
||||||
|
> a {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background-color: #e9e9e9;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 25%;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:hover > i {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> button {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> button[disabled] {
|
||||||
|
background-color: #eee;
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
>i,
|
||||||
|
&:hover,
|
||||||
|
&:hover > i {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
>.list_per_role {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
border: $border;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: $padding_smaller;
|
||||||
|
margin: 0;
|
||||||
|
min-width: $min_col_width;
|
||||||
|
|
||||||
|
>.candidates {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: $gap;
|
||||||
|
|
||||||
|
>.candidate {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
list-style-type: none;
|
||||||
|
gap: $gap;
|
||||||
|
|
||||||
|
>input[type="radio"]:checked + label,
|
||||||
|
>input[type="checkbox"]:checked + label {
|
||||||
|
background-color: lightgray;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
>figure>.edit_btns>a:hover{
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
>label>figure,
|
||||||
|
>figure {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: $gap;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
>img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
>figcaption {
|
||||||
|
h5 {
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
q {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
>.edit_btns {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
top: $gap;
|
||||||
|
right: $gap;
|
||||||
|
gap: $gap;
|
||||||
|
|
||||||
|
> a {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background-color: #e9e9e9;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 25%;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #d8d8d8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.election_details {
|
||||||
|
margin: .5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
border: none;
|
||||||
|
color: black;
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
padding: 0.4em;
|
||||||
|
margin: 0.1em;
|
||||||
|
font-size: 1.18em;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: #dfdfdf 0px 0px 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: black;
|
||||||
|
background: #d4d4d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_send {
|
||||||
|
background-color: #59aee2;
|
||||||
|
&:hover {
|
||||||
|
background-color: rgb(130, 186, 235);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,11 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script src="{{ static('core/js/sentry/bundle.min.js') }}" crossorigin="anonymous"></script>
|
<script
|
||||||
|
src="https://browser.sentry-cdn.com/7.11.1/bundle.min.js"
|
||||||
|
integrity="sha384-qcYSo5+/E8hEkPmHFa79GRDsGT84SRhBJHRw3+dbQyh0UwueiFP1jCsRBClEREcs"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
></script>
|
||||||
{% endblock head %}
|
{% endblock head %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@ -9,7 +13,15 @@
|
|||||||
{% if settings.SENTRY_DSN %}
|
{% if settings.SENTRY_DSN %}
|
||||||
<script>
|
<script>
|
||||||
Sentry.init({ dsn: '{{ settings.SENTRY_DSN }}' });
|
Sentry.init({ dsn: '{{ settings.SENTRY_DSN }}' });
|
||||||
Sentry.showReportDialog({ eventId: '{{ request.sentry_last_event_id() }}' })
|
Sentry.showReportDialog({
|
||||||
|
eventId: '{{ request.sentry_last_event_id() }}',
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
user: {
|
||||||
|
'name': '{{user.first_name}} {{user.last_name}}',
|
||||||
|
'email': '{{user.email}}'
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
@ -3,14 +3,16 @@
|
|||||||
<head>
|
<head>
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>{% block title %}{% trans %}Welcome!{% endtrans %}{% endblock %} - Association des Étudiants UTBM</title>
|
<title>{% block title %}{% trans %}Welcome!{% endtrans %}{% endblock %} - Association des Étudiants UTBM</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
|
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
|
||||||
<link rel="stylesheet" href="{{ static('core/base.css') }}">
|
<link rel="stylesheet" href="{{ static('core/base.css') }}">
|
||||||
<link rel="stylesheet" href="{{ static('core/jquery.datetimepicker.min.css') }}">
|
<link rel="stylesheet" href="{{ static('core/jquery.datetimepicker.min.css') }}">
|
||||||
<link rel="stylesheet" href="{{ static('ajax_select/css/ajax_select.css') }}">
|
<link rel="stylesheet" href="{{ static('ajax_select/css/ajax_select.css') }}">
|
||||||
<link rel="stylesheet" href="{{ scss('core/style.scss') }}">
|
<link rel="stylesheet" href="{{ scss('core/style.scss') }}">
|
||||||
<link rel="stylesheet" href="{{ static('core/js/ui/jquery-ui.min.css') }}">
|
<link rel="stylesheet" href="{{ static('core/js/ui/jquery-ui.min.css') }}">
|
||||||
<link rel="stylesheet" href="{{ static('core/font-awesome/css/font-awesome.min.css') }}">
|
<link rel="preload" as="style" href="{{ static('core/font-awesome/css/font-awesome.min.css') }}" onload="this.onload=null;this.rel='stylesheet'">
|
||||||
<script href="{{ static('core/font-awesome/js/fontawesone.min.js') }}"></script>
|
<noscript><link rel="stylesheet" href="{{ static('core/font-awesome/css/font-awesome.min.css') }}"></noscript>
|
||||||
|
<script defer href="{{ static('core/font-awesome/js/fontawesone.min.js') }}"></script>
|
||||||
|
|
||||||
<!-- Jquery declared here to be accessible in every django widgets -->
|
<!-- Jquery declared here to be accessible in every django widgets -->
|
||||||
<script src="{{ static('core/js/jquery-3.1.0.min.js') }}"></script>
|
<script src="{{ static('core/js/jquery-3.1.0.min.js') }}"></script>
|
||||||
@ -27,6 +29,7 @@
|
|||||||
<!-- BEGIN HEADER -->
|
<!-- BEGIN HEADER -->
|
||||||
{% block header %}
|
{% block header %}
|
||||||
{% if not popup %}
|
{% if not popup %}
|
||||||
|
<header>
|
||||||
<div id="header_language_chooser">
|
<div id="header_language_chooser">
|
||||||
{% for language in LANGUAGES %}
|
{% for language in LANGUAGES %}
|
||||||
<form action="{{ url('set_language') }}" method="post">{% csrf_token %}
|
<form action="{{ url('set_language') }}" method="post">{% csrf_token %}
|
||||||
@ -37,10 +40,11 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<header>
|
|
||||||
{% if not user.is_authenticated %}
|
{% if not user.is_authenticated %}
|
||||||
<div id="header_logo" style="background-image: url('{{ static('core/img/logo.png') }}'); width: 185px; height: 100px;">
|
<div id="header_logo">
|
||||||
<a href="{{ url('core:index') }}"></a>
|
<a href="{{ url('core:index') }}">
|
||||||
|
<img src="{{ static('core/img/logo.png') }}" alt="AE logo">
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="header_connect_links">
|
<div id="header_connect_links">
|
||||||
<form method="post" action="{{ url('core:login') }}">
|
<form method="post" action="{{ url('core:login') }}">
|
||||||
@ -54,12 +58,14 @@
|
|||||||
<a href="{{ url('core:register') }}"><button type="button">{% trans %}Register{% endtrans %}</button></a>
|
<a href="{{ url('core:register') }}"><button type="button">{% trans %}Register{% endtrans %}</button></a>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div id="header_logo" style="background-image: url('{{ static('core/img/logo.png') }}'); width: 92px; height: 52px;">
|
<div id="header_logo">
|
||||||
<a href="{{ url('core:index') }}"></a>
|
<a href="{{ url('core:index') }}">
|
||||||
|
<img src="{{ static('core/img/logo.png') }}" alt="AE logo">
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="header_bar">
|
<div id="header_bar">
|
||||||
<ul id="header_bars_infos">
|
<ul id="header_bars_infos">
|
||||||
{% cache 100 counters_activity %}
|
{% cache 100 "counters_activity" %}
|
||||||
{% for bar in Counter.objects.filter(type="BAR").all() %}
|
{% for bar in Counter.objects.filter(type="BAR").all() %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ url('counter:activity', counter_id=bar.id) }}" style="padding: 0px">
|
<a href="{{ url('counter:activity', counter_id=bar.id) }}" style="padding: 0px">
|
||||||
@ -85,7 +91,7 @@
|
|||||||
<a href="{{ url('core:user_profile', user_id=user.id) }}">{{ user.get_display_name() }}</a>
|
<a href="{{ url('core:user_profile', user_id=user.id) }}">{{ user.get_display_name() }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a href="#" onclick="display_notif()"><i class="fa fa-bell-o"></i> ({{ user.notifications.filter(viewed=False).count() }})</a>
|
<a href="#" onclick="display_notif()" style="white-space: nowrap;"><i class="fa fa-bell-o"></i> ({{ user.notifications.filter(viewed=False).count() }})</a>
|
||||||
<ul id="header_notif">
|
<ul id="header_notif">
|
||||||
{% for n in user.notifications.filter(viewed=False).order_by('-date') %}
|
{% for n in user.notifications.filter(viewed=False).order_by('-date') %}
|
||||||
<li>
|
<li>
|
||||||
@ -126,17 +132,19 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div id="info_boxes">
|
<div id="info_boxes">
|
||||||
{% set sith = get_sith() %}
|
{% block info_boxes %}
|
||||||
{% if sith.alert_msg %}
|
{% set sith = get_sith() %}
|
||||||
<div id="alert_box">
|
{% if sith.alert_msg %}
|
||||||
{{ sith.alert_msg|markdown }}
|
<div id="alert_box">
|
||||||
</div>
|
{{ sith.alert_msg|markdown }}
|
||||||
{% endif %}
|
</div>
|
||||||
{% if sith.info_msg %}
|
{% endif %}
|
||||||
<div id="info_box">
|
{% if sith.info_msg %}
|
||||||
{{ sith.info_msg|markdown }}
|
<div id="info_box">
|
||||||
</div>
|
{{ sith.info_msg|markdown }}
|
||||||
{% endif %}
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% else %}{# if not popup #}
|
{% else %}{# if not popup #}
|
||||||
@ -171,7 +179,7 @@
|
|||||||
<i class="fa fa-caret-down"></i>
|
<i class="fa fa-caret-down"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-content">
|
<div class="dropdown-content">
|
||||||
<a href="{{ url('core:page', page_name='Index/calendrier_evenements') }}">{% trans %}Calendar{% endtrans %}</a>
|
<a href="{{ url('election:list') }}">{% trans %}Elections{% endtrans %}</a>
|
||||||
<a href="{{ url('core:page', page_name='ga') }}">{% trans %}Big event{% endtrans %}</a>
|
<a href="{{ url('core:page', page_name='ga') }}">{% trans %}Big event{% endtrans %}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,6 +4,12 @@
|
|||||||
{% trans %}Delete confirmation{% endtrans %}
|
{% trans %}Delete confirmation{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block info_boxes %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block nav %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>{% trans %}Delete confirmation{% endtrans %}</h2>
|
<h2>{% trans %}Delete confirmation{% endtrans %}</h2>
|
||||||
<form action="" method="post">{% csrf_token %}
|
<form action="" method="post">{% csrf_token %}
|
||||||
|
@ -55,6 +55,9 @@
|
|||||||
{% if user.nick_name %}
|
{% if user.nick_name %}
|
||||||
<div class="user_mini_profile_nick">« {{ user.nick_name }} »</div>
|
<div class="user_mini_profile_nick">« {{ user.nick_name }} »</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if user.pronouns %}
|
||||||
|
<div class="user_mini_profile_pronouns">{{ user.pronouns }}</div>
|
||||||
|
{% endif %}
|
||||||
{% if user.date_of_birth %}
|
{% if user.date_of_birth %}
|
||||||
<div class="user_mini_profile_dob">
|
<div class="user_mini_profile_dob">
|
||||||
{{ user.date_of_birth|date("d/m/Y") }} ({{ user.get_age() }})
|
{{ user.date_of_birth|date("d/m/Y") }} ({{ user.get_age() }})
|
||||||
@ -113,10 +116,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro paginate(page_obj, paginator) %}
|
{% 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() or page_obj.has_next() %}
|
||||||
{% if page_obj.has_previous() %}
|
{% if page_obj.has_previous() %}
|
||||||
<a href="?page={{ page_obj.previous_page_number() }}">{% trans %}Previous{% endtrans %}</a>
|
<a {% if js %} type="submit" onclick="{{ js }}" {% endif %} href="?page={{ page_obj.previous_page_number() }}">{% trans %}Previous{% endtrans %}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="disabled">{% trans %}Previous{% endtrans %}</span>
|
<span class="disabled">{% trans %}Previous{% endtrans %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -124,11 +128,11 @@
|
|||||||
{% if page_obj.number == i %}
|
{% if page_obj.number == i %}
|
||||||
<span class="active">{{ i }} <span class="sr-only">({% trans %}current{% endtrans %})</span></span>
|
<span class="active">{{ i }} <span class="sr-only">({% trans %}current{% endtrans %})</span></span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="?page={{ i }}">{{ i }}</a>
|
<a {% if js %} type="submit" onclick="{{ js }}" {% endif %} href="?page={{ i }}">{{ i }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if page_obj.has_next() %}
|
{% if page_obj.has_next() %}
|
||||||
<a href="?page={{ page_obj.next_page_number() }}">{% trans %}Next{% endtrans %}</a>
|
<a {% if js %} type="submit" onclick="{{ js }}" {% endif %} href="?page={{ page_obj.next_page_number() }}">{% trans %}Next{% endtrans %}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="disabled">{% trans %}Next{% endtrans %}</span>
|
<span class="disabled">{% trans %}Next{% endtrans %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -148,4 +152,4 @@
|
|||||||
</script>
|
</script>
|
||||||
<button type="button" onclick="checkbox_{{form_id}}(true);">{% trans %}Select All{% endtrans %}</button>
|
<button type="button" onclick="checkbox_{{form_id}}(true);">{% trans %}Select All{% endtrans %}</button>
|
||||||
<button type="button" onclick="checkbox_{{form_id}}(false);">{% trans %}Unselect All{% endtrans %}</button>
|
<button type="button" onclick="checkbox_{{form_id}}(false);">{% trans %}Unselect All{% endtrans %}</button>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
@ -63,7 +63,7 @@
|
|||||||
<td>{{ i.amount }} €</td>
|
<td>{{ i.amount }} €</td>
|
||||||
<td>{{ i.get_payment_method_display() }}</td>
|
<td>{{ i.get_payment_method_display() }}</td>
|
||||||
{% if i.is_owned_by(user) %}
|
{% if i.is_owned_by(user) %}
|
||||||
<td><a href="{{ url('counter:refilling_delete', refilling_id=i.id) }}">Delete</a></td>
|
<td><a href="{{ url('counter:refilling_delete', refilling_id=i.id) }}">{% trans %}Delete{% endtrans %}</a></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -28,6 +28,9 @@
|
|||||||
{% if m.can_be_edited_by(user) %}
|
{% if m.can_be_edited_by(user) %}
|
||||||
<td><a href="{{ url('club:membership_set_old', membership_id=m.id) }}">{% trans %}Mark as old{% endtrans %}</a></td>
|
<td><a href="{{ url('club:membership_set_old', membership_id=m.id) }}">{% trans %}Mark as old{% endtrans %}</a></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if user.is_root %}
|
||||||
|
<td><a href="{{ url('club:membership_delete', membership_id=m.id) }}">{% trans %}Delete{% endtrans %}</a></td>
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -54,6 +57,9 @@
|
|||||||
<td>{{ m.description }}</td>
|
<td>{{ m.description }}</td>
|
||||||
<td>{{ m.start_date }}</td>
|
<td>{{ m.start_date }}</td>
|
||||||
<td>{{ m.end_date }}</td>
|
<td>{{ m.end_date }}</td>
|
||||||
|
{% if user.is_root %}
|
||||||
|
<td><a href="{{ url('club:membership_delete', membership_id=m.id) }}">{% trans %}Delete{% endtrans %}</a></td>
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
<div id="user_profile_infos_nick">« {{ profile.nick_name }} »</div>
|
<div id="user_profile_infos_nick">« {{ profile.nick_name }} »</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% if profile.quote %}
|
{% if profile.quote %}
|
||||||
<div id="user_profile_infos_quote">
|
<div id="user_profile_infos_quote">
|
||||||
{{ profile.quote }}
|
{{ profile.quote }}
|
||||||
@ -22,6 +24,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div id="user_profile_infos_items">
|
<div id="user_profile_infos_items">
|
||||||
|
{% if profile.pronouns %}
|
||||||
|
<div>
|
||||||
|
<span class="user_profile_infos_item">{% trans %}Pronouns: {% endtrans %}</span>
|
||||||
|
<span class="user_profile_infos_item_value">{{ profile.pronouns }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if profile.date_of_birth %}
|
{% if profile.date_of_birth %}
|
||||||
<div>
|
<div>
|
||||||
<span class="user_profile_infos_item">{% trans %}Born: {% endtrans %}</span>
|
<span class="user_profile_infos_item">{% trans %}Born: {% endtrans %}</span>
|
||||||
@ -137,7 +145,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if profile.was_subscribed and (user == profile or user.is_root or user.is_board_member)%}
|
{% if profile.was_subscribed and (user == profile or user.can_read_subscription_history)%}
|
||||||
<div id="drop_subscriptions">
|
<div id="drop_subscriptions">
|
||||||
<h5>{% trans %}Subscription history{% endtrans %}</h5>
|
<h5>{% trans %}Subscription history{% endtrans %}</h5>
|
||||||
<table>
|
<table>
|
||||||
@ -230,4 +238,3 @@ $(function(){
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -52,6 +52,7 @@
|
|||||||
{% if not form.instance.profile_pict %}
|
{% if not form.instance.profile_pict %}
|
||||||
<script src="{{ static('core/js/webcam.js') }}"></script>
|
<script src="{{ static('core/js/webcam.js') }}"></script>
|
||||||
<script language="JavaScript">
|
<script language="JavaScript">
|
||||||
|
Webcam.on('error', function(msg) { console.log('Webcam.js error: ' + msg) })
|
||||||
Webcam.set({
|
Webcam.set({
|
||||||
width: 320,
|
width: 320,
|
||||||
height: 240,
|
height: 240,
|
||||||
|
@ -2,14 +2,14 @@
|
|||||||
{% from "core/macros.jinja" import user_link_with_pict, delete_godfather %}
|
{% from "core/macros.jinja" import user_link_with_pict, delete_godfather %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans user_name=profile.get_display_name() %}{{ user_name }}'s godfathers{% endtrans %}
|
{% trans user_name=profile.get_display_name() %}{{ user_name }}'s family{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<p><a href="{{ url("core:user_godfathers_tree_pict", user_id=profile.id) }}?family">
|
<p><a href="{{ url("core:user_godfathers_tree_pict", user_id=profile.id) }}?family">
|
||||||
{% trans %}Show family picture{% endtrans %}</a></p>
|
{% trans %}Show family picture{% endtrans %}</a></p>
|
||||||
{% if profile.godfathers.exists() %}
|
{% if profile.godfathers.exists() %}
|
||||||
<h4>{% trans %}Godfathers{% endtrans %}</h4>
|
<h4>{% trans %}Godfathers / Godmothers{% endtrans %}</h4>
|
||||||
<ul>
|
<ul>
|
||||||
{% for u in profile.godfathers.all() %}
|
{% for u in profile.godfathers.all() %}
|
||||||
<li> <a href="{{ url("core:user_godfathers", user_id=u.id) }}" class="mini_profile_link" >
|
<li> <a href="{{ url("core:user_godfathers", user_id=u.id) }}" class="mini_profile_link" >
|
||||||
@ -19,7 +19,7 @@
|
|||||||
<p><a href="{{ url("core:user_godfathers_tree", user_id=profile.id) }}">
|
<p><a href="{{ url("core:user_godfathers_tree", user_id=profile.id) }}">
|
||||||
{% trans %}Show ancestors tree{% endtrans %}</a></p>
|
{% trans %}Show ancestors tree{% endtrans %}</a></p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>{% trans %}No godfathers{% endtrans %}
|
<p>{% trans %}No godfathers / godmothers{% endtrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if profile.godchildren.exists() %}
|
{% if profile.godchildren.exists() %}
|
||||||
<h4>{% trans %}Godchildren{% endtrans %}</h4>
|
<h4>{% trans %}Godchildren{% endtrans %}</h4>
|
||||||
|
@ -47,7 +47,7 @@
|
|||||||
{% if param == "godchildren" %}
|
{% if param == "godchildren" %}
|
||||||
<p>{% trans %}No godchildren{% endtrans %}
|
<p>{% trans %}No godchildren{% endtrans %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>{% trans %}No godfathers{% endtrans %}
|
<p>{% trans %}No godfathers / godmothers{% endtrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -5,6 +5,9 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{% if can_edit(profile, user) %}
|
||||||
|
<button id="download_all_pictures", onclick=download_pictures()>{% trans %}Download all my pictures{% endtrans %}</button>
|
||||||
|
{% endif %}
|
||||||
{% for a in albums %}
|
{% for a in albums %}
|
||||||
<div style="padding: 10px">
|
<div style="padding: 10px">
|
||||||
<h4>{{ a.name }}</h4>
|
<h4>{{ a.name }}</h4>
|
||||||
@ -12,7 +15,7 @@
|
|||||||
{% for picture in pictures[a.id] %}
|
{% for picture in pictures[a.id] %}
|
||||||
<div class="picture">
|
<div class="picture">
|
||||||
<a href="{{ url("sas:picture", picture_id=picture.id) }}#pict">
|
<a href="{{ url("sas:picture", picture_id=picture.id) }}#pict">
|
||||||
<img src="{{ picture.get_download_thumb_url() }}" alt="{{ picture.get_display_name() }}" style="max-width: 100%"/>
|
<img src="{{ picture.get_download_thumb_url() }}" alt="{{ picture.get_display_name() }}" style="max-width: 100%" loading="lazy"/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -20,3 +23,67 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
{{ super() }}
|
||||||
|
<script type="text/javascript">
|
||||||
|
/**
|
||||||
|
* Download a list of files.
|
||||||
|
* @author speedplane
|
||||||
|
*/
|
||||||
|
function download_files(files) {
|
||||||
|
function download_next(i) {
|
||||||
|
if (i >= files.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = files[i].download;
|
||||||
|
a.target = '_parent';
|
||||||
|
// Use a.download if available, it prevents plugins from opening.
|
||||||
|
if ('download' in a) {
|
||||||
|
a.download = files[i].filename;
|
||||||
|
}
|
||||||
|
// Add a to the doc for click to work.
|
||||||
|
(document.body || document.documentElement).appendChild(a);
|
||||||
|
if (a.click) {
|
||||||
|
a.click(); // The click method is supported by most browsers.
|
||||||
|
} else {
|
||||||
|
$(a).click(); // Backup using jquery
|
||||||
|
}
|
||||||
|
// Delete the temporary link.
|
||||||
|
a.parentNode.removeChild(a);
|
||||||
|
// Download the next file with a small timeout. The timeout is necessary
|
||||||
|
// for IE, which will otherwise only download the first file.
|
||||||
|
setTimeout(function() {
|
||||||
|
download_next(i + 1);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
// Initiate the first download.
|
||||||
|
download_next(0);
|
||||||
|
}
|
||||||
|
function download_pictures() {
|
||||||
|
$("#download_all_pictures").prop("disabled", true);
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
$.ajax({
|
||||||
|
type: "GET",
|
||||||
|
url: "{{ url('api:all_pictures_of_user', user=object.id) }}",
|
||||||
|
tryCount: 0,
|
||||||
|
xhr: function(){
|
||||||
|
return xhr;
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
$("#download_all_pictures").prop("disabled", false);
|
||||||
|
to_download = [];
|
||||||
|
data.forEach(picture =>
|
||||||
|
to_download.push({ download: picture["full_size_url"], filename: picture["name"] })
|
||||||
|
);
|
||||||
|
download_files(to_download);
|
||||||
|
},
|
||||||
|
error: function(data){
|
||||||
|
console.log("Error retrieving data from url: " + data);
|
||||||
|
$("#download_all_pictures").text("{% trans %}Error downloading your pictures{% endtrans %}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
{% if user.is_root %}
|
{% if user.is_root %}
|
||||||
<li><a href="{{ url('core:group_list') }}">{% trans %}Groups{% endtrans %}</a></li>
|
<li><a href="{{ url('core:group_list') }}">{% trans %}Groups{% endtrans %}</a></li>
|
||||||
<li><a href="{{ url('rootplace:merge') }}">{% trans %}Merge users{% endtrans %}</a></li>
|
<li><a href="{{ url('rootplace:merge') }}">{% trans %}Merge users{% endtrans %}</a></li>
|
||||||
|
<li><a href="{{ url('rootplace:operation_logs') }}">{% trans %}Operation logs{% endtrans %}</a></li>
|
||||||
<li><a href="{{ url('rootplace:delete_forum_messages') }}">{% trans %}Delete user's forum messages{% endtrans %}</a></li>
|
<li><a href="{{ url('rootplace:delete_forum_messages') }}">{% trans %}Delete user's forum messages{% endtrans %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if user.can_create_subscription or user.is_root %}
|
{% if user.can_create_subscription or user.is_root %}
|
||||||
|
@ -1,3 +1,13 @@
|
|||||||
{{ object.first_name }}
|
{% load search_helpers %}
|
||||||
{{ object.last_name }}
|
|
||||||
{{ object.nick_name }}
|
{% with first=object.first_name|safe|slugify last=object.last_name|safe|slugify nick=object.nick_name|default_if_none:""|safe|slugify %}
|
||||||
|
|
||||||
|
{{ first|replace:"|-| " }}
|
||||||
|
{{ last|replace:"|-| " }}
|
||||||
|
{{ nick|replace:"|-| " }}
|
||||||
|
{% if first|count:"-" != 0 %}{{ first|cut:"-" }}{% endif %}
|
||||||
|
{% if last|count:"-" != 0 %}{{ last|cut:"-" }}{% endif %}
|
||||||
|
{% if nick|count:"-" != 0 %}{{ nick|cut:"-" }}{% endif %}
|
||||||
|
{{ first|cut:"-" }}{{ last|cut:"-" }}
|
||||||
|
|
||||||
|
{% endwith %}
|
||||||
|
@ -94,7 +94,7 @@ def datetime_format_python_to_PHP(python_format_string):
|
|||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def scss(path):
|
def scss(path):
|
||||||
"""
|
"""
|
||||||
Return path of the corresponding css file after compilation
|
Return path of the corresponding css file after compilation
|
||||||
"""
|
"""
|
||||||
processor = ScssProcessor(path)
|
processor = ScssProcessor(path)
|
||||||
return processor.get_converted_scss()
|
return processor.get_converted_scss()
|
||||||
|
27
core/templatetags/search_helpers.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from django.template.exceptions import TemplateSyntaxError
|
||||||
|
from django import template
|
||||||
|
from django.template.defaultfilters import stringfilter
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
# arg should be of the form "|foo|bar" where the first character is the
|
||||||
|
# separator between old and new in value.replace(old, new)
|
||||||
|
@register.filter
|
||||||
|
@stringfilter
|
||||||
|
def replace(value, arg):
|
||||||
|
# s.replace('', '') == s so len(arg) == 2 is fine
|
||||||
|
if len(arg) < 2:
|
||||||
|
raise TemplateSyntaxError("badly formatted argument")
|
||||||
|
|
||||||
|
arg = arg.split(arg[0])
|
||||||
|
|
||||||
|
if len(arg) != 3:
|
||||||
|
raise TemplateSyntaxError("badly formatted argument")
|
||||||
|
|
||||||
|
return value.replace(arg[1], arg[2])
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def count(value, arg):
|
||||||
|
return value.count(arg)
|
@ -402,32 +402,32 @@ class UserToolsTest(TestCase):
|
|||||||
|
|
||||||
def test_anonymous_user_unauthorized(self):
|
def test_anonymous_user_unauthorized(self):
|
||||||
response = self.client.get(reverse("core:user_tools"))
|
response = self.client.get(reverse("core:user_tools"))
|
||||||
self.assertEquals(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
def test_page_is_working(self):
|
def test_page_is_working(self):
|
||||||
# Test for simple user
|
# Test for simple user
|
||||||
self.client.login(username="guy", password="plop")
|
self.client.login(username="guy", password="plop")
|
||||||
response = self.client.get(reverse("core:user_tools"))
|
response = self.client.get(reverse("core:user_tools"))
|
||||||
self.assertNotEquals(response.status_code, 500)
|
self.assertNotEqual(response.status_code, 500)
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
# Test for root
|
# Test for root
|
||||||
self.client.login(username="root", password="plop")
|
self.client.login(username="root", password="plop")
|
||||||
response = self.client.get(reverse("core:user_tools"))
|
response = self.client.get(reverse("core:user_tools"))
|
||||||
self.assertNotEquals(response.status_code, 500)
|
self.assertNotEqual(response.status_code, 500)
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
# Test for skia
|
# Test for skia
|
||||||
self.client.login(username="skia", password="plop")
|
self.client.login(username="skia", password="plop")
|
||||||
response = self.client.get(reverse("core:user_tools"))
|
response = self.client.get(reverse("core:user_tools"))
|
||||||
self.assertNotEquals(response.status_code, 500)
|
self.assertNotEqual(response.status_code, 500)
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
# Test for comunity
|
# Test for comunity
|
||||||
self.client.login(username="comunity", password="plop")
|
self.client.login(username="comunity", password="plop")
|
||||||
response = self.client.get(reverse("core:user_tools"))
|
response = self.client.get(reverse("core:user_tools"))
|
||||||
self.assertNotEquals(response.status_code, 500)
|
self.assertNotEqual(response.status_code, 500)
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
# TODO: many tests on the pages:
|
# TODO: many tests on the pages:
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
from django.urls import re_path
|
from django.urls import re_path, path
|
||||||
|
|
||||||
from core.views import *
|
from core.views import *
|
||||||
|
|
||||||
@ -60,8 +60,8 @@ urlpatterns = [
|
|||||||
SithPasswordResetDoneView.as_view(),
|
SithPasswordResetDoneView.as_view(),
|
||||||
name="password_reset_done",
|
name="password_reset_done",
|
||||||
),
|
),
|
||||||
re_path(
|
path(
|
||||||
r"^reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$",
|
r"reset/<str:uidb64>/<str:token>/",
|
||||||
SithPasswordResetConfirmView.as_view(),
|
SithPasswordResetConfirmView.as_view(),
|
||||||
name="password_reset_confirm",
|
name="password_reset_confirm",
|
||||||
),
|
),
|
||||||
@ -73,7 +73,7 @@ urlpatterns = [
|
|||||||
re_path(r"^register$", register, name="register"),
|
re_path(r"^register$", register, name="register"),
|
||||||
# Group handling
|
# Group handling
|
||||||
re_path(r"^group/$", GroupListView.as_view(), name="group_list"),
|
re_path(r"^group/$", GroupListView.as_view(), name="group_list"),
|
||||||
re_path(r"^group/new$", GroupCreateView.as_view(), name="group_new"),
|
re_path(r"^group/new/$", GroupCreateView.as_view(), name="group_new"),
|
||||||
re_path(
|
re_path(
|
||||||
r"^group/(?P<group_id>[0-9]+)/$", GroupEditView.as_view(), name="group_edit"
|
r"^group/(?P<group_id>[0-9]+)/$", GroupEditView.as_view(), name="group_edit"
|
||||||
),
|
),
|
||||||
|
@ -242,6 +242,16 @@ class CanViewMixin(GenericContentPermissionMixinBuilder):
|
|||||||
permission_function = can_view
|
permission_function = can_view
|
||||||
|
|
||||||
|
|
||||||
|
class UserIsRootMixin(GenericContentPermissionMixinBuilder):
|
||||||
|
"""
|
||||||
|
This view check if the user is root
|
||||||
|
|
||||||
|
:raises: PermissionDenied
|
||||||
|
"""
|
||||||
|
|
||||||
|
permission_function = lambda obj, user: user.is_root
|
||||||
|
|
||||||
|
|
||||||
class FormerSubscriberMixin(View):
|
class FormerSubscriberMixin(View):
|
||||||
"""
|
"""
|
||||||
This view check if the user was at least an old subscriber
|
This view check if the user was at least an old subscriber
|
||||||
@ -334,19 +344,19 @@ class QuickNotifMixin:
|
|||||||
|
|
||||||
class DetailFormView(SingleObjectMixin, FormView):
|
class DetailFormView(SingleObjectMixin, FormView):
|
||||||
"""
|
"""
|
||||||
Class that allow both a detail view and a form view
|
Class that allow both a detail view and a form view
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
"""
|
"""
|
||||||
Get current group from id in url
|
Get current group from id in url
|
||||||
"""
|
"""
|
||||||
return self.cached_object
|
return self.cached_object
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def cached_object(self):
|
def cached_object(self):
|
||||||
"""
|
"""
|
||||||
Optimisation on group retrieval
|
Optimisation on group retrieval
|
||||||
"""
|
"""
|
||||||
return super(DetailFormView, self).get_object()
|
return super(DetailFormView, self).get_object()
|
||||||
|
|
||||||
|
@ -24,12 +24,13 @@
|
|||||||
|
|
||||||
# This file contains all the views that concern the page model
|
# This file contains all the views that concern the page model
|
||||||
from django.shortcuts import redirect, get_object_or_404
|
from django.shortcuts import redirect, get_object_or_404
|
||||||
|
from django.utils.http import http_date
|
||||||
from django.views.generic import ListView, DetailView, TemplateView
|
from django.views.generic import ListView, DetailView, TemplateView
|
||||||
from django.views.generic.edit import UpdateView, FormMixin, DeleteView
|
from django.views.generic.edit import UpdateView, FormMixin, DeleteView
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
from django.forms.models import modelform_factory
|
from django.forms.models import modelform_factory
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from wsgiref.util import FileWrapper
|
from wsgiref.util import FileWrapper
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -68,6 +69,7 @@ def send_file(request, file_id, file_class=SithFile, file_attr="file"):
|
|||||||
with open(filepath.encode("utf-8"), "rb") as filename:
|
with open(filepath.encode("utf-8"), "rb") as filename:
|
||||||
wrapper = FileWrapper(filename)
|
wrapper = FileWrapper(filename)
|
||||||
response = HttpResponse(wrapper, content_type=f.mime_type)
|
response = HttpResponse(wrapper, content_type=f.mime_type)
|
||||||
|
response["Last-Modified"] = http_date(f.date.timestamp())
|
||||||
response["Content-Length"] = os.path.getsize(filepath.encode("utf-8"))
|
response["Content-Length"] = os.path.getsize(filepath.encode("utf-8"))
|
||||||
response["Content-Disposition"] = ('inline; filename="%s"' % f.name).encode(
|
response["Content-Disposition"] = ('inline; filename="%s"' % f.name).encode(
|
||||||
"utf-8"
|
"utf-8"
|
||||||
@ -147,7 +149,7 @@ class FileListView(ListView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs = super(FileListView, self).get_context_data(**kwargs)
|
kwargs = super(FileListView, self).get_context_data(**kwargs)
|
||||||
kwargs["popup"] = ""
|
kwargs["popup"] = ""
|
||||||
if self.kwargs["popup"]:
|
if self.kwargs.get("popup") is not None:
|
||||||
kwargs["popup"] = "popup"
|
kwargs["popup"] = "popup"
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
@ -165,7 +167,7 @@ class FileEditView(CanEditMixin, UpdateView):
|
|||||||
return modelform_factory(SithFile, fields=fields)
|
return modelform_factory(SithFile, fields=fields)
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
if self.kwargs["popup"]:
|
if self.kwargs.get("popup") is not None:
|
||||||
return reverse(
|
return reverse(
|
||||||
"core:file_detail", kwargs={"file_id": self.object.id, "popup": "popup"}
|
"core:file_detail", kwargs={"file_id": self.object.id, "popup": "popup"}
|
||||||
)
|
)
|
||||||
@ -176,7 +178,7 @@ class FileEditView(CanEditMixin, UpdateView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs = super(FileEditView, self).get_context_data(**kwargs)
|
kwargs = super(FileEditView, self).get_context_data(**kwargs)
|
||||||
kwargs["popup"] = ""
|
kwargs["popup"] = ""
|
||||||
if self.kwargs["popup"]:
|
if self.kwargs.get("popup") is not None:
|
||||||
kwargs["popup"] = "popup"
|
kwargs["popup"] = "popup"
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
@ -217,13 +219,13 @@ class FileEditPropView(CanEditPropMixin, UpdateView):
|
|||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse(
|
return reverse(
|
||||||
"core:file_detail",
|
"core:file_detail",
|
||||||
kwargs={"file_id": self.object.id, "popup": self.kwargs["popup"] or ""},
|
kwargs={"file_id": self.object.id, "popup": self.kwargs.get("popup", "")},
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs = super(FileEditPropView, self).get_context_data(**kwargs)
|
kwargs = super(FileEditPropView, self).get_context_data(**kwargs)
|
||||||
kwargs["popup"] = ""
|
kwargs["popup"] = ""
|
||||||
if self.kwargs["popup"]:
|
if self.kwargs.get("popup") is not None:
|
||||||
kwargs["popup"] = "popup"
|
kwargs["popup"] = "popup"
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
@ -301,14 +303,14 @@ class FileView(CanViewMixin, DetailView, FormMixin):
|
|||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse(
|
return reverse(
|
||||||
"core:file_detail",
|
"core:file_detail",
|
||||||
kwargs={"file_id": self.object.id, "popup": self.kwargs["popup"] or ""},
|
kwargs={"file_id": self.object.id, "popup": self.kwargs.get("popup", "")},
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs = super(FileView, self).get_context_data(**kwargs)
|
kwargs = super(FileView, self).get_context_data(**kwargs)
|
||||||
kwargs["popup"] = ""
|
kwargs["popup"] = ""
|
||||||
kwargs["form"] = self.form
|
kwargs["form"] = self.form
|
||||||
if self.kwargs["popup"]:
|
if self.kwargs.get("popup") is not None:
|
||||||
kwargs["popup"] = "popup"
|
kwargs["popup"] = "popup"
|
||||||
kwargs["clipboard"] = SithFile.objects.filter(
|
kwargs["clipboard"] = SithFile.objects.filter(
|
||||||
id__in=self.request.session["clipboard"]
|
id__in=self.request.session["clipboard"]
|
||||||
@ -328,20 +330,20 @@ class FileDeleteView(CanEditPropMixin, DeleteView):
|
|||||||
return self.request.GET["next"]
|
return self.request.GET["next"]
|
||||||
if self.object.parent is None:
|
if self.object.parent is None:
|
||||||
return reverse(
|
return reverse(
|
||||||
"core:file_list", kwargs={"popup": self.kwargs["popup"] or ""}
|
"core:file_list", kwargs={"popup": self.kwargs.get("popup", "")}
|
||||||
)
|
)
|
||||||
return reverse(
|
return reverse(
|
||||||
"core:file_detail",
|
"core:file_detail",
|
||||||
kwargs={
|
kwargs={
|
||||||
"file_id": self.object.parent.id,
|
"file_id": self.object.parent.id,
|
||||||
"popup": self.kwargs["popup"] or "",
|
"popup": self.kwargs.get("popup", ""),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs = super(FileDeleteView, self).get_context_data(**kwargs)
|
kwargs = super(FileDeleteView, self).get_context_data(**kwargs)
|
||||||
kwargs["popup"] = ""
|
kwargs["popup"] = ""
|
||||||
if self.kwargs["popup"]:
|
if self.kwargs.get("popup") is not None:
|
||||||
kwargs["popup"] = "popup"
|
kwargs["popup"] = "popup"
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
@ -37,11 +37,15 @@ from django.forms import (
|
|||||||
DateTimeInput,
|
DateTimeInput,
|
||||||
Textarea,
|
Textarea,
|
||||||
)
|
)
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.utils.translation import ugettext
|
from django.utils.translation import gettext
|
||||||
from phonenumber_field.widgets import PhoneNumberInternationalFallbackWidget
|
from phonenumber_field.widgets import PhoneNumberInternationalFallbackWidget
|
||||||
from ajax_select.fields import AutoCompleteSelectField
|
from ajax_select.fields import AutoCompleteSelectField
|
||||||
from ajax_select import make_ajax_field
|
from ajax_select import make_ajax_field
|
||||||
|
from django.utils.dateparse import parse_datetime
|
||||||
|
from django.utils import timezone
|
||||||
|
import datetime
|
||||||
|
from django.forms.utils import to_current_timezone
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@ -126,7 +130,7 @@ class SelectFile(TextInput):
|
|||||||
'<span name="'
|
'<span name="'
|
||||||
+ name
|
+ name
|
||||||
+ '" class="choose_file_button">'
|
+ '" class="choose_file_button">'
|
||||||
+ ugettext("Choose file")
|
+ gettext("Choose file")
|
||||||
+ "</span>"
|
+ "</span>"
|
||||||
)
|
)
|
||||||
return output
|
return output
|
||||||
@ -150,7 +154,7 @@ class SelectUser(TextInput):
|
|||||||
'<span name="'
|
'<span name="'
|
||||||
+ name
|
+ name
|
||||||
+ '" class="choose_user_button">'
|
+ '" class="choose_user_button">'
|
||||||
+ ugettext("Choose user")
|
+ gettext("Choose user")
|
||||||
+ "</span>"
|
+ "</span>"
|
||||||
)
|
)
|
||||||
return output
|
return output
|
||||||
@ -222,6 +226,7 @@ class UserProfileForm(forms.ModelForm):
|
|||||||
"avatar_pict",
|
"avatar_pict",
|
||||||
"scrub_pict",
|
"scrub_pict",
|
||||||
"sex",
|
"sex",
|
||||||
|
"pronouns",
|
||||||
"second_email",
|
"second_email",
|
||||||
"address",
|
"address",
|
||||||
"parent_address",
|
"parent_address",
|
||||||
@ -333,7 +338,10 @@ class UserPropForm(forms.ModelForm):
|
|||||||
|
|
||||||
class UserGodfathersForm(forms.Form):
|
class UserGodfathersForm(forms.Form):
|
||||||
type = forms.ChoiceField(
|
type = forms.ChoiceField(
|
||||||
choices=[("godfather", _("Godfather")), ("godchild", _("Godchild"))],
|
choices=[
|
||||||
|
("godfather", _("Godfather / Godmother")),
|
||||||
|
("godchild", _("Godchild")),
|
||||||
|
],
|
||||||
label=_("Add"),
|
label=_("Add"),
|
||||||
)
|
)
|
||||||
user = AutoCompleteSelectField(
|
user = AutoCompleteSelectField(
|
||||||
@ -398,3 +406,27 @@ class GiftForm(forms.ModelForm):
|
|||||||
id=user_id
|
id=user_id
|
||||||
)
|
)
|
||||||
self.fields["user"].widget = forms.HiddenInput()
|
self.fields["user"].widget = forms.HiddenInput()
|
||||||
|
|
||||||
|
|
||||||
|
class TzAwareDateTimeField(forms.DateTimeField):
|
||||||
|
def __init__(
|
||||||
|
self, input_formats=["%Y-%m-%d %H:%M:%S"], widget=SelectDateTime, **kwargs
|
||||||
|
):
|
||||||
|
super().__init__(input_formats=input_formats, widget=widget, **kwargs)
|
||||||
|
|
||||||
|
def prepare_value(self, value):
|
||||||
|
# the db value is a datetime as a string in UTC
|
||||||
|
if isinstance(value, str):
|
||||||
|
# convert it into a naive datetime (no timezone attached)
|
||||||
|
value = parse_datetime(value)
|
||||||
|
# attach it to the UTC timezone (so that to_current_timezone()) if not None
|
||||||
|
# converts it to the local timezone)
|
||||||
|
if value is not None:
|
||||||
|
value = timezone.make_aware(value, timezone.utc)
|
||||||
|
|
||||||
|
if isinstance(value, datetime.datetime):
|
||||||
|
value = to_current_timezone(value)
|
||||||
|
# otherwise it is formatted according to locale (in french)
|
||||||
|
value = str(value)
|
||||||
|
|
||||||
|
return value
|
||||||
|
@ -31,20 +31,20 @@ from django.views.generic import ListView
|
|||||||
from django.views.generic.edit import FormView
|
from django.views.generic.edit import FormView
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from ajax_select.fields import AutoCompleteSelectMultipleField
|
from ajax_select.fields import AutoCompleteSelectMultipleField
|
||||||
|
|
||||||
from core.models import RealGroup, User
|
from core.models import RealGroup, User
|
||||||
from core.views import CanEditMixin, DetailFormView
|
from core.views import CanCreateMixin, CanEditMixin, DetailFormView
|
||||||
|
|
||||||
# Forms
|
# Forms
|
||||||
|
|
||||||
|
|
||||||
class EditMembersForm(forms.Form):
|
class EditMembersForm(forms.Form):
|
||||||
"""
|
"""
|
||||||
Add and remove members from a Group
|
Add and remove members from a Group
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -66,7 +66,7 @@ class EditMembersForm(forms.Form):
|
|||||||
|
|
||||||
def clean_users_added(self):
|
def clean_users_added(self):
|
||||||
"""
|
"""
|
||||||
Check that the user is not trying to add an user already in the group
|
Check that the user is not trying to add an user already in the group
|
||||||
"""
|
"""
|
||||||
cleaned_data = super(EditMembersForm, self).clean()
|
cleaned_data = super(EditMembersForm, self).clean()
|
||||||
users_added = cleaned_data.get("users_added", None)
|
users_added = cleaned_data.get("users_added", None)
|
||||||
@ -100,7 +100,7 @@ class GroupListView(CanEditMixin, ListView):
|
|||||||
|
|
||||||
class GroupEditView(CanEditMixin, UpdateView):
|
class GroupEditView(CanEditMixin, UpdateView):
|
||||||
"""
|
"""
|
||||||
Edit infos of a Group
|
Edit infos of a Group
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model = RealGroup
|
model = RealGroup
|
||||||
@ -109,20 +109,20 @@ class GroupEditView(CanEditMixin, UpdateView):
|
|||||||
fields = ["name", "description"]
|
fields = ["name", "description"]
|
||||||
|
|
||||||
|
|
||||||
class GroupCreateView(CanEditMixin, CreateView):
|
class GroupCreateView(CanCreateMixin, CreateView):
|
||||||
"""
|
"""
|
||||||
Add a new Group
|
Add a new Group
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model = RealGroup
|
model = RealGroup
|
||||||
template_name = "core/group_edit.jinja"
|
template_name = "core/create.jinja"
|
||||||
fields = ["name", "description"]
|
fields = ["name", "description"]
|
||||||
|
|
||||||
|
|
||||||
class GroupTemplateView(CanEditMixin, DetailFormView):
|
class GroupTemplateView(CanEditMixin, DetailFormView):
|
||||||
"""
|
"""
|
||||||
Display all users in a given Group
|
Display all users in a given Group
|
||||||
Allow adding and removing users from it
|
Allow adding and removing users from it
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model = RealGroup
|
model = RealGroup
|
||||||
@ -156,7 +156,7 @@ class GroupTemplateView(CanEditMixin, DetailFormView):
|
|||||||
|
|
||||||
class GroupDeleteView(CanEditMixin, DeleteView):
|
class GroupDeleteView(CanEditMixin, DeleteView):
|
||||||
"""
|
"""
|
||||||
Delete a Group
|
Delete a Group
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model = RealGroup
|
model = RealGroup
|
||||||
|
@ -30,6 +30,7 @@ from django.contrib.auth.decorators import login_required
|
|||||||
from django.utils import html
|
from django.utils import html
|
||||||
from django.views.generic import ListView, TemplateView
|
from django.views.generic import ListView, TemplateView
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.utils.text import slugify
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@ -73,7 +74,18 @@ def notification(request, notif_id):
|
|||||||
|
|
||||||
def search_user(query, as_json=False):
|
def search_user(query, as_json=False):
|
||||||
try:
|
try:
|
||||||
res = SearchQuerySet().models(User).autocomplete(auto=html.escape(query))[:20]
|
# slugify turns everything into ascii and every whitespace into -
|
||||||
|
# it ends by removing duplicate - (so ' - ' will turn into '-')
|
||||||
|
# replace('-', ' ') because search is whitespace based
|
||||||
|
query = slugify(query).replace("-", " ")
|
||||||
|
# TODO: is this necessary?
|
||||||
|
query = html.escape(query)
|
||||||
|
res = (
|
||||||
|
SearchQuerySet()
|
||||||
|
.models(User)
|
||||||
|
.autocomplete(auto=query)
|
||||||
|
.order_by("-last_update")[:20]
|
||||||
|
)
|
||||||
return [r.object for r in res]
|
return [r.object for r in res]
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return []
|
return []
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
from django.shortcuts import render, redirect, get_object_or_404
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
from django.contrib.auth import views
|
from django.contrib.auth import views
|
||||||
from django.contrib.auth.forms import PasswordChangeForm
|
from django.contrib.auth.forms import PasswordChangeForm
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.core.exceptions import PermissionDenied, ValidationError
|
from django.core.exceptions import PermissionDenied, ValidationError
|
||||||
from django.http import Http404, HttpResponse
|
from django.http import Http404, HttpResponse
|
||||||
@ -48,6 +48,7 @@ from django.views.generic.dates import YearMixin, MonthMixin
|
|||||||
|
|
||||||
from datetime import timedelta, date
|
from datetime import timedelta, date
|
||||||
import logging
|
import logging
|
||||||
|
from api.views.sas import all_pictures_of_user
|
||||||
|
|
||||||
from core.views import (
|
from core.views import (
|
||||||
CanViewMixin,
|
CanViewMixin,
|
||||||
@ -202,7 +203,7 @@ class UserTabsMixin(TabedViewMixin):
|
|||||||
"core:user_godfathers", kwargs={"user_id": self.object.id}
|
"core:user_godfathers", kwargs={"user_id": self.object.id}
|
||||||
),
|
),
|
||||||
"slug": "godfathers",
|
"slug": "godfathers",
|
||||||
"name": _("Godfathers"),
|
"name": _("Family"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
tab_list.append(
|
tab_list.append(
|
||||||
@ -325,19 +326,15 @@ class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView):
|
|||||||
kwargs = super(UserPicturesView, self).get_context_data(**kwargs)
|
kwargs = super(UserPicturesView, self).get_context_data(**kwargs)
|
||||||
kwargs["albums"] = []
|
kwargs["albums"] = []
|
||||||
kwargs["pictures"] = {}
|
kwargs["pictures"] = {}
|
||||||
picture_qs = (
|
picture_qs = all_pictures_of_user(self.object)
|
||||||
self.object.pictures.exclude(picture=None)
|
|
||||||
.order_by("-picture__parent__date", "id")
|
|
||||||
.select_related("picture__parent")
|
|
||||||
)
|
|
||||||
last_album = None
|
last_album = None
|
||||||
for pict_relation in picture_qs:
|
for picture in picture_qs:
|
||||||
album = pict_relation.picture.parent
|
album = picture.parent
|
||||||
if album.id != last_album:
|
if album.id != last_album:
|
||||||
kwargs["albums"].append(album)
|
kwargs["albums"].append(album)
|
||||||
kwargs["pictures"][album.id] = []
|
kwargs["pictures"][album.id] = []
|
||||||
last_album = album.id
|
last_album = album.id
|
||||||
kwargs["pictures"][album.id].append(pict_relation.picture)
|
kwargs["pictures"][album.id].append(picture)
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
@ -474,7 +471,7 @@ class UserGodfathersTreePictureView(CanViewMixin, DetailView):
|
|||||||
if self.param == "godchildren":
|
if self.param == "godchildren":
|
||||||
self.graph.graph_attr["label"] = _("Godchildren")
|
self.graph.graph_attr["label"] = _("Godchildren")
|
||||||
elif self.param == "godfathers":
|
elif self.param == "godfathers":
|
||||||
self.graph.graph_attr["label"] = _("Godfathers")
|
self.graph.graph_attr["label"] = _("Family")
|
||||||
else:
|
else:
|
||||||
self.graph.graph_attr["label"] = _("Family")
|
self.graph.graph_attr["label"] = _("Family")
|
||||||
img = self.graph.draw(format="png", prog="dot")
|
img = self.graph.draw(format="png", prog="dot")
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2016,2017,2019
|
||||||
# - Skia <skia@libskia.so>
|
# - Skia <skia@libskia.so>
|
||||||
|
# - Sli <antoine@bartuccio.fr>
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
||||||
# http://ae.utbm.fr.
|
# http://ae.utbm.fr.
|
||||||
|
34
counter/app.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# -*- coding:utf-8 -*
|
||||||
|
#
|
||||||
|
# Copyright 2019
|
||||||
|
# - Sli <antoine@bartuccio.fr>
|
||||||
|
#
|
||||||
|
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
||||||
|
# http://ae.utbm.fr.
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License a published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
|
||||||
|
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class CounterConfig(AppConfig):
|
||||||
|
name = "counter"
|
||||||
|
verbose_name = _("counter")
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
import counter.signals
|
@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
@ -22,8 +22,9 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
|
from sith.settings import SITH_COUNTER_OFFICES, SITH_MAIN_CLUB
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -39,7 +40,7 @@ import os
|
|||||||
import base64
|
import base64
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from club.models import Club
|
from club.models import Club, Membership
|
||||||
from accounting.models import CurrencyField
|
from accounting.models import CurrencyField
|
||||||
from core.models import Group, User, Notification
|
from core.models import Group, User, Notification
|
||||||
from subscription.models import Subscription
|
from subscription.models import Subscription
|
||||||
@ -89,9 +90,9 @@ class Customer(models.Model):
|
|||||||
|
|
||||||
def save(self, allow_negative=False, is_selling=False, *args, **kwargs):
|
def save(self, allow_negative=False, is_selling=False, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
is_selling : tell if the current action is a selling
|
is_selling : tell if the current action is a selling
|
||||||
allow_negative : ignored if not a selling. Allow a selling to put the account in negative
|
allow_negative : ignored if not a selling. Allow a selling to put the account in negative
|
||||||
Those two parameters avoid blocking the save method of a customer if his account is negative
|
Those two parameters avoid blocking the save method of a customer if his account is negative
|
||||||
"""
|
"""
|
||||||
if self.amount < 0 and (is_selling and not allow_negative):
|
if self.amount < 0 and (is_selling and not allow_negative):
|
||||||
raise ValidationError(_("Not enough money"))
|
raise ValidationError(_("Not enough money"))
|
||||||
@ -342,6 +343,23 @@ class Counter(models.Model):
|
|||||||
"""
|
"""
|
||||||
return [b.id for b in self.get_barmen_list()]
|
return [b.id for b in self.get_barmen_list()]
|
||||||
|
|
||||||
|
def can_refill(self):
|
||||||
|
"""
|
||||||
|
Show if the counter authorize the refilling with physic money
|
||||||
|
"""
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.id in SITH_COUNTER_OFFICES
|
||||||
|
): # If the counter is the counters 'AE' or 'BdF', the refiling are authorized
|
||||||
|
return True
|
||||||
|
|
||||||
|
is_ae_member = False
|
||||||
|
ae = Club.objects.get(unix_name=SITH_MAIN_CLUB["unix_name"])
|
||||||
|
for barman in self.get_barmen_list():
|
||||||
|
if ae.get_membership_for(barman):
|
||||||
|
is_ae_member = True
|
||||||
|
return is_ae_member
|
||||||
|
|
||||||
|
|
||||||
class Refilling(models.Model):
|
class Refilling(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -527,7 +545,7 @@ class Selling(models.Model):
|
|||||||
|
|
||||||
def save(self, allow_negative=False, *args, **kwargs):
|
def save(self, allow_negative=False, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
allow_negative : Allow this selling to use more money than available for this user
|
allow_negative : Allow this selling to use more money than available for this user
|
||||||
"""
|
"""
|
||||||
if not self.date:
|
if not self.date:
|
||||||
self.date = timezone.now()
|
self.date = timezone.now()
|
||||||
|
71
counter/signals.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# -*- coding:utf-8 -*
|
||||||
|
#
|
||||||
|
# Copyright 2019
|
||||||
|
# - Sli <antoine@bartuccio.fr>
|
||||||
|
#
|
||||||
|
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
||||||
|
# http://ae.utbm.fr.
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License a published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
|
||||||
|
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
from django.db.models.signals import pre_delete
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from core.middleware import get_signal_request
|
||||||
|
from core.models import OperationLog
|
||||||
|
|
||||||
|
from counter.models import Selling, Refilling, Counter
|
||||||
|
|
||||||
|
|
||||||
|
def write_log(instance, operation_type):
|
||||||
|
def get_user():
|
||||||
|
request = get_signal_request()
|
||||||
|
|
||||||
|
if not request:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get a random barmen if deletion is from a counter
|
||||||
|
session = getattr(request, "session", {})
|
||||||
|
session_token = session.get("counter_token", None)
|
||||||
|
if session_token:
|
||||||
|
counter = Counter.objects.filter(token=session_token).first()
|
||||||
|
if counter and len(counter.get_barmen_list()) > 0:
|
||||||
|
return counter.get_random_barman()
|
||||||
|
|
||||||
|
# Get the current logged user if not from a counter
|
||||||
|
if request.user and not request.user.is_anonymous:
|
||||||
|
return request.user
|
||||||
|
|
||||||
|
# Return None by default
|
||||||
|
return None
|
||||||
|
|
||||||
|
log = OperationLog(
|
||||||
|
label=str(instance),
|
||||||
|
operator=get_user(),
|
||||||
|
operation_type=operation_type,
|
||||||
|
).save()
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(pre_delete, sender=Refilling, dispatch_uid="write_log_refilling_deletion")
|
||||||
|
def write_log_refilling_deletion(sender, instance, **kwargs):
|
||||||
|
write_log(instance, "REFILLING_DELETION")
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(pre_delete, sender=Selling, dispatch_uid="write_log_refilling_deletion")
|
||||||
|
def write_log_selling_deletion(sender, instance, **kwargs):
|
||||||
|
write_log(instance, "SELLING_DELETION")
|
@ -4,6 +4,12 @@
|
|||||||
{% trans obj=object %}Edit {{ obj }}{% endtrans %}
|
{% trans obj=object %}Edit {{ obj }}{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block info_boxes %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block nav %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>{% trans %}Make a cash register summary{% endtrans %}</h2>
|
<h2>{% trans %}Make a cash register summary{% endtrans %}</h2>
|
||||||
<form action="" method="post" id="cash_summary_form">
|
<form action="" method="post" id="cash_summary_form">
|
||||||
|
@ -1,163 +1,224 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
{% from "core/macros.jinja" import user_mini_profile, user_subscription %}
|
{% from "core/macros.jinja" import user_mini_profile, user_subscription %}
|
||||||
|
|
||||||
|
|
||||||
{% macro add_product(id, content, class="") %}
|
|
||||||
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="{{ class }}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="action" value="add_product">
|
|
||||||
<button type="submit" name="product_id" value="{{ id }}"> {{ content|safe }} </button>
|
|
||||||
</form>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro del_product(id, content, class="") %}
|
|
||||||
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="{{ class }}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="action" value="del_product">
|
|
||||||
<button type="submit" name="product_id" value="{{ id }}"> {{ content }} </button>
|
|
||||||
</form>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{{ counter }}
|
{{ counter }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block info_boxes %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block nav %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h4 id="click_interface">{{ counter }}</h4>
|
<h4 id="click_interface">{{ counter }}</h4>
|
||||||
|
|
||||||
<div id="user_info">
|
|
||||||
<h5>{% trans %}Customer{% endtrans %}</h5>
|
|
||||||
{{ user_mini_profile(customer.user) }}
|
|
||||||
{{ user_subscription(customer.user) }}
|
|
||||||
<p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p>
|
|
||||||
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="action" value="add_student_card">
|
|
||||||
{% trans %}Add a student card{% endtrans %}
|
|
||||||
<input type="input" name="student_card_uid" />
|
|
||||||
{% if request.session['not_valid_student_card_uid'] %}
|
|
||||||
<p><strong>{% trans %}This is not a valid student card UID{% endtrans %}</strong></p>
|
|
||||||
{% endif %}
|
|
||||||
<input type="submit" value="{% trans %}Go{% endtrans %}" />
|
|
||||||
</form>
|
|
||||||
<h6>{% trans %}Registered cards{% endtrans %}</h6>
|
|
||||||
{% if customer.student_cards.exists() %}
|
|
||||||
<ul>
|
|
||||||
{% for card in customer.student_cards.all() %}
|
|
||||||
<li>{{ card.uid }}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% else %}
|
|
||||||
{% trans %}No card registered{% endtrans %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div id="bar_ui">
|
<div id="bar_ui">
|
||||||
<h5>{% trans %}Selling{% endtrans %}</h5>
|
<noscript>
|
||||||
<div>
|
<p class="important">Javascript is required for the counter UI.</p>
|
||||||
<div class="important">
|
</noscript>
|
||||||
{% if request.session['too_young'] %}
|
|
||||||
<p><strong>{% trans %}Too young for that product{% endtrans %}</strong></p>
|
<div id="user_info">
|
||||||
{% endif %}
|
<h5>{% trans %}Customer{% endtrans %}</h5>
|
||||||
{% if request.session['not_allowed'] %}
|
{{ user_mini_profile(customer.user) }}
|
||||||
<p><strong>{% trans %}Not allowed for that product{% endtrans %}</strong></p>
|
{{ user_subscription(customer.user) }}
|
||||||
{% endif %}
|
<p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p>
|
||||||
{% if request.session['no_age'] %}
|
|
||||||
<p><strong>{% trans %}No date of birth provided{% endtrans %}</strong></p>
|
|
||||||
{% endif %}
|
|
||||||
{% if request.session['not_enough'] %}
|
|
||||||
<p><strong>{% trans %}Not enough money{% endtrans %}</strong></p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
|
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="action" value="code">
|
<input type="hidden" name="action" value="add_student_card">
|
||||||
<input type="input" name="code" value="" class="focus" id="code_field"/>
|
{% trans %}Add a student card{% endtrans %}
|
||||||
|
<input type="input" name="student_card_uid" />
|
||||||
|
{% if request.session['not_valid_student_card_uid'] %}
|
||||||
|
<p><strong>{% trans %}This is not a valid student card UID{% endtrans %}</strong></p>
|
||||||
|
{% endif %}
|
||||||
<input type="submit" value="{% trans %}Go{% endtrans %}" />
|
<input type="submit" value="{% trans %}Go{% endtrans %}" />
|
||||||
</form>
|
</form>
|
||||||
<p>{% trans %}Basket: {% endtrans %}</p>
|
<h6>{% trans %}Registered cards{% endtrans %}</h6>
|
||||||
|
{% if customer.student_cards.exists() %}
|
||||||
|
<ul>
|
||||||
|
{% for card in customer.student_cards.all() %}
|
||||||
|
<li>{{ card.uid }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
{% trans %}No card registered{% endtrans %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="click_form">
|
||||||
|
<h5>{% trans %}Selling{% endtrans %}</h5>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
{% raw %}
|
||||||
|
<div class="important">
|
||||||
|
<p v-for="error in errors"><strong>{{ error }}</strong></p>
|
||||||
|
</div>
|
||||||
|
{% endraw %}
|
||||||
|
|
||||||
|
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="code_form" @submit.prevent="handle_code">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="code">
|
||||||
|
<input type="input" name="code" value="" class="focus" id="code_field"/>
|
||||||
|
<input type="submit" value="{% trans %}Go{% endtrans %}" />
|
||||||
|
</form>
|
||||||
|
<p>{% trans %}Basket: {% endtrans %}</p>
|
||||||
|
|
||||||
|
{% raw %}
|
||||||
|
<ul>
|
||||||
|
<li v-for="p_info,p_id in basket">
|
||||||
|
|
||||||
|
<form method="post" action="" class="inline del_product_form" @submit.prevent="handle_action">
|
||||||
|
<input type="hidden" name="csrfmiddlewaretoken" v-bind:value="js_csrf_token">
|
||||||
|
<input type="hidden" name="action" value="del_product">
|
||||||
|
<input type="hidden" name="product_id" v-bind:value="p_id">
|
||||||
|
<button type="submit"> - </button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{{ p_info["qty"] + p_info["bonus_qty"] }}
|
||||||
|
|
||||||
|
<form method="post" action="" class="inline add_product_form" @submit.prevent="handle_action">
|
||||||
|
<input type="hidden" name="csrfmiddlewaretoken" v-bind:value="js_csrf_token">
|
||||||
|
<input type="hidden" name="action" value="add_product">
|
||||||
|
<input type="hidden" name="product_id" v-bind:value="p_id">
|
||||||
|
<button type="submit"> + </button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{{ products[p_id].name }}: {{ (p_info["qty"]*p_info["price"]/100).toLocaleString(undefined, { minimumFractionDigits: 2 }) }} € <span v-if="p_info['bonus_qty'] > 0">P</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
<strong>Total: {{ sum_basket().toLocaleString(undefined, { minimumFractionDigits: 2 }) }} €</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="important">
|
||||||
|
<p v-for="error in errors"><strong>{{ error }}</strong></p>
|
||||||
|
</div>
|
||||||
|
{% endraw %}
|
||||||
|
|
||||||
|
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="finish">
|
||||||
|
<input type="submit" value="{% trans %}Finish{% endtrans %}" />
|
||||||
|
</form>
|
||||||
|
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="cancel">
|
||||||
|
<input type="submit" value="{% trans %}Cancel{% endtrans %}" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% if (counter.type == 'BAR' and barmens_can_refill) %}
|
||||||
|
<h5>{% trans %}Refilling{% endtrans %}</h5>
|
||||||
|
<div>
|
||||||
|
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ refill_form.as_p() }}
|
||||||
|
<input type="hidden" name="action" value="refill">
|
||||||
|
<input type="submit" value="{% trans %}Go{% endtrans %}" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="products">
|
||||||
<ul>
|
<ul>
|
||||||
{% for id,infos in request.session['basket']|dictsort %}
|
{% for category in categories.keys() -%}
|
||||||
{% set product = counter.products.filter(id=id).first() %}
|
<li><a href="#cat_{{ category|slugify }}">{{ category }}</a></li>
|
||||||
{% set s = infos['qty'] * infos['price'] / 100 %}
|
{%- endfor %}
|
||||||
<li>{{ del_product(id, '-', "inline") }} {{ infos['qty'] + infos['bonus_qty'] }} {{ add_product(id, '+', "inline") }}
|
|
||||||
{{ product.name }}: {{ "%0.2f"|format(s) }} €
|
|
||||||
{% if infos['bonus_qty'] %}
|
|
||||||
P
|
|
||||||
{% endif %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
</ul>
|
||||||
<p><strong>{% trans %}Total: {% endtrans %}{{ "%0.2f"|format(basket_total) }} €</strong></p>
|
|
||||||
<div class="important">
|
|
||||||
{% if request.session['too_young'] %}
|
|
||||||
<p><strong>{% trans %}Too young for that product{% endtrans %}</strong></p>
|
|
||||||
{% endif %}
|
|
||||||
{% if request.session['not_allowed'] %}
|
|
||||||
<p><strong>{% trans %}Not allowed for that product{% endtrans %}</strong></p>
|
|
||||||
{% endif %}
|
|
||||||
{% if request.session['no_age'] %}
|
|
||||||
<p><strong>{% trans %}No date of birth provided{% endtrans %}</strong></p>
|
|
||||||
{% endif %}
|
|
||||||
{% if request.session['not_enough'] %}
|
|
||||||
<p><strong>{% trans %}Not enough money{% endtrans %}</strong></p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="action" value="finish">
|
|
||||||
<input type="submit" value="{% trans %}Finish{% endtrans %}" />
|
|
||||||
</form>
|
|
||||||
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="action" value="cancel">
|
|
||||||
<input type="submit" value="{% trans %}Cancel{% endtrans %}" />
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% if counter.type == 'BAR' %}
|
|
||||||
<h5>{% trans %}Refilling{% endtrans %}</h5>
|
|
||||||
<div>
|
|
||||||
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ refill_form.as_p() }}
|
|
||||||
<input type="hidden" name="action" value="refill">
|
|
||||||
<input type="submit" value="{% trans %}Go{% endtrans %}" />
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div id="products">
|
|
||||||
<ul>
|
|
||||||
{% for category in categories.keys() -%}
|
{% for category in categories.keys() -%}
|
||||||
<li><a href="#cat_{{ category|slugify }}">{{ category }}</a></li>
|
<div id="cat_{{ category|slugify }}">
|
||||||
{%- endfor %}
|
<h5>{{ category }}</h5>
|
||||||
</ul>
|
{% for p in categories[category] -%}
|
||||||
{% for category in categories.keys() -%}
|
{% set file = None %}
|
||||||
<div id="cat_{{ category|slugify }}">
|
{% if p.icon %}
|
||||||
<h5>{{ category }}</h5>
|
{% set file = p.icon.url %}
|
||||||
{% for p in categories[category] -%}
|
{% else %}
|
||||||
{% set file = None %}
|
{% set file = static('core/img/na.gif') %}
|
||||||
{% if p.icon %}
|
{% endif %}
|
||||||
{% set file = p.icon.url %}
|
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="form_button add_product_form" @submit.prevent="handle_action">
|
||||||
{% else %}
|
{% csrf_token %}
|
||||||
{% set file = static('core/img/na.gif') %}
|
<input type="hidden" name="action" value="add_product">
|
||||||
{% endif %}
|
<input type="hidden" name="product_id" value="{{ p.id }}">
|
||||||
{% set prod = '<strong>%s</strong><hr><img src="%s" /><span>%s €<br>%s</span>' % (p.name, file, p.selling_price, p.code) %}
|
<button type="submit"><strong>{{ p.name }}</strong><hr><img src="{{ file }}" /><span>{{ p.selling_price }} €<br>{{ p.code }}</span></button>
|
||||||
{{ add_product(p.id, prod, "form_button") }}
|
</form>
|
||||||
|
{%- endfor %}
|
||||||
|
</div>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
</div>
|
</div>
|
||||||
{%- endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script>
|
|
||||||
document.getElementById("click_interface").scrollIntoView();
|
|
||||||
</script>
|
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
|
<script src="{{ static('core/js/vue.global.prod.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
$( function() {
|
$( function() {
|
||||||
var products = [
|
/* Vue.JS dynamic form */
|
||||||
|
const click_form_vue = Vue.createApp({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
js_csrf_token: "{{ csrf_token }}",
|
||||||
|
products: {
|
||||||
|
{% for p in products -%}
|
||||||
|
{{ p.id }}: {
|
||||||
|
code: "{{ p.code }}",
|
||||||
|
name: "{{ p.name }}",
|
||||||
|
selling_price: "{{ p.selling_price }}",
|
||||||
|
special_selling_price: "{{ p.special_selling_price }}",
|
||||||
|
},
|
||||||
|
{%- endfor %}
|
||||||
|
},
|
||||||
|
basket: {{ request.session["basket"]|tojson }},
|
||||||
|
errors: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
sum_basket() {
|
||||||
|
var vm = this;
|
||||||
|
var total = 0;
|
||||||
|
for(idx in vm.basket) {
|
||||||
|
var item = vm.basket[idx];
|
||||||
|
console.log(item);
|
||||||
|
total += item["qty"] * item["price"];
|
||||||
|
}
|
||||||
|
return total / 100;
|
||||||
|
},
|
||||||
|
handle_code(event) {
|
||||||
|
var vm = this;
|
||||||
|
var code = $(event.target).find("#code_field").val().toUpperCase();
|
||||||
|
console.log("Code:");
|
||||||
|
console.log(code);
|
||||||
|
if(code == "{% trans %}END{% endtrans %}" || code == "{% trans %}CAN{% endtrans %}") {
|
||||||
|
$(event.target).submit();
|
||||||
|
} else {
|
||||||
|
vm.handle_action(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handle_action(event) {
|
||||||
|
var vm = this;
|
||||||
|
var payload = $(event.target).serialize();
|
||||||
|
$.ajax({
|
||||||
|
type: 'post',
|
||||||
|
dataType: 'json',
|
||||||
|
data: payload,
|
||||||
|
success: function(response) {
|
||||||
|
vm.basket = response.basket;
|
||||||
|
vm.errors = [];
|
||||||
|
},
|
||||||
|
error: function(error) {
|
||||||
|
vm.basket = error.responseJSON.basket;
|
||||||
|
vm.errors = error.responseJSON.errors;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$('form.code_form #code_field').val("").focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).mount('#bar_ui');
|
||||||
|
|
||||||
|
/* Autocompletion in the code field */
|
||||||
|
var products_autocomplete = [
|
||||||
{% for p in products -%}
|
{% for p in products -%}
|
||||||
{
|
{
|
||||||
value: "{{ p.code }}",
|
value: "{{ p.code }}",
|
||||||
@ -166,6 +227,7 @@ $( function() {
|
|||||||
},
|
},
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
];
|
];
|
||||||
|
|
||||||
var quantity = "";
|
var quantity = "";
|
||||||
var search = "";
|
var search = "";
|
||||||
var pattern = /^(\d+x)?(.*)/i;
|
var pattern = /^(\d+x)?(.*)/i;
|
||||||
@ -183,21 +245,22 @@ $( function() {
|
|||||||
quantity = res[1] || "";
|
quantity = res[1] || "";
|
||||||
search = res[2];
|
search = res[2];
|
||||||
var matcher = new RegExp( $.ui.autocomplete.escapeRegex( search ), "i" );
|
var matcher = new RegExp( $.ui.autocomplete.escapeRegex( search ), "i" );
|
||||||
response($.grep( products, function( value ) {
|
response($.grep( products_autocomplete, function( value ) {
|
||||||
value = value.tags;
|
value = value.tags;
|
||||||
return matcher.test( value );
|
return matcher.test( value );
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
|
||||||
$( function() {
|
/* Accordion UI between basket and refills */
|
||||||
$("#bar_ui").accordion({
|
$("#click_form").accordion({
|
||||||
heightStyle: "content",
|
heightStyle: "content",
|
||||||
activate: function(event, ui){
|
activate: function(event, ui){
|
||||||
$(".focus").focus();
|
$(".focus").focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$("#products").tabs();
|
$("#products").tabs();
|
||||||
|
|
||||||
$("#code_field").focus();
|
$("#code_field").focus();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -12,6 +12,12 @@
|
|||||||
{% trans counter_name=counter %}{{ counter_name }} counter{% endtrans %}
|
{% trans counter_name=counter %}{{ counter_name }} counter{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block info_boxes %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block nav %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h3>{% trans counter_name=counter %}{{ counter_name }} counter{% endtrans %}</h3>
|
<h3>{% trans counter_name=counter %}{{ counter_name }} counter{% endtrans %}</h3>
|
||||||
|
|
||||||
|
@ -5,6 +5,12 @@
|
|||||||
{% trans counter_name=counter %}{{ counter_name }} last operations{% endtrans %}
|
{% trans counter_name=counter %}{{ counter_name }} last operations{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block info_boxes %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block nav %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h3>{% trans counter_name=counter %}{{ counter_name }} last operations{% endtrans %}</h3>
|
<h3>{% trans counter_name=counter %}{{ counter_name }} last operations{% endtrans %}</h3>
|
||||||
<h4>{% trans %}Refillings{% endtrans %}</h4>
|
<h4>{% trans %}Refillings{% endtrans %}</h4>
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
<th>{% trans %}Amount{% endtrans %}</th>
|
<th>{% trans %}Amount{% endtrans %}</th>
|
||||||
<th>{% trans %}Payment method{% endtrans %}</th>
|
<th>{% trans %}Payment method{% endtrans %}</th>
|
||||||
<th>{% trans %}Seller{% endtrans %}</th>
|
<th>{% trans %}Seller{% endtrans %}</th>
|
||||||
|
<th>{% trans %}Date{% endtrans %}
|
||||||
<th>{% trans %}Actions{% endtrans %}</th>
|
<th>{% trans %}Actions{% endtrans %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
{%- for refilling in object_list %}
|
{%- for refilling in object_list %}
|
||||||
@ -21,6 +22,7 @@
|
|||||||
<td>{{ refilling.amount }}</td>
|
<td>{{ refilling.amount }}</td>
|
||||||
<td>{{ refilling.payment_method }}</td>
|
<td>{{ refilling.payment_method }}</td>
|
||||||
<td>{{ refilling.operator }}</td>
|
<td>{{ refilling.operator }}</td>
|
||||||
|
<td>{{ refilling.date }}</td>
|
||||||
<td><a href="{{ url('counter:refilling_delete', refilling_id=refilling.id)}}">Delete</a></td>
|
<td><a href="{{ url('counter:refilling_delete', refilling_id=refilling.id)}}">Delete</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
@ -36,7 +36,10 @@ class CounterTest(TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
call_command("populate")
|
call_command("populate")
|
||||||
self.skia = User.objects.filter(username="skia").first()
|
self.skia = User.objects.filter(username="skia").first()
|
||||||
|
self.sli = User.objects.filter(username="sli").first()
|
||||||
|
self.krophil = User.objects.filter(username="krophil").first()
|
||||||
self.mde = Counter.objects.filter(name="MDE").first()
|
self.mde = Counter.objects.filter(name="MDE").first()
|
||||||
|
self.foyer = Counter.objects.get(id=2)
|
||||||
|
|
||||||
def test_full_click(self):
|
def test_full_click(self):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@ -68,20 +71,77 @@ class CounterTest(TestCase):
|
|||||||
location,
|
location,
|
||||||
{
|
{
|
||||||
"action": "refill",
|
"action": "refill",
|
||||||
"amount": "10",
|
"amount": "5",
|
||||||
"payment_method": "CASH",
|
"payment_method": "CASH",
|
||||||
"bank": "OTHER",
|
"bank": "OTHER",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
response = self.client.post(location, {"action": "code", "code": "BARB"})
|
response = self.client.post(location, {"action": "code", "code": "BARB"})
|
||||||
|
response = self.client.post(
|
||||||
|
location, {"action": "add_product", "product_id": "4"}
|
||||||
|
)
|
||||||
|
response = self.client.post(
|
||||||
|
location, {"action": "del_product", "product_id": "4"}
|
||||||
|
)
|
||||||
|
response = self.client.post(location, {"action": "code", "code": "2xdeco"})
|
||||||
|
response = self.client.post(location, {"action": "code", "code": "1xbarb"})
|
||||||
response = self.client.post(location, {"action": "code", "code": "fin"})
|
response = self.client.post(location, {"action": "code", "code": "fin"})
|
||||||
|
|
||||||
response_get = self.client.get(response.get("location"))
|
response_get = self.client.get(response.get("location"))
|
||||||
|
response_content = response_get.content.decode("utf-8")
|
||||||
|
self.assertTrue("<li>2 x Barbar" in str(response_content))
|
||||||
|
self.assertTrue("<li>2 x Déconsigne Eco-cup" in str(response_content))
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
"<p>Client : Richard Batsbak - Nouveau montant : 8.30"
|
"<p>Client : Richard Batsbak - Nouveau montant : 3.60"
|
||||||
in str(response_get.content)
|
in str(response_content)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("counter:login", kwargs={"counter_id": self.mde.id}),
|
||||||
|
{"username": self.sli.username, "password": "plop"},
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
location,
|
||||||
|
{
|
||||||
|
"action": "refill",
|
||||||
|
"amount": "5",
|
||||||
|
"payment_method": "CASH",
|
||||||
|
"bank": "OTHER",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertTrue(response.status_code == 200)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("counter:login", kwargs={"counter_id": self.foyer.id}),
|
||||||
|
{"username": self.krophil.username, "password": "plop"},
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("counter:details", kwargs={"counter_id": self.foyer.id})
|
||||||
|
)
|
||||||
|
|
||||||
|
counter_token = re.search(
|
||||||
|
r'name="counter_token" value="([^"]*)"', str(response.content)
|
||||||
|
).group(1)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("counter:details", kwargs={"counter_id": self.foyer.id}),
|
||||||
|
{"code": "4000k", "counter_token": counter_token},
|
||||||
|
)
|
||||||
|
location = response.get("location")
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
location,
|
||||||
|
{
|
||||||
|
"action": "refill",
|
||||||
|
"amount": "5",
|
||||||
|
"payment_method": "CASH",
|
||||||
|
"bank": "OTHER",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertTrue(response.status_code == 200)
|
||||||
|
|
||||||
|
|
||||||
class CounterStatsTest(TestCase):
|
class CounterStatsTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -38,16 +38,17 @@ from django.views.generic.edit import (
|
|||||||
from django.forms.models import modelform_factory
|
from django.forms.models import modelform_factory
|
||||||
from django.forms import CheckboxSelectMultiple
|
from django.forms import CheckboxSelectMultiple
|
||||||
from django.urls import reverse_lazy, reverse
|
from django.urls import reverse_lazy, reverse
|
||||||
from django.http import HttpResponseRedirect, HttpResponse
|
from django.http import HttpResponseRedirect, HttpResponse, JsonResponse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import DataError, transaction, models
|
from django.db import DataError, transaction, models
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import pytz
|
import pytz
|
||||||
from datetime import date, timedelta, datetime
|
from datetime import date, timedelta, datetime
|
||||||
|
from http import HTTPStatus
|
||||||
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
|
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
|
||||||
from ajax_select import make_ajax_field
|
from ajax_select import make_ajax_field
|
||||||
|
|
||||||
@ -69,6 +70,7 @@ from counter.models import (
|
|||||||
Permanency,
|
Permanency,
|
||||||
)
|
)
|
||||||
from accounting.models import CurrencyField
|
from accounting.models import CurrencyField
|
||||||
|
from core.views.forms import TzAwareDateTimeField
|
||||||
|
|
||||||
|
|
||||||
class CounterAdminMixin(View):
|
class CounterAdminMixin(View):
|
||||||
@ -357,6 +359,34 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
pk_url_kwarg = "counter_id"
|
pk_url_kwarg = "counter_id"
|
||||||
current_tab = "counter"
|
current_tab = "counter"
|
||||||
|
|
||||||
|
def render_to_response(self, *args, **kwargs):
|
||||||
|
if self.request.is_ajax(): # JSON response for AJAX requests
|
||||||
|
response = {"errors": []}
|
||||||
|
status = HTTPStatus.OK
|
||||||
|
|
||||||
|
if self.request.session["too_young"]:
|
||||||
|
response["errors"].append(_("Too young for that product"))
|
||||||
|
status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS
|
||||||
|
if self.request.session["not_allowed"]:
|
||||||
|
response["errors"].append(_("Not allowed for that product"))
|
||||||
|
status = HTTPStatus.FORBIDDEN
|
||||||
|
if self.request.session["no_age"]:
|
||||||
|
response["errors"].append(_("No date of birth provided"))
|
||||||
|
status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS
|
||||||
|
if self.request.session["not_enough"]:
|
||||||
|
response["errors"].append(_("Not enough money"))
|
||||||
|
status = HTTPStatus.PAYMENT_REQUIRED
|
||||||
|
|
||||||
|
if len(response["errors"]) > 1:
|
||||||
|
status = HTTPStatus.BAD_REQUEST
|
||||||
|
|
||||||
|
response["basket"] = self.request.session["basket"]
|
||||||
|
|
||||||
|
return JsonResponse(response, status=status)
|
||||||
|
|
||||||
|
else: # Standard HTML page
|
||||||
|
return super().render_to_response(*args, **kwargs)
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"])
|
self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"])
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
@ -370,7 +400,9 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
)
|
)
|
||||||
or len(obj.get_barmen_list()) < 1
|
or len(obj.get_barmen_list()) < 1
|
||||||
):
|
):
|
||||||
raise PermissionDenied
|
return HttpResponseRedirect(
|
||||||
|
reverse_lazy("counter:details", kwargs={"counter_id": obj.id})
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
@ -394,7 +426,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
""" Handle the many possibilities of the post request """
|
"""Handle the many possibilities of the post request"""
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
self.refill_form = None
|
self.refill_form = None
|
||||||
if (self.object.type != "BAR" and not request.user.is_authenticated) or (
|
if (self.object.type != "BAR" and not request.user.is_authenticated) or (
|
||||||
@ -590,7 +622,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def del_product(self, request):
|
def del_product(self, request):
|
||||||
""" Delete a product from the basket """
|
"""Delete a product from the basket"""
|
||||||
pid = str(request.POST["product_id"])
|
pid = str(request.POST["product_id"])
|
||||||
product = self.get_product(pid)
|
product = self.get_product(pid)
|
||||||
if pid in request.session["basket"]:
|
if pid in request.session["basket"]:
|
||||||
@ -632,7 +664,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
return self.render_to_response(context)
|
return self.render_to_response(context)
|
||||||
|
|
||||||
def finish(self, request):
|
def finish(self, request):
|
||||||
""" Finish the click session, and validate the basket """
|
"""Finish the click session, and validate the basket"""
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
request.session["last_basket"] = []
|
request.session["last_basket"] = []
|
||||||
if self.sum_basket(request) > self.customer.amount:
|
if self.sum_basket(request) > self.customer.amount:
|
||||||
@ -684,7 +716,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def cancel(self, request):
|
def cancel(self, request):
|
||||||
""" Cancel the click session """
|
"""Cancel the click session"""
|
||||||
kwargs = {"counter_id": self.object.id}
|
kwargs = {"counter_id": self.object.id}
|
||||||
request.session.pop("basket", None)
|
request.session.pop("basket", None)
|
||||||
return HttpResponseRedirect(
|
return HttpResponseRedirect(
|
||||||
@ -693,7 +725,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
|
|
||||||
def refill(self, request):
|
def refill(self, request):
|
||||||
"""Refill the customer's account"""
|
"""Refill the customer's account"""
|
||||||
if self.get_object().type == "BAR":
|
if self.get_object().type == "BAR" and self.object.can_refill():
|
||||||
form = RefillForm(request.POST)
|
form = RefillForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
form.instance.counter = self.object
|
form.instance.counter = self.object
|
||||||
@ -706,7 +738,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add customer to the context """
|
"""Add customer to the context"""
|
||||||
kwargs = super(CounterClick, self).get_context_data(**kwargs)
|
kwargs = super(CounterClick, self).get_context_data(**kwargs)
|
||||||
kwargs["products"] = self.object.products.select_related("product_type")
|
kwargs["products"] = self.object.products.select_related("product_type")
|
||||||
kwargs["categories"] = {}
|
kwargs["categories"] = {}
|
||||||
@ -719,6 +751,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
kwargs["basket_total"] = self.sum_basket(self.request)
|
kwargs["basket_total"] = self.sum_basket(self.request)
|
||||||
kwargs["refill_form"] = self.refill_form or RefillForm()
|
kwargs["refill_form"] = self.refill_form or RefillForm()
|
||||||
kwargs["student_card_max_uid_size"] = StudentCard.UID_SIZE
|
kwargs["student_card_max_uid_size"] = StudentCard.UID_SIZE
|
||||||
|
kwargs["barmens_can_refill"] = self.object.can_refill()
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
@ -1360,7 +1393,7 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add form to the context """
|
"""Add form to the context"""
|
||||||
kwargs = super(CounterLastOperationsView, self).get_context_data(**kwargs)
|
kwargs = super(CounterLastOperationsView, self).get_context_data(**kwargs)
|
||||||
threshold = timezone.now() - timedelta(
|
threshold = timezone.now() - timedelta(
|
||||||
minutes=settings.SITH_LAST_OPERATIONS_LIMIT
|
minutes=settings.SITH_LAST_OPERATIONS_LIMIT
|
||||||
@ -1422,7 +1455,7 @@ class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
return reverse_lazy("counter:details", kwargs={"counter_id": self.object.id})
|
return reverse_lazy("counter:details", kwargs={"counter_id": self.object.id})
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add form to the context """
|
"""Add form to the context"""
|
||||||
kwargs = super(CounterCashSummaryView, self).get_context_data(**kwargs)
|
kwargs = super(CounterCashSummaryView, self).get_context_data(**kwargs)
|
||||||
kwargs["form"] = self.form
|
kwargs["form"] = self.form
|
||||||
return kwargs
|
return kwargs
|
||||||
@ -1448,7 +1481,7 @@ class CounterStatView(DetailView, CounterAdminMixin):
|
|||||||
template_name = "counter/stats.jinja"
|
template_name = "counter/stats.jinja"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add stats to the context """
|
"""Add stats to the context"""
|
||||||
from django.db.models import Sum, Case, When, F, DecimalField
|
from django.db.models import Sum, Case, When, F, DecimalField
|
||||||
|
|
||||||
kwargs = super(CounterStatView, self).get_context_data(**kwargs)
|
kwargs = super(CounterStatView, self).get_context_data(**kwargs)
|
||||||
@ -1553,18 +1586,8 @@ class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class CashSummaryFormBase(forms.Form):
|
class CashSummaryFormBase(forms.Form):
|
||||||
begin_date = forms.DateTimeField(
|
begin_date = TzAwareDateTimeField(label=_("Begin date"), required=False)
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
end_date = TzAwareDateTimeField(label=_("End date"), required=False)
|
||||||
label=_("Begin date"),
|
|
||||||
required=False,
|
|
||||||
widget=SelectDateTime,
|
|
||||||
)
|
|
||||||
end_date = forms.DateTimeField(
|
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
|
||||||
label=_("End date"),
|
|
||||||
required=False,
|
|
||||||
widget=SelectDateTime,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
||||||
@ -1578,7 +1601,7 @@ class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
|||||||
paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH
|
paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add sums to the context """
|
"""Add sums to the context"""
|
||||||
kwargs = super(CashSummaryListView, self).get_context_data(**kwargs)
|
kwargs = super(CashSummaryListView, self).get_context_data(**kwargs)
|
||||||
form = CashSummaryFormBase(self.request.GET)
|
form = CashSummaryFormBase(self.request.GET)
|
||||||
kwargs["form"] = form
|
kwargs["form"] = form
|
||||||
@ -1629,7 +1652,7 @@ class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView):
|
|||||||
current_tab = "invoices_call"
|
current_tab = "invoices_call"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add sums to the context """
|
"""Add sums to the context"""
|
||||||
kwargs = super(InvoiceCallView, self).get_context_data(**kwargs)
|
kwargs = super(InvoiceCallView, self).get_context_data(**kwargs)
|
||||||
kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC")
|
kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC")
|
||||||
start_date = None
|
start_date = None
|
||||||
|
@ -8,7 +8,7 @@ C'est un projet bénévole qui tire ses origines des années 2000. Il s'agit de
|
|||||||
Pourquoi réécrire le site
|
Pourquoi réécrire le site
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
L'ancienne version du site, sobrement baptisée `ae2 <https://ae-dev.utbm.fr/ae/ae2>`_ présentait un nombre impressionnant de fonctionnalités. Il avait été écrit en PHP et se basait sur son propre framework maison.
|
L'ancienne version du site, sobrement baptisée `ae2 <https://github.com/ae-utbm/sith2>`_ présentait un nombre impressionnant de fonctionnalités. Il avait été écrit en PHP et se basait sur son propre framework maison.
|
||||||
|
|
||||||
Malheureusement, son entretiens était plus ou moins hasardeux et son framework reposait sur des principes assez différents de ce qui se fait aujourd'hui, rendant la maintenance difficile. De plus, la version de PHP qu'il utilisait était plus que déprécié et à l'heure de l'arrivée de PHP 7 et de sa non rétrocompatibilité il était vital de faire quelque chose. Il a donc été décidé de le réécrire.
|
Malheureusement, son entretiens était plus ou moins hasardeux et son framework reposait sur des principes assez différents de ce qui se fait aujourd'hui, rendant la maintenance difficile. De plus, la version de PHP qu'il utilisait était plus que déprécié et à l'heure de l'arrivée de PHP 7 et de sa non rétrocompatibilité il était vital de faire quelque chose. Il a donc été décidé de le réécrire.
|
||||||
|
|
||||||
|
@ -146,7 +146,7 @@ GitLab
|
|||||||
~~~~~~
|
~~~~~~
|
||||||
|
|
||||||
| `Site officiel <https://about.gitlab.com/>`__
|
| `Site officiel <https://about.gitlab.com/>`__
|
||||||
| `Instance de l'AE <https://ae-dev.utbm.fr/>`__
|
| `Instance de l'AE <https://github.com/ae-utbm/>`__
|
||||||
|
|
||||||
GitLab est une alternative libre à GitHub. C'est une plate-forme avec interface web permettant de déposer du code géré avec Git offrant également de l'intégration continue et du déploiement automatique.
|
GitLab est une alternative libre à GitHub. C'est une plate-forme avec interface web permettant de déposer du code géré avec Git offrant également de l'intégration continue et du déploiement automatique.
|
||||||
|
|
||||||
@ -160,12 +160,13 @@ Sentry
|
|||||||
|
|
||||||
Sentry est une plate-forme libre qui permet de se tenir informer des bugs qui ont lieu sur le site. À chaque crash du logiciel (erreur 500), une erreur est envoyée sur la plate-forme et est indiqué précisément à quelle ligne de code celle-ci a eu lieu, à quelle heure, combien de fois, avec quel navigateur la page a été visitée et même éventuellement un commentaire de l'utilisateur qui a rencontré le bug.
|
Sentry est une plate-forme libre qui permet de se tenir informer des bugs qui ont lieu sur le site. À chaque crash du logiciel (erreur 500), une erreur est envoyée sur la plate-forme et est indiqué précisément à quelle ligne de code celle-ci a eu lieu, à quelle heure, combien de fois, avec quel navigateur la page a été visitée et même éventuellement un commentaire de l'utilisateur qui a rencontré le bug.
|
||||||
|
|
||||||
Virtualenv
|
Poetry
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
`Utiliser virtualenv <https://python-guide-pt-br.readthedocs.io/fr/latest/dev/virtualenvs.html>`__
|
`Utiliser Poetry <https://python-poetry.org/docs/basic-usage/>`__
|
||||||
|
|
||||||
Virtualenv est un utilitaire permettant d'installer un environnement Python de manière locale sans avoir besoin des droits root pour y installer des dépendances. Il est très utilisé pour gérer plusieurs projets différents en parallèles puisqu'il permet d'avoir sur sa machine plusieurs environnements différents et donc plusieurs versions d'une même dépendance dans plusieurs projets différent sans impacter le système sur lequel le tout est installé.
|
Poetry est un utilitaire qui permet de créer et gérer des environements Python de manière simple et intuitive. Il permet également de gérer et mettre à jour le fichier de dépendances.
|
||||||
|
L'avantage d'utiliser poetry (et les environnements virtuels en général) est de pouvoir gérer plusieurs projets différents en parallèles puisqu'il permet d'avoir sur sa machine plusieurs environnements différents et donc plusieurs versions d'une même dépendance dans plusieurs projets différent sans impacter le système sur lequel le tout est installé.
|
||||||
|
|
||||||
Black
|
Black
|
||||||
~~~~~
|
~~~~~
|
||||||
|
87
doc/devenv/populate.rst
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
Générer l'environnement avec populate
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
Lors de l'installation du site en local (via la commande `setup`), la commande **populate** est appelée.
|
||||||
|
|
||||||
|
Cette commande génère entièrement la base de données de développement. Elle se situe dans `core/management/commands/populate.py`.
|
||||||
|
|
||||||
|
Utilisations :
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
./manage.py setup # Génère la base de test
|
||||||
|
./manage.py setup --prod # Ne génère que le schéma de base et les données strictement nécessaires au fonctionnement
|
||||||
|
|
||||||
|
Les données générées du site dev
|
||||||
|
================================
|
||||||
|
|
||||||
|
Par défaut, la base de données du site de prod contient des données nécessaires au fonctionnement du site comme les groupes (voir :ref:`groups-list`), un utilisateur root, les clubs de base et quelques autres instances indispensables. En plus de ces données par défaut, la base de données du site de dev contient des données de test (*fixtures*) pour remplir le site et le rendre exploitable.
|
||||||
|
|
||||||
|
**Voici les clubs générés pour le site de dev :**
|
||||||
|
|
||||||
|
* AE
|
||||||
|
|
||||||
|
- Bibo'UT
|
||||||
|
- Carte AE
|
||||||
|
- Guy'UT
|
||||||
|
|
||||||
|
+ Woenzel'UT
|
||||||
|
|
||||||
|
- Troll Penché
|
||||||
|
|
||||||
|
* BdF
|
||||||
|
* Laverie
|
||||||
|
|
||||||
|
**Voici utilisateurs générés pour le site de dev :**
|
||||||
|
|
||||||
|
Le mot de passe de tous les utilisateurs est **plop**.
|
||||||
|
|
||||||
|
* **root** -> Dans le groupe Root et cotisant
|
||||||
|
* **skia** -> responsable info AE et cotisant, barmen MDE
|
||||||
|
* **public** -> utilisateur non cotisant et sans groupe
|
||||||
|
* **subscriber** -> utilisateur cotisant et sans groupe
|
||||||
|
* **old_subscriber** -> utilisateur anciennement cotisant et sans groupe
|
||||||
|
* **counter** -> administrateur comptoir
|
||||||
|
* **comptable** -> administrateur comptabilité
|
||||||
|
* **guy** -> utilisateur non cotisant et sans groupe
|
||||||
|
* **rbatsbak** -> utilisateur non cotisant et sans groupe
|
||||||
|
* **sli** -> cotisant avec carte étudiante attachée au compte, barmen MDE
|
||||||
|
* **krophil** -> cotisant avec des plein d'écocups, barmen foyer
|
||||||
|
* **comunity** -> administrateur communication
|
||||||
|
* **tutu** -> administrateur pédagogie
|
||||||
|
|
||||||
|
Ajouter des fixtures
|
||||||
|
====================
|
||||||
|
.. role:: python(code)
|
||||||
|
:language: python
|
||||||
|
|
||||||
|
Les fixtures sont contenus dans *core/management/commands/populate.py* après la ligne 205 : :python:`if not options["prod"]:`.
|
||||||
|
|
||||||
|
Pour ajouter une fixtures, il faut :
|
||||||
|
|
||||||
|
* importer la classe à instancier en début de fichier
|
||||||
|
* créer un objet avec les attributs nécessaires en fin de fichier
|
||||||
|
* enregistrer l'objet dans la base de données
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# Exemple pour ajouter un utilisateur
|
||||||
|
|
||||||
|
# Importation de la classe
|
||||||
|
import core.models import User
|
||||||
|
|
||||||
|
# [...]
|
||||||
|
|
||||||
|
# Création de l'objet
|
||||||
|
jesus = User(
|
||||||
|
username="jc",
|
||||||
|
last_name="Jesus",
|
||||||
|
first_name="Christ",
|
||||||
|
email="son@god.cloud",
|
||||||
|
date_of_birth="2020-24-12",
|
||||||
|
is_superuser=False,
|
||||||
|
is_staff=True,
|
||||||
|
)
|
||||||
|
jesus.set_password("plop")
|
||||||
|
# Enregistrement dans la base de donnée
|
||||||
|
jesus.save()
|
@ -12,7 +12,7 @@ Pour modifier les cotisations disponnibles, tout se gère dans la configuration
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
SITH_SUBSCRIPTIONS = {
|
SITH_SUBSCRIPTIONS = {
|
||||||
# Voici un échantillon de la véritable configuration à l'heure de l'écriture.
|
# Voici un échantillon de la véritable configuration à l'heure de l'écriture.
|
||||||
|