mirror of
https://github.com/ae-utbm/sith.git
synced 2025-07-12 21:09:24 +00:00
Compare commits
558 Commits
5ccb499665
...
features/m
Author | SHA1 | Date | |
---|---|---|---|
f234f94171 | |||
fa758867cc | |||
f41ff281fb | |||
321cb72ca8 | |||
c436d39014 | |||
b9298792ae | |||
4f9d5ae7b1 | |||
259337dff1 | |||
288764b551 | |||
910a6f8b34 | |||
fa6527b24f | |||
e638bc04ed | |||
4830c3ea2d | |||
982fc09908 | |||
b12e8dc147 | |||
dd3ad42eb5 | |||
b7f20fed6c | |||
73305c0b28 | |||
310f1a2283 | |||
ce3e2bb32b | |||
7b6eed9a47 | |||
13bae8d2fa | |||
6b2027550c | |||
022b365bb2 | |||
d8867fc9ea | |||
118c58b5fa | |||
823bd578f2 | |||
3e5c36b39e | |||
8fb0897160 | |||
6a0a8e8ab4 | |||
e43d53e564 | |||
d4a5039efc | |||
35506e0175 | |||
1c27831f92 | |||
cdbf07a835 | |||
b92580943a | |||
60eff1000f | |||
96510b270d | |||
1281104d96 | |||
3c1724fa81 | |||
1630af4fbd | |||
e76e2b1537 | |||
6c276dc596 | |||
d3c115e3f9 | |||
c245ef7149 | |||
8b09ba2924 | |||
52eb310f95 | |||
5bff38fc7b | |||
2813a59323 | |||
eef33fa263 | |||
241d3cea53 | |||
89d6db4208 | |||
e0ad288cf4 | |||
f4d7fae8ca | |||
95a7493fc1 | |||
8243dbcbef | |||
c3a4071627 | |||
cef3f22e0d | |||
c206b965ad | |||
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
|
|||
3501703c15 | |||
129f2e53ee | |||
209867b3a8
|
|||
59511d255f | |||
f42daa01c5 | |||
29ee1b05af | |||
42055b9001 | |||
00c96f5b71 | |||
5cc7eff94f | |||
28077ef0b0 | |||
143b128891 | |||
6b06b647bc | |||
413c613c9f | |||
1c0d15ba2a | |||
28bd6b8708 | |||
419a48ac3a | |||
6fce27113a | |||
53a7633700 | |||
4094394cef | |||
f533c39e67
|
|||
86bc491df4 | |||
4759551c16 | |||
b057dbfd60 | |||
bddb88d97f | |||
dbe44a9c1c | |||
eeb791c460 | |||
6d0eba6bcf | |||
4d04b21f04 | |||
2f1b26053b | |||
1848945d64 | |||
9278419345
|
|||
566dcc7aee
|
|||
a6088c0e4a
|
|||
60c9498a56 | |||
241650c171
|
|||
811809895e
|
|||
fe9164bfef
|
|||
ad3f003fbb | |||
7ecb057b68 | |||
e932abfa74 | |||
0011f4c7b0 | |||
13312e9879 | |||
ced90c23db | |||
42f5773f51 | |||
b270c76249 | |||
34df825718 | |||
aac4e3b99c | |||
5a55a6c642 | |||
65c3483c1f | |||
660a3161f5 | |||
9e6c4b32e3 | |||
25225fc451
|
|||
c3f2d0a134
|
|||
cd2d3ee6b4
|
|||
81fcf411c1
|
|||
d7075eb762
|
|||
cf3f5ea60c
|
|||
59185ab2a8
|
|||
a177fa8232
|
|||
308cf30a5a
|
|||
99c8d95443
|
|||
97c316b62e
|
|||
90921fd4cd
|
|||
296cc4144c
|
|||
f7548ab8d1
|
|||
3cb306bc91
|
|||
c20d5855e4
|
|||
00bd60ef4f
|
|||
b8c7fb6f74
|
|||
df531198c9
|
|||
12b6f0d488
|
|||
6cc234e8d3
|
|||
4dadb1dbc0
|
|||
2616e8b24c
|
|||
be855c6c90
|
|||
7be9077fce | |||
d48c09a914
|
|||
d5c3dbf864 | |||
2a9b89fd2a
|
|||
c73f4ca847
|
|||
d63b5335d4
|
|||
a766f7137c
|
|||
5c3c14ab37
|
|||
775413ac7e
|
|||
1f271c75f0
|
|||
4df152185e
|
|||
c83b30f27b
|
|||
db10f7b963
|
|||
ed68c2cb38
|
|||
a6c8dea190
|
|||
124eaf42cd
|
|||
5489096bf5
|
|||
3a425c6792
|
|||
8809753108
|
|||
4428a2e89c
|
|||
0616597bf2
|
|||
782a763046
|
|||
8dcade6890
|
|||
dd49d71cb7
|
|||
b0b52fd714
|
|||
ef40baaa84
|
|||
7c259bf26b
|
|||
05e5008305
|
|||
448f5ff40f | |||
5482f1174d | |||
2da0560ec8
|
|||
5151fc3792
|
|||
fd5cd56f81 | |||
35d9c05abf
|
|||
fcb3035b67
|
|||
b7db969f08 | |||
14303fd46c | |||
5e6b17cd19 | |||
8232ff59a0 | |||
411c117f0f | |||
298499c749
|
|||
b8ad2d4835
|
|||
d37eb134e2 | |||
63ec5d68f4
|
|||
8330e1eaf2
|
|||
1f86827e46
|
|||
321e5e3ff5
|
|||
3eb8292d15 | |||
5a3f90fd28
|
|||
eb975f4de1 | |||
405b938e08
|
|||
f899e32fb0 | |||
9181e77d55
|
|||
f1b3a174b6
|
|||
eb9821ed36 | |||
defb7fb3a3 | |||
83e225a744
|
|||
f30bea3dc9
|
|||
a69f7b12b1
|
|||
ca042fe75e | |||
dc9111dbcd
|
|||
8a16a66299 | |||
d7a7613807
|
|||
3fc8688941
|
|||
d7b351a1aa | |||
9e0c4e70d4 | |||
2232c495be | |||
18b1bea664 | |||
a5d5c41dd6
|
|||
66d5c71a92
|
|||
824ea37f44 | |||
eb29f98c37 | |||
d903dc58cf
|
|||
f09de0ab7d
|
|||
d29603c584
|
|||
3380980c5c
|
|||
38ef13d9b6 | |||
6c43b1c43d
|
|||
2b34c46412
|
|||
f9227fa29d | |||
96a3eaff1c
|
|||
65cb85a887
|
|||
640a72c52d
|
|||
9b7b96a310 | |||
b18746e769
|
|||
a2b431b1ab | |||
d844bccb04
|
|||
49f928e754 | |||
07fc1014be
|
|||
facb6faf75 | |||
e72338a7d9
|
|||
f37c022538
|
|||
5229628d48
|
|||
b4b7bf05b4
|
|||
231415a772
|
|||
f052d307d7
|
|||
f15971cecf
|
|||
99cf59c7a4
|
|||
0d13014e8a
|
|||
fd1f89de1d
|
|||
78b616427f | |||
c15ea345dd
|
|||
1d319e90f0
|
|||
e6e500e2f9
|
|||
cf1ec1dc86
|
|||
46a042cde2
|
|||
52129d7511
|
|||
d03835d737
|
|||
b4b7817baa
|
|||
d85152e58c
|
|||
f118040432
|
|||
9f1aff8c07
|
|||
94bbdf372b
|
|||
240d94bd57
|
|||
3ee7ff2752
|
|||
2c5385cf5c
|
|||
c8a691044f
|
|||
f93eaff876
|
|||
10faa14bef
|
|||
30ccbdc32d
|
|||
79243aece3
|
|||
a61322b83f
|
|||
3df73f4d1f
|
|||
7165a63e97
|
|||
2404edd289
|
|||
3bff09b04c
|
|||
28748af5d3
|
|||
a56a4e2cb8
|
|||
339497b2c2
|
|||
c05168a2b5
|
|||
782ee35779
|
|||
43acee8f1b
|
|||
4a19441a17
|
|||
11acf5897f
|
|||
4be99fe828 | |||
601193ff3c | |||
f500dec1f1 | |||
bfb7380715
|
|||
3e3c576ad7
|
|||
2aa1314fac
|
|||
3063e4a24f
|
|||
6f8ec4740c
|
|||
cbcd84c931
|
|||
e475273cd3
|
|||
851231869b
|
|||
3376f4dfb4
|
|||
205f93569a
|
|||
4f7a8661ba | |||
6e7d351e8e
|
|||
73f1927ce4 | |||
56e3f39de1 | |||
75a2aefd69
|
|||
55e822412a
|
|||
171d9a4381
|
|||
806084e707
|
|||
04009a6a5b
|
|||
3d0f5c0a15
|
|||
437af4dd04 | |||
ba61455017 | |||
624f1d653d
|
|||
e21821ace5
|
|||
22c028af11
|
|||
f0560f0d2a
|
|||
502ae09523
|
|||
2cbef2babc
|
|||
e11d45b51e
|
|||
061320a5df
|
|||
2aa465b138
|
|||
d18f0aa829
|
|||
e7b8ddb631
|
|||
358a625cc4
|
|||
d44fa73b2a
|
14
.envrc
Normal file
14
.envrc
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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"
|
8
.github/actions/compile_messages/action.yml
vendored
Normal file
8
.github/actions/compile_messages/action.yml
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
name: "Compile messages"
|
||||||
|
description: "Compile the gettext translation messages"
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Setup project
|
||||||
|
run: poetry run ./manage.py compilemessages
|
||||||
|
shell: bash
|
53
.github/actions/setup_project/action.yml
vendored
Normal file
53
.github/actions/setup_project/action.yml
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
name: "Setup project"
|
||||||
|
description: "Setup Python and Poetry"
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Install apt packages
|
||||||
|
uses: awalsh128/cache-apt-pkgs-action@latest
|
||||||
|
with:
|
||||||
|
packages: gettext libgraphviz-dev
|
||||||
|
version: 1.0 # increment to reset cache
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install gettext libgraphviz-dev
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Set up python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.10"
|
||||||
|
|
||||||
|
- name: Load cached Poetry installation
|
||||||
|
id: cached-poetry
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.local
|
||||||
|
key: poetry-0 # increment to reset cache
|
||||||
|
|
||||||
|
- name: Install Poetry
|
||||||
|
if: steps.cached-poetry.outputs.cache-hit != 'true'
|
||||||
|
shell: bash
|
||||||
|
run: curl -sSL https://install.python-poetry.org | python3 -
|
||||||
|
|
||||||
|
- name: Check pyproject.toml syntax
|
||||||
|
shell: bash
|
||||||
|
run: poetry check
|
||||||
|
|
||||||
|
- name: Load cached dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.cache/pypoetry
|
||||||
|
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-poetry-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: poetry install -E testing -E docs
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Compile gettext messages
|
||||||
|
run: poetry run ./manage.py compilemessages
|
||||||
|
shell: bash
|
13
.github/auto_assign.yml
vendored
Normal file
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
|
18
.github/dependabot.yml
vendored
Normal file
18
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "pip" # See documentation for possible values
|
||||||
|
directory: "/" # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
# Raise pull requests for version updates
|
||||||
|
# to pip against the `develop` branch
|
||||||
|
target-branch: "taiste"
|
||||||
|
reviewers:
|
||||||
|
- "ae-utbm/developpers-v3"
|
||||||
|
commit-message:
|
||||||
|
prefix: "[UPDATE] "
|
45
.github/workflows/ci.yml
vendored
Normal file
45
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
name: Sith 3 CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master, taiste, features/**]
|
||||||
|
pull_request:
|
||||||
|
branches: [master, taiste]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
black:
|
||||||
|
name: Black format
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Setup Project
|
||||||
|
uses: ./.github/actions/setup_project
|
||||||
|
- run: poetry run black --check .
|
||||||
|
|
||||||
|
tests:
|
||||||
|
name: Run tests and generate coverage report
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.14
|
||||||
|
env:
|
||||||
|
discovery.type: single-node
|
||||||
|
ports:
|
||||||
|
- 9200
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- uses: ./.github/actions/setup_project
|
||||||
|
- uses: ./.github/actions/compile_messages
|
||||||
|
- name: Run tests
|
||||||
|
run: poetry run coverage run ./manage.py test
|
||||||
|
- name: Generate coverage report
|
||||||
|
run: |
|
||||||
|
poetry run coverage report
|
||||||
|
poetry run coverage html
|
||||||
|
- name: Archive code coverage results
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: coverage-report
|
||||||
|
path: coverage_report
|
64
.github/workflows/deploy.yml
vendored
Normal file
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/sith/.local/bin:$PATH"
|
||||||
|
pushd ${{secrets.SITH_PATH}}
|
||||||
|
|
||||||
|
git pull
|
||||||
|
poetry install
|
||||||
|
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
|
63
.github/workflows/taiste.yml
vendored
Normal file
63
.github/workflows/taiste.yml
vendored
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
name: Sith3 taiste
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [taiste]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deployment:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: taiste
|
||||||
|
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 install
|
||||||
|
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: taiste
|
||||||
|
timeout-minutes: 30
|
||||||
|
needs: deployment
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Sentry Release
|
||||||
|
uses: getsentry/action-release@v1.2.0
|
||||||
|
env:
|
||||||
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||||
|
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||||
|
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
||||||
|
with:
|
||||||
|
environment: taiste
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -4,12 +4,17 @@ db.sqlite3
|
|||||||
*.mo
|
*.mo
|
||||||
*__pycache__*
|
*__pycache__*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
pyrightconfig.json
|
||||||
|
dist/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.idea/
|
||||||
env/
|
env/
|
||||||
doc/html
|
doc/html
|
||||||
data/
|
data/
|
||||||
|
galaxy/test_galaxy_state.json
|
||||||
/static/
|
/static/
|
||||||
sith/settings_custom.py
|
sith/settings_custom.py
|
||||||
sith/search_indexes/
|
sith/search_indexes/
|
||||||
.coverage
|
.coverage
|
||||||
coverage_report/
|
coverage_report/
|
||||||
|
doc/_build
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
stages:
|
|
||||||
- test
|
|
||||||
|
|
||||||
test:
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
- apt-get update
|
|
||||||
- apt-get install -y gettext python3-xapian
|
|
||||||
- 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
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- coverage_report/
|
|
||||||
|
|
||||||
black:
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
- pip install black
|
|
||||||
- black --check .
|
|
19
.mailmap
Normal file
19
.mailmap
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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>
|
||||||
|
Maréchal <thgirod@hotmail.com>
|
26
.readthedocs.yml
Normal file
26
.readthedocs.yml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Read the Docs configuration file
|
||||||
|
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||||
|
|
||||||
|
# Required
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
# Allow installing xapian-bindings in pip
|
||||||
|
build:
|
||||||
|
apt_packages:
|
||||||
|
- libxapian-dev
|
||||||
|
|
||||||
|
# Build documentation in the doc/ directory with Sphinx
|
||||||
|
sphinx:
|
||||||
|
configuration: doc/conf.py
|
||||||
|
|
||||||
|
# Optionally build your docs in additional formats such as PDF and ePub
|
||||||
|
formats: all
|
||||||
|
|
||||||
|
# Optionally set the version of Python and requirements required to build your docs
|
||||||
|
python:
|
||||||
|
version: "3.8"
|
||||||
|
install:
|
||||||
|
- method: pip
|
||||||
|
path: .
|
||||||
|
extra_requirements:
|
||||||
|
- docs
|
106
CONTRIBUTING.md
106
CONTRIBUTING.md
@ -1,106 +0,0 @@
|
|||||||
*Contribuer c'est la vie*
|
|
||||||
=========================
|
|
||||||
|
|
||||||
Hey ! Tu veux devenir un mec bien et en plus devenir bon en python si tu l'es pas déjà ?
|
|
||||||
Il se trouve que le sith AE prévu pour l'été 2016 a besoin de toi !
|
|
||||||
|
|
||||||
Pour faire le sith, on utilise le framework Web [Django](https://docs.djangoproject.com/fr/1.11/intro/)
|
|
||||||
N'hésite pas à lire les tutos et à nous demander (ae.info@utbm.fr).
|
|
||||||
|
|
||||||
Bon, passons aux choses sérieuses, pour bidouiller le sith sans le casser :
|
|
||||||
Ben en fait, tu peux pas le casser, tu vas juste t'amuser comme un petit fou sur un clone du sith.
|
|
||||||
|
|
||||||
C'est pas compliqué, il suffit d'avoir [Git](http://www.git-scm.com/book/fr/v2), python et pip (pour faciliter la gestion des paquets python).
|
|
||||||
|
|
||||||
Tout d'abord, tu vas avoir besoin d'un compte Gitlab pour pouvoir te connecter.
|
|
||||||
Ensuite, tu fais :
|
|
||||||
`git clone https://ae-dev.utbm.fr/ae/Sith.git`
|
|
||||||
Avec cette commande, tu clones le sith AE dans le dossier courant.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd Sith
|
|
||||||
virtualenv --system-site-packages --python=python3 env
|
|
||||||
source env/bin/activate
|
|
||||||
pip install -r requirements.txt
|
|
||||||
./manage runserver
|
|
||||||
```
|
|
||||||
|
|
||||||
Attention aux dépendances système, à voir dans le README.md
|
|
||||||
|
|
||||||
Maintenant, faut passer le sith en mode debug dans le fichier de settings personnalisé.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo "DEBUG=True" > sith/settings_custom.py
|
|
||||||
echo 'SITH_URL = "localhost:8000"' >> sith/settings_custom.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Enfin, il s'agit de créer la base de donnée de test lors de la première utilisation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./manage.py setup
|
|
||||||
```
|
|
||||||
|
|
||||||
Et pour lancer le sith, tu fais `python3 manage.py runserver`
|
|
||||||
|
|
||||||
Voilà, c'est le sith AE. Il y a des issues dans le gitlab qui sont à régler. Si tu as un domaine qui t'intéresse, une appli que tu voudrais développer, n'hésites pas et contacte-nous.
|
|
||||||
Va, et que l'AE soit avec toi.
|
|
||||||
|
|
||||||
# Black
|
|
||||||
|
|
||||||
Pour uniformiser le formattage du code nous utilisons [Black](https://github.com/ambv/black). Cela permet d'avoir le même codestyle et donc le codereview prend moins de temps. Tout étant dans le même format, il est plus facile pour chacun de comprendre le code de chacun ! Cela permet aussi d'éviter des erreurs (y parait 🤷♀️).
|
|
||||||
|
|
||||||
Installation de black:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install black
|
|
||||||
```
|
|
||||||
|
|
||||||
## Sous VsCode:
|
|
||||||
Attention, pour VsCode, Black doit être installé dans votre virtualenv !
|
|
||||||
Ajouter ces deux lignes dans les settings de VsCode
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"python.formatting.provider": "black",
|
|
||||||
"editor.formatOnSave": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Sous Sublime Text
|
|
||||||
Il faut installer le plugin [sublack](https://packagecontrol.io/packages/sublack) depuis Package Control.
|
|
||||||
|
|
||||||
Il suffit ensuite d'ajouter dans les settings du projet (ou en global)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"sublack.black_on_save": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Si vous utilisez le plugin [anaconda](http://damnwidget.github.io/anaconda/), pensez à modifier les paramètres du linter pep8 pour éviter de recevoir des warnings dans le formatage de black
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"pep8_ignore": [
|
|
||||||
"E203",
|
|
||||||
"E266",
|
|
||||||
"E501",
|
|
||||||
"W503"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Sites et doc cools
|
|
||||||
------------------
|
|
||||||
|
|
||||||
[Classy Class-Based Views](http://ccbv.co.uk/projects/Django/1.11/)
|
|
||||||
|
|
||||||
Helpers:
|
|
||||||
|
|
||||||
`./manage.py makemessages --ignore "env/*" -e py,jinja`
|
|
||||||
|
|
||||||
`for f in $(find . -name "*.py" ! -path "*migration*" ! -path "./env/*" ! -path "./doc/*"); do cat ./doc/header "$f" > /tmp/temp && mv /tmp/temp "$f"; done`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
3
CONTRIBUTING.rst
Normal file
3
CONTRIBUTING.rst
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Pour contribuer au projet, vous pouvez vous référer à la documentation disponible à https://sith-ae.readthedocs.io/.
|
||||||
|
|
||||||
|
Et n'oubliez pas, contribuer c'est la vie !
|
21
LICENSE.old
21
LICENSE.old
@ -1,21 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2016 Skia
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
134
README.md
134
README.md
@ -1,104 +1,40 @@
|
|||||||
[](https://ae-dev.utbm.fr/ae/Sith/commits/master)
|
<p align="center">
|
||||||
[](https://ae-dev.utbm.fr/ae/Sith/commits/master)
|
<a href="#">
|
||||||
[](https://github.com/ambv/black)
|
<img src="https://img.shields.io/badge/Code%20Style-Black-000000?style=for-the-badge">
|
||||||
[](https://ae-dev.zulipchat.com)
|
</a>
|
||||||
|
<a href="#">
|
||||||
|
<img src="https://img.shields.io/github/checks-status/ae-utbm/sith3/master?logo=github&style=for-the-badge&label=BUILD">
|
||||||
|
</a>
|
||||||
|
<a href="https://sith-ae.readthedocs.io/">
|
||||||
|
<img src="https://img.shields.io/readthedocs/sith-ae?logo=readthedocs&style=for-the-badge">
|
||||||
|
</a>
|
||||||
|
<a href="https://discord.gg/XK9WfPsUFm">
|
||||||
|
<img src="https://img.shields.io/discord/971448179075731476?label=Discord&logo=discord&style=for-the-badge">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
## Sith AE
|
<h3 align="center">This is the source code of the UTBM's student association available at https://ae.utbm.fr/.</h3>
|
||||||
|
|
||||||
### Dependencies:
|
<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>
|
||||||
See requirements.txt
|
|
||||||
|
|
||||||
You may need to install some dev libraries like `libmysqlclient-dev`, `libssl-dev`, `libjpeg-dev`, `python3-xapian`, or `zlib1g-dev` to install all the
|
<h4>If you want to contribute, here's how we recommend to read the docs:</h4>
|
||||||
requiered dependancies with pip. You may also need `mysql-client`. Don't also forget `python3-dev` if you don't have it
|
|
||||||
already.
|
|
||||||
|
|
||||||
You can check all of them with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install libmysqlclient-dev libssl-dev libjpeg-dev zlib1g-dev python3-dev libffi-dev python3-dev libgraphviz-dev pkg-config python3-xapian gettext
|
|
||||||
```
|
|
||||||
|
|
||||||
On macos, you will need homebrew
|
|
||||||
|
|
||||||
```bash
|
|
||||||
brew install xapian
|
|
||||||
```
|
|
||||||
|
|
||||||
If it doesn't work it's because it need [this pull request](https://github.com/Homebrew/homebrew-core/pull/34835) to be validated.
|
|
||||||
|
|
||||||
The development is done with sqlite, but it is advised to set a more robust DBMS for production (Postgresql for example)
|
|
||||||
|
|
||||||
### Get started
|
|
||||||
|
|
||||||
To start working on the project, just run the following commands:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://ae-dev.utbm.fr/ae/Sith.git
|
|
||||||
cd Sith
|
|
||||||
virtualenv --system-site-packages --python=python3 env
|
|
||||||
source env/bin/activate
|
|
||||||
pip install -r requirements.txt
|
|
||||||
./manage.py setup
|
|
||||||
```
|
|
||||||
|
|
||||||
To start the simple development server, just run `python3 manage.py runserver`
|
|
||||||
|
|
||||||
For more informations, check out the CONTRIBUTING.md file.
|
|
||||||
|
|
||||||
### Logging errors with sentry
|
|
||||||
|
|
||||||
To connect the app to sentry.io, you must set the variable SENTRY_DSN in your settings custom. It's composed of the full link given on your sentry project
|
|
||||||
|
|
||||||
### Generating documentation
|
|
||||||
|
|
||||||
There is a Doxyfile at the root of the project, meaning that if you have Doxygen, you can run `doxygen Doxyfile` to
|
|
||||||
generate a complete HTML documentation that will be available in the *./doc/html/* folder.
|
|
||||||
|
|
||||||
### Collecting statics for production:
|
|
||||||
|
|
||||||
We use scss in the project. In development environment (DEBUG=True), scss is compiled every time the file is needed. For production, it assumes you have already compiled every files and to do so, you need to use the following commands :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./manage.py collectstatic # To collect statics
|
|
||||||
./manage.py compilestatic # To compile scss in those statics
|
|
||||||
```
|
|
||||||
|
|
||||||
### Misc about development
|
|
||||||
|
|
||||||
#### Controlling the rights
|
|
||||||
|
|
||||||
When you need to protect an object, there are three levels:
|
|
||||||
* Editing the object properties
|
|
||||||
* Editing the object various values
|
|
||||||
* Viewing the object
|
|
||||||
|
|
||||||
Now you have many solutions in your model:
|
|
||||||
* You can define a `is_owned_by(self, user)`, a `can_be_edited_by(self, user)`, and/or a `can_be_viewed_by(self, user)`
|
|
||||||
method, each returning True is the user passed can edit/view the object, False otherwise.
|
|
||||||
This allows you to make complex request when the group solution is not powerful enough.
|
|
||||||
It's useful too when you want to define class-wide permissions, e.g. the club members, that are viewable only for
|
|
||||||
Subscribers.
|
|
||||||
* You can add an `owner_group` field, as a ForeignKey to Group. Second is an `edit_groups` field, as a ManyToMany to
|
|
||||||
Group, and third is a `view_groups`, same as for edit.
|
|
||||||
|
|
||||||
Finally, when building a class based view, which is highly advised, you just have to inherit it from CanEditPropMixin,
|
|
||||||
CanEditMixin, or CanViewMixin, which are located in core.views. Your view will then be protected using either the
|
|
||||||
appropriate group fields, or the right method to check user permissions.
|
|
||||||
|
|
||||||
#### Counting the number of line of code
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install cloc
|
|
||||||
cloc --exclude-dir=doc,env .
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Updating doc/SYNTAX.md
|
|
||||||
|
|
||||||
If you make an update in the Markdown syntax parser, it's good to document
|
|
||||||
update the syntax reference page in `doc/SYNTAX.md`. But updating this file will
|
|
||||||
break the tests if you don't update the corresponding `doc/SYNTAX.html` file at
|
|
||||||
the same time.
|
|
||||||
To do that, simply run `./manage.py markdown > doc/SYNTAX.html`,
|
|
||||||
and the tests should pass again.
|
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p align="justify">
|
||||||
|
First, it's advised to read the about part of the project to understand the goals and the mindset of the current and previous maintainers and know what to expect to learn.
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p align="justify">
|
||||||
|
If in the first part you realize that you need more background about what we use, we provide some links to tutorials and documentation at the end of our documentation. Feel free to use it and complete it with what you found helpful.
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p align="justify">
|
||||||
|
Keep in mind that this documentation is thought to be read in order.
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
> This project is licensed under GNU GPL, see the LICENSE file at the top of the repository for more details.
|
||||||
|
9
TODO.md
9
TODO.md
@ -1,9 +0,0 @@
|
|||||||
# TODO
|
|
||||||
|
|
||||||
## Easter eggs
|
|
||||||
|
|
||||||
* 'A' 'L' 'L' 'O': Entendre le Allooo de Madame Coucoune
|
|
||||||
* idem avec cacafe
|
|
||||||
* Un meat spin quelque part
|
|
||||||
* Konami code
|
|
||||||
|
|
@ -1,23 +1,15 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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.
|
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
@ -1,24 +1,16 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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.
|
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@ -4,10 +4,10 @@ from __future__ import unicode_literals
|
|||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
import accounting.models
|
import accounting.models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@ -243,6 +243,7 @@ class Migration(migrations.Migration):
|
|||||||
verbose_name="accounting type",
|
verbose_name="accounting type",
|
||||||
to="accounting.AccountingType",
|
to="accounting.AccountingType",
|
||||||
blank=True,
|
blank=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -267,6 +268,7 @@ class Migration(migrations.Migration):
|
|||||||
verbose_name="simplified accounting types",
|
verbose_name="simplified accounting types",
|
||||||
to="accounting.AccountingType",
|
to="accounting.AccountingType",
|
||||||
related_name="simplified_types",
|
related_name="simplified_types",
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("club", "0001_initial"),
|
("club", "0001_initial"),
|
||||||
("accounting", "0001_initial"),
|
("accounting", "0001_initial"),
|
||||||
@ -22,6 +22,7 @@ class Migration(migrations.Migration):
|
|||||||
verbose_name="invoice",
|
verbose_name="invoice",
|
||||||
to="core.SithFile",
|
to="core.SithFile",
|
||||||
blank=True,
|
blank=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
@ -31,12 +32,14 @@ class Migration(migrations.Migration):
|
|||||||
verbose_name="journal",
|
verbose_name="journal",
|
||||||
to="accounting.GeneralJournal",
|
to="accounting.GeneralJournal",
|
||||||
related_name="operations",
|
related_name="operations",
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="operation",
|
model_name="operation",
|
||||||
name="linked_operation",
|
name="linked_operation",
|
||||||
field=models.OneToOneField(
|
field=models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
blank=True,
|
blank=True,
|
||||||
to="accounting.Operation",
|
to="accounting.Operation",
|
||||||
null=True,
|
null=True,
|
||||||
@ -54,6 +57,7 @@ class Migration(migrations.Migration):
|
|||||||
verbose_name="simple type",
|
verbose_name="simple type",
|
||||||
to="accounting.SimplifiedAccountingType",
|
to="accounting.SimplifiedAccountingType",
|
||||||
blank=True,
|
blank=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
@ -63,6 +67,7 @@ class Migration(migrations.Migration):
|
|||||||
verbose_name="club account",
|
verbose_name="club account",
|
||||||
to="accounting.ClubAccount",
|
to="accounting.ClubAccount",
|
||||||
related_name="journals",
|
related_name="journals",
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
@ -72,20 +77,27 @@ class Migration(migrations.Migration):
|
|||||||
verbose_name="bank account",
|
verbose_name="bank account",
|
||||||
to="accounting.BankAccount",
|
to="accounting.BankAccount",
|
||||||
related_name="club_accounts",
|
related_name="club_accounts",
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="clubaccount",
|
model_name="clubaccount",
|
||||||
name="club",
|
name="club",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
verbose_name="club", to="club.Club", related_name="club_account"
|
verbose_name="club",
|
||||||
|
to="club.Club",
|
||||||
|
related_name="club_account",
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="bankaccount",
|
model_name="bankaccount",
|
||||||
name="club",
|
name="club",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
verbose_name="club", to="club.Club", related_name="bank_accounts"
|
verbose_name="club",
|
||||||
|
to="club.Club",
|
||||||
|
related_name="bank_accounts",
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
|
@ -6,7 +6,6 @@ import phonenumber_field.modelfields
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("accounting", "0002_auto_20160824_2152")]
|
dependencies = [("accounting", "0002_auto_20160824_2152")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -6,7 +6,6 @@ import django.db.models.deletion
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("accounting", "0003_auto_20160824_2203")]
|
dependencies = [("accounting", "0003_auto_20160824_2203")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@ -29,6 +28,7 @@ class Migration(migrations.Migration):
|
|||||||
related_name="labels",
|
related_name="labels",
|
||||||
verbose_name="club account",
|
verbose_name="club account",
|
||||||
to="accounting.ClubAccount",
|
to="accounting.ClubAccount",
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -5,7 +5,6 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("accounting", "0004_auto_20161005_1505")]
|
dependencies = [("accounting", "0004_auto_20161005_1505")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -1,33 +1,25 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.core.exceptions import ValidationError
|
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
|
||||||
@ -74,7 +66,7 @@ class Company(models.Model):
|
|||||||
"""
|
"""
|
||||||
Method to see if that object can be edited by the given user
|
Method to see if that object can be edited by the given user
|
||||||
"""
|
"""
|
||||||
if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -110,7 +102,12 @@ class BankAccount(models.Model):
|
|||||||
name = models.CharField(_("name"), max_length=30)
|
name = models.CharField(_("name"), max_length=30)
|
||||||
iban = models.CharField(_("iban"), max_length=255, blank=True)
|
iban = models.CharField(_("iban"), max_length=255, blank=True)
|
||||||
number = models.CharField(_("account number"), max_length=255, blank=True)
|
number = models.CharField(_("account number"), max_length=255, blank=True)
|
||||||
club = models.ForeignKey(Club, related_name="bank_accounts", verbose_name=_("club"))
|
club = models.ForeignKey(
|
||||||
|
Club,
|
||||||
|
related_name="bank_accounts",
|
||||||
|
verbose_name=_("club"),
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Bank account")
|
verbose_name = _("Bank account")
|
||||||
@ -120,7 +117,9 @@ class BankAccount(models.Model):
|
|||||||
"""
|
"""
|
||||||
Method to see if that object can be edited by the given user
|
Method to see if that object can be edited by the given user
|
||||||
"""
|
"""
|
||||||
if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
if user.is_anonymous:
|
||||||
|
return False
|
||||||
|
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||||
return True
|
return True
|
||||||
m = self.club.get_membership_for(user)
|
m = self.club.get_membership_for(user)
|
||||||
if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
||||||
@ -136,9 +135,17 @@ class BankAccount(models.Model):
|
|||||||
|
|
||||||
class ClubAccount(models.Model):
|
class ClubAccount(models.Model):
|
||||||
name = models.CharField(_("name"), max_length=30)
|
name = models.CharField(_("name"), max_length=30)
|
||||||
club = models.ForeignKey(Club, related_name="club_account", verbose_name=_("club"))
|
club = models.ForeignKey(
|
||||||
|
Club,
|
||||||
|
related_name="club_account",
|
||||||
|
verbose_name=_("club"),
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
bank_account = models.ForeignKey(
|
bank_account = models.ForeignKey(
|
||||||
BankAccount, related_name="club_accounts", verbose_name=_("bank account")
|
BankAccount,
|
||||||
|
related_name="club_accounts",
|
||||||
|
verbose_name=_("bank account"),
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -149,7 +156,9 @@ class ClubAccount(models.Model):
|
|||||||
"""
|
"""
|
||||||
Method to see if that object can be edited by the given user
|
Method to see if that object can be edited by the given user
|
||||||
"""
|
"""
|
||||||
if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
if user.is_anonymous:
|
||||||
|
return False
|
||||||
|
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -203,7 +212,11 @@ class GeneralJournal(models.Model):
|
|||||||
name = models.CharField(_("name"), max_length=40)
|
name = models.CharField(_("name"), max_length=40)
|
||||||
closed = models.BooleanField(_("is closed"), default=False)
|
closed = models.BooleanField(_("is closed"), default=False)
|
||||||
club_account = models.ForeignKey(
|
club_account = models.ForeignKey(
|
||||||
ClubAccount, related_name="journals", null=False, verbose_name=_("club account")
|
ClubAccount,
|
||||||
|
related_name="journals",
|
||||||
|
null=False,
|
||||||
|
verbose_name=_("club account"),
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
amount = CurrencyField(_("amount"), default=0)
|
amount = CurrencyField(_("amount"), default=0)
|
||||||
effective_amount = CurrencyField(_("effective_amount"), default=0)
|
effective_amount = CurrencyField(_("effective_amount"), default=0)
|
||||||
@ -216,7 +229,9 @@ class GeneralJournal(models.Model):
|
|||||||
"""
|
"""
|
||||||
Method to see if that object can be edited by the given user
|
Method to see if that object can be edited by the given user
|
||||||
"""
|
"""
|
||||||
if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
if user.is_anonymous:
|
||||||
|
return False
|
||||||
|
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||||
return True
|
return True
|
||||||
if self.club_account.can_be_edited_by(user):
|
if self.club_account.can_be_edited_by(user):
|
||||||
return True
|
return True
|
||||||
@ -226,7 +241,7 @@ class GeneralJournal(models.Model):
|
|||||||
"""
|
"""
|
||||||
Method to see if that object can be edited by the given user
|
Method to see if that object can be edited by the given user
|
||||||
"""
|
"""
|
||||||
if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||||
return True
|
return True
|
||||||
if self.club_account.can_be_edited_by(user):
|
if self.club_account.can_be_edited_by(user):
|
||||||
return True
|
return True
|
||||||
@ -263,7 +278,11 @@ class Operation(models.Model):
|
|||||||
|
|
||||||
number = models.IntegerField(_("number"))
|
number = models.IntegerField(_("number"))
|
||||||
journal = models.ForeignKey(
|
journal = models.ForeignKey(
|
||||||
GeneralJournal, related_name="operations", null=False, verbose_name=_("journal")
|
GeneralJournal,
|
||||||
|
related_name="operations",
|
||||||
|
null=False,
|
||||||
|
verbose_name=_("journal"),
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
amount = CurrencyField(_("amount"))
|
amount = CurrencyField(_("amount"))
|
||||||
date = models.DateField(_("date"))
|
date = models.DateField(_("date"))
|
||||||
@ -282,6 +301,7 @@ class Operation(models.Model):
|
|||||||
verbose_name=_("invoice"),
|
verbose_name=_("invoice"),
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
done = models.BooleanField(_("is done"), default=False)
|
done = models.BooleanField(_("is done"), default=False)
|
||||||
simpleaccounting_type = models.ForeignKey(
|
simpleaccounting_type = models.ForeignKey(
|
||||||
@ -290,6 +310,7 @@ class Operation(models.Model):
|
|||||||
verbose_name=_("simple type"),
|
verbose_name=_("simple type"),
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
accounting_type = models.ForeignKey(
|
accounting_type = models.ForeignKey(
|
||||||
"AccountingType",
|
"AccountingType",
|
||||||
@ -297,6 +318,7 @@ class Operation(models.Model):
|
|||||||
verbose_name=_("accounting type"),
|
verbose_name=_("accounting type"),
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
label = models.ForeignKey(
|
label = models.ForeignKey(
|
||||||
"Label",
|
"Label",
|
||||||
@ -328,6 +350,7 @@ class Operation(models.Model):
|
|||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
default=None,
|
default=None,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -397,7 +420,9 @@ class Operation(models.Model):
|
|||||||
"""
|
"""
|
||||||
Method to see if that object can be edited by the given user
|
Method to see if that object can be edited by the given user
|
||||||
"""
|
"""
|
||||||
if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
if user.is_anonymous:
|
||||||
|
return False
|
||||||
|
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||||
return True
|
return True
|
||||||
if self.journal.closed:
|
if self.journal.closed:
|
||||||
return False
|
return False
|
||||||
@ -410,7 +435,7 @@ class Operation(models.Model):
|
|||||||
"""
|
"""
|
||||||
Method to see if that object can be edited by the given user
|
Method to see if that object can be edited by the given user
|
||||||
"""
|
"""
|
||||||
if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||||
return True
|
return True
|
||||||
if self.journal.closed:
|
if self.journal.closed:
|
||||||
return False
|
return False
|
||||||
@ -466,7 +491,9 @@ class AccountingType(models.Model):
|
|||||||
"""
|
"""
|
||||||
Method to see if that object can be edited by the given user
|
Method to see if that object can be edited by the given user
|
||||||
"""
|
"""
|
||||||
if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
if user.is_anonymous:
|
||||||
|
return False
|
||||||
|
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -487,6 +514,7 @@ class SimplifiedAccountingType(models.Model):
|
|||||||
AccountingType,
|
AccountingType,
|
||||||
related_name="simplified_types",
|
related_name="simplified_types",
|
||||||
verbose_name=_("simplified accounting types"),
|
verbose_name=_("simplified accounting types"),
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -518,7 +546,10 @@ class Label(models.Model):
|
|||||||
|
|
||||||
name = models.CharField(_("label"), max_length=64)
|
name = models.CharField(_("label"), max_length=64)
|
||||||
club_account = models.ForeignKey(
|
club_account = models.ForeignKey(
|
||||||
ClubAccount, related_name="labels", verbose_name=_("club account")
|
ClubAccount,
|
||||||
|
related_name="labels",
|
||||||
|
verbose_name=_("club account"),
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -533,6 +564,8 @@ class Label(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
|
if user.is_anonymous:
|
||||||
|
return False
|
||||||
return self.club_account.is_owned_by(user)
|
return self.club_account.is_owned_by(user)
|
||||||
|
|
||||||
def can_be_edited_by(self, user):
|
def can_be_edited_by(self, user):
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<hr>
|
<hr>
|
||||||
<h2>{% trans %}Bank account: {% endtrans %}{{ object.name }}</h2>
|
<h2>{% trans %}Bank account: {% endtrans %}{{ object.name }}</h2>
|
||||||
{% if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) and not object.club_accounts.exists() %}
|
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) and not object.club_accounts.exists() %}
|
||||||
<a href="{{ url('accounting:bank_delete', b_account_id=object.id) }}">{% trans %}Delete{% endtrans %}</a>
|
<a href="{{ url('accounting:bank_delete', b_account_id=object.id) }}">{% trans %}Delete{% endtrans %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<h4>{% trans %}Infos{% endtrans %}</h4>
|
<h4>{% trans %}Infos{% endtrans %}</h4>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<h4>
|
<h4>
|
||||||
{% trans %}Accounting{% endtrans %}
|
{% trans %}Accounting{% endtrans %}
|
||||||
</h4>
|
</h4>
|
||||||
{% if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
|
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
|
||||||
<p><a href="{{ url('accounting:simple_type_list') }}">{% trans %}Manage simplified types{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:simple_type_list') }}">{% trans %}Manage simplified types{% endtrans %}</a></p>
|
||||||
<p><a href="{{ url('accounting:type_list') }}">{% trans %}Manage accounting types{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:type_list') }}">{% trans %}Manage accounting types{% endtrans %}</a></p>
|
||||||
<p><a href="{{ url('accounting:bank_new') }}">{% trans %}New bank account{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:bank_new') }}">{% trans %}New bank account{% endtrans %}</a></p>
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
{% if user.is_root and not object.journals.exists() %}
|
{% if user.is_root and not object.journals.exists() %}
|
||||||
<a href="{{ url('accounting:club_delete', c_account_id=object.id) }}">{% trans %}Delete{% endtrans %}</a>
|
<a href="{{ url('accounting:club_delete', c_account_id=object.id) }}">{% trans %}Delete{% endtrans %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
|
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
|
||||||
<p><a href="{{ url('accounting:label_new') }}?parent={{ object.id }}">{% trans %}New label{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:label_new') }}?parent={{ object.id }}">{% trans %}New label{% endtrans %}</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p><a href="{{ url('accounting:label_list', clubaccount_id=object.id) }}">{% trans %}Label list{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:label_list', clubaccount_id=object.id) }}">{% trans %}Label list{% endtrans %}</a></p>
|
||||||
@ -56,7 +56,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<td> <a href="{{ url('accounting:journal_details', j_id=j.id) }}">{% trans %}View{% endtrans %}</a>
|
<td> <a href="{{ url('accounting:journal_details', j_id=j.id) }}">{% trans %}View{% endtrans %}</a>
|
||||||
<a href="{{ url('accounting:journal_edit', j_id=j.id) }}">{% trans %}Edit{% endtrans %}</a>
|
<a href="{{ url('accounting:journal_edit', j_id=j.id) }}">{% trans %}Edit{% endtrans %}</a>
|
||||||
{% if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) and j.operations.count() == 0 %}
|
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) and j.operations.count() == 0 %}
|
||||||
<a href="{{ url('accounting:journal_delete', j_id=j.id) }}">{% trans %}Delete{% endtrans %}</a>
|
<a href="{{ url('accounting:journal_delete', j_id=j.id) }}">{% trans %}Delete{% endtrans %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
@ -6,11 +6,12 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="accounting">
|
<div id="accounting">
|
||||||
{% if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) or user.is_root %}
|
{% if user.is_root
|
||||||
|
or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
|
||||||
|
%}
|
||||||
<p><a href="{{ url('accounting:co_new') }}">{% trans %}Create new company{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:co_new') }}">{% trans %}Create new company{% endtrans %}</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<br/>
|
||||||
</br>
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -84,10 +84,13 @@
|
|||||||
<td>-</td>
|
<td>-</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>
|
<td>
|
||||||
{% if o.journal.club_account.bank_account.name != "AE TI" and o.journal.club_account.bank_account.name != "TI" or user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
|
{%
|
||||||
{% if not o.journal.closed %}
|
if o.journal.club_account.bank_account.name not in ["AE TI", "TI"]
|
||||||
<a href="{{ url('accounting:op_edit', op_id=o.id) }}">{% trans %}Edit{% endtrans %}</a>
|
or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
|
||||||
{% endif %}
|
%}
|
||||||
|
{% if not o.journal.closed %}
|
||||||
|
<a href="{{ url('accounting:op_edit', op_id=o.id) }}">{% trans %}Edit{% endtrans %}</a>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td><a href="{{ url('accounting:op_pdf', op_id=o.id) }}">{% trans %}Generate{% endtrans %}</a></td>
|
<td><a href="{{ url('accounting:op_pdf', op_id=o.id) }}">{% trans %}Generate{% endtrans %}</a></td>
|
||||||
|
@ -20,14 +20,14 @@
|
|||||||
{% for k,v in statement.items() %}
|
{% for k,v in statement.items() %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ k }}</td>
|
<td>{{ k }}</td>
|
||||||
<td>{{ v }}</td>
|
<td>{{ "%.2f" % v }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<p><strong>{% trans %}Amount: {% endtrans %}</strong>{{ object.amount }} €</p>
|
<p><strong>{% trans %}Amount: {% endtrans %}</strong>{{ "%.2f" % object.amount }} €</p>
|
||||||
<p><strong>{% trans %}Effective amount: {% endtrans %}</strong>{{ object.effective_amount }} €</p>
|
<p><strong>{% trans %}Effective amount: {% endtrans %}</strong>{{ "%.2f" %object.effective_amount }} €</p>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -18,12 +18,12 @@
|
|||||||
{% for k,v in dict['CREDIT'].items() %}
|
{% for k,v in dict['CREDIT'].items() %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ k }}</td>
|
<td>{{ k }}</td>
|
||||||
<td>{{ v }}</td>
|
<td>{{ "%.2f" % v }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% trans %}Total: {% endtrans %}{{ dict['CREDIT_sum'] }}
|
{% trans %}Total: {% endtrans %}{{ "%.2f" % dict['CREDIT_sum'] }}
|
||||||
|
|
||||||
<h6>{% trans %}Debit{% endtrans %}</h6>
|
<h6>{% trans %}Debit{% endtrans %}</h6>
|
||||||
<table>
|
<table>
|
||||||
@ -37,19 +37,19 @@
|
|||||||
{% for k,v in dict['DEBIT'].items() %}
|
{% for k,v in dict['DEBIT'].items() %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ k }}</td>
|
<td>{{ k }}</td>
|
||||||
<td>{{ v }}</td>
|
<td>{{ "%.2f" % v }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% trans %}Total: {% endtrans %}{{ dict['DEBIT_sum'] }}
|
{% trans %}Total: {% endtrans %}{{ "%.2f" % dict['DEBIT_sum'] }}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h3>{% trans %}Statement by nature: {% endtrans %} {{ object.name }}</h3>
|
<h3>{% trans %}Statement by nature: {% endtrans %} {{ object.name }}</h3>
|
||||||
|
|
||||||
{% for k,v in statement.items() %}
|
{% for k,v in statement.items() %}
|
||||||
<h4 style="background: lightblue; padding: 4px;">{{ k }} : {{ v['CREDIT_sum'] - v['DEBIT_sum'] }}</h4>
|
<h4 style="background: lightblue; padding: 4px;">{{ k }} : {{ "%.2f" % (v['CREDIT_sum'] - v['DEBIT_sum']) }}</h4>
|
||||||
{{ display_tables(v) }}
|
{{ display_tables(v) }}
|
||||||
<hr>
|
<hr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -28,14 +28,14 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<td></td>
|
<td></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>{{ credit_statement[key] }}</td>
|
<td>{{ "%.2f" % credit_statement[key] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<p>Total : {{ total_credit }}</p>
|
<p>Total : {{ "%.2f" % total_credit }}</p>
|
||||||
|
|
||||||
<h4>{% trans %}Debit{% endtrans %}</h4>
|
<h4>{% trans %}Debit{% endtrans %}</h4>
|
||||||
|
|
||||||
@ -56,13 +56,13 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<td></td>
|
<td></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>{{ debit_statement[key] }}</td>
|
<td>{{ "%.2f" % debit_statement[key] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<p>Total : {{ total_debit }}</p>
|
<p>Total : {{ "%.2f" % total_debit }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<hr>
|
<hr>
|
||||||
<p><a href="{{ url('accounting:club_details', c_account_id=object.id) }}">{% trans %}Back to club account{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:club_details', c_account_id=object.id) }}">{% trans %}Back to club account{% endtrans %}</a></p>
|
||||||
{% if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
|
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
|
||||||
<p><a href="{{ url('accounting:label_new') }}?parent={{ object.id }}">{% trans %}New label{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:label_new') }}?parent={{ object.id }}">{% trans %}New label{% endtrans %}</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if object.labels.all() %}
|
{% if object.labels.all() %}
|
||||||
@ -21,7 +21,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
{% for l in object.labels.all() %}
|
{% for l in object.labels.all() %}
|
||||||
<li><a href="{{ url('accounting:label_edit', label_id=l.id) }}">{{ l }}</a>
|
<li><a href="{{ url('accounting:label_edit', label_id=l.id) }}">{{ l }}</a>
|
||||||
{% if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
|
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
|
||||||
-
|
-
|
||||||
<a href="{{ url('accounting:label_delete', label_id=l.id) }}">{% trans %}Delete{% endtrans %}</a>
|
<a href="{{ url('accounting:label_delete', label_id=l.id) }}">{% trans %}Delete{% endtrans %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,31 +1,23 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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.test import TestCase
|
from django.test import TestCase
|
||||||
from django.core.urlresolvers 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 (
|
||||||
@ -39,7 +31,6 @@ from accounting.models import (
|
|||||||
|
|
||||||
class RefoundAccountTest(TestCase):
|
class RefoundAccountTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
call_command("populate")
|
|
||||||
self.skia = User.objects.filter(username="skia").first()
|
self.skia = User.objects.filter(username="skia").first()
|
||||||
# reffil skia's account
|
# reffil skia's account
|
||||||
self.skia.customer.amount = 800
|
self.skia.customer.amount = 800
|
||||||
@ -81,7 +72,6 @@ class RefoundAccountTest(TestCase):
|
|||||||
|
|
||||||
class JournalTest(TestCase):
|
class JournalTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
call_command("populate")
|
|
||||||
self.journal = GeneralJournal.objects.filter(id=1).first()
|
self.journal = GeneralJournal.objects.filter(id=1).first()
|
||||||
|
|
||||||
def test_permission_granted(self):
|
def test_permission_granted(self):
|
||||||
@ -109,7 +99,9 @@ class JournalTest(TestCase):
|
|||||||
|
|
||||||
class OperationTest(TestCase):
|
class OperationTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
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 +150,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 +183,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 +210,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 +237,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": "",
|
||||||
@ -272,30 +264,50 @@ class OperationTest(TestCase):
|
|||||||
|
|
||||||
def test_nature_statement(self):
|
def test_nature_statement(self):
|
||||||
self.client.login(username="comptable", password="plop")
|
self.client.login(username="comptable", password="plop")
|
||||||
response_get = self.client.get(
|
response = self.client.get(
|
||||||
reverse("accounting:journal_nature_statement", args=[self.journal.id])
|
reverse("accounting:journal_nature_statement", args=[self.journal.id])
|
||||||
)
|
)
|
||||||
self.assertTrue(
|
self.assertContains(response, "bob (Troll Penché) : 3.00", status_code=200)
|
||||||
"bob (Troll Pench\\xc3\\xa9) : 3.00" in str(response_get.content)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_person_statement(self):
|
def test_person_statement(self):
|
||||||
self.client.login(username="comptable", password="plop")
|
self.client.login(username="comptable", password="plop")
|
||||||
response_get = self.client.get(
|
response = self.client.get(
|
||||||
reverse("accounting:journal_person_statement", args=[self.journal.id])
|
reverse("accounting:journal_person_statement", args=[self.journal.id])
|
||||||
)
|
)
|
||||||
self.assertTrue(
|
self.assertContains(response, "Total : 5575.72", status_code=200)
|
||||||
"<td>3.00</td>" in str(response_get.content)
|
self.assertContains(response, "Total : 71.42")
|
||||||
and '<td><a href="/user/1/">S' Kia</a></td>'
|
self.assertContains(
|
||||||
in str(response_get.content)
|
response,
|
||||||
|
"""
|
||||||
|
<td><a href="/user/1/">S' Kia</a></td>
|
||||||
|
|
||||||
|
<td>3.00</td>""",
|
||||||
|
)
|
||||||
|
self.assertContains(
|
||||||
|
response,
|
||||||
|
"""
|
||||||
|
<td><a href="/user/1/">S' Kia</a></td>
|
||||||
|
|
||||||
|
<td>823.00</td>""",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_accounting_statement(self):
|
def test_accounting_statement(self):
|
||||||
self.client.login(username="comptable", password="plop")
|
self.client.login(username="comptable", password="plop")
|
||||||
response_get = self.client.get(
|
response = self.client.get(
|
||||||
reverse("accounting:journal_accounting_statement", args=[self.journal.id])
|
reverse("accounting:journal_accounting_statement", args=[self.journal.id])
|
||||||
)
|
)
|
||||||
self.assertTrue(
|
self.assertContains(
|
||||||
"<td>443 - Cr\\xc3\\xa9dit - Ce code n'existe pas</td>"
|
response,
|
||||||
in str(response_get.content)
|
"""
|
||||||
|
<tr>
|
||||||
|
<td>443 - Crédit - Ce code n'existe pas</td>
|
||||||
|
<td>3.00</td>
|
||||||
|
</tr>""",
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
|
self.assertContains(
|
||||||
|
response,
|
||||||
|
"""
|
||||||
|
<p><strong>Montant : </strong>-5504.30 €</p>
|
||||||
|
<p><strong>Montant effectif: </strong>-5504.30 €</p>""",
|
||||||
)
|
)
|
||||||
|
@ -1,152 +1,140 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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.conf.urls import url
|
from django.urls import path
|
||||||
|
|
||||||
from accounting.views import *
|
from accounting.views import *
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Accounting types
|
# Accounting types
|
||||||
url(
|
path(
|
||||||
r"^simple_type$",
|
"simple_type/",
|
||||||
SimplifiedAccountingTypeListView.as_view(),
|
SimplifiedAccountingTypeListView.as_view(),
|
||||||
name="simple_type_list",
|
name="simple_type_list",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^simple_type/create$",
|
"simple_type/create/",
|
||||||
SimplifiedAccountingTypeCreateView.as_view(),
|
SimplifiedAccountingTypeCreateView.as_view(),
|
||||||
name="simple_type_new",
|
name="simple_type_new",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^simple_type/(?P<type_id>[0-9]+)/edit$",
|
"simple_type/<int:type_id>/edit/",
|
||||||
SimplifiedAccountingTypeEditView.as_view(),
|
SimplifiedAccountingTypeEditView.as_view(),
|
||||||
name="simple_type_edit",
|
name="simple_type_edit",
|
||||||
),
|
),
|
||||||
# Accounting types
|
# Accounting types
|
||||||
url(r"^type$", AccountingTypeListView.as_view(), name="type_list"),
|
path("type/", AccountingTypeListView.as_view(), name="type_list"),
|
||||||
url(r"^type/create$", AccountingTypeCreateView.as_view(), name="type_new"),
|
path("type/create/", AccountingTypeCreateView.as_view(), name="type_new"),
|
||||||
url(
|
path(
|
||||||
r"^type/(?P<type_id>[0-9]+)/edit$",
|
"type/<int:type_id>/edit/",
|
||||||
AccountingTypeEditView.as_view(),
|
AccountingTypeEditView.as_view(),
|
||||||
name="type_edit",
|
name="type_edit",
|
||||||
),
|
),
|
||||||
# Bank accounts
|
# Bank accounts
|
||||||
url(r"^$", BankAccountListView.as_view(), name="bank_list"),
|
path("", BankAccountListView.as_view(), name="bank_list"),
|
||||||
url(r"^bank/create$", BankAccountCreateView.as_view(), name="bank_new"),
|
path("bank/create", BankAccountCreateView.as_view(), name="bank_new"),
|
||||||
url(
|
path(
|
||||||
r"^bank/(?P<b_account_id>[0-9]+)$",
|
"bank/<int:b_account_id>/",
|
||||||
BankAccountDetailView.as_view(),
|
BankAccountDetailView.as_view(),
|
||||||
name="bank_details",
|
name="bank_details",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^bank/(?P<b_account_id>[0-9]+)/edit$",
|
"bank/<int:b_account_id>/edit/",
|
||||||
BankAccountEditView.as_view(),
|
BankAccountEditView.as_view(),
|
||||||
name="bank_edit",
|
name="bank_edit",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^bank/(?P<b_account_id>[0-9]+)/delete$",
|
"bank/<int:b_account_id>/delete/",
|
||||||
BankAccountDeleteView.as_view(),
|
BankAccountDeleteView.as_view(),
|
||||||
name="bank_delete",
|
name="bank_delete",
|
||||||
),
|
),
|
||||||
# Club accounts
|
# Club accounts
|
||||||
url(r"^club/create$", ClubAccountCreateView.as_view(), name="club_new"),
|
path("club/create/", ClubAccountCreateView.as_view(), name="club_new"),
|
||||||
url(
|
path(
|
||||||
r"^club/(?P<c_account_id>[0-9]+)$",
|
"club/<int:c_account_id>/",
|
||||||
ClubAccountDetailView.as_view(),
|
ClubAccountDetailView.as_view(),
|
||||||
name="club_details",
|
name="club_details",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^club/(?P<c_account_id>[0-9]+)/edit$",
|
"club/<int:c_account_id>/edit/",
|
||||||
ClubAccountEditView.as_view(),
|
ClubAccountEditView.as_view(),
|
||||||
name="club_edit",
|
name="club_edit",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^club/(?P<c_account_id>[0-9]+)/delete$",
|
"club/<int:c_account_id>/delete/",
|
||||||
ClubAccountDeleteView.as_view(),
|
ClubAccountDeleteView.as_view(),
|
||||||
name="club_delete",
|
name="club_delete",
|
||||||
),
|
),
|
||||||
# Journals
|
# Journals
|
||||||
url(r"^journal/create$", JournalCreateView.as_view(), name="journal_new"),
|
path("journal/create/", JournalCreateView.as_view(), name="journal_new"),
|
||||||
url(
|
path(
|
||||||
r"^journal/(?P<j_id>[0-9]+)$",
|
"journal/<int:j_id>/",
|
||||||
JournalDetailView.as_view(),
|
JournalDetailView.as_view(),
|
||||||
name="journal_details",
|
name="journal_details",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^journal/(?P<j_id>[0-9]+)/edit$",
|
"journal/<int:j_id>/edit/",
|
||||||
JournalEditView.as_view(),
|
JournalEditView.as_view(),
|
||||||
name="journal_edit",
|
name="journal_edit",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^journal/(?P<j_id>[0-9]+)/delete$",
|
"journal/<int:j_id>/delete/",
|
||||||
JournalDeleteView.as_view(),
|
JournalDeleteView.as_view(),
|
||||||
name="journal_delete",
|
name="journal_delete",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^journal/(?P<j_id>[0-9]+)/statement/nature$",
|
"journal/<int:j_id>/statement/nature/",
|
||||||
JournalNatureStatementView.as_view(),
|
JournalNatureStatementView.as_view(),
|
||||||
name="journal_nature_statement",
|
name="journal_nature_statement",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^journal/(?P<j_id>[0-9]+)/statement/person$",
|
"journal/<int:j_id>/statement/person/",
|
||||||
JournalPersonStatementView.as_view(),
|
JournalPersonStatementView.as_view(),
|
||||||
name="journal_person_statement",
|
name="journal_person_statement",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^journal/(?P<j_id>[0-9]+)/statement/accounting$",
|
"journal/<int:j_id>/statement/accounting/",
|
||||||
JournalAccountingStatementView.as_view(),
|
JournalAccountingStatementView.as_view(),
|
||||||
name="journal_accounting_statement",
|
name="journal_accounting_statement",
|
||||||
),
|
),
|
||||||
# Operations
|
# Operations
|
||||||
url(
|
path(
|
||||||
r"^operation/create/(?P<j_id>[0-9]+)$",
|
"operation/create/<int:j_id>/",
|
||||||
OperationCreateView.as_view(),
|
OperationCreateView.as_view(),
|
||||||
name="op_new",
|
name="op_new",
|
||||||
),
|
),
|
||||||
url(r"^operation/(?P<op_id>[0-9]+)$", OperationEditView.as_view(), name="op_edit"),
|
path("operation/<int:op_id>/", OperationEditView.as_view(), name="op_edit"),
|
||||||
url(
|
path("operation/<int:op_id>/pdf/", OperationPDFView.as_view(), name="op_pdf"),
|
||||||
r"^operation/(?P<op_id>[0-9]+)/pdf$", OperationPDFView.as_view(), name="op_pdf"
|
|
||||||
),
|
|
||||||
# Companies
|
# Companies
|
||||||
url(r"^company/list$", CompanyListView.as_view(), name="co_list"),
|
path("company/list/", CompanyListView.as_view(), name="co_list"),
|
||||||
url(r"^company/create$", CompanyCreateView.as_view(), name="co_new"),
|
path("company/create/", CompanyCreateView.as_view(), name="co_new"),
|
||||||
url(r"^company/(?P<co_id>[0-9]+)$", CompanyEditView.as_view(), name="co_edit"),
|
path("company/<int:co_id>/", CompanyEditView.as_view(), name="co_edit"),
|
||||||
# Labels
|
# Labels
|
||||||
url(r"^label/new$", LabelCreateView.as_view(), name="label_new"),
|
path("label/new/", LabelCreateView.as_view(), name="label_new"),
|
||||||
url(
|
path(
|
||||||
r"^label/(?P<clubaccount_id>[0-9]+)$",
|
"label/<int:clubaccount_id>/",
|
||||||
LabelListView.as_view(),
|
LabelListView.as_view(),
|
||||||
name="label_list",
|
name="label_list",
|
||||||
),
|
),
|
||||||
url(
|
path("label/<int:label_id>/edit/", LabelEditView.as_view(), name="label_edit"),
|
||||||
r"^label/(?P<label_id>[0-9]+)/edit$", LabelEditView.as_view(), name="label_edit"
|
path(
|
||||||
),
|
"label/<int:label_id>/delete/",
|
||||||
url(
|
|
||||||
r"^label/(?P<label_id>[0-9]+)/delete$",
|
|
||||||
LabelDeleteView.as_view(),
|
LabelDeleteView.as_view(),
|
||||||
name="label_delete",
|
name="label_delete",
|
||||||
),
|
),
|
||||||
# User account
|
# User account
|
||||||
url(r"^refound/account$", RefoundAccountView.as_view(), name="refound_account"),
|
path("refound/account/", RefoundAccountView.as_view(), name="refound_account"),
|
||||||
]
|
]
|
||||||
|
@ -1,31 +1,23 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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.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.core.urlresolvers 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 +488,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 +506,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 +727,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 +766,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 +796,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
|
||||||
@ -899,7 +891,7 @@ class RefoundAccountView(FormView):
|
|||||||
form_class = CloseCustomerAccountForm
|
form_class = CloseCustomerAccountForm
|
||||||
|
|
||||||
def permission(self, user):
|
def permission(self, user):
|
||||||
if user.is_root or user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
@ -1,23 +1,15 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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.
|
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
24
api/admin.py
24
api/admin.py
@ -1,24 +1,16 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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.
|
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@ -1,24 +1,16 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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.
|
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
|
24
api/tests.py
24
api/tests.py
@ -1,24 +1,16 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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.
|
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
|
50
api/urls.py
50
api/urls.py
@ -1,56 +1,50 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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.conf.urls import url, 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
|
||||||
|
|
||||||
# Router config
|
# Router config
|
||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
router.register(r"counter", CounterViewSet, base_name="api_counter")
|
router.register(r"counter", CounterViewSet, basename="api_counter")
|
||||||
router.register(r"user", UserViewSet, base_name="api_user")
|
router.register(r"user", UserViewSet, basename="api_user")
|
||||||
router.register(r"club", ClubViewSet, base_name="api_club")
|
router.register(r"club", ClubViewSet, basename="api_club")
|
||||||
router.register(r"group", GroupViewSet, base_name="api_group")
|
router.register(r"group", GroupViewSet, basename="api_group")
|
||||||
|
|
||||||
# Launderette
|
# Launderette
|
||||||
router.register(
|
router.register(
|
||||||
r"launderette/place", LaunderettePlaceViewSet, base_name="api_launderette_place"
|
r"launderette/place", LaunderettePlaceViewSet, basename="api_launderette_place"
|
||||||
)
|
)
|
||||||
router.register(
|
router.register(
|
||||||
r"launderette/machine",
|
r"launderette/machine",
|
||||||
LaunderetteMachineViewSet,
|
LaunderetteMachineViewSet,
|
||||||
base_name="api_launderette_machine",
|
basename="api_launderette_machine",
|
||||||
)
|
)
|
||||||
router.register(
|
router.register(
|
||||||
r"launderette/token", LaunderetteTokenViewSet, base_name="api_launderette_token"
|
r"launderette/token", LaunderetteTokenViewSet, basename="api_launderette_token"
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# API
|
# API
|
||||||
url(r"^", include(router.urls)),
|
re_path(r"^", include(router.urls)),
|
||||||
url(r"^login/", include("rest_framework.urls", namespace="rest_framework")),
|
re_path(r"^login/", include("rest_framework.urls", namespace="rest_framework")),
|
||||||
url(r"^markdown$", RenderMarkdown, name="api_markdown"),
|
re_path(r"^markdown$", RenderMarkdown, name="api_markdown"),
|
||||||
url(r"^mailings$", FetchMailingLists, name="mailings_fetch"),
|
re_path(r"^mailings$", FetchMailingLists, name="mailings_fetch"),
|
||||||
|
re_path(r"^uv$", uv_endpoint, name="uv_endpoint"),
|
||||||
|
path("sas/<int:user>", all_pictures_of_user_endpoint, name="all_pictures_of_user"),
|
||||||
]
|
]
|
||||||
|
@ -1,31 +1,23 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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 rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import action
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
|
|
||||||
from core.views import can_view, can_edit
|
from core.views import can_view, can_edit
|
||||||
@ -33,8 +25,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:
|
||||||
@ -46,10 +38,10 @@ def check_if(obj, user, test):
|
|||||||
|
|
||||||
|
|
||||||
class ManageModelMixin:
|
class ManageModelMixin:
|
||||||
@detail_route()
|
@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)
|
||||||
@ -77,3 +69,5 @@ from .user import *
|
|||||||
from .club import *
|
from .club import *
|
||||||
from .group import *
|
from .group import *
|
||||||
from .launderette import *
|
from .launderette import *
|
||||||
|
from .uv import *
|
||||||
|
from .sas import *
|
||||||
|
@ -1,31 +1,22 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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 rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.decorators import api_view, renderer_classes
|
from rest_framework.decorators import api_view, renderer_classes
|
||||||
from rest_framework.renderers import StaticHTMLRenderer
|
from rest_framework.renderers import StaticHTMLRenderer
|
||||||
from rest_framework.views import APIView
|
|
||||||
|
|
||||||
from core.templatetags.renderer import markdown
|
from core.templatetags.renderer import markdown
|
||||||
|
|
||||||
@ -34,7 +25,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"])
|
||||||
|
@ -1,24 +1,16 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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.
|
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
@ -43,7 +35,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
|
||||||
|
@ -1,30 +1,22 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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 rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.decorators import list_route
|
from rest_framework.decorators import action
|
||||||
|
|
||||||
from counter.models import Counter
|
from counter.models import Counter
|
||||||
|
|
||||||
@ -32,7 +24,6 @@ from api.views import RightModelViewSet
|
|||||||
|
|
||||||
|
|
||||||
class CounterSerializer(serializers.ModelSerializer):
|
class CounterSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
is_open = serializers.BooleanField(read_only=True)
|
is_open = serializers.BooleanField(read_only=True)
|
||||||
barman_list = serializers.ListField(
|
barman_list = serializers.ListField(
|
||||||
child=serializers.IntegerField(), read_only=True
|
child=serializers.IntegerField(), read_only=True
|
||||||
@ -45,16 +36,16 @@ 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
|
||||||
queryset = Counter.objects.all()
|
queryset = Counter.objects.all()
|
||||||
|
|
||||||
@list_route()
|
@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)
|
||||||
|
@ -1,24 +1,16 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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.
|
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
@ -36,7 +28,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
|
||||||
|
@ -1,30 +1,22 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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 rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.decorators import list_route
|
from rest_framework.decorators import action
|
||||||
|
|
||||||
from launderette.models import Launderette, Machine, Token
|
from launderette.models import Launderette, Machine, Token
|
||||||
|
|
||||||
@ -32,7 +24,6 @@ from api.views import RightModelViewSet
|
|||||||
|
|
||||||
|
|
||||||
class LaunderettePlaceSerializer(serializers.ModelSerializer):
|
class LaunderettePlaceSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
machine_list = serializers.ListField(
|
machine_list = serializers.ListField(
|
||||||
child=serializers.IntegerField(), read_only=True
|
child=serializers.IntegerField(), read_only=True
|
||||||
)
|
)
|
||||||
@ -72,7 +63,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 +72,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,34 +81,34 @@ 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
|
||||||
queryset = Token.objects.all()
|
queryset = Token.objects.all()
|
||||||
|
|
||||||
@list_route()
|
@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)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@list_route()
|
@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)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@list_route()
|
@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
|
||||||
@ -125,10 +116,10 @@ class LaunderetteTokenViewSet(RightModelViewSet):
|
|||||||
serializer = self.get_serializer(self.queryset, many=True)
|
serializer = self.get_serializer(self.queryset, many=True)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@list_route()
|
@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
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)
|
||||||
|
]
|
||||||
|
)
|
@ -1,24 +1,16 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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.
|
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
@ -26,7 +18,7 @@ import datetime
|
|||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.decorators import list_route
|
from rest_framework.decorators import action
|
||||||
|
|
||||||
from core.models import User
|
from core.models import User
|
||||||
|
|
||||||
@ -50,17 +42,17 @@ 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
|
||||||
queryset = User.objects.filter(is_active=True)
|
queryset = User.objects.filter(is_active=True)
|
||||||
|
|
||||||
@list_route()
|
@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)
|
||||||
|
127
api/views/uv.py
Normal file
127
api/views/uv.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.decorators import api_view, renderer_classes
|
||||||
|
from rest_framework.renderers import JSONRenderer
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.conf import settings
|
||||||
|
from rest_framework import serializers
|
||||||
|
import urllib.request
|
||||||
|
import json
|
||||||
|
|
||||||
|
from pedagogy.views import CanCreateUVFunctionMixin
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(["GET"])
|
||||||
|
@renderer_classes((JSONRenderer,))
|
||||||
|
def uv_endpoint(request):
|
||||||
|
if not CanCreateUVFunctionMixin.can_create_uv(request.user):
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
|
params = request.query_params
|
||||||
|
if "year" not in params or "code" not in params:
|
||||||
|
raise serializers.ValidationError("Missing query parameter")
|
||||||
|
|
||||||
|
short_uv, full_uv = find_uv("fr", params["year"], params["code"])
|
||||||
|
if short_uv is None or full_uv is None:
|
||||||
|
return Response(status=204)
|
||||||
|
|
||||||
|
return Response(make_clean_uv(short_uv, full_uv))
|
||||||
|
|
||||||
|
|
||||||
|
def find_uv(lang, year, code):
|
||||||
|
"""
|
||||||
|
Uses the UTBM API to find an UV.
|
||||||
|
short_uv is the UV entry in the UV list. It is returned as it contains
|
||||||
|
information which are not in full_uv.
|
||||||
|
full_uv is the detailed representation of an UV.
|
||||||
|
"""
|
||||||
|
# query the UV list
|
||||||
|
uvs_url = settings.SITH_PEDAGOGY_UTBM_API + "/uvs/{}/{}".format(lang, year)
|
||||||
|
response = urllib.request.urlopen(uvs_url)
|
||||||
|
uvs = json.loads(response.read().decode("utf-8"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# find the first UV which matches the code
|
||||||
|
short_uv = next(uv for uv in uvs if uv["code"] == code)
|
||||||
|
except StopIteration:
|
||||||
|
return (None, None)
|
||||||
|
|
||||||
|
# get detailed information about the UV
|
||||||
|
uv_url = settings.SITH_PEDAGOGY_UTBM_API + "/uv/{}/{}/{}/{}".format(
|
||||||
|
lang, year, code, short_uv["codeFormation"]
|
||||||
|
)
|
||||||
|
response = urllib.request.urlopen(uv_url)
|
||||||
|
full_uv = json.loads(response.read().decode("utf-8"))
|
||||||
|
|
||||||
|
return (short_uv, full_uv)
|
||||||
|
|
||||||
|
|
||||||
|
def make_clean_uv(short_uv, full_uv):
|
||||||
|
"""
|
||||||
|
Cleans the data up so that it corresponds to our data representation.
|
||||||
|
"""
|
||||||
|
res = {}
|
||||||
|
|
||||||
|
res["credit_type"] = short_uv["codeCategorie"]
|
||||||
|
|
||||||
|
# probably wrong on a few UVs as we pick the first UV we find but
|
||||||
|
# availability depends on the formation
|
||||||
|
semesters = {
|
||||||
|
(True, True): "AUTUMN_AND_SPRING",
|
||||||
|
(True, False): "AUTUMN",
|
||||||
|
(False, True): "SPRING",
|
||||||
|
}
|
||||||
|
res["semester"] = semesters.get(
|
||||||
|
(short_uv["ouvertAutomne"], short_uv["ouvertPrintemps"]), "CLOSED"
|
||||||
|
)
|
||||||
|
|
||||||
|
langs = {"es": "SP", "en": "EN", "de": "DE"}
|
||||||
|
res["language"] = langs.get(full_uv["codeLangue"], "FR")
|
||||||
|
|
||||||
|
if full_uv["departement"] == "Pôle Humanités":
|
||||||
|
res["department"] = "HUMA"
|
||||||
|
else:
|
||||||
|
departments = {
|
||||||
|
"AL": "IMSI",
|
||||||
|
"AE": "EE",
|
||||||
|
"GI": "GI",
|
||||||
|
"GC": "EE",
|
||||||
|
"GM": "MC",
|
||||||
|
"TC": "TC",
|
||||||
|
"GP": "IMSI",
|
||||||
|
"ED": "EDIM",
|
||||||
|
"AI": "GI",
|
||||||
|
"AM": "MC",
|
||||||
|
}
|
||||||
|
res["department"] = departments.get(full_uv["codeFormation"], "NA")
|
||||||
|
|
||||||
|
res["credits"] = full_uv["creditsEcts"]
|
||||||
|
|
||||||
|
activities = ("CM", "TD", "TP", "THE", "TE")
|
||||||
|
for activity in activities:
|
||||||
|
res["hours_{}".format(activity)] = 0
|
||||||
|
for activity in full_uv["activites"]:
|
||||||
|
if activity["code"] in activities:
|
||||||
|
res["hours_{}".format(activity["code"])] += activity["nbh"] // 60
|
||||||
|
|
||||||
|
# wrong if the manager changes depending on the semester
|
||||||
|
semester = full_uv.get("automne", None)
|
||||||
|
if not semester:
|
||||||
|
semester = full_uv.get("printemps", {})
|
||||||
|
res["manager"] = semester.get("responsable", "")
|
||||||
|
|
||||||
|
res["title"] = full_uv["libelle"]
|
||||||
|
|
||||||
|
descriptions = {
|
||||||
|
"objectives": "objectifs",
|
||||||
|
"program": "programme",
|
||||||
|
"skills": "acquisitionCompetences",
|
||||||
|
"key_concepts": "acquisitionNotions",
|
||||||
|
}
|
||||||
|
|
||||||
|
for res_key, full_uv_key in descriptions.items():
|
||||||
|
res[res_key] = full_uv[full_uv_key]
|
||||||
|
# if not found or the API did not return a string
|
||||||
|
if type(res[res_key]) != str:
|
||||||
|
res[res_key] = ""
|
||||||
|
|
||||||
|
return res
|
@ -1,23 +1,15 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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.
|
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
@ -1,31 +1,36 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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 ajax_select import make_ajax_form
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from club.models import Club, Membership
|
from club.models import Club, Membership
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Club)
|
@admin.register(Club)
|
||||||
admin.site.register(Membership)
|
class ClubAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name", "unix_name", "parent", "is_active")
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Membership)
|
||||||
|
class MembershipAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("user", "club", "role", "start_date", "end_date")
|
||||||
|
search_fields = (
|
||||||
|
"user__username",
|
||||||
|
"user__first_name",
|
||||||
|
"user__last_name",
|
||||||
|
"club__name",
|
||||||
|
)
|
||||||
|
form = make_ajax_form(Membership, {"user": "users"})
|
||||||
|
@ -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):
|
||||||
@ -66,7 +67,7 @@ class MailingForm(forms.Form):
|
|||||||
super(MailingForm, self).__init__(*args, **kwargs)
|
super(MailingForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.fields["action"] = forms.TypedChoiceField(
|
self.fields["action"] = forms.TypedChoiceField(
|
||||||
(
|
choices=(
|
||||||
(self.ACTION_NEW_MAILING, _("New Mailing")),
|
(self.ACTION_NEW_MAILING, _("New Mailing")),
|
||||||
(self.ACTION_NEW_SUBSCRIPTION, _("Subscribe")),
|
(self.ACTION_NEW_SUBSCRIPTION, _("Subscribe")),
|
||||||
(self.ACTION_REMOVE_SUBSCRIPTION, _("Remove")),
|
(self.ACTION_REMOVE_SUBSCRIPTION, _("Remove")),
|
||||||
@ -157,23 +158,27 @@ 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)
|
||||||
["%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(
|
|
||||||
["%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):
|
||||||
"""
|
"""
|
||||||
@ -224,9 +229,7 @@ class ClubMemberForm(forms.Form):
|
|||||||
id__in=[
|
id__in=[
|
||||||
ms.user.id
|
ms.user.id
|
||||||
for ms in self.club_members
|
for ms in self.club_members
|
||||||
if ms.can_be_edited_by(
|
if ms.can_be_edited_by(self.request_user)
|
||||||
self.request_user, self.request_user_membership
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
).all(),
|
).all(),
|
||||||
label=_("Mark as old"),
|
label=_("Mark as old"),
|
||||||
@ -238,8 +241,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 +265,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()
|
||||||
|
|
||||||
|
@ -3,10 +3,10 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@ -90,7 +90,10 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"club",
|
"club",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
verbose_name="club", to="club.Club", related_name="members"
|
verbose_name="club",
|
||||||
|
to="club.Club",
|
||||||
|
related_name="members",
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -3,10 +3,10 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
("club", "0001_initial"),
|
("club", "0001_initial"),
|
||||||
@ -18,6 +18,7 @@ class Migration(migrations.Migration):
|
|||||||
model_name="membership",
|
model_name="membership",
|
||||||
name="user",
|
name="user",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
verbose_name="user",
|
verbose_name="user",
|
||||||
to=settings.AUTH_USER_MODEL,
|
to=settings.AUTH_USER_MODEL,
|
||||||
related_name="membership",
|
related_name="membership",
|
||||||
@ -34,6 +35,7 @@ class Migration(migrations.Migration):
|
|||||||
model_name="club",
|
model_name="club",
|
||||||
name="home",
|
name="home",
|
||||||
field=models.OneToOneField(
|
field=models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
related_name="home_of_club",
|
related_name="home_of_club",
|
||||||
@ -45,14 +47,21 @@ class Migration(migrations.Migration):
|
|||||||
model_name="club",
|
model_name="club",
|
||||||
name="owner_group",
|
name="owner_group",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
default=1, to="core.Group", related_name="owned_club"
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
default=1,
|
||||||
|
to="core.Group",
|
||||||
|
related_name="owned_club",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="club",
|
model_name="club",
|
||||||
name="parent",
|
name="parent",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
null=True, to="club.Club", related_name="children", blank=True
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
null=True,
|
||||||
|
to="club.Club",
|
||||||
|
related_name="children",
|
||||||
|
blank=True,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
|
@ -5,7 +5,6 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("club", "0002_auto_20160824_2152")]
|
dependencies = [("club", "0002_auto_20160824_2152")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -3,10 +3,10 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("club", "0003_auto_20160902_2042")]
|
dependencies = [("club", "0003_auto_20160902_2042")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@ -14,6 +14,7 @@ class Migration(migrations.Migration):
|
|||||||
model_name="membership",
|
model_name="membership",
|
||||||
name="user",
|
name="user",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
verbose_name="user",
|
verbose_name="user",
|
||||||
related_name="memberships",
|
related_name="memberships",
|
||||||
to=settings.AUTH_USER_MODEL,
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
@ -6,7 +6,6 @@ import django.db.models.deletion
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("club", "0004_auto_20160915_1057")]
|
dependencies = [("club", "0004_auto_20160915_1057")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -6,7 +6,6 @@ import django.utils.timezone
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("club", "0005_auto_20161120_1149")]
|
dependencies = [("club", "0005_auto_20161120_1149")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -5,7 +5,6 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("club", "0006_auto_20161229_0040")]
|
dependencies = [("club", "0006_auto_20161229_0040")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -5,7 +5,6 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("club", "0007_auto_20170324_0917")]
|
dependencies = [("club", "0007_auto_20170324_0917")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -5,10 +5,10 @@ from django.db import migrations, models
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
import re
|
import re
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
("club", "0008_auto_20170515_2214"),
|
("club", "0008_auto_20170515_2214"),
|
||||||
@ -51,12 +51,16 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"club",
|
"club",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
verbose_name="Club", related_name="mailings", to="club.Club"
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
verbose_name="Club",
|
||||||
|
related_name="mailings",
|
||||||
|
to="club.Club",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"moderator",
|
"moderator",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name="moderator",
|
verbose_name="moderator",
|
||||||
related_name="moderated_mailings",
|
related_name="moderated_mailings",
|
||||||
@ -84,6 +88,7 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"mailing",
|
"mailing",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
verbose_name="Mailing",
|
verbose_name="Mailing",
|
||||||
related_name="subscriptions",
|
related_name="subscriptions",
|
||||||
to="club.Mailing",
|
to="club.Mailing",
|
||||||
@ -92,6 +97,7 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"user",
|
"user",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name="User",
|
verbose_name="User",
|
||||||
related_name="mailing_subscriptions",
|
related_name="mailing_subscriptions",
|
||||||
|
@ -5,6 +5,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
from club.models import Club
|
from club.models import Club
|
||||||
from core.operations import PsqlRunOnly
|
from core.operations import PsqlRunOnly
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
def generate_club_pages(apps, schema_editor):
|
def generate_club_pages(apps, schema_editor):
|
||||||
@ -18,7 +19,6 @@ def generate_club_pages(apps, schema_editor):
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("core", "0024_auto_20170906_1317"), ("club", "0010_club_logo")]
|
dependencies = [("core", "0024_auto_20170906_1317"), ("club", "0010_club_logo")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@ -31,7 +31,11 @@ class Migration(migrations.Migration):
|
|||||||
model_name="club",
|
model_name="club",
|
||||||
name="page",
|
name="page",
|
||||||
field=models.OneToOneField(
|
field=models.OneToOneField(
|
||||||
related_name="club", blank=True, null=True, to="core.Page"
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="club",
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
to="core.Page",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
|
@ -5,7 +5,6 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("club", "0009_auto_20170822_2232")]
|
dependencies = [("club", "0009_auto_20170822_2232")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -3,10 +3,10 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import club.models
|
import club.models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("club", "0010_auto_20170912_2028")]
|
dependencies = [("club", "0010_auto_20170912_2028")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@ -14,6 +14,7 @@ class Migration(migrations.Migration):
|
|||||||
model_name="club",
|
model_name="club",
|
||||||
name="owner_group",
|
name="owner_group",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
default=club.models.Club.get_default_owner_group,
|
default=club.models.Club.get_default_owner_group,
|
||||||
related_name="owned_club",
|
related_name="owned_club",
|
||||||
to="core.Group",
|
to="core.Group",
|
||||||
|
256
club/models.py
256
club/models.py
@ -22,14 +22,18 @@
|
|||||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from django.core.cache import cache
|
||||||
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.db.models import Q
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.core.validators import RegexValidator, validate_email
|
from django.core.validators import RegexValidator, validate_email
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
@ -46,7 +50,9 @@ class Club(models.Model):
|
|||||||
|
|
||||||
id = models.AutoField(primary_key=True, db_index=True)
|
id = models.AutoField(primary_key=True, db_index=True)
|
||||||
name = models.CharField(_("name"), max_length=64)
|
name = models.CharField(_("name"), max_length=64)
|
||||||
parent = models.ForeignKey("Club", related_name="children", null=True, blank=True)
|
parent = models.ForeignKey(
|
||||||
|
"Club", related_name="children", null=True, blank=True, on_delete=models.CASCADE
|
||||||
|
)
|
||||||
unix_name = models.CharField(
|
unix_name = models.CharField(
|
||||||
_("unix name"),
|
_("unix name"),
|
||||||
max_length=30,
|
max_length=30,
|
||||||
@ -70,12 +76,16 @@ class Club(models.Model):
|
|||||||
_("short description"), max_length=1000, default="", blank=True, null=True
|
_("short description"), max_length=1000, default="", blank=True, null=True
|
||||||
)
|
)
|
||||||
address = models.CharField(_("address"), max_length=254)
|
address = models.CharField(_("address"), max_length=254)
|
||||||
|
|
||||||
# This function prevents generating migration upon settings change
|
# This function prevents generating migration upon settings change
|
||||||
def get_default_owner_group():
|
def get_default_owner_group():
|
||||||
return settings.SITH_GROUP_ROOT_ID
|
return settings.SITH_GROUP_ROOT_ID
|
||||||
|
|
||||||
owner_group = models.ForeignKey(
|
owner_group = models.ForeignKey(
|
||||||
Group, related_name="owned_club", default=get_default_owner_group
|
Group,
|
||||||
|
related_name="owned_club",
|
||||||
|
default=get_default_owner_group,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
edit_groups = models.ManyToManyField(
|
edit_groups = models.ManyToManyField(
|
||||||
Group, related_name="editable_club", blank=True
|
Group, related_name="editable_club", blank=True
|
||||||
@ -91,7 +101,9 @@ class Club(models.Model):
|
|||||||
blank=True,
|
blank=True,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
)
|
)
|
||||||
page = models.OneToOneField(Page, related_name="club", blank=True, null=True)
|
page = models.OneToOneField(
|
||||||
|
Page, related_name="club", blank=True, null=True, on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["name", "unix_name"]
|
ordering = ["name", "unix_name"]
|
||||||
@ -115,12 +127,22 @@ class Club(models.Model):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
self.check_loop()
|
self.check_loop()
|
||||||
|
|
||||||
def _change_unixname(self, new_name):
|
def _change_unixname(self, old_name, new_name):
|
||||||
c = Club.objects.filter(unix_name=new_name).first()
|
c = Club.objects.filter(unix_name=new_name).first()
|
||||||
if c is None:
|
if c is None:
|
||||||
|
# Update all the groups names
|
||||||
|
Group.objects.filter(name=old_name).update(name=new_name)
|
||||||
|
Group.objects.filter(name=old_name + settings.SITH_BOARD_SUFFIX).update(
|
||||||
|
name=new_name + settings.SITH_BOARD_SUFFIX
|
||||||
|
)
|
||||||
|
Group.objects.filter(name=old_name + settings.SITH_MEMBER_SUFFIX).update(
|
||||||
|
name=new_name + settings.SITH_MEMBER_SUFFIX
|
||||||
|
)
|
||||||
|
|
||||||
if self.home:
|
if self.home:
|
||||||
self.home.name = new_name
|
self.home.name = new_name
|
||||||
self.home.save()
|
self.home.save()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ValidationError(_("A club with that unix_name already exists"))
|
raise ValidationError(_("A club with that unix_name already exists"))
|
||||||
|
|
||||||
@ -164,29 +186,34 @@ class Club(models.Model):
|
|||||||
self.page.parent = self.parent.page
|
self.page.parent = self.parent.page
|
||||||
self.page.save(force_lock=True)
|
self.page.save(force_lock=True)
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
with transaction.atomic():
|
old = Club.objects.filter(id=self.id).first()
|
||||||
creation = False
|
creation = old is None
|
||||||
old = Club.objects.filter(id=self.id).first()
|
if not creation and old.unix_name != self.unix_name:
|
||||||
if not old:
|
self._change_unixname(self.unix_name)
|
||||||
creation = True
|
super(Club, self).save(*args, **kwargs)
|
||||||
else:
|
if creation:
|
||||||
if old.unix_name != self.unix_name:
|
board = MetaGroup(name=self.unix_name + settings.SITH_BOARD_SUFFIX)
|
||||||
self._change_unixname(self.unix_name)
|
board.save()
|
||||||
super(Club, self).save(*args, **kwargs)
|
member = MetaGroup(name=self.unix_name + settings.SITH_MEMBER_SUFFIX)
|
||||||
if creation:
|
member.save()
|
||||||
board = MetaGroup(name=self.unix_name + settings.SITH_BOARD_SUFFIX)
|
subscribers = Group.objects.filter(
|
||||||
board.save()
|
name=settings.SITH_MAIN_MEMBERS_GROUP
|
||||||
member = MetaGroup(name=self.unix_name + settings.SITH_MEMBER_SUFFIX)
|
).first()
|
||||||
member.save()
|
self.make_home()
|
||||||
subscribers = Group.objects.filter(
|
self.home.edit_groups.set([board])
|
||||||
name=settings.SITH_MAIN_MEMBERS_GROUP
|
self.home.view_groups.set([member, subscribers])
|
||||||
).first()
|
self.home.save()
|
||||||
self.make_home()
|
self.make_page()
|
||||||
self.home.edit_groups = [board]
|
cache.set(f"sith_club_{self.unix_name}", self)
|
||||||
self.home.view_groups = [member, subscribers]
|
|
||||||
self.home.save()
|
def delete(self, *args, **kwargs):
|
||||||
self.make_page()
|
super().delete(*args, **kwargs)
|
||||||
|
# Invalidate the cache of this club and of its memberships
|
||||||
|
for membership in self.members.ongoing().select_related("user"):
|
||||||
|
cache.delete(f"membership_{self.id}_{membership.user.id}")
|
||||||
|
cache.delete(f"sith_club_{self.unix_name}")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -201,7 +228,9 @@ class Club(models.Model):
|
|||||||
"""
|
"""
|
||||||
Method to see if that object can be super edited by the given user
|
Method to see if that object can be super edited by the given user
|
||||||
"""
|
"""
|
||||||
return user.is_in_group(settings.SITH_MAIN_BOARD_GROUP)
|
if user.is_anonymous:
|
||||||
|
return False
|
||||||
|
return user.is_board_member
|
||||||
|
|
||||||
def get_full_logo_url(self):
|
def get_full_logo_url(self):
|
||||||
return "https://%s%s" % (settings.SITH_URL, self.logo.url)
|
return "https://%s%s" % (settings.SITH_URL, self.logo.url)
|
||||||
@ -221,28 +250,89 @@ class Club(models.Model):
|
|||||||
return False
|
return False
|
||||||
return sub.was_subscribed
|
return sub.was_subscribed
|
||||||
|
|
||||||
_memberships = {}
|
def get_membership_for(self, user: User) -> Optional["Membership"]:
|
||||||
|
|
||||||
def get_membership_for(self, user):
|
|
||||||
"""
|
"""
|
||||||
Returns the current membership the given user
|
Return the current membership the given user.
|
||||||
|
The result is cached.
|
||||||
"""
|
"""
|
||||||
try:
|
if user.is_anonymous:
|
||||||
return Club._memberships[self.id][user.id]
|
return None
|
||||||
except:
|
membership = cache.get(f"membership_{self.id}_{user.id}")
|
||||||
m = self.members.filter(user=user.id).filter(end_date=None).first()
|
if membership == "not_member":
|
||||||
try:
|
return None
|
||||||
Club._memberships[self.id][user.id] = m
|
if membership is None:
|
||||||
except:
|
membership = self.members.filter(user=user, end_date=None).first()
|
||||||
Club._memberships[self.id] = {}
|
if membership is None:
|
||||||
Club._memberships[self.id][user.id] = m
|
cache.set(f"membership_{self.id}_{user.id}", "not_member")
|
||||||
return m
|
else:
|
||||||
|
cache.set(f"membership_{self.id}_{user.id}", membership)
|
||||||
|
return membership
|
||||||
|
|
||||||
def has_rights_in_club(self, user):
|
def has_rights_in_club(self, user):
|
||||||
m = self.get_membership_for(user)
|
m = self.get_membership_for(user)
|
||||||
return m is not None and m.role > settings.SITH_MAXIMUM_FREE_ROLE
|
return m is not None and m.role > settings.SITH_MAXIMUM_FREE_ROLE
|
||||||
|
|
||||||
|
|
||||||
|
class MembershipQuerySet(models.QuerySet):
|
||||||
|
def ongoing(self) -> "MembershipQuerySet":
|
||||||
|
"""
|
||||||
|
Filter all memberships which are not finished yet
|
||||||
|
"""
|
||||||
|
# noinspection PyTypeChecker
|
||||||
|
return self.filter(Q(end_date=None) | Q(end_date__gte=timezone.now()))
|
||||||
|
|
||||||
|
def board(self) -> "MembershipQuerySet":
|
||||||
|
"""
|
||||||
|
Filter all memberships where the user is/was in the board.
|
||||||
|
|
||||||
|
Be aware that users who were in the board in the past
|
||||||
|
are included, even if there are no more members.
|
||||||
|
|
||||||
|
If you want to get the users who are currently in the board,
|
||||||
|
mind combining this with the :meth:`ongoing` queryset method
|
||||||
|
"""
|
||||||
|
# noinspection PyTypeChecker
|
||||||
|
return self.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE)
|
||||||
|
|
||||||
|
def update(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Work just like the default Django's update() method,
|
||||||
|
but add a cache refresh for the elements of the queryset.
|
||||||
|
|
||||||
|
Be aware that this adds a db query to retrieve the updated objects
|
||||||
|
"""
|
||||||
|
nb_rows = super().update(**kwargs)
|
||||||
|
if nb_rows > 0:
|
||||||
|
# if at least a row was affected, refresh the cache
|
||||||
|
for membership in self.all():
|
||||||
|
if membership.end_date is not None:
|
||||||
|
cache.set(
|
||||||
|
f"membership_{membership.club_id}_{membership.user_id}",
|
||||||
|
"not_member",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cache.set(
|
||||||
|
f"membership_{membership.club_id}_{membership.user_id}",
|
||||||
|
membership,
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
"""
|
||||||
|
Work just like the default Django's delete() method,
|
||||||
|
but add a cache invalidation for the elements of the queryset
|
||||||
|
before the deletion.
|
||||||
|
|
||||||
|
Be aware that this adds a db query to retrieve the deleted element.
|
||||||
|
As this first query take place before the deletion operation,
|
||||||
|
it will be performed even if the deletion fails.
|
||||||
|
"""
|
||||||
|
ids = list(self.values_list("club_id", "user_id"))
|
||||||
|
nb_rows, _ = super().delete()
|
||||||
|
if nb_rows > 0:
|
||||||
|
for club_id, user_id in ids:
|
||||||
|
cache.set(f"membership_{club_id}_{user_id}", "not_member")
|
||||||
|
|
||||||
|
|
||||||
class Membership(models.Model):
|
class Membership(models.Model):
|
||||||
"""
|
"""
|
||||||
The Membership class makes the connection between User and Clubs
|
The Membership class makes the connection between User and Clubs
|
||||||
@ -261,9 +351,15 @@ class Membership(models.Model):
|
|||||||
related_name="memberships",
|
related_name="memberships",
|
||||||
null=False,
|
null=False,
|
||||||
blank=False,
|
blank=False,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
club = models.ForeignKey(
|
club = models.ForeignKey(
|
||||||
Club, verbose_name=_("club"), related_name="members", null=False, blank=False
|
Club,
|
||||||
|
verbose_name=_("club"),
|
||||||
|
related_name="members",
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
start_date = models.DateField(_("start date"), default=timezone.now)
|
start_date = models.DateField(_("start date"), default=timezone.now)
|
||||||
end_date = models.DateField(_("end date"), null=True, blank=True)
|
end_date = models.DateField(_("end date"), null=True, blank=True)
|
||||||
@ -276,6 +372,8 @@ class Membership(models.Model):
|
|||||||
_("description"), max_length=128, null=False, blank=True
|
_("description"), max_length=128, null=False, blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
objects = MembershipQuerySet.as_manager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return (
|
return (
|
||||||
self.club.name
|
self.club.name
|
||||||
@ -290,24 +388,34 @@ class Membership(models.Model):
|
|||||||
"""
|
"""
|
||||||
Method to see if that object can be super edited by the given user
|
Method to see if that object can be super edited by the given user
|
||||||
"""
|
"""
|
||||||
return user.is_in_group(settings.SITH_MAIN_BOARD_GROUP)
|
if user.is_anonymous:
|
||||||
|
return False
|
||||||
|
return user.is_board_member
|
||||||
|
|
||||||
def can_be_edited_by(self, user, membership=None):
|
def can_be_edited_by(self, user: User) -> bool:
|
||||||
"""
|
"""
|
||||||
Method to see if that object can be edited by the given user
|
Check if that object can be edited by the given user
|
||||||
"""
|
"""
|
||||||
if user.memberships:
|
if user.is_root or user.is_board_member:
|
||||||
if membership: # This is for optimisation purpose
|
return True
|
||||||
ms = membership
|
membership = self.club.get_membership_for(user)
|
||||||
else:
|
if membership is not None and membership.role >= self.role:
|
||||||
ms = user.memberships.filter(club=self.club, end_date=None).first()
|
return True
|
||||||
return (ms and ms.role >= self.role) or user.is_in_group(
|
return False
|
||||||
settings.SITH_MAIN_BOARD_GROUP
|
|
||||||
)
|
|
||||||
return user.is_in_group(settings.SITH_MAIN_BOARD_GROUP)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("club:club_members", kwargs={"club_id": self.club.id})
|
return reverse("club:club_members", kwargs={"club_id": self.club_id})
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
if self.end_date is None:
|
||||||
|
cache.set(f"membership_{self.club_id}_{self.user_id}", self)
|
||||||
|
else:
|
||||||
|
cache.set(f"membership_{self.club_id}_{self.user_id}", "not_member")
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
super().delete(*args, **kwargs)
|
||||||
|
cache.delete(f"membership_{self.club_id}_{self.user_id}")
|
||||||
|
|
||||||
|
|
||||||
class Mailing(models.Model):
|
class Mailing(models.Model):
|
||||||
@ -317,7 +425,12 @@ class Mailing(models.Model):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
club = models.ForeignKey(
|
club = models.ForeignKey(
|
||||||
Club, verbose_name=_("Club"), related_name="mailings", null=False, blank=False
|
Club,
|
||||||
|
verbose_name=_("Club"),
|
||||||
|
related_name="mailings",
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
email = models.CharField(
|
email = models.CharField(
|
||||||
_("Email address"),
|
_("Email address"),
|
||||||
@ -334,7 +447,11 @@ class Mailing(models.Model):
|
|||||||
)
|
)
|
||||||
is_moderated = models.BooleanField(_("is moderated"), default=False)
|
is_moderated = models.BooleanField(_("is moderated"), default=False)
|
||||||
moderator = models.ForeignKey(
|
moderator = models.ForeignKey(
|
||||||
User, related_name="moderated_mailings", verbose_name=_("moderator"), null=True
|
User,
|
||||||
|
related_name="moderated_mailings",
|
||||||
|
verbose_name=_("moderator"),
|
||||||
|
null=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
@ -351,14 +468,12 @@ class Mailing(models.Model):
|
|||||||
return self.email + "@" + settings.SITH_MAILING_DOMAIN
|
return self.email + "@" + settings.SITH_MAILING_DOMAIN
|
||||||
|
|
||||||
def can_moderate(self, user):
|
def can_moderate(self, user):
|
||||||
return user.is_root or user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID)
|
return user.is_root or user.is_com_admin
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
return (
|
if user.is_anonymous:
|
||||||
user.is_in_group(self)
|
return False
|
||||||
or user.is_root
|
return user.is_root or user.is_com_admin
|
||||||
or user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID)
|
|
||||||
)
|
|
||||||
|
|
||||||
def can_view(self, user):
|
def can_view(self, user):
|
||||||
return self.club.has_rights_in_club(user)
|
return self.club.has_rights_in_club(user)
|
||||||
@ -366,9 +481,8 @@ class Mailing(models.Model):
|
|||||||
def can_be_edited_by(self, user):
|
def can_be_edited_by(self, user):
|
||||||
return self.club.has_rights_in_club(user)
|
return self.club.has_rights_in_club(user)
|
||||||
|
|
||||||
def delete(self):
|
def delete(self, *args, **kwargs):
|
||||||
for sub in self.subscriptions.all():
|
self.subscriptions.all().delete()
|
||||||
sub.delete()
|
|
||||||
super(Mailing, self).delete()
|
super(Mailing, self).delete()
|
||||||
|
|
||||||
def fetch_format(self):
|
def fetch_format(self):
|
||||||
@ -409,6 +523,7 @@ class MailingSubscription(models.Model):
|
|||||||
related_name="subscriptions",
|
related_name="subscriptions",
|
||||||
null=False,
|
null=False,
|
||||||
blank=False,
|
blank=False,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
User,
|
User,
|
||||||
@ -416,6 +531,7 @@ class MailingSubscription(models.Model):
|
|||||||
related_name="mailing_subscriptions",
|
related_name="mailing_subscriptions",
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
email = models.EmailField(_("Email address"), blank=False, null=False)
|
email = models.EmailField(_("Email address"), blank=False, null=False)
|
||||||
|
|
||||||
@ -439,10 +555,12 @@ class MailingSubscription(models.Model):
|
|||||||
super(MailingSubscription, self).clean()
|
super(MailingSubscription, self).clean()
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
|
if user.is_anonymous:
|
||||||
|
return False
|
||||||
return (
|
return (
|
||||||
self.mailing.club.has_rights_in_club(user)
|
self.mailing.club.has_rights_in_club(user)
|
||||||
or user.is_root
|
or user.is_root
|
||||||
or self.user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID)
|
or self.user.is_com_admin
|
||||||
)
|
)
|
||||||
|
|
||||||
def can_be_edited_by(self, user):
|
def can_be_edited_by(self, user):
|
||||||
|
@ -13,13 +13,15 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<td>{% trans %}User{% endtrans %}</td>
|
<tr>
|
||||||
<td>{% trans %}Role{% endtrans %}</td>
|
<td>{% trans %}User{% endtrans %}</td>
|
||||||
<td>{% trans %}Description{% endtrans %}</td>
|
<td>{% trans %}Role{% endtrans %}</td>
|
||||||
<td>{% trans %}Since{% endtrans %}</td>
|
<td>{% trans %}Description{% endtrans %}</td>
|
||||||
{% if users_old %}
|
<td>{% trans %}Since{% endtrans %}</td>
|
||||||
<td>{% trans %}Mark as old{% endtrans %}</td>
|
{% if users_old %}
|
||||||
{% endif %}
|
<td>{% trans %}Mark as old{% endtrans %}</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for m in members %}
|
{% for m in members %}
|
||||||
|
@ -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 %}Sales{% 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 %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
{% if object.club_account.exists() %}
|
{% if object.club_account.exists() %}
|
||||||
<h4>{% trans %}Accouting: {% endtrans %}</h4>
|
<h4>{% trans %}Accounting: {% endtrans %}</h4>
|
||||||
<ul>
|
<ul>
|
||||||
{% for ca in object.club_account.all() %}
|
{% for ca in object.club_account.all() %}
|
||||||
<li><a href="{{ url('accounting:club_details', c_account_id=ca.id) }}">{{ ca.get_display_name() }}</a></li>
|
<li><a href="{{ url('accounting:club_details', c_account_id=ca.id) }}">{{ ca.get_display_name() }}</a></li>
|
||||||
|
@ -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>
|
||||||
|
811
club/tests.py
811
club/tests.py
@ -1,395 +1,576 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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 datetime import timedelta
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.cache import cache
|
||||||
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.timezone import now, localtime
|
||||||
from django.core.urlresolvers import reverse
|
from django.utils.translation import gettext as _
|
||||||
|
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 core.models import User
|
from core.models import User, AnonymousUser
|
||||||
from club.models import Club, Membership, Mailing
|
from club.models import Club, Membership, Mailing
|
||||||
from club.forms import MailingForm
|
from club.forms import MailingForm
|
||||||
|
from sith.settings import SITH_BAR_MANAGER, SITH_MAIN_CLUB_ID
|
||||||
# Create your tests here.
|
|
||||||
|
|
||||||
|
|
||||||
class ClubTest(TestCase):
|
class ClubTest(TestCase):
|
||||||
|
"""
|
||||||
|
Set up data for test cases related to clubs and membership
|
||||||
|
The generated dataset is the one created by the populate command,
|
||||||
|
plus the following modifications :
|
||||||
|
|
||||||
|
- `self.club` is a dummy club recreated for each test
|
||||||
|
- `self.club` has two board members : skia (role 3) and comptable (role 10)
|
||||||
|
- `self.club` has one regular member : richard
|
||||||
|
- `self.club` has one former member : sli (who had role 2)
|
||||||
|
- None of the `self.club` members are in the AE club.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
# subscribed users - initial members
|
||||||
|
cls.skia = User.objects.get(username="skia")
|
||||||
|
cls.richard = User.objects.get(username="rbatsbak")
|
||||||
|
cls.comptable = User.objects.get(username="comptable")
|
||||||
|
cls.sli = User.objects.get(username="sli")
|
||||||
|
|
||||||
|
# subscribed users - not initial members
|
||||||
|
cls.krophil = User.objects.get(username="krophil")
|
||||||
|
cls.subscriber = User.objects.get(username="subscriber")
|
||||||
|
|
||||||
|
# old subscriber
|
||||||
|
cls.old_subscriber = User.objects.get(username="old_subscriber")
|
||||||
|
|
||||||
|
# not subscribed
|
||||||
|
cls.public = User.objects.get(username="public")
|
||||||
|
|
||||||
|
cls.ae = Club.objects.filter(pk=SITH_MAIN_CLUB_ID)[0]
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
call_command("populate")
|
# by default, Skia is in the AE, which creates side effect
|
||||||
self.skia = User.objects.filter(username="skia").first()
|
self.skia.memberships.all().delete()
|
||||||
self.rbatsbak = User.objects.filter(username="rbatsbak").first()
|
|
||||||
self.guy = User.objects.filter(username="guy").first()
|
|
||||||
self.bdf = Club.objects.filter(unix_name="bdf").first()
|
|
||||||
|
|
||||||
def test_create_add_user_to_club_from_root_ok(self):
|
# create a fake club
|
||||||
self.client.login(username="root", password="plop")
|
self.club = Club.objects.create(
|
||||||
self.client.post(
|
name="Fake Club",
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
unix_name="fake-club",
|
||||||
{"users": self.skia.id, "start_date": "12/06/2016", "role": 3},
|
address="5 rue de la République, 90000 Belfort",
|
||||||
)
|
)
|
||||||
response = self.client.get(
|
self.members_url = reverse(
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id})
|
"club:club_members", kwargs={"club_id": self.club.id}
|
||||||
)
|
)
|
||||||
self.assertTrue(response.status_code == 200)
|
a_month_ago = now() - timedelta(days=30)
|
||||||
self.assertTrue(
|
yesterday = now() - timedelta(days=1)
|
||||||
"S' Kia</a></td>\\n <td>Responsable info</td>"
|
Membership.objects.create(
|
||||||
in str(response.content)
|
club=self.club, user=self.skia, start_date=a_month_ago, role=3
|
||||||
|
)
|
||||||
|
Membership.objects.create(club=self.club, user=self.richard, role=1)
|
||||||
|
Membership.objects.create(
|
||||||
|
club=self.club, user=self.comptable, start_date=a_month_ago, role=10
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_create_add_multiple_user_to_club_from_root_ok(self):
|
# sli was a member but isn't anymore
|
||||||
|
Membership.objects.create(
|
||||||
|
club=self.club,
|
||||||
|
user=self.sli,
|
||||||
|
start_date=a_month_ago,
|
||||||
|
end_date=yesterday,
|
||||||
|
role=2,
|
||||||
|
)
|
||||||
|
cache.clear()
|
||||||
|
|
||||||
|
|
||||||
|
class MembershipQuerySetTest(ClubTest):
|
||||||
|
def test_ongoing(self):
|
||||||
|
"""
|
||||||
|
Test that the ongoing queryset method returns the memberships that
|
||||||
|
are not ended.
|
||||||
|
"""
|
||||||
|
current_members = self.club.members.ongoing()
|
||||||
|
expected = [
|
||||||
|
self.skia.memberships.get(club=self.club),
|
||||||
|
self.comptable.memberships.get(club=self.club),
|
||||||
|
self.richard.memberships.get(club=self.club),
|
||||||
|
]
|
||||||
|
self.assertEqual(len(current_members), len(expected))
|
||||||
|
for member in current_members:
|
||||||
|
self.assertIn(member, expected)
|
||||||
|
|
||||||
|
def test_board(self):
|
||||||
|
"""
|
||||||
|
Test that the board queryset method returns the memberships
|
||||||
|
of user in the club board
|
||||||
|
"""
|
||||||
|
board_members = list(self.club.members.board())
|
||||||
|
expected = [
|
||||||
|
self.skia.memberships.get(club=self.club),
|
||||||
|
self.comptable.memberships.get(club=self.club),
|
||||||
|
# sli is no more member, but he was in the board
|
||||||
|
self.sli.memberships.get(club=self.club),
|
||||||
|
]
|
||||||
|
self.assertEqual(len(board_members), len(expected))
|
||||||
|
for member in board_members:
|
||||||
|
self.assertIn(member, expected)
|
||||||
|
|
||||||
|
def test_ongoing_board(self):
|
||||||
|
"""
|
||||||
|
Test that combining ongoing and board returns users
|
||||||
|
who are currently board members of the club
|
||||||
|
"""
|
||||||
|
members = list(self.club.members.ongoing().board())
|
||||||
|
expected = [
|
||||||
|
self.skia.memberships.get(club=self.club),
|
||||||
|
self.comptable.memberships.get(club=self.club),
|
||||||
|
]
|
||||||
|
self.assertEqual(len(members), len(expected))
|
||||||
|
for member in members:
|
||||||
|
self.assertIn(member, expected)
|
||||||
|
|
||||||
|
def test_update_invalidate_cache(self):
|
||||||
|
"""
|
||||||
|
Test that the `update` queryset method properly invalidate cache
|
||||||
|
"""
|
||||||
|
mem_skia = self.skia.memberships.get(club=self.club)
|
||||||
|
cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia)
|
||||||
|
self.skia.memberships.update(end_date=localtime(now()).date())
|
||||||
|
self.assertEqual(
|
||||||
|
cache.get(f"membership_{mem_skia.club_id}_{mem_skia.user_id}"), "not_member"
|
||||||
|
)
|
||||||
|
|
||||||
|
mem_richard = self.richard.memberships.get(club=self.club)
|
||||||
|
cache.set(
|
||||||
|
f"membership_{mem_richard.club_id}_{mem_richard.user_id}", mem_richard
|
||||||
|
)
|
||||||
|
self.richard.memberships.update(role=5)
|
||||||
|
new_mem = self.richard.memberships.get(club=self.club)
|
||||||
|
self.assertNotEqual(new_mem, "not_member")
|
||||||
|
self.assertEqual(new_mem.role, 5)
|
||||||
|
|
||||||
|
def test_delete_invalidate_cache(self):
|
||||||
|
"""
|
||||||
|
Test that the `delete` queryset properly invalidate cache
|
||||||
|
"""
|
||||||
|
|
||||||
|
mem_skia = self.skia.memberships.get(club=self.club)
|
||||||
|
mem_comptable = self.comptable.memberships.get(club=self.club)
|
||||||
|
cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia)
|
||||||
|
cache.set(
|
||||||
|
f"membership_{mem_comptable.club_id}_{mem_comptable.user_id}", mem_comptable
|
||||||
|
)
|
||||||
|
|
||||||
|
# should delete the subscriptions of skia and comptable
|
||||||
|
self.club.members.ongoing().board().delete()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
cache.get(f"membership_{mem_skia.club_id}_{mem_skia.user_id}"), "not_member"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
cache.get(f"membership_{mem_comptable.club_id}_{mem_comptable.user_id}"),
|
||||||
|
"not_member",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ClubModelTest(ClubTest):
|
||||||
|
def assert_membership_just_started(self, user: User, role: int):
|
||||||
|
"""
|
||||||
|
Assert that the given membership is active and started today
|
||||||
|
"""
|
||||||
|
membership = user.memberships.ongoing().filter(club=self.club).first()
|
||||||
|
self.assertIsNotNone(membership)
|
||||||
|
self.assertEqual(localtime(now()).date(), membership.start_date)
|
||||||
|
self.assertIsNone(membership.end_date)
|
||||||
|
self.assertEqual(membership.role, role)
|
||||||
|
self.assertEqual(membership.club.get_membership_for(user), membership)
|
||||||
|
member_group = self.club.unix_name + settings.SITH_MEMBER_SUFFIX
|
||||||
|
board_group = self.club.unix_name + settings.SITH_BOARD_SUFFIX
|
||||||
|
self.assertTrue(user.is_in_group(name=member_group))
|
||||||
|
self.assertTrue(user.is_in_group(name=board_group))
|
||||||
|
|
||||||
|
def assert_membership_just_ended(self, user: User):
|
||||||
|
"""
|
||||||
|
Assert that the given user have a membership which ended today
|
||||||
|
"""
|
||||||
|
today = localtime(now()).date()
|
||||||
|
self.assertIsNotNone(
|
||||||
|
user.memberships.filter(club=self.club, end_date=today).first()
|
||||||
|
)
|
||||||
|
self.assertIsNone(self.club.get_membership_for(user))
|
||||||
|
|
||||||
|
def test_access_unauthorized(self):
|
||||||
|
"""
|
||||||
|
Test that users who never subscribed and anonymous users
|
||||||
|
cannot see the page
|
||||||
|
"""
|
||||||
|
response = self.client.post(self.members_url)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
self.client.login(username="public", password="plop")
|
||||||
|
response = self.client.post(self.members_url)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
def test_display(self):
|
||||||
|
"""
|
||||||
|
Test that a GET request return a page where the requested
|
||||||
|
information are displayed.
|
||||||
|
"""
|
||||||
|
self.client.login(username=self.skia.username, password="plop")
|
||||||
|
response = self.client.get(self.members_url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
expected_html = (
|
||||||
|
"<table><thead><tr>"
|
||||||
|
"<td>Utilisateur</td><td>Rôle</td><td>Description</td>"
|
||||||
|
"<td>Depuis</td><td>Marquer comme ancien</td>"
|
||||||
|
"</tr></thead><tbody>"
|
||||||
|
)
|
||||||
|
memberships = self.club.members.ongoing().order_by("-role")
|
||||||
|
input_id = 0
|
||||||
|
for membership in memberships.select_related("user"):
|
||||||
|
user = membership.user
|
||||||
|
expected_html += (
|
||||||
|
f"<tr><td><a href=\"{reverse('core:user_profile', args=[user.id])}\">"
|
||||||
|
f"{user.get_display_name()}</a></td>"
|
||||||
|
f"<td>{settings.SITH_CLUB_ROLES[membership.role]}</td>"
|
||||||
|
f"<td>{membership.description}</td>"
|
||||||
|
f"<td>{membership.start_date}</td><td>"
|
||||||
|
)
|
||||||
|
if membership.role <= 3: # 3 is the role of skia
|
||||||
|
expected_html += (
|
||||||
|
'<input type="checkbox" name="users_old" '
|
||||||
|
f'value="{user.id}" '
|
||||||
|
f'id="id_users_old_{input_id}">'
|
||||||
|
)
|
||||||
|
input_id += 1
|
||||||
|
expected_html += "</td></tr>"
|
||||||
|
expected_html += "</tbody></table>"
|
||||||
|
self.assertInHTML(expected_html, response.content.decode())
|
||||||
|
|
||||||
|
def test_root_add_one_club_member(self):
|
||||||
|
"""
|
||||||
|
Test that root users can add members to clubs, one at a time
|
||||||
|
"""
|
||||||
self.client.login(username="root", password="plop")
|
self.client.login(username="root", password="plop")
|
||||||
self.client.post(
|
response = self.client.post(
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
self.members_url,
|
||||||
|
{"users": self.subscriber.id, "role": 3},
|
||||||
|
)
|
||||||
|
self.assertRedirects(response, self.members_url)
|
||||||
|
self.subscriber.refresh_from_db()
|
||||||
|
self.assert_membership_just_started(self.subscriber, role=3)
|
||||||
|
|
||||||
|
def test_root_add_multiple_club_member(self):
|
||||||
|
"""
|
||||||
|
Test that root users can add multiple members at once to clubs
|
||||||
|
"""
|
||||||
|
self.client.login(username="root", password="plop")
|
||||||
|
response = self.client.post(
|
||||||
|
self.members_url,
|
||||||
{
|
{
|
||||||
"users": "|%d|%d|" % (self.skia.id, self.rbatsbak.id),
|
"users": f"|{self.subscriber.id}|{self.krophil.id}|",
|
||||||
"start_date": "12/06/2016",
|
|
||||||
"role": 3,
|
"role": 3,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
response = self.client.get(
|
self.assertRedirects(response, self.members_url)
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id})
|
self.subscriber.refresh_from_db()
|
||||||
)
|
self.assert_membership_just_started(self.subscriber, role=3)
|
||||||
self.assertTrue(response.status_code == 200)
|
self.assert_membership_just_started(self.krophil, role=3)
|
||||||
content = str(response.content)
|
|
||||||
self.assertTrue(
|
|
||||||
"S' Kia</a></td>\\n <td>Responsable info</td>"
|
|
||||||
in content
|
|
||||||
)
|
|
||||||
self.assertTrue(
|
|
||||||
"Richard Batsbak</a></td>\\n <td>Responsable info</td>"
|
|
||||||
in content
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_create_add_user_to_club_from_root_fail_not_subscriber(self):
|
def test_add_unauthorized_members(self):
|
||||||
|
"""
|
||||||
|
Test that users who are not currently subscribed
|
||||||
|
cannot be members of clubs.
|
||||||
|
"""
|
||||||
self.client.login(username="root", password="plop")
|
self.client.login(username="root", password="plop")
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
self.members_url,
|
||||||
{"users": self.guy.id, "start_date": "12/06/2016", "role": 3},
|
{"users": self.public.id, "role": 1},
|
||||||
)
|
)
|
||||||
self.assertTrue(response.status_code == 200)
|
self.assertIsNone(self.public.memberships.filter(club=self.club).first())
|
||||||
self.assertTrue('<ul class="errorlist"><li>' in str(response.content))
|
self.assertTrue('<ul class="errorlist"><li>' in str(response.content))
|
||||||
response = self.client.get(
|
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id})
|
|
||||||
)
|
|
||||||
self.assertFalse(
|
|
||||||
"Guy Carlier</a></td>\\n <td>Responsable info</td>"
|
|
||||||
in str(response.content)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_create_add_user_to_club_from_root_fail_already_in_club(self):
|
response = self.client.post(
|
||||||
|
self.members_url,
|
||||||
|
{"users": self.old_subscriber.id, "role": 1},
|
||||||
|
)
|
||||||
|
self.assertIsNone(self.public.memberships.filter(club=self.club).first())
|
||||||
|
self.assertIsNone(self.club.get_membership_for(self.public))
|
||||||
|
self.assertTrue('<ul class="errorlist"><li>' in str(response.content))
|
||||||
|
|
||||||
|
def test_add_members_already_members(self):
|
||||||
|
"""
|
||||||
|
Test that users who are already members of a club
|
||||||
|
cannot be added again to this club
|
||||||
|
"""
|
||||||
self.client.login(username="root", password="plop")
|
self.client.login(username="root", password="plop")
|
||||||
|
current_membership = self.skia.memberships.ongoing().get(club=self.club)
|
||||||
|
nb_memberships = self.skia.memberships.count()
|
||||||
self.client.post(
|
self.client.post(
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
self.members_url,
|
||||||
{"users": self.skia.id, "start_date": "12/06/2016", "role": 3},
|
{"users": self.skia.id, "role": current_membership.role + 1},
|
||||||
)
|
|
||||||
response = self.client.get(
|
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id})
|
|
||||||
)
|
|
||||||
self.assertTrue(
|
|
||||||
"S' Kia</a></td>\\n <td>Responsable info</td>"
|
|
||||||
in str(response.content)
|
|
||||||
)
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
|
||||||
{"users": self.skia.id, "start_date": "12/06/2016", "role": 4},
|
|
||||||
)
|
|
||||||
self.assertTrue(response.status_code == 200)
|
|
||||||
self.assertFalse(
|
|
||||||
"S' Kia</a></td>\\n <td>Secrétaire</td>"
|
|
||||||
in str(response.content)
|
|
||||||
)
|
)
|
||||||
|
self.skia.refresh_from_db()
|
||||||
|
self.assertEqual(nb_memberships, self.skia.memberships.count())
|
||||||
|
new_membership = self.skia.memberships.ongoing().get(club=self.club)
|
||||||
|
self.assertEqual(current_membership, new_membership)
|
||||||
|
self.assertEqual(self.club.get_membership_for(self.skia), new_membership)
|
||||||
|
|
||||||
def test_create_add_user_non_existent_to_club_from_root_fail(self):
|
def test_add_not_existing_users(self):
|
||||||
|
"""
|
||||||
|
Test that not existing users cannot be added in clubs.
|
||||||
|
If one user in the request is invalid, no membership creation at all
|
||||||
|
can take place.
|
||||||
|
"""
|
||||||
self.client.login(username="root", password="plop")
|
self.client.login(username="root", password="plop")
|
||||||
|
nb_memberships = self.club.members.count()
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
self.members_url,
|
||||||
{"users": [9999], "start_date": "12/06/2016", "role": 3},
|
{"users": [9999], "role": 1},
|
||||||
)
|
)
|
||||||
self.assertTrue(response.status_code == 200)
|
self.assertContains(response, '<ul class="errorlist"><li>')
|
||||||
content = str(response.content)
|
self.club.refresh_from_db()
|
||||||
self.assertTrue('<ul class="errorlist"><li>' in content)
|
self.assertEqual(self.club.members.count(), nb_memberships)
|
||||||
self.assertFalse("<td>Responsable info</td>" in content)
|
|
||||||
self.client.login(username="root", password="plop")
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
self.members_url,
|
||||||
{
|
{
|
||||||
"users": "|%d|%d|" % (self.skia.id, 9999),
|
"users": f"|{self.subscriber.id}|{9999}|",
|
||||||
"start_date": "12/06/2016",
|
"start_date": "12/06/2016",
|
||||||
"role": 3,
|
"role": 3,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertTrue(response.status_code == 200)
|
self.assertContains(response, '<ul class="errorlist"><li>')
|
||||||
content = str(response.content)
|
self.club.refresh_from_db()
|
||||||
self.assertTrue('<ul class="errorlist"><li>' in content)
|
self.assertEqual(self.club.members.count(), nb_memberships)
|
||||||
self.assertFalse("<td>Responsable info</td>" in content)
|
|
||||||
|
|
||||||
def test_create_add_user_to_club_from_skia_ok(self):
|
def test_president_add_members(self):
|
||||||
self.client.login(username="root", password="plop")
|
"""
|
||||||
self.client.post(
|
Test that the president of the club can add members
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
"""
|
||||||
{"users": self.skia.id, "start_date": "12/06/2016", "role": 10},
|
president = self.club.members.get(role=10).user
|
||||||
)
|
nb_club_membership = self.club.members.count()
|
||||||
self.client.login(username="skia", password="plop")
|
nb_subscriber_memberships = self.subscriber.memberships.count()
|
||||||
self.client.post(
|
self.client.login(username=president.username, password="plop")
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
|
||||||
{"users": self.rbatsbak.id, "start_date": "12/06/2016", "role": 9},
|
|
||||||
)
|
|
||||||
response = self.client.get(
|
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id})
|
|
||||||
)
|
|
||||||
self.assertTrue(response.status_code == 200)
|
|
||||||
self.assertTrue(
|
|
||||||
"""Richard Batsbak</a></td>\\n <td>Vice-Pr\\xc3\\xa9sident</td>"""
|
|
||||||
in str(response.content)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_create_add_user_to_club_from_richard_fail(self):
|
|
||||||
self.client.login(username="root", password="plop")
|
|
||||||
self.client.post(
|
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
|
||||||
{"users": self.rbatsbak.id, "start_date": "12/06/2016", "role": 3},
|
|
||||||
)
|
|
||||||
self.client.login(username="rbatsbak", password="plop")
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
self.members_url,
|
||||||
{"users": self.skia.id, "start_date": "12/06/2016", "role": 10},
|
{"users": self.subscriber.id, "role": 9},
|
||||||
)
|
)
|
||||||
self.assertTrue(response.status_code == 200)
|
self.assertRedirects(response, self.members_url)
|
||||||
self.assertTrue(
|
self.club.refresh_from_db()
|
||||||
"<li>Vous n'avez pas la permission de faire cela</li>"
|
self.subscriber.refresh_from_db()
|
||||||
in str(response.content)
|
self.assertEqual(self.club.members.count(), nb_club_membership + 1)
|
||||||
|
self.assertEqual(
|
||||||
|
self.subscriber.memberships.count(), nb_subscriber_memberships + 1
|
||||||
)
|
)
|
||||||
|
self.assert_membership_just_started(self.subscriber, role=9)
|
||||||
|
|
||||||
def test_role_required_if_users_specified(self):
|
def test_add_member_greater_role(self):
|
||||||
|
"""
|
||||||
|
Test that a member of the club member cannot create
|
||||||
|
a membership with a greater role than its own.
|
||||||
|
"""
|
||||||
|
self.client.login(username=self.skia.username, password="plop")
|
||||||
|
nb_memberships = self.club.members.count()
|
||||||
|
response = self.client.post(
|
||||||
|
self.members_url,
|
||||||
|
{"users": self.subscriber.id, "role": 10},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertInHTML(
|
||||||
|
"<li>Vous n'avez pas la permission de faire cela</li>",
|
||||||
|
response.content.decode(),
|
||||||
|
)
|
||||||
|
self.club.refresh_from_db()
|
||||||
|
self.assertEqual(nb_memberships, self.club.members.count())
|
||||||
|
self.assertIsNone(self.subscriber.memberships.filter(club=self.club).first())
|
||||||
|
|
||||||
|
def test_add_member_without_role(self):
|
||||||
|
"""
|
||||||
|
Test that trying to add members without specifying their role fails
|
||||||
|
"""
|
||||||
self.client.login(username="root", password="plop")
|
self.client.login(username="root", password="plop")
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
self.members_url,
|
||||||
{"users": self.rbatsbak.id, "start_date": "12/06/2016"},
|
{"users": self.subscriber.id, "start_date": "12/06/2016"},
|
||||||
)
|
)
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
'<ul class="errorlist"><li>Vous devez choisir un r' in str(response.content)
|
'<ul class="errorlist"><li>Vous devez choisir un r' in str(response.content)
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_mark_old_user_to_club_from_skia_ok(self):
|
def test_end_membership_self(self):
|
||||||
self.client.login(username="root", password="plop")
|
"""
|
||||||
self.client.post(
|
Test that a member can end its own membership
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
"""
|
||||||
{
|
|
||||||
"users": "|%d|%d|" % (self.skia.id, self.rbatsbak.id),
|
|
||||||
"start_date": "12/06/2016",
|
|
||||||
"role": 3,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.client.login(username="skia", password="plop")
|
self.client.login(username="skia", password="plop")
|
||||||
response = self.client.post(
|
self.client.post(
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
self.members_url,
|
||||||
{"users_old": self.rbatsbak.id},
|
|
||||||
)
|
|
||||||
self.assertTrue(response.status_code == 302)
|
|
||||||
|
|
||||||
response = self.client.get(
|
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id})
|
|
||||||
)
|
|
||||||
self.assertTrue(response.status_code == 200)
|
|
||||||
content = str(response.content)
|
|
||||||
self.assertFalse(
|
|
||||||
"Richard Batsbak</a></td>\\n <td>Responsable info</td>"
|
|
||||||
in content
|
|
||||||
)
|
|
||||||
self.assertTrue(
|
|
||||||
"S' Kia</a></td>\\n <td>Responsable info</td>"
|
|
||||||
in content
|
|
||||||
)
|
|
||||||
|
|
||||||
# Skia is board member so he should be able to mark as old even without being in the club
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
|
||||||
{"users_old": self.skia.id},
|
{"users_old": self.skia.id},
|
||||||
)
|
)
|
||||||
|
self.skia.refresh_from_db()
|
||||||
|
self.assert_membership_just_ended(self.skia)
|
||||||
|
|
||||||
|
def test_end_membership_lower_role(self):
|
||||||
|
"""
|
||||||
|
Test that board members of the club can end memberships
|
||||||
|
of users with lower roles
|
||||||
|
"""
|
||||||
|
# remainder : skia has role 3, comptable has role 10, richard has role 1
|
||||||
|
self.client.login(username=self.skia.username, password="plop")
|
||||||
|
response = self.client.post(
|
||||||
|
self.members_url,
|
||||||
|
{"users_old": self.richard.id},
|
||||||
|
)
|
||||||
|
self.assertRedirects(response, self.members_url)
|
||||||
|
self.club.refresh_from_db()
|
||||||
|
self.assert_membership_just_ended(self.richard)
|
||||||
|
|
||||||
|
def test_end_membership_higher_role(self):
|
||||||
|
"""
|
||||||
|
Test that board members of the club cannot end memberships
|
||||||
|
of users with higher roles
|
||||||
|
"""
|
||||||
|
membership = self.comptable.memberships.filter(club=self.club).first()
|
||||||
|
self.client.login(username=self.skia.username, password="plop")
|
||||||
|
self.client.post(
|
||||||
|
self.members_url,
|
||||||
|
{"users_old": self.comptable.id},
|
||||||
|
)
|
||||||
|
self.club.refresh_from_db()
|
||||||
|
new_membership = self.club.get_membership_for(self.comptable)
|
||||||
|
self.assertIsNotNone(new_membership)
|
||||||
|
self.assertEqual(new_membership, membership)
|
||||||
|
|
||||||
|
membership = self.comptable.memberships.filter(club=self.club).first()
|
||||||
|
self.assertIsNone(membership.end_date)
|
||||||
|
|
||||||
|
def test_end_membership_as_main_club_board(self):
|
||||||
|
"""
|
||||||
|
Test that board members of the main club can end the membership
|
||||||
|
of anyone
|
||||||
|
"""
|
||||||
|
# make subscriber a board member
|
||||||
|
self.subscriber.memberships.all().delete()
|
||||||
|
Membership.objects.create(club=self.ae, user=self.subscriber, role=3)
|
||||||
|
|
||||||
|
nb_memberships = self.club.members.count()
|
||||||
|
self.client.login(username=self.subscriber.username, password="plop")
|
||||||
|
response = self.client.post(
|
||||||
|
self.members_url,
|
||||||
|
{"users_old": self.comptable.id},
|
||||||
|
)
|
||||||
|
self.assertRedirects(response, self.members_url)
|
||||||
|
self.assert_membership_just_ended(self.comptable)
|
||||||
|
self.assertEqual(self.club.members.ongoing().count(), nb_memberships - 1)
|
||||||
|
|
||||||
|
def test_end_membership_as_root(self):
|
||||||
|
"""
|
||||||
|
Test that root users can end the membership of anyone
|
||||||
|
"""
|
||||||
|
nb_memberships = self.club.members.count()
|
||||||
self.client.login(username="root", password="plop")
|
self.client.login(username="root", password="plop")
|
||||||
self.client.post(
|
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
|
||||||
{"users": self.rbatsbak.id, "start_date": "12/06/2016", "role": 3},
|
|
||||||
)
|
|
||||||
self.client.login(username="skia", password="plop")
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
self.members_url,
|
||||||
{"users_old": self.rbatsbak.id},
|
{"users_old": [self.comptable.id]},
|
||||||
)
|
|
||||||
self.assertFalse(
|
|
||||||
"Richard Batsbak</a></td>\\n <td>Responsable info</td>"
|
|
||||||
in str(response.content)
|
|
||||||
)
|
)
|
||||||
|
self.assertRedirects(response, self.members_url)
|
||||||
|
self.assert_membership_just_ended(self.comptable)
|
||||||
|
self.assertEqual(self.club.members.ongoing().count(), nb_memberships - 1)
|
||||||
|
self.assertEqual(self.club.members.count(), nb_memberships)
|
||||||
|
|
||||||
def test_mark_old_multiple_users_from_skia_ok(self):
|
def test_end_membership_as_foreigner(self):
|
||||||
self.client.login(username="root", password="plop")
|
"""
|
||||||
|
Test that users who are not in this club cannot end its memberships
|
||||||
|
"""
|
||||||
|
nb_memberships = self.club.members.count()
|
||||||
|
membership = self.richard.memberships.filter(club=self.club).first()
|
||||||
|
self.client.login(username="subscriber", password="root")
|
||||||
self.client.post(
|
self.client.post(
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
self.members_url,
|
||||||
{
|
{"users_old": [self.richard.id]},
|
||||||
"users": "|%d|%d|" % (self.skia.id, self.rbatsbak.id),
|
|
||||||
"start_date": "12/06/2016",
|
|
||||||
"role": 3,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
self.client.login(username="skia", password="plop")
|
# nothing should have changed
|
||||||
response = self.client.post(
|
new_mem = self.club.get_membership_for(self.richard)
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
self.assertIsNotNone(new_mem)
|
||||||
{"users_old": [self.rbatsbak.id, self.skia.id]},
|
self.assertEqual(self.club.members.count(), nb_memberships)
|
||||||
)
|
self.assertEqual(membership, new_mem)
|
||||||
self.assertTrue(response.status_code == 302)
|
|
||||||
|
|
||||||
response = self.client.get(
|
def test_delete_remove_from_meta_group(self):
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id})
|
"""
|
||||||
)
|
Test that when a club is deleted, all its members are removed from the
|
||||||
self.assertTrue(response.status_code == 200)
|
associated metagroup
|
||||||
content = str(response.content)
|
"""
|
||||||
self.assertFalse(
|
memberships = self.club.members.select_related("user")
|
||||||
"Richard Batsbak</a></td>\\n <td>Responsable info</td>"
|
users = [membership.user for membership in memberships]
|
||||||
in content
|
meta_group = self.club.unix_name + settings.SITH_MEMBER_SUFFIX
|
||||||
)
|
|
||||||
self.assertFalse(
|
|
||||||
"S' Kia</a></td>\\n <td>Responsable info</td>"
|
|
||||||
in content
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_mark_old_user_to_club_from_richard_ok(self):
|
self.club.delete()
|
||||||
self.client.login(username="root", password="plop")
|
for user in users:
|
||||||
self.client.post(
|
self.assertFalse(user.is_in_group(name=meta_group))
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
|
||||||
{
|
|
||||||
"users": "|%d|%d|" % (self.skia.id, self.rbatsbak.id),
|
|
||||||
"start_date": "12/06/2016",
|
|
||||||
"role": 3,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test with equal rights
|
def test_add_to_meta_group(self):
|
||||||
self.client.login(username="rbatsbak", password="plop")
|
"""
|
||||||
response = self.client.post(
|
Test that when a membership begins, the user is added to the meta group
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
"""
|
||||||
{"users_old": self.skia.id},
|
group_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX
|
||||||
)
|
board_members = self.club.unix_name + settings.SITH_BOARD_SUFFIX
|
||||||
self.assertTrue(response.status_code == 302)
|
self.assertFalse(self.subscriber.is_in_group(name=group_members))
|
||||||
|
self.assertFalse(self.subscriber.is_in_group(name=board_members))
|
||||||
|
Membership.objects.create(club=self.club, user=self.subscriber, role=3)
|
||||||
|
self.assertTrue(self.subscriber.is_in_group(name=group_members))
|
||||||
|
self.assertTrue(self.subscriber.is_in_group(name=board_members))
|
||||||
|
|
||||||
response = self.client.get(
|
def test_remove_from_meta_group(self):
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id})
|
"""
|
||||||
)
|
Test that when a membership ends, the user is removed from meta group
|
||||||
self.assertTrue(response.status_code == 200)
|
"""
|
||||||
content = str(response.content)
|
group_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX
|
||||||
self.assertTrue(
|
board_members = self.club.unix_name + settings.SITH_BOARD_SUFFIX
|
||||||
"Richard Batsbak</a></td>\\n <td>Responsable info</td>"
|
self.assertTrue(self.comptable.is_in_group(name=group_members))
|
||||||
in content
|
self.assertTrue(self.comptable.is_in_group(name=board_members))
|
||||||
)
|
self.comptable.memberships.update(end_date=localtime(now()))
|
||||||
self.assertFalse(
|
self.assertFalse(self.comptable.is_in_group(name=group_members))
|
||||||
"S' Kia</a></td>\\n <td>Responsable info</td>"
|
self.assertFalse(self.comptable.is_in_group(name=board_members))
|
||||||
in content
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test with lower rights
|
def test_club_owner(self):
|
||||||
self.client.post(
|
"""
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
Test that a club is owned only by board members of the main club
|
||||||
{"users": self.skia.id, "start_date": "12/06/2016", "role": 0},
|
"""
|
||||||
)
|
anonymous = AnonymousUser()
|
||||||
|
self.assertFalse(self.club.is_owned_by(anonymous))
|
||||||
|
self.assertFalse(self.club.is_owned_by(self.subscriber))
|
||||||
|
|
||||||
self.client.post(
|
# make sli a board member
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
self.sli.memberships.all().delete()
|
||||||
{"users_old": self.skia.id},
|
Membership(club=self.ae, user=self.sli, role=3).save()
|
||||||
)
|
self.assertTrue(self.club.is_owned_by(self.sli))
|
||||||
response = self.client.get(
|
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id})
|
|
||||||
)
|
|
||||||
self.assertTrue(response.status_code == 200)
|
|
||||||
content = str(response.content)
|
|
||||||
self.assertTrue(
|
|
||||||
"Richard Batsbak</a></td>\\n <td>Responsable info</td>"
|
|
||||||
in content
|
|
||||||
)
|
|
||||||
self.assertFalse(
|
|
||||||
"S' Kia</a></td>\\n <td>Curieux</td>" in content
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_mark_old_user_to_club_from_richard_fail(self):
|
|
||||||
self.client.login(username="root", password="plop")
|
|
||||||
self.client.post(
|
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
|
||||||
{"users": self.skia.id, "start_date": "12/06/2016", "role": 3},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test with richard outside of the club
|
|
||||||
self.client.login(username="rbatsbak", password="plop")
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
|
||||||
{"users_old": self.skia.id},
|
|
||||||
)
|
|
||||||
self.assertTrue(response.status_code == 200)
|
|
||||||
|
|
||||||
response = self.client.get(
|
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id})
|
|
||||||
)
|
|
||||||
self.assertTrue(response.status_code == 200)
|
|
||||||
self.assertTrue(
|
|
||||||
"S' Kia</a></td>\\n <td>Responsable info</td>"
|
|
||||||
in str(response.content)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test with lower rights
|
|
||||||
self.client.post(
|
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
|
||||||
{"users": self.rbatsbak.id, "start_date": "12/06/2016", "role": 0},
|
|
||||||
)
|
|
||||||
|
|
||||||
self.client.post(
|
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id}),
|
|
||||||
{"users_old": self.skia.id},
|
|
||||||
)
|
|
||||||
response = self.client.get(
|
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id})
|
|
||||||
)
|
|
||||||
self.assertTrue(response.status_code == 200)
|
|
||||||
content = str(response.content)
|
|
||||||
self.assertTrue(
|
|
||||||
"Richard Batsbak</a></td>\\n <td>Curieux</td>" in content
|
|
||||||
)
|
|
||||||
self.assertTrue(
|
|
||||||
"S' Kia</a></td>\\n <td>Responsable info</td>"
|
|
||||||
in content
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MailingFormTest(TestCase):
|
class MailingFormTest(TestCase):
|
||||||
"""Perform validation tests for MailingForm"""
|
"""Perform validation tests for MailingForm"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.skia = User.objects.filter(username="skia").first()
|
||||||
|
cls.rbatsbak = User.objects.filter(username="rbatsbak").first()
|
||||||
|
cls.krophil = User.objects.filter(username="krophil").first()
|
||||||
|
cls.comunity = User.objects.filter(username="comunity").first()
|
||||||
|
cls.bdf = Club.objects.filter(unix_name=SITH_BAR_MANAGER["unix_name"]).first()
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
call_command("populate")
|
|
||||||
self.skia = User.objects.filter(username="skia").first()
|
|
||||||
self.rbatsbak = User.objects.filter(username="rbatsbak").first()
|
|
||||||
self.krophil = User.objects.filter(username="krophil").first()
|
|
||||||
self.comunity = User.objects.filter(username="comunity").first()
|
|
||||||
self.bdf = Club.objects.filter(unix_name="bdf").first()
|
|
||||||
Membership(
|
Membership(
|
||||||
user=self.rbatsbak,
|
user=self.rbatsbak,
|
||||||
club=self.bdf,
|
club=self.bdf,
|
||||||
@ -641,7 +822,7 @@ class MailingFormTest(TestCase):
|
|||||||
{"action": MailingForm.ACTION_NEW_MAILING, "mailing_email": "mde"},
|
{"action": MailingForm.ACTION_NEW_MAILING, "mailing_email": "mde"},
|
||||||
)
|
)
|
||||||
mde = Mailing.objects.get(email="mde")
|
mde = Mailing.objects.get(email="mde")
|
||||||
response = self.client.post(
|
self.client.post(
|
||||||
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
|
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
|
||||||
{
|
{
|
||||||
"action": MailingForm.ACTION_NEW_SUBSCRIPTION,
|
"action": MailingForm.ACTION_NEW_SUBSCRIPTION,
|
||||||
@ -650,6 +831,11 @@ class MailingFormTest(TestCase):
|
|||||||
"subscription_mailing": mde.id,
|
"subscription_mailing": mde.id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("club:mailing", kwargs={"club_id": self.bdf.id})
|
||||||
|
)
|
||||||
|
|
||||||
self.assertContains(response, "comunity@git.an")
|
self.assertContains(response, "comunity@git.an")
|
||||||
self.assertContains(response, "richard@git.an")
|
self.assertContains(response, "richard@git.an")
|
||||||
self.assertContains(response, "krophil@git.an")
|
self.assertContains(response, "krophil@git.an")
|
||||||
@ -699,7 +885,6 @@ class ClubSellingViewTest(TestCase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
call_command("populate")
|
|
||||||
self.ae = Club.objects.filter(unix_name="ae").first()
|
self.ae = Club.objects.filter(unix_name="ae").first()
|
||||||
|
|
||||||
def test_page_not_internal_error(self):
|
def test_page_not_internal_error(self):
|
||||||
|
79
club/urls.py
79
club/urls.py
@ -23,81 +23,84 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.urls import path
|
||||||
|
|
||||||
from club.views import *
|
from club.views import *
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r"^$", ClubListView.as_view(), name="club_list"),
|
path("", ClubListView.as_view(), name="club_list"),
|
||||||
url(r"^new$", ClubCreateView.as_view(), name="club_new"),
|
path("new/", ClubCreateView.as_view(), name="club_new"),
|
||||||
url(r"^stats$", ClubStatView.as_view(), name="club_stats"),
|
path("stats/", ClubStatView.as_view(), name="club_stats"),
|
||||||
url(r"^(?P<club_id>[0-9]+)/$", ClubView.as_view(), name="club_view"),
|
path("<int:club_id>/", ClubView.as_view(), name="club_view"),
|
||||||
url(
|
path(
|
||||||
r"^(?P<club_id>[0-9]+)/rev/(?P<rev_id>[0-9]+)/$",
|
"<int:club_id>/rev/<int:rev_id>/",
|
||||||
ClubRevView.as_view(),
|
ClubRevView.as_view(),
|
||||||
name="club_view_rev",
|
name="club_view_rev",
|
||||||
),
|
),
|
||||||
url(r"^(?P<club_id>[0-9]+)/hist$", ClubPageHistView.as_view(), name="club_hist"),
|
path("<int:club_id>/hist/", ClubPageHistView.as_view(), name="club_hist"),
|
||||||
url(r"^(?P<club_id>[0-9]+)/edit$", ClubEditView.as_view(), name="club_edit"),
|
path("<int:club_id>/edit/", ClubEditView.as_view(), name="club_edit"),
|
||||||
url(
|
path(
|
||||||
r"^(?P<club_id>[0-9]+)/edit/page$",
|
"<int:club_id>/edit/page/",
|
||||||
ClubPageEditView.as_view(),
|
ClubPageEditView.as_view(),
|
||||||
name="club_edit_page",
|
name="club_edit_page",
|
||||||
),
|
),
|
||||||
url(
|
path("<int:club_id>/members/", ClubMembersView.as_view(), name="club_members"),
|
||||||
r"^(?P<club_id>[0-9]+)/members$", ClubMembersView.as_view(), name="club_members"
|
path(
|
||||||
),
|
"<int:club_id>/elderlies/",
|
||||||
url(
|
|
||||||
r"^(?P<club_id>[0-9]+)/elderlies$",
|
|
||||||
ClubOldMembersView.as_view(),
|
ClubOldMembersView.as_view(),
|
||||||
name="club_old_members",
|
name="club_old_members",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^(?P<club_id>[0-9]+)/sellings$",
|
"<int:club_id>/sellings/",
|
||||||
ClubSellingView.as_view(),
|
ClubSellingView.as_view(),
|
||||||
name="club_sellings",
|
name="club_sellings",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^(?P<club_id>[0-9]+)/sellings/csv$",
|
"<int:club_id>/sellings/csv/",
|
||||||
ClubSellingCSVView.as_view(),
|
ClubSellingCSVView.as_view(),
|
||||||
name="sellings_csv",
|
name="sellings_csv",
|
||||||
),
|
),
|
||||||
url(r"^(?P<club_id>[0-9]+)/prop$", ClubEditPropView.as_view(), name="club_prop"),
|
path("<int:club_id>/prop/", ClubEditPropView.as_view(), name="club_prop"),
|
||||||
url(r"^(?P<club_id>[0-9]+)/tools$", ClubToolsView.as_view(), name="tools"),
|
path("<int:club_id>/tools/", ClubToolsView.as_view(), name="tools"),
|
||||||
url(r"^(?P<club_id>[0-9]+)/mailing$", ClubMailingView.as_view(), name="mailing"),
|
path("<int:club_id>/mailing/", ClubMailingView.as_view(), name="mailing"),
|
||||||
url(
|
path(
|
||||||
r"^(?P<mailing_id>[0-9]+)/mailing/generate$",
|
"<int:mailing_id>/mailing/generate/",
|
||||||
MailingAutoGenerationView.as_view(),
|
MailingAutoGenerationView.as_view(),
|
||||||
name="mailing_generate",
|
name="mailing_generate",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^(?P<mailing_id>[0-9]+)/mailing/delete$",
|
"<int:mailing_id>/mailing/delete/",
|
||||||
MailingDeleteView.as_view(),
|
MailingDeleteView.as_view(),
|
||||||
name="mailing_delete",
|
name="mailing_delete",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^(?P<mailing_subscription_id>[0-9]+)/mailing/delete/subscription$",
|
"<int:mailing_subscription_id>/mailing/delete/subscription/",
|
||||||
MailingSubscriptionDeleteView.as_view(),
|
MailingSubscriptionDeleteView.as_view(),
|
||||||
name="mailing_subscription_delete",
|
name="mailing_subscription_delete",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^membership/(?P<membership_id>[0-9]+)/set_old$",
|
"membership/<int:membership_id>/set_old/",
|
||||||
MembershipSetOldView.as_view(),
|
MembershipSetOldView.as_view(),
|
||||||
name="membership_set_old",
|
name="membership_set_old",
|
||||||
),
|
),
|
||||||
url(r"^(?P<club_id>[0-9]+)/poster$", PosterListView.as_view(), name="poster_list"),
|
path(
|
||||||
url(
|
"membership/<int:membership_id>/delete/",
|
||||||
r"^(?P<club_id>[0-9]+)/poster/create$",
|
MembershipDeleteView.as_view(),
|
||||||
|
name="membership_delete",
|
||||||
|
),
|
||||||
|
path("<int:club_id>/poster/", PosterListView.as_view(), name="poster_list"),
|
||||||
|
path(
|
||||||
|
"<int:club_id>/poster/create/",
|
||||||
PosterCreateView.as_view(),
|
PosterCreateView.as_view(),
|
||||||
name="poster_create",
|
name="poster_create",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^(?P<club_id>[0-9]+)/poster/(?P<poster_id>[0-9]+)/edit$",
|
"<int:club_id>/poster/<int:poster_id>/edit/",
|
||||||
PosterEditView.as_view(),
|
PosterEditView.as_view(),
|
||||||
name="poster_edit",
|
name="poster_edit",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^(?P<club_id>[0-9]+)/poster/(?P<poster_id>[0-9]+)/delete$",
|
"<int:club_id>/poster/<int:poster_id>/delete/",
|
||||||
PosterDeleteView.as_view(),
|
PosterDeleteView.as_view(),
|
||||||
name="poster_delete",
|
name="poster_delete",
|
||||||
),
|
),
|
||||||
|
196
club/views.py
196
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 (
|
||||||
from django.core.urlresolvers import reverse, reverse_lazy
|
HttpResponseRedirect,
|
||||||
|
HttpResponse,
|
||||||
|
Http404,
|
||||||
|
StreamingHttpResponse,
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
|
||||||
@ -296,9 +306,7 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
|
|||||||
return resp
|
return resp
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
self.members = (
|
self.members = self.get_object().members.ongoing().order_by("-role")
|
||||||
self.get_object().members.filter(end_date=None).order_by("-role").all()
|
|
||||||
)
|
|
||||||
return super(ClubMembersView, self).dispatch(request, *args, **kwargs)
|
return super(ClubMembersView, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_success_url(self, **kwargs):
|
def get_success_url(self, **kwargs):
|
||||||
@ -318,7 +326,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 +335,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 +371,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 +409,45 @@ 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"""
|
||||||
|
|
||||||
response = HttpResponse(content_type="text/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):
|
||||||
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 +468,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
|
||||||
|
|
||||||
@ -451,7 +507,7 @@ class ClubEditPropView(ClubTabsMixin, CanEditPropMixin, UpdateView):
|
|||||||
current_tab = "props"
|
current_tab = "props"
|
||||||
|
|
||||||
|
|
||||||
class ClubCreateView(CanEditPropMixin, CreateView):
|
class ClubCreateView(CanCreateMixin, CreateView):
|
||||||
"""
|
"""
|
||||||
Create a club (for the Sith admin)
|
Create a club (for the Sith admin)
|
||||||
"""
|
"""
|
||||||
@ -493,6 +549,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"
|
||||||
|
|
||||||
@ -574,7 +643,8 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
|||||||
except ValidationError as validation_error:
|
except ValidationError as validation_error:
|
||||||
return validation_error
|
return validation_error
|
||||||
|
|
||||||
users_to_save.append(sub.save())
|
sub.save()
|
||||||
|
users_to_save.append(sub)
|
||||||
|
|
||||||
if cleaned_data["subscription_email"]:
|
if cleaned_data["subscription_email"]:
|
||||||
sub = MailingSubscription(
|
sub = MailingSubscription(
|
||||||
@ -633,7 +703,6 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
|||||||
|
|
||||||
|
|
||||||
class MailingDeleteView(CanEditMixin, DeleteView):
|
class MailingDeleteView(CanEditMixin, DeleteView):
|
||||||
|
|
||||||
model = Mailing
|
model = Mailing
|
||||||
template_name = "core/delete_confirm.jinja"
|
template_name = "core/delete_confirm.jinja"
|
||||||
pk_url_kwarg = "mailing_id"
|
pk_url_kwarg = "mailing_id"
|
||||||
@ -651,7 +720,6 @@ class MailingDeleteView(CanEditMixin, DeleteView):
|
|||||||
|
|
||||||
|
|
||||||
class MailingSubscriptionDeleteView(CanEditMixin, DeleteView):
|
class MailingSubscriptionDeleteView(CanEditMixin, DeleteView):
|
||||||
|
|
||||||
model = MailingSubscription
|
model = MailingSubscription
|
||||||
template_name = "core/delete_confirm.jinja"
|
template_name = "core/delete_confirm.jinja"
|
||||||
pk_url_kwarg = "mailing_subscription_id"
|
pk_url_kwarg = "mailing_subscription_id"
|
||||||
|
@ -1,23 +1,15 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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.
|
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
50
com/admin.py
50
com/admin.py
@ -1,43 +1,49 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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 ajax_select import make_ajax_form
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from haystack.admin import SearchModelAdmin
|
from haystack.admin import SearchModelAdmin
|
||||||
|
|
||||||
from com.models import *
|
from com.models import *
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(News)
|
||||||
class NewsAdmin(SearchModelAdmin):
|
class NewsAdmin(SearchModelAdmin):
|
||||||
search_fields = ["title", "summary", "content"]
|
list_display = ("title", "type", "club", "author")
|
||||||
|
search_fields = ("title", "summary", "content")
|
||||||
|
form = make_ajax_form(
|
||||||
|
News,
|
||||||
|
{
|
||||||
|
"author": "users",
|
||||||
|
"moderator": "users",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Poster)
|
||||||
|
class PosterAdmin(SearchModelAdmin):
|
||||||
|
list_display = ("name", "club", "date_begin", "date_end", "moderator")
|
||||||
|
form = make_ajax_form(Poster, {"moderator": "users"})
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Weekmail)
|
||||||
class WeekmailAdmin(SearchModelAdmin):
|
class WeekmailAdmin(SearchModelAdmin):
|
||||||
search_fields = ["title"]
|
list_display = ("title", "sent")
|
||||||
|
search_fields = ("title",)
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Sith)
|
admin.site.register(Sith)
|
||||||
admin.site.register(News, NewsAdmin)
|
|
||||||
admin.site.register(Weekmail, WeekmailAdmin)
|
|
||||||
admin.site.register(Screen)
|
admin.site.register(Screen)
|
||||||
admin.site.register(Poster)
|
|
||||||
|
@ -5,7 +5,6 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -3,10 +3,10 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("club", "0005_auto_20161120_1149"),
|
("club", "0005_auto_20161120_1149"),
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
@ -50,6 +50,7 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"author",
|
"author",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
related_name="owned_news",
|
related_name="owned_news",
|
||||||
to=settings.AUTH_USER_MODEL,
|
to=settings.AUTH_USER_MODEL,
|
||||||
verbose_name="author",
|
verbose_name="author",
|
||||||
@ -58,12 +59,16 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"club",
|
"club",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
related_name="news", to="club.Club", verbose_name="club"
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="news",
|
||||||
|
to="club.Club",
|
||||||
|
verbose_name="club",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"moderator",
|
"moderator",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
related_name="moderated_news",
|
related_name="moderated_news",
|
||||||
null=True,
|
null=True,
|
||||||
to=settings.AUTH_USER_MODEL,
|
to=settings.AUTH_USER_MODEL,
|
||||||
@ -99,7 +104,10 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"news",
|
"news",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
related_name="dates", to="com.News", verbose_name="news_date"
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="dates",
|
||||||
|
to="com.News",
|
||||||
|
verbose_name="news_date",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -3,10 +3,10 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("club", "0006_auto_20161229_0040"),
|
("club", "0006_auto_20161229_0040"),
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
@ -56,6 +56,7 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"author",
|
"author",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
to=settings.AUTH_USER_MODEL,
|
to=settings.AUTH_USER_MODEL,
|
||||||
verbose_name="author",
|
verbose_name="author",
|
||||||
related_name="owned_weekmail_articles",
|
related_name="owned_weekmail_articles",
|
||||||
@ -64,6 +65,7 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"club",
|
"club",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
to="club.Club",
|
to="club.Club",
|
||||||
verbose_name="club",
|
verbose_name="club",
|
||||||
related_name="weekmail_articles",
|
related_name="weekmail_articles",
|
||||||
@ -72,6 +74,7 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"weekmail",
|
"weekmail",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
to="com.Weekmail",
|
to="com.Weekmail",
|
||||||
verbose_name="weekmail",
|
verbose_name="weekmail",
|
||||||
related_name="articles",
|
related_name="articles",
|
||||||
|
@ -4,10 +4,10 @@ from __future__ import unicode_literals
|
|||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("club", "0010_auto_20170912_2028"),
|
("club", "0010_auto_20170912_2028"),
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
@ -48,12 +48,16 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"club",
|
"club",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
verbose_name="club", related_name="posters", to="club.Club"
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
verbose_name="club",
|
||||||
|
related_name="posters",
|
||||||
|
to="club.Club",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"moderator",
|
"moderator",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
verbose_name="moderator",
|
verbose_name="moderator",
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
|
@ -5,7 +5,6 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("com", "0004_auto_20171221_1614")]
|
dependencies = [("com", "0004_auto_20171221_1614")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
11
com/migrations/0006_remove_sith_index_page.py
Normal file
11
com/migrations/0006_remove_sith_index_page.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.23 on 2019-08-18 17:00
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [("com", "0005_auto_20180318_2227")]
|
||||||
|
|
||||||
|
operations = [migrations.RemoveField(model_name="sith", name="index_page")]
|
115
com/models.py
115
com/models.py
@ -26,16 +26,17 @@
|
|||||||
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.core.urlresolvers 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
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from core import utils
|
||||||
from core.models import User, Preferences, RealGroup, Notification, SithFile
|
from core.models import User, Preferences, RealGroup, Notification, SithFile
|
||||||
from club.models import Club
|
from club.models import Club
|
||||||
|
|
||||||
@ -45,11 +46,13 @@ class Sith(models.Model):
|
|||||||
|
|
||||||
alert_msg = models.TextField(_("alert message"), default="", blank=True)
|
alert_msg = models.TextField(_("alert message"), default="", blank=True)
|
||||||
info_msg = models.TextField(_("info message"), default="", blank=True)
|
info_msg = models.TextField(_("info message"), default="", blank=True)
|
||||||
index_page = models.TextField(_("index page"), default="", blank=True)
|
|
||||||
weekmail_destinations = models.TextField(_("weekmail destinations"), default="")
|
weekmail_destinations = models.TextField(_("weekmail destinations"), default="")
|
||||||
|
version = utils.get_git_revision_short_hash()
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
return user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID)
|
if user.is_anonymous:
|
||||||
|
return False
|
||||||
|
return user.is_com_admin
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "⛩ Sith ⛩"
|
return "⛩ Sith ⛩"
|
||||||
@ -72,23 +75,34 @@ class News(models.Model):
|
|||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
_("type"), max_length=16, choices=NEWS_TYPES, default="EVENT"
|
_("type"), max_length=16, choices=NEWS_TYPES, default="EVENT"
|
||||||
)
|
)
|
||||||
club = models.ForeignKey(Club, related_name="news", verbose_name=_("club"))
|
club = models.ForeignKey(
|
||||||
|
Club, related_name="news", verbose_name=_("club"), on_delete=models.CASCADE
|
||||||
|
)
|
||||||
author = models.ForeignKey(
|
author = models.ForeignKey(
|
||||||
User, related_name="owned_news", verbose_name=_("author")
|
User,
|
||||||
|
related_name="owned_news",
|
||||||
|
verbose_name=_("author"),
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
is_moderated = models.BooleanField(_("is moderated"), default=False)
|
is_moderated = models.BooleanField(_("is moderated"), default=False)
|
||||||
moderator = models.ForeignKey(
|
moderator = models.ForeignKey(
|
||||||
User, related_name="moderated_news", verbose_name=_("moderator"), null=True
|
User,
|
||||||
|
related_name="moderated_news",
|
||||||
|
verbose_name=_("moderator"),
|
||||||
|
null=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
return user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) or user == self.author
|
if user.is_anonymous:
|
||||||
|
return False
|
||||||
|
return user.is_com_admin or user == self.author
|
||||||
|
|
||||||
def can_be_edited_by(self, user):
|
def can_be_edited_by(self, user):
|
||||||
return user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID)
|
return user.is_com_admin
|
||||||
|
|
||||||
def can_be_viewed_by(self, user):
|
def can_be_viewed_by(self, user):
|
||||||
return self.is_moderated or user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID)
|
return self.is_moderated or user.is_com_admin
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("com:news_detail", kwargs={"news_id": self.id})
|
return reverse("com:news_detail", kwargs={"news_id": self.id})
|
||||||
@ -139,7 +153,12 @@ class NewsDate(models.Model):
|
|||||||
we don't have to make copies
|
we don't have to make copies
|
||||||
"""
|
"""
|
||||||
|
|
||||||
news = models.ForeignKey(News, related_name="dates", verbose_name=_("news_date"))
|
news = models.ForeignKey(
|
||||||
|
News,
|
||||||
|
related_name="dates",
|
||||||
|
verbose_name=_("news_date"),
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
start_date = models.DateTimeField(_("start_date"), null=True, blank=True)
|
start_date = models.DateTimeField(_("start_date"), null=True, blank=True)
|
||||||
end_date = models.DateTimeField(_("end_date"), null=True, blank=True)
|
end_date = models.DateTimeField(_("end_date"), null=True, blank=True)
|
||||||
|
|
||||||
@ -150,6 +169,13 @@ class NewsDate(models.Model):
|
|||||||
class Weekmail(models.Model):
|
class Weekmail(models.Model):
|
||||||
"""
|
"""
|
||||||
The weekmail class
|
The weekmail class
|
||||||
|
|
||||||
|
:ivar title: Title of the weekmail
|
||||||
|
:ivar intro: Introduction of the weekmail
|
||||||
|
:ivar joke: Joke of the week
|
||||||
|
:ivar protip: Tip of the week
|
||||||
|
:ivar conclusion: Conclusion of the weekmail
|
||||||
|
:ivar sent: Track if the weekmail has been sent
|
||||||
"""
|
"""
|
||||||
|
|
||||||
title = models.CharField(_("title"), max_length=64, blank=True)
|
title = models.CharField(_("title"), max_length=64, blank=True)
|
||||||
@ -163,6 +189,10 @@ class Weekmail(models.Model):
|
|||||||
ordering = ["-id"]
|
ordering = ["-id"]
|
||||||
|
|
||||||
def send(self):
|
def send(self):
|
||||||
|
"""
|
||||||
|
Send the weekmail to all users with the receive weekmail option opt-in.
|
||||||
|
Also send the weekmail to the mailing list in settings.SITH_COM_EMAIL.
|
||||||
|
"""
|
||||||
dest = [
|
dest = [
|
||||||
i[0]
|
i[0]
|
||||||
for i in Preferences.objects.filter(receive_weekmail=True).values_list(
|
for i in Preferences.objects.filter(receive_weekmail=True).values_list(
|
||||||
@ -184,44 +214,72 @@ class Weekmail(models.Model):
|
|||||||
Weekmail().save()
|
Weekmail().save()
|
||||||
|
|
||||||
def render_text(self):
|
def render_text(self):
|
||||||
|
"""
|
||||||
|
Renders a pure text version of the mail for readers without HTML support.
|
||||||
|
"""
|
||||||
return render(
|
return render(
|
||||||
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 render_html(self):
|
def render_html(self):
|
||||||
|
"""
|
||||||
|
Renders an HTML version of the mail with images and fancy CSS.
|
||||||
|
"""
|
||||||
return render(
|
return render(
|
||||||
None, "com/weekmail_renderer_html.jinja", context={"weekmail": self}
|
None, "com/weekmail_renderer_html.jinja", context={"weekmail": self}
|
||||||
).content.decode("utf-8")
|
).content.decode("utf-8")
|
||||||
|
|
||||||
def get_banner(self):
|
def get_banner(self):
|
||||||
return "http://" + settings.SITH_URL + static("com/img/weekmail_bannerA18.jpg")
|
"""
|
||||||
|
Return an absolute link to the banner.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
"http://" + settings.SITH_URL + static("com/img/weekmail_bannerV2P22.png")
|
||||||
|
)
|
||||||
|
|
||||||
def get_footer(self):
|
def get_footer(self):
|
||||||
return "http://" + settings.SITH_URL + static("com/img/weekmail_footerA18.jpg")
|
"""
|
||||||
|
Return an absolute link to the footer.
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
return user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID)
|
if user.is_anonymous:
|
||||||
|
return False
|
||||||
|
return user.is_com_admin
|
||||||
|
|
||||||
|
|
||||||
class WeekmailArticle(models.Model):
|
class WeekmailArticle(models.Model):
|
||||||
weekmail = models.ForeignKey(
|
weekmail = models.ForeignKey(
|
||||||
Weekmail, related_name="articles", verbose_name=_("weekmail"), null=True
|
Weekmail,
|
||||||
|
related_name="articles",
|
||||||
|
verbose_name=_("weekmail"),
|
||||||
|
null=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
title = models.CharField(_("title"), max_length=64)
|
title = models.CharField(_("title"), max_length=64)
|
||||||
content = models.TextField(_("content"))
|
content = models.TextField(_("content"))
|
||||||
author = models.ForeignKey(
|
author = models.ForeignKey(
|
||||||
User, related_name="owned_weekmail_articles", verbose_name=_("author")
|
User,
|
||||||
|
related_name="owned_weekmail_articles",
|
||||||
|
verbose_name=_("author"),
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
club = models.ForeignKey(
|
club = models.ForeignKey(
|
||||||
Club, related_name="weekmail_articles", verbose_name=_("club")
|
Club,
|
||||||
|
related_name="weekmail_articles",
|
||||||
|
verbose_name=_("club"),
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
rank = models.IntegerField(_("rank"), default=-1)
|
rank = models.IntegerField(_("rank"), default=-1)
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
return user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID)
|
if user.is_anonymous:
|
||||||
|
return False
|
||||||
|
return user.is_com_admin
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s - %s (%s)" % (self.title, self.author, self.club)
|
return "%s - %s (%s)" % (self.title, self.author, self.club)
|
||||||
@ -237,7 +295,9 @@ class Screen(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
return user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID)
|
if user.is_anonymous:
|
||||||
|
return False
|
||||||
|
return user.is_com_admin
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s" % (self.name)
|
return "%s" % (self.name)
|
||||||
@ -249,7 +309,11 @@ class Poster(models.Model):
|
|||||||
)
|
)
|
||||||
file = models.ImageField(_("file"), null=False, upload_to="com/posters")
|
file = models.ImageField(_("file"), null=False, upload_to="com/posters")
|
||||||
club = models.ForeignKey(
|
club = models.ForeignKey(
|
||||||
Club, related_name="posters", verbose_name=_("club"), null=False
|
Club,
|
||||||
|
related_name="posters",
|
||||||
|
verbose_name=_("club"),
|
||||||
|
null=False,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
screens = models.ManyToManyField(Screen, related_name="posters")
|
screens = models.ManyToManyField(Screen, related_name="posters")
|
||||||
date_begin = models.DateTimeField(blank=False, null=False, default=timezone.now)
|
date_begin = models.DateTimeField(blank=False, null=False, default=timezone.now)
|
||||||
@ -264,6 +328,7 @@ class Poster(models.Model):
|
|||||||
verbose_name=_("moderator"),
|
verbose_name=_("moderator"),
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
@ -285,12 +350,12 @@ class Poster(models.Model):
|
|||||||
raise ValidationError(_("Begin date should be before end date"))
|
raise ValidationError(_("Begin date should be before end date"))
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
return user.is_in_group(
|
if user.is_anonymous:
|
||||||
settings.SITH_GROUP_COM_ADMIN_ID
|
return False
|
||||||
) or Club.objects.filter(id__in=user.clubs_with_rights)
|
return user.is_com_admin or len(user.clubs_with_rights) > 0
|
||||||
|
|
||||||
def can_be_moderated_by(self, user):
|
def can_be_moderated_by(self, user):
|
||||||
return user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID)
|
return user.is_com_admin
|
||||||
|
|
||||||
def get_display_name(self):
|
def get_display_name(self):
|
||||||
return self.club.get_display_name()
|
return self.club.get_display_name()
|
||||||
|
@ -35,11 +35,11 @@
|
|||||||
<p>{% trans %}Author: {% endtrans %}{{ user_profile_link(news.author) }}</p>
|
<p>{% trans %}Author: {% endtrans %}{{ user_profile_link(news.author) }}</p>
|
||||||
{% if news.moderator %}
|
{% if news.moderator %}
|
||||||
<p>{% trans %}Moderator: {% endtrans %}{{ user_profile_link(news.moderator) }}</p>
|
<p>{% trans %}Moderator: {% endtrans %}{{ user_profile_link(news.moderator) }}</p>
|
||||||
{% elif user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %}
|
{% elif user.is_com_admin %}
|
||||||
<p> <a href="{{ url('com:news_moderate', news_id=news.id) }}">{% trans %}Moderate{% endtrans %}</a></p>
|
<p> <a href="{{ url('com:news_moderate', news_id=news.id) }}">{% trans %}Moderate{% endtrans %}</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if user.can_edit(news) %}
|
{% if user.can_edit(news) %}
|
||||||
<p> <a href="{{ url('com:news_edit', news_id=news.id) }}">{% trans %}Edit (will be remoderated){% endtrans %}</a></p>
|
<p> <a href="{{ url('com:news_edit', news_id=news.id) }}">{% trans %}Edit (will be moderated again){% endtrans %}</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
<p>{{ form.club.errors }}<label for="{{ form.club.name }}">{{ form.club.label }}</label> {{ form.club }}</p>
|
<p>{{ form.club.errors }}<label for="{{ form.club.name }}">{{ form.club.label }}</label> {{ form.club }}</p>
|
||||||
<p>{{ form.summary.errors }}<label for="{{ form.summary.name }}">{{ form.summary.label }}</label> {{ form.summary }}</p>
|
<p>{{ form.summary.errors }}<label for="{{ form.summary.name }}">{{ form.summary.label }}</label> {{ form.summary }}</p>
|
||||||
<p>{{ form.content.errors }}<label for="{{ form.content.name }}">{{ form.content.label }}</label> {{ form.content }}</p>
|
<p>{{ form.content.errors }}<label for="{{ form.content.name }}">{{ form.content.label }}</label> {{ form.content }}</p>
|
||||||
{% if user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %}
|
{% if user.is_com_admin %}
|
||||||
<p>{{ form.automoderation.errors }}<label for="{{ form.automoderation.name }}">{{ form.automoderation.label }}</label>
|
<p>{{ form.automoderation.errors }}<label for="{{ form.automoderation.name }}">{{ form.automoderation.label }}</label>
|
||||||
{{ form.automoderation }}</p>
|
{{ form.automoderation }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -6,145 +6,159 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{% if user.is_com_admin %}
|
||||||
|
<div id="news_admin">
|
||||||
|
<a class="button" href="{{ url('com:news_admin_list') }}">{% trans %}Administrate news{% endtrans %}</a>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div id="news">
|
<div id="news">
|
||||||
{% if user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %}
|
<div id="left_column" class="news_column">
|
||||||
<div id="news_admin">
|
{% for news in object_list.filter(type="NOTICE") %}
|
||||||
<a href="{{ url('com:news_admin_list') }}">{% trans %}Administrate news{% endtrans %}</a>
|
<section class="news_notice">
|
||||||
|
<h4><a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
|
||||||
|
<div class="news_content">{{ news.summary|markdown }}</div>
|
||||||
|
</section>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% for news in object_list.filter(dates__start_date__lte=timezone.now(), dates__end_date__gte=timezone.now(), type="CALL") %}
|
||||||
|
<section class="news_call">
|
||||||
|
<h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
|
||||||
|
<div class="news_date">
|
||||||
|
<span>{{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }}
|
||||||
|
{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
|
||||||
|
<span>{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }}
|
||||||
|
{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="news_content">{{ news.summary|markdown }}</div>
|
||||||
|
</section>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% 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') %}
|
||||||
|
<h3>{% trans %}Events today and the next few days{% endtrans %}</h3>
|
||||||
|
{% if events_dates %}
|
||||||
|
{% for d in events_dates %}
|
||||||
|
<div class="news_events_group">
|
||||||
|
<div class="news_events_group_date">
|
||||||
|
<div>
|
||||||
|
<div>{{ d|localtime|date('D') }}</div>
|
||||||
|
<div class="day">{{ d|localtime|date('d') }}</div>
|
||||||
|
<div>{{ d|localtime|date('b') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="news_events_group_items">
|
||||||
|
{% for news in object_list.filter(dates__start_date__gte=d,
|
||||||
|
dates__start_date__lte=d+timedelta(days=1),
|
||||||
|
type="EVENT").exclude(dates__end_date__lt=timezone.now())
|
||||||
|
.order_by('dates__start_date') %}
|
||||||
|
<section class="news_event">
|
||||||
|
<div class="club_logo">
|
||||||
|
{% if news.club.logo %}
|
||||||
|
<img src="{{ news.club.logo.url }}" alt="{{ news.club }}" />
|
||||||
|
{% else %}
|
||||||
|
<img src="{{ static("com/img/news.png") }}" alt="{{ news.club }}" />
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<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 class="news_date">
|
||||||
|
<span>{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
|
||||||
|
<span>{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="news_content">{{ news.summary|markdown }}
|
||||||
|
<div class="button_bar">
|
||||||
|
{{ fb_quick(news) }}
|
||||||
|
{{ tweet_quick(news) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="news_empty">
|
||||||
|
<em>{% trans %}Nothing to come...{% endtrans %}</em>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% set coming_soon = object_list.filter(dates__start_date__gte=timezone.now()+timedelta(days=5),
|
||||||
|
type="EVENT").order_by('dates__start_date') %}
|
||||||
|
{% if coming_soon %}
|
||||||
|
<h3>{% trans %}Coming soon... don't miss!{% endtrans %}</h3>
|
||||||
|
{% for news in coming_soon %}
|
||||||
|
<section class="news_coming_soon">
|
||||||
|
<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) }}
|
||||||
|
{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }} -
|
||||||
|
{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }}
|
||||||
|
{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
||||||
|
</section>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h3>{% trans %}All coming events{% endtrans %}</h3>
|
||||||
|
<iframe
|
||||||
|
src="https://embed.styledcalendar.com/#2mF2is8CEXhr4ADcX6qN"
|
||||||
|
title="Styled Calendar"
|
||||||
|
class="styled-calendar-container"
|
||||||
|
style="width: 100%; border: none; height: 1060px"
|
||||||
|
data-cy="calendar-embed-iframe">
|
||||||
|
</iframe>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div id="right_column" class="news_column">
|
<div id="right_column" class="news_column">
|
||||||
<div id="agenda">
|
<div id="agenda">
|
||||||
<div id="agenda_title">{% trans %}Agenda{% endtrans %}</div>
|
<div id="agenda_title">{% trans %}Agenda{% endtrans %}</div>
|
||||||
<div id="agenda_content">
|
<div id="agenda_content">
|
||||||
{% for d in NewsDate.objects.filter(end_date__gte=timezone.now(),
|
{% for d in NewsDate.objects.filter(end_date__gte=timezone.now(),
|
||||||
news__is_moderated=True, news__type__in=["WEEKLY",
|
news__is_moderated=True, news__type__in=["WEEKLY",
|
||||||
"EVENT"]).order_by('start_date', 'end_date') %}
|
"EVENT"]).order_by('start_date', 'end_date') %}
|
||||||
<div class="agenda_item">
|
<div class="agenda_item">
|
||||||
<div class="agenda_date">
|
<div class="agenda_date">
|
||||||
<strong>{{ d.start_date|localtime|date('D d M Y') }}</strong>
|
<strong>{{ d.start_date|localtime|date('D d M Y') }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="agenda_time">
|
<div class="agenda_time">
|
||||||
<span>{{ d.start_date|localtime|time(DATETIME_FORMAT) }}</span> -
|
<span>{{ d.start_date|localtime|time(DATETIME_FORMAT) }}</span> -
|
||||||
<span>{{ d.end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
<span>{{ d.end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong><a href="{{ url('com:news_detail', news_id=d.news.id) }}">{{ d.news.title }}</a></strong>
|
<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>
|
<a href="{{ d.news.club.get_absolute_url() }}">{{ d.news.club }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="agenda_item_content">{{ d.news.summary|markdown }}</div>
|
<div class="agenda_item_content">{{ d.news.summary|markdown }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="birthdays">
|
<div id="birthdays">
|
||||||
<div id="birthdays_title">{% trans %}Birthdays{% endtrans %}</div>
|
<div id="birthdays_title">{% trans %}Birthdays{% endtrans %}</div>
|
||||||
<div id="birthdays_content">
|
<div id="birthdays_content">
|
||||||
<ul class="birthdays_year">
|
{% if user.is_subscribed %}
|
||||||
{% for d in birthdays.dates('date_of_birth', 'year', 'DESC') %}
|
{# Cache request for 1 hour #}
|
||||||
<li>
|
{% cache 3600 "birthdays" %}
|
||||||
{% trans age=timezone.now().year - d.year %}{{ age }} year old{% endtrans %}
|
<ul class="birthdays_year">
|
||||||
<ul>
|
{% for d in birthdays.dates('date_of_birth', 'year', 'DESC') %}
|
||||||
{% for u in birthdays.filter(date_of_birth__year=d.year) %}
|
<li>
|
||||||
<li><a href="{{ u.get_absolute_url() }}">{{ u.get_short_name() }}</a></li>
|
{% trans age=timezone.now().year - d.year %}{{ age }} year old{% endtrans %}
|
||||||
{% endfor %}
|
<ul>
|
||||||
</ul>
|
{% for u in birthdays.filter(date_of_birth__year=d.year) %}
|
||||||
</li>
|
<li><a href="{{ u.get_absolute_url() }}">{{ u.get_short_name() }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</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>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div id="left_column" class="news_column">
|
|
||||||
|
|
||||||
{% for news in object_list.filter(type="NOTICE") %}
|
|
||||||
<section class="news_notice">
|
|
||||||
<h4><a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
|
|
||||||
<div class="news_content">{{ news.summary|markdown }}</div>
|
|
||||||
</section>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% for news in object_list.filter(dates__start_date__lte=timezone.now(),
|
|
||||||
dates__end_date__gte=timezone.now(), type="CALL") %}
|
|
||||||
<section class="news_call">
|
|
||||||
<h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
|
|
||||||
<div class="news_date">
|
|
||||||
<span>{{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }}
|
|
||||||
{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
|
|
||||||
<span>{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }}
|
|
||||||
{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="news_content">{{ news.summary|markdown }}</div>
|
|
||||||
</section>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% 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') %}
|
|
||||||
<h3>{% trans %}Events today and the next few days{% endtrans %}</h3>
|
|
||||||
{% if events_dates %}
|
|
||||||
{% for d in events_dates %}
|
|
||||||
<div class="news_events_group">
|
|
||||||
<div class="news_events_group_date">
|
|
||||||
<div>
|
|
||||||
<div>{{ d|localtime|date('D') }}</div>
|
|
||||||
<div class="day">{{ d|localtime|date('d') }}</div>
|
|
||||||
<div>{{ d|localtime|date('b') }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="news_events_group_items">
|
|
||||||
{% for news in object_list.filter(dates__start_date__gte=d,
|
|
||||||
dates__start_date__lte=d+timedelta(days=1),
|
|
||||||
type="EVENT").exclude(dates__end_date__lt=timezone.now())
|
|
||||||
.order_by('dates__start_date') %}
|
|
||||||
<section class="news_event">
|
|
||||||
<div class="club_logo">
|
|
||||||
{% if news.club.logo %}
|
|
||||||
<img src="{{ news.club.logo.url }}" alt="{{ news.club }}" />
|
|
||||||
{% else %}
|
|
||||||
<img src="{{ static("com/img/news.png") }}" alt="{{ news.club }}" />
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<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 class="news_date">
|
|
||||||
<span>{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
|
|
||||||
<span>{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="news_content">{{ news.summary|markdown }}
|
|
||||||
<div class="button_bar">
|
|
||||||
{{ fb_quick(news) }}
|
|
||||||
{{ tweet_quick(news) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<div class="news_empty">
|
|
||||||
<em>{% trans %}Nothing to come...{% endtrans %}</em>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% set coming_soon = object_list.filter(dates__start_date__gte=timezone.now()+timedelta(days=5),
|
|
||||||
type="EVENT").order_by('dates__start_date') %}
|
|
||||||
{% if coming_soon %}
|
|
||||||
<h3>{% trans %}Coming soon... don't miss!{% endtrans %}</h3>
|
|
||||||
{% for news in coming_soon %}
|
|
||||||
<section class="news_coming_soon">
|
|
||||||
<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) }}
|
|
||||||
{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }} -
|
|
||||||
{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }}
|
|
||||||
{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
|
||||||
</section>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</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>
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
<div id="progress_bar"></div>
|
<div id="progress_bar"></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<script src="{{ static('core/js/jquery-3.1.0.min.js') }}"></script>
|
<script src="{{ static('core/js/jquery-3.6.2.min.js') }}"></script>
|
||||||
<script src="{{ static('com/js/slideshow.js') }}"></script>
|
<script src="{{ static('com/js/slideshow.js') }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -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 }}
|
||||||
|
226
com/tests.py
226
com/tests.py
@ -1,44 +1,59 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.urlresolvers 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.timezone import localtime, now
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from core.models import User, RealGroup
|
from club.models import Club, Membership
|
||||||
|
from com.models import Sith, News, Weekmail, WeekmailArticle, Poster
|
||||||
|
from core.models import User, RealGroup, AnonymousUser
|
||||||
|
|
||||||
|
|
||||||
|
class ComAlertTest(TestCase):
|
||||||
|
def test_page_is_working(self):
|
||||||
|
self.client.login(username="comunity", password="plop")
|
||||||
|
response = self.client.get(reverse("com:alert_edit"))
|
||||||
|
self.assertNotEqual(response.status_code, 500)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
class ComInfoTest(TestCase):
|
||||||
|
def test_page_is_working(self):
|
||||||
|
self.client.login(username="comunity", password="plop")
|
||||||
|
response = self.client.get(reverse("com:info_edit"))
|
||||||
|
self.assertNotEqual(response.status_code, 500)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
class ComTest(TestCase):
|
class ComTest(TestCase):
|
||||||
def setUp(self):
|
@classmethod
|
||||||
call_command("populate")
|
def setUpTestData(cls):
|
||||||
self.skia = User.objects.filter(username="skia").first()
|
cls.skia = User.objects.filter(username="skia").first()
|
||||||
self.com_group = RealGroup.objects.filter(
|
cls.com_group = RealGroup.objects.filter(
|
||||||
id=settings.SITH_GROUP_COM_ADMIN_ID
|
id=settings.SITH_GROUP_COM_ADMIN_ID
|
||||||
).first()
|
).first()
|
||||||
self.skia.groups = [self.com_group]
|
cls.skia.groups.set([cls.com_group])
|
||||||
self.skia.save()
|
cls.skia.save()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
self.client.login(username=self.skia.username, password="plop")
|
self.client.login(username=self.skia.username, password="plop")
|
||||||
|
|
||||||
def test_alert_msg(self):
|
def test_alert_msg(self):
|
||||||
@ -54,9 +69,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):
|
||||||
@ -70,7 +87,154 @@ 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):
|
||||||
|
self.client.login(username="guy", password="plop")
|
||||||
|
response = self.client.get(reverse("core:index"))
|
||||||
|
self.assertContains(
|
||||||
|
response,
|
||||||
|
text=html.escape(
|
||||||
|
_("You need an up to date subscription to access this content")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_birthday_subscibed_user(self):
|
||||||
|
response = self.client.get(reverse("core:index"))
|
||||||
|
|
||||||
|
self.assertNotContains(
|
||||||
|
response,
|
||||||
|
text=html.escape(
|
||||||
|
_("You need an up to date subscription to access this content")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SithTest(TestCase):
|
||||||
|
def test_sith_owner(self):
|
||||||
|
"""
|
||||||
|
Test that the sith instance is owned by com admins
|
||||||
|
and nobody else
|
||||||
|
"""
|
||||||
|
sith: Sith = Sith.objects.first()
|
||||||
|
|
||||||
|
com_admin = User.objects.get(username="comunity")
|
||||||
|
self.assertTrue(sith.is_owned_by(com_admin))
|
||||||
|
|
||||||
|
anonymous = AnonymousUser()
|
||||||
|
self.assertFalse(sith.is_owned_by(anonymous))
|
||||||
|
|
||||||
|
sli = User.objects.get(username="sli")
|
||||||
|
self.assertFalse(sith.is_owned_by(sli))
|
||||||
|
|
||||||
|
|
||||||
|
class NewsTest(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.com_admin = User.objects.get(username="comunity")
|
||||||
|
new = News.objects.create(
|
||||||
|
title="dummy new",
|
||||||
|
summary="This is a dummy new",
|
||||||
|
content="Look at that beautiful dummy new",
|
||||||
|
author=User.objects.get(username="subscriber"),
|
||||||
|
club=Club.objects.first(),
|
||||||
|
)
|
||||||
|
cls.new = new
|
||||||
|
cls.author = new.author
|
||||||
|
cls.sli = User.objects.get(username="sli")
|
||||||
|
cls.anonymous = AnonymousUser()
|
||||||
|
|
||||||
|
def test_news_owner(self):
|
||||||
|
"""
|
||||||
|
Test that news are owned by com admins
|
||||||
|
or by their author but nobody else
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.assertTrue(self.new.is_owned_by(self.com_admin))
|
||||||
|
self.assertTrue(self.new.is_owned_by(self.author))
|
||||||
|
self.assertFalse(self.new.is_owned_by(self.anonymous))
|
||||||
|
self.assertFalse(self.new.is_owned_by(self.sli))
|
||||||
|
|
||||||
|
def test_news_viewer(self):
|
||||||
|
"""
|
||||||
|
Test that moderated news can be viewed by anyone
|
||||||
|
and not moderated news only by com admins
|
||||||
|
"""
|
||||||
|
# by default a news isn't moderated
|
||||||
|
self.assertTrue(self.new.can_be_viewed_by(self.com_admin))
|
||||||
|
self.assertFalse(self.new.can_be_viewed_by(self.sli))
|
||||||
|
self.assertFalse(self.new.can_be_viewed_by(self.anonymous))
|
||||||
|
self.assertFalse(self.new.can_be_viewed_by(self.author))
|
||||||
|
|
||||||
|
self.new.is_moderated = True
|
||||||
|
self.new.save()
|
||||||
|
self.assertTrue(self.new.can_be_viewed_by(self.com_admin))
|
||||||
|
self.assertTrue(self.new.can_be_viewed_by(self.sli))
|
||||||
|
self.assertTrue(self.new.can_be_viewed_by(self.anonymous))
|
||||||
|
self.assertTrue(self.new.can_be_viewed_by(self.author))
|
||||||
|
|
||||||
|
def test_news_editor(self):
|
||||||
|
"""
|
||||||
|
Test that only com admins can edit news
|
||||||
|
"""
|
||||||
|
self.assertTrue(self.new.can_be_edited_by(self.com_admin))
|
||||||
|
self.assertFalse(self.new.can_be_edited_by(self.sli))
|
||||||
|
self.assertFalse(self.new.can_be_edited_by(self.anonymous))
|
||||||
|
self.assertFalse(self.new.can_be_edited_by(self.author))
|
||||||
|
|
||||||
|
|
||||||
|
class WeekmailArticleTest(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.com_admin = User.objects.get(username="comunity")
|
||||||
|
author = User.objects.get(username="subscriber")
|
||||||
|
cls.article = WeekmailArticle.objects.create(
|
||||||
|
weekmail=Weekmail.objects.create(),
|
||||||
|
author=author,
|
||||||
|
title="title",
|
||||||
|
content="Some content",
|
||||||
|
club=Club.objects.first(),
|
||||||
|
)
|
||||||
|
cls.author = author
|
||||||
|
cls.sli = User.objects.get(username="sli")
|
||||||
|
cls.anonymous = AnonymousUser()
|
||||||
|
|
||||||
|
def test_weekmail_owner(self):
|
||||||
|
"""
|
||||||
|
Test that weekmails are owned only by com admins
|
||||||
|
"""
|
||||||
|
self.assertTrue(self.article.is_owned_by(self.com_admin))
|
||||||
|
self.assertFalse(self.article.is_owned_by(self.author))
|
||||||
|
self.assertFalse(self.article.is_owned_by(self.anonymous))
|
||||||
|
self.assertFalse(self.article.is_owned_by(self.sli))
|
||||||
|
|
||||||
|
|
||||||
|
class PosterTest(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.com_admin = User.objects.get(username="comunity")
|
||||||
|
cls.poster = Poster.objects.create(
|
||||||
|
name="dummy",
|
||||||
|
file=SimpleUploadedFile("dummy.jpg", b"azertyuiop"),
|
||||||
|
club=Club.objects.first(),
|
||||||
|
date_begin=localtime(now()),
|
||||||
|
)
|
||||||
|
cls.sli = User.objects.get(username="sli")
|
||||||
|
cls.sli.memberships.all().delete()
|
||||||
|
Membership(user=cls.sli, club=Club.objects.first(), role=5).save()
|
||||||
|
cls.susbcriber = User.objects.get(username="subscriber")
|
||||||
|
cls.anonymous = AnonymousUser()
|
||||||
|
|
||||||
|
def test_poster_owner(self):
|
||||||
|
"""
|
||||||
|
Test that poster are owned by com admins and board members in clubs
|
||||||
|
"""
|
||||||
|
self.assertTrue(self.poster.is_owned_by(self.com_admin))
|
||||||
|
self.assertFalse(self.poster.is_owned_by(self.anonymous))
|
||||||
|
|
||||||
|
self.assertFalse(self.poster.is_owned_by(self.susbcriber))
|
||||||
|
self.assertTrue(self.poster.is_owned_by(self.sli))
|
||||||
|
117
com/urls.py
117
com/urls.py
@ -1,120 +1,111 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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.conf.urls import url
|
from django.urls import path
|
||||||
|
|
||||||
from com.views import *
|
|
||||||
from club.views import MailingDeleteView
|
from club.views import MailingDeleteView
|
||||||
|
from com.views import *
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r"^sith/edit/alert$", AlertMsgEditView.as_view(), name="alert_edit"),
|
path("sith/edit/alert/", AlertMsgEditView.as_view(), name="alert_edit"),
|
||||||
url(r"^sith/edit/info$", InfoMsgEditView.as_view(), name="info_edit"),
|
path("sith/edit/info/", InfoMsgEditView.as_view(), name="info_edit"),
|
||||||
url(r"^sith/edit/index$", IndexEditView.as_view(), name="index_edit"),
|
path(
|
||||||
url(
|
"sith/edit/weekmail_destinations/",
|
||||||
r"^sith/edit/weekmail_destinations$",
|
|
||||||
WeekmailDestinationEditView.as_view(),
|
WeekmailDestinationEditView.as_view(),
|
||||||
name="weekmail_destinations",
|
name="weekmail_destinations",
|
||||||
),
|
),
|
||||||
url(r"^weekmail$", WeekmailEditView.as_view(), name="weekmail"),
|
path("weekmail/", WeekmailEditView.as_view(), name="weekmail"),
|
||||||
url(r"^weekmail/preview$", WeekmailPreviewView.as_view(), name="weekmail_preview"),
|
path("weekmail/preview/", WeekmailPreviewView.as_view(), name="weekmail_preview"),
|
||||||
url(
|
path(
|
||||||
r"^weekmail/new_article$",
|
"weekmail/new_article/",
|
||||||
WeekmailArticleCreateView.as_view(),
|
WeekmailArticleCreateView.as_view(),
|
||||||
name="weekmail_article",
|
name="weekmail_article",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^weekmail/article/(?P<article_id>[0-9]+)/delete$",
|
"weekmail/article/<int:article_id>/delete/",
|
||||||
WeekmailArticleDeleteView.as_view(),
|
WeekmailArticleDeleteView.as_view(),
|
||||||
name="weekmail_article_delete",
|
name="weekmail_article_delete",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^weekmail/article/(?P<article_id>[0-9]+)/edit$",
|
"weekmail/article/<int:article_id>/edit/",
|
||||||
WeekmailArticleEditView.as_view(),
|
WeekmailArticleEditView.as_view(),
|
||||||
name="weekmail_article_edit",
|
name="weekmail_article_edit",
|
||||||
),
|
),
|
||||||
url(r"^news$", NewsListView.as_view(), name="news_list"),
|
path("news/", NewsListView.as_view(), name="news_list"),
|
||||||
url(r"^news/admin$", NewsAdminListView.as_view(), name="news_admin_list"),
|
path("news/admin/", NewsAdminListView.as_view(), name="news_admin_list"),
|
||||||
url(r"^news/create$", NewsCreateView.as_view(), name="news_new"),
|
path("news/create/", NewsCreateView.as_view(), name="news_new"),
|
||||||
url(
|
path(
|
||||||
r"^news/(?P<news_id>[0-9]+)/delete$",
|
"news/<int:news_id>/delete/",
|
||||||
NewsDeleteView.as_view(),
|
NewsDeleteView.as_view(),
|
||||||
name="news_delete",
|
name="news_delete",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^news/(?P<news_id>[0-9]+)/moderate$",
|
"news/<int:news_id>/moderate/",
|
||||||
NewsModerateView.as_view(),
|
NewsModerateView.as_view(),
|
||||||
name="news_moderate",
|
name="news_moderate",
|
||||||
),
|
),
|
||||||
url(r"^news/(?P<news_id>[0-9]+)/edit$", NewsEditView.as_view(), name="news_edit"),
|
path("news/<int:news_id>/edit/", NewsEditView.as_view(), name="news_edit"),
|
||||||
url(r"^news/(?P<news_id>[0-9]+)$", NewsDetailView.as_view(), name="news_detail"),
|
path("news/<int:news_id>/", NewsDetailView.as_view(), name="news_detail"),
|
||||||
url(r"^mailings$", MailingListAdminView.as_view(), name="mailing_admin"),
|
path("mailings/", MailingListAdminView.as_view(), name="mailing_admin"),
|
||||||
url(
|
path(
|
||||||
r"^mailings/(?P<mailing_id>[0-9]+)/moderate$",
|
"mailings/<int:mailing_id>/moderate/",
|
||||||
MailingModerateView.as_view(),
|
MailingModerateView.as_view(),
|
||||||
name="mailing_moderate",
|
name="mailing_moderate",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^mailings/(?P<mailing_id>[0-9]+)/delete$",
|
"mailings/<int:mailing_id>/delete/",
|
||||||
MailingDeleteView.as_view(redirect_page="com:mailing_admin"),
|
MailingDeleteView.as_view(redirect_page="com:mailing_admin"),
|
||||||
name="mailing_delete",
|
name="mailing_delete",
|
||||||
),
|
),
|
||||||
url(r"^poster$", PosterListView.as_view(), name="poster_list"),
|
path("poster/", PosterListView.as_view(), name="poster_list"),
|
||||||
url(r"^poster/create$", PosterCreateView.as_view(), name="poster_create"),
|
path("poster/create/", PosterCreateView.as_view(), name="poster_create"),
|
||||||
url(
|
path(
|
||||||
r"^poster/(?P<poster_id>[0-9]+)/edit$",
|
"poster/<int:poster_id>/edit/",
|
||||||
PosterEditView.as_view(),
|
PosterEditView.as_view(),
|
||||||
name="poster_edit",
|
name="poster_edit",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^poster/(?P<poster_id>[0-9]+)/delete$",
|
"poster/<int:poster_id>/delete/",
|
||||||
PosterDeleteView.as_view(),
|
PosterDeleteView.as_view(),
|
||||||
name="poster_delete",
|
name="poster_delete",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^poster/moderate$",
|
"poster/moderate/",
|
||||||
PosterModerateListView.as_view(),
|
PosterModerateListView.as_view(),
|
||||||
name="poster_moderate_list",
|
name="poster_moderate_list",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^poster/(?P<object_id>[0-9]+)/moderate$",
|
"poster/<int:object_id>/moderate/",
|
||||||
PosterModerateView.as_view(),
|
PosterModerateView.as_view(),
|
||||||
name="poster_moderate",
|
name="poster_moderate",
|
||||||
),
|
),
|
||||||
url(r"^screen$", ScreenListView.as_view(), name="screen_list"),
|
path("screen/", ScreenListView.as_view(), name="screen_list"),
|
||||||
url(r"^screen/create$", ScreenCreateView.as_view(), name="screen_create"),
|
path("screen/create/", ScreenCreateView.as_view(), name="screen_create"),
|
||||||
url(
|
path(
|
||||||
r"^screen/(?P<screen_id>[0-9]+)/slideshow$",
|
"screen/<int:screen_id>/slideshow/",
|
||||||
ScreenSlideshowView.as_view(),
|
ScreenSlideshowView.as_view(),
|
||||||
name="screen_slideshow",
|
name="screen_slideshow",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^screen/(?P<screen_id>[0-9]+)/edit$",
|
"screen/<int:screen_id>/edit/",
|
||||||
ScreenEditView.as_view(),
|
ScreenEditView.as_view(),
|
||||||
name="screen_edit",
|
name="screen_edit",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^screen/(?P<screen_id>[0-9]+)/delete$",
|
"screen/<int:screen_id>/delete/",
|
||||||
ScreenDeleteView.as_view(),
|
ScreenDeleteView.as_view(),
|
||||||
name="screen_delete",
|
name="screen_delete",
|
||||||
),
|
),
|
||||||
|
99
com/views.py
99
com/views.py
@ -28,8 +28,8 @@ 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.core.urlresolvers 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
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -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(
|
||||||
["%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)
|
||||||
["%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)
|
||||||
@ -114,9 +110,6 @@ class ComTabsMixin(TabedViewMixin):
|
|||||||
"name": _("Weekmail destinations"),
|
"name": _("Weekmail destinations"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
tab_list.append(
|
|
||||||
{"url": reverse("com:index_edit"), "slug": "index", "name": _("Index page")}
|
|
||||||
)
|
|
||||||
tab_list.append(
|
tab_list.append(
|
||||||
{"url": reverse("com:info_edit"), "slug": "info", "name": _("Info message")}
|
{"url": reverse("com:info_edit"), "slug": "info", "name": _("Info message")}
|
||||||
)
|
)
|
||||||
@ -153,7 +146,7 @@ class ComTabsMixin(TabedViewMixin):
|
|||||||
|
|
||||||
class IsComAdminMixin(View):
|
class IsComAdminMixin(View):
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
if not (request.user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID)):
|
if not request.user.is_com_admin:
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
return super(IsComAdminMixin, self).dispatch(request, *args, **kwargs)
|
return super(IsComAdminMixin, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
@ -182,14 +175,6 @@ class InfoMsgEditView(ComEditView):
|
|||||||
success_url = reverse_lazy("com:info_edit")
|
success_url = reverse_lazy("com:info_edit")
|
||||||
|
|
||||||
|
|
||||||
class IndexEditView(ComEditView):
|
|
||||||
form_class = modelform_factory(
|
|
||||||
Sith, fields=["index_page"], widgets={"index_page": MarkdownInput}
|
|
||||||
)
|
|
||||||
current_tab = "index"
|
|
||||||
success_url = reverse_lazy("com:index_edit")
|
|
||||||
|
|
||||||
|
|
||||||
class WeekmailDestinationEditView(ComEditView):
|
class WeekmailDestinationEditView(ComEditView):
|
||||||
fields = ["weekmail_destinations"]
|
fields = ["weekmail_destinations"]
|
||||||
current_tab = "weekmail_destinations"
|
current_tab = "weekmail_destinations"
|
||||||
@ -210,21 +195,10 @@ class NewsForm(forms.ModelForm):
|
|||||||
"content": MarkdownInput,
|
"content": MarkdownInput,
|
||||||
}
|
}
|
||||||
|
|
||||||
start_date = forms.DateTimeField(
|
start_date = TzAwareDateTimeField(label=_("Start date"), required=False)
|
||||||
["%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(
|
|
||||||
["%Y-%m-%d %H:%M:%S"],
|
|
||||||
label=_("End date"),
|
|
||||||
widget=SelectDateTime,
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
until = forms.DateTimeField(
|
|
||||||
["%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):
|
||||||
@ -238,7 +212,11 @@ class NewsForm(forms.ModelForm):
|
|||||||
self.add_error(
|
self.add_error(
|
||||||
"end_date", ValidationError(_("This field is required."))
|
"end_date", ValidationError(_("This field is required."))
|
||||||
)
|
)
|
||||||
if self.cleaned_data["start_date"] > self.cleaned_data["end_date"]:
|
if (
|
||||||
|
not self.has_error("start_date")
|
||||||
|
and not self.has_error("end_date")
|
||||||
|
and self.cleaned_data["start_date"] > self.cleaned_data["end_date"]
|
||||||
|
):
|
||||||
self.add_error(
|
self.add_error(
|
||||||
"end_date",
|
"end_date",
|
||||||
ValidationError(
|
ValidationError(
|
||||||
@ -305,9 +283,7 @@ class NewsEditView(CanEditMixin, UpdateView):
|
|||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
self.object = form.save()
|
self.object = form.save()
|
||||||
if form.cleaned_data["automoderation"] and self.request.user.is_in_group(
|
if form.cleaned_data["automoderation"] and self.request.user.is_com_admin:
|
||||||
settings.SITH_GROUP_COM_ADMIN_ID
|
|
||||||
):
|
|
||||||
self.object.moderator = self.request.user
|
self.object.moderator = self.request.user
|
||||||
self.object.is_moderated = True
|
self.object.is_moderated = True
|
||||||
self.object.save()
|
self.object.save()
|
||||||
@ -355,9 +331,7 @@ class NewsCreateView(CanCreateMixin, CreateView):
|
|||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
self.object = form.save()
|
self.object = form.save()
|
||||||
if form.cleaned_data["automoderation"] and self.request.user.is_in_group(
|
if form.cleaned_data["automoderation"] and self.request.user.is_com_admin:
|
||||||
settings.SITH_GROUP_COM_ADMIN_ID
|
|
||||||
):
|
|
||||||
self.object.moderator = self.request.user
|
self.object.moderator = self.request.user
|
||||||
self.object.is_moderated = True
|
self.object.is_moderated = True
|
||||||
self.object.save()
|
self.object.save()
|
||||||
@ -437,23 +411,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()
|
||||||
@ -462,6 +449,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
|
||||||
|
|
||||||
|
|
||||||
@ -538,7 +526,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
|
||||||
@ -625,10 +613,7 @@ class MailingListAdminView(ComTabsMixin, ListView):
|
|||||||
current_tab = "mailings"
|
current_tab = "mailings"
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
if not (
|
if not (request.user.is_com_admin or request.user.is_root):
|
||||||
request.user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID)
|
|
||||||
or request.user.is_root
|
|
||||||
):
|
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
return super(MailingListAdminView, self).dispatch(request, *args, **kwargs)
|
return super(MailingListAdminView, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
@ -750,7 +735,7 @@ class PosterEditBaseView(UpdateView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs = super(PosterEditBaseView, self).get_context_data(**kwargs)
|
kwargs = super(PosterEditBaseView, self).get_context_data(**kwargs)
|
||||||
if not self.request.user.is_com_admin:
|
if hasattr(self, "club"):
|
||||||
kwargs["club"] = self.club
|
kwargs["club"] = self.club
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
@ -1,25 +1,15 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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.
|
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
default_app_config = "core.apps.SithConfig"
|
|
||||||
|
@ -1,40 +1,34 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2023 © AE UTBM
|
||||||
# - Skia <skia@libskia.so>
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# http://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
# 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
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
# 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.contrib import admin
|
from django.contrib import admin
|
||||||
from ajax_select import make_ajax_form
|
from ajax_select import make_ajax_form
|
||||||
from core.models import User, Page, RealGroup, SithFile
|
from core.models import User, Page, RealGroup, MetaGroup, SithFile
|
||||||
from django.contrib.auth.models import Group as AuthGroup
|
from django.contrib.auth.models import Group as AuthGroup
|
||||||
from haystack.admin import SearchModelAdmin
|
from haystack.admin import SearchModelAdmin
|
||||||
|
|
||||||
|
|
||||||
admin.site.unregister(AuthGroup)
|
admin.site.unregister(AuthGroup)
|
||||||
|
admin.site.register(MetaGroup)
|
||||||
admin.site.register(RealGroup)
|
admin.site.register(RealGroup)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(User)
|
||||||
class UserAdmin(SearchModelAdmin):
|
class UserAdmin(SearchModelAdmin):
|
||||||
list_display = ["first_name", "last_name", "username", "email", "nick_name"]
|
list_display = ("first_name", "last_name", "username", "email", "nick_name")
|
||||||
form = make_ajax_form(
|
form = make_ajax_form(
|
||||||
User,
|
User,
|
||||||
{
|
{
|
||||||
@ -48,11 +42,9 @@ class UserAdmin(SearchModelAdmin):
|
|||||||
search_fields = ["first_name", "last_name", "username"]
|
search_fields = ["first_name", "last_name", "username"]
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(User, UserAdmin)
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Page)
|
@admin.register(Page)
|
||||||
class PageAdmin(admin.ModelAdmin):
|
class PageAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name", "_full_name", "owner_group")
|
||||||
form = make_ajax_form(
|
form = make_ajax_form(
|
||||||
Page,
|
Page,
|
||||||
{
|
{
|
||||||
@ -66,4 +58,12 @@ class PageAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(SithFile)
|
@admin.register(SithFile)
|
||||||
class SithFileAdmin(admin.ModelAdmin):
|
class SithFileAdmin(admin.ModelAdmin):
|
||||||
form = make_ajax_form(SithFile, {"parent": "files"}) # ManyToManyField
|
list_display = ("name", "owner", "size", "date", "is_in_sas")
|
||||||
|
form = make_ajax_form(
|
||||||
|
SithFile,
|
||||||
|
{
|
||||||
|
"parent": "files",
|
||||||
|
"owner": "users",
|
||||||
|
"moderator": "users",
|
||||||
|
},
|
||||||
|
) # ManyToManyField
|
||||||
|
14
core/apps.py
14
core/apps.py
@ -25,6 +25,7 @@
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
from django.core.cache import cache
|
||||||
from django.core.signals import request_started
|
from django.core.signals import request_started
|
||||||
|
|
||||||
|
|
||||||
@ -33,26 +34,17 @@ class SithConfig(AppConfig):
|
|||||||
verbose_name = "Core app of the Sith"
|
verbose_name = "Core app of the Sith"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from core.models import User
|
|
||||||
from club.models import Club
|
|
||||||
from forum.models import Forum
|
from forum.models import Forum
|
||||||
|
import core.signals
|
||||||
|
|
||||||
def clear_cached_groups(**kwargs):
|
cache.clear()
|
||||||
User._group_ids = {}
|
|
||||||
User._group_name = {}
|
|
||||||
|
|
||||||
def clear_cached_memberships(**kwargs):
|
def clear_cached_memberships(**kwargs):
|
||||||
User._club_memberships = {}
|
|
||||||
Club._memberships = {}
|
|
||||||
Forum._club_memberships = {}
|
Forum._club_memberships = {}
|
||||||
|
|
||||||
print("Connecting signals!", file=sys.stderr)
|
print("Connecting signals!", file=sys.stderr)
|
||||||
request_started.connect(
|
|
||||||
clear_cached_groups, weak=False, dispatch_uid="clear_cached_groups"
|
|
||||||
)
|
|
||||||
request_started.connect(
|
request_started.connect(
|
||||||
clear_cached_memberships,
|
clear_cached_memberships,
|
||||||
weak=False,
|
weak=False,
|
||||||
dispatch_uid="clear_cached_memberships",
|
dispatch_uid="clear_cached_memberships",
|
||||||
)
|
)
|
||||||
# TODO: there may be a need to add more cache clearing
|
|
||||||
|
35
core/converters.py
Normal file
35
core/converters.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from core.models import Page
|
||||||
|
|
||||||
|
|
||||||
|
class FourDigitYearConverter:
|
||||||
|
regex = "[0-9]{4}"
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
return int(value)
|
||||||
|
|
||||||
|
def to_url(self, value):
|
||||||
|
return str(value).zfill(4)
|
||||||
|
|
||||||
|
|
||||||
|
class TwoDigitMonthConverter:
|
||||||
|
regex = "[0-9]{2}"
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
return int(value)
|
||||||
|
|
||||||
|
def to_url(self, value):
|
||||||
|
return str(value).zfill(2)
|
||||||
|
|
||||||
|
|
||||||
|
class BooleanStringConverter:
|
||||||
|
"""
|
||||||
|
Converter whose regex match either True or False
|
||||||
|
"""
|
||||||
|
|
||||||
|
regex = r"(True)|(False)"
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
return str(value) == "True"
|
||||||
|
|
||||||
|
def to_url(self, value):
|
||||||
|
return str(value)
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user