Compare commits
508 Commits
38f6c27983
...
feature/ea
Author | SHA1 | Date | |
---|---|---|---|
858199e476 | |||
f6ecbd899d | |||
e868946fd7 | |||
254044c36b | |||
c695d6f7a0 | |||
feef855f01 | |||
b3a48ca5af | |||
f3a52d094e | |||
2901bd919f | |||
0396a5bf2b | |||
b48ad16f04 | |||
7cc6250860 | |||
ae2e4b518d | |||
e9b9f3a62b | |||
3321669726 | |||
21fc85670e | |||
18a5ad6541 | |||
71c5456225 | |||
50e04164a2 | |||
3b1d71f317 | |||
65c2689578 | |||
b45673f04a | |||
cb6e037f5e | |||
5e6d60bb3a | |||
64f8d9bad3 | |||
05b86e1f7a | |||
700fed860d | |||
820bf6279b | |||
b97ce81dd2 | |||
f4dfd8f99c | |||
29139bf360 | |||
4f9c2724f5 | |||
7a914f5e94 | |||
121d04e1d5 | |||
fc6cdba8e2 | |||
7f39ead159 | |||
1da82ac2dd | |||
f2dcc39c14 | |||
705dc56153 | |||
02047b62d7 | |||
895d4b33a6 | |||
142cb3316e | |||
997fcc9fff | |||
ec65ca11d6 | |||
0198027544 | |||
69e0550d4f | |||
9a1a5635e2 | |||
863f9ff77e | |||
4146c4c5cb | |||
b3ad5c5df9 | |||
9388e2dc88 | |||
56dec9eaa1 | |||
596126f4f4 | |||
8646b2c8f7 | |||
c81bb1fb90 | |||
d17a52a8d6 | |||
55e0eecc0b | |||
496adc17ea | |||
ab43d7d2df | |||
13f0bfe546 | |||
83a384145b | |||
8a923761a5 | |||
6e4a99eba3 | |||
0470aa185e | |||
273371db8b | |||
ed3aa0c328 | |||
acfff6b103 | |||
ada4579193 | |||
3a17c3079e | |||
26e46de8e1 | |||
111bcc8e60 | |||
cdaa204ba2 | |||
e85511fcb9 | |||
35c120a29f | |||
7c4c1bc387 | |||
6e77edcf67 | |||
effed9c760 | |||
0e5c8b53b0 | |||
47a332445c | |||
c904b2d827 | |||
f56263d6bd | |||
0c2494cb34 | |||
9e5743a64c | |||
b5241ec75e | |||
4f00224f0d | |||
320a896610 | |||
08924c5e05 | |||
98bfc308a7 | |||
dee24fbc9c | |||
2556427c7d | |||
a2b35e5bba | |||
3e8f1acb96 | |||
85788977fe | |||
066ca5bada | |||
41369f738e | |||
67377b3cbf | |||
ac3d668655 | |||
c57b15e159 | |||
66efb8012e | |||
cad0c0dadb | |||
b32c90ed5d | |||
4d361dc67b | |||
2b170d91f7 | |||
9e074d6ca6 | |||
b655b2695b | |||
366aeed2ba | |||
454ae5f9e3 | |||
b811114425 | |||
712e7c8939 | |||
713cd92141 | |||
4154b499b1 | |||
253f204225 | |||
7241f3eb1d | |||
2422f60898 | |||
ba6599fa56 | |||
f2666f6fb0 | |||
b33839191d | |||
ee3e375dde | |||
5b0f7ca21b | |||
f581d91730 | |||
bbf362691b | |||
15e2c8c7b3 | |||
f838127730 | |||
d4c0bb3b0e | |||
b81aee3f1c | |||
c6caf5dbce | |||
7acc59f2cd | |||
757ff7ead7 | |||
bc2fe16b74 | |||
35363d9ee7 | |||
52106db6fd | |||
c4b1829e78 | |||
489a9378c5 | |||
28ae109b32 | |||
e7a6a94ff2 | |||
234556a172 | |||
e4ddceabea | |||
05dd3ad642 | |||
6c5db61a97 | |||
a0e4e9e8e3 | |||
c66df77d4a | |||
cfb6b34630 | |||
d8fd0adf47 | |||
928ae13a8a | |||
c2e0ea70e4 | |||
782ce24895 | |||
b630742fd4 | |||
b20df930a2 | |||
d60a96fc5c | |||
05b0a0ab2f | |||
9eb137e503 | |||
7d797009bb | |||
3c1818f229 | |||
d8b69e9b45 | |||
9177c9d4c2 | |||
5195352975 | |||
deb8f865df | |||
5b2c70e4fb | |||
f66db0859e | |||
b6488d1d00 | |||
6a4ac336ad | |||
7ac6dcf8a0 | |||
c6a3677cc5 | |||
707459acd6 | |||
6390c3320e | |||
b8aabc466c | |||
c66e4232b9 | |||
336450d43f | |||
7e66aadd6f | |||
bf2b796936 | |||
85623f48a9 | |||
4fbee9c3de | |||
bfa3b45547 | |||
677a9da469 | |||
1f7752d457 | |||
89979dbf61 | |||
8d1abb8f33 | |||
2df3494c3b | |||
39bb490257 | |||
7a7aad0503 | |||
b157a3fa90 | |||
1b688a8aa5 | |||
e8978cc065 | |||
7fd68e4825 | |||
4119eefe37 | |||
aafc2e6e96 | |||
2cbe6fa11c | |||
eec7bcf296 | |||
6c45de34a4 | |||
61a40c47d2 | |||
007157e2e8 | |||
49a0ade315 | |||
782cd9a45a | |||
6382e631b6 | |||
12493cffca | |||
a38ab57ddf | |||
30091ef69c | |||
1a483bfa2c | |||
1a091951e8 | |||
bfb66b352a | |||
be26e3df7f | |||
cb3307509d | |||
a3158253a7 | |||
406380e4f1 | |||
efb70652af | |||
05256bb99a | |||
64d0cc2fa8 | |||
f5d7267ba7 | |||
24c0a21cc1 | |||
6a352d642b | |||
48ae1f7c1c | |||
aaf1adaaa1 | |||
f34f5fe693
|
|||
f485178422 | |||
797ca0f926 | |||
390a4b0064 | |||
94b029dc9c
|
|||
45d5728c3e | |||
6eabbaf209 | |||
03fdd0b947 | |||
fb8faacddc
|
|||
7ee4557ab5 | |||
5accdbccbb | |||
7fb26f9e45 | |||
26a07f722d | |||
9176a03a8a | |||
4a1bfc366d
|
|||
ebee8c34e1
|
|||
4ecad1c73b
|
|||
d1b3a4d3f6
|
|||
40832bb3bf | |||
4a78157f9a
|
|||
bf5fc8750d
|
|||
274a7b7137
|
|||
8dd2c02d3e
|
|||
a73f5cb270
|
|||
7d40e11144
|
|||
af48553e35
|
|||
ad8bcc7282 | |||
22a44415e4 | |||
6a153719f9
|
|||
5c8fa1b9e7
|
|||
d82679e3d7 | |||
9cb432a082 | |||
869d29d4a4 | |||
c3d2e64a43
|
|||
e1770ec52c
|
|||
1256744f1b
|
|||
77dddbc581
|
|||
bfa4000365 | |||
50c2f8164d | |||
5c30de5f22
|
|||
1c03ce621f
|
|||
e634cda318
|
|||
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
|
|||
5ccb499665
|
|||
c467165bf3
|
|||
8512f3c5d0
|
|||
5003e57338
|
|||
b7c2da53fe
|
|||
598cdc0284
|
|||
692d9a25e3
|
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"
|
13
.github/auto_assign.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Set to true to add reviewers to pull requests
|
||||||
|
addReviewers: true
|
||||||
|
|
||||||
|
# Set to true to add assignees to pull requests
|
||||||
|
addAssignees: author
|
||||||
|
|
||||||
|
# A list of team reviewers to be added to pull requests (GitHub team slug)
|
||||||
|
reviewers:
|
||||||
|
- ae-utbm/sith-3-developers
|
||||||
|
|
||||||
|
# Number of reviewers has no impact on GitHub teams
|
||||||
|
# Set 0 to add all the reviewers (default: 0)
|
||||||
|
numberOfReviewers: 0
|
64
.github/workflows/deploy.yml
vendored
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
name: Deploy to production
|
||||||
|
concurrency: production
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deployment:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: production
|
||||||
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: SSH Remote Commands
|
||||||
|
uses: appleboy/ssh-action@dce9d565de8d876c11d93fa4fe677c0285a66d78
|
||||||
|
with:
|
||||||
|
# Proxy
|
||||||
|
proxy_host : ${{secrets.PROXY_HOST}}
|
||||||
|
proxy_port : ${{secrets.PROXY_PORT}}
|
||||||
|
proxy_username : ${{secrets.PROXY_USER}}
|
||||||
|
proxy_passphrase: ${{secrets.PROXY_PASSPHRASE}}
|
||||||
|
proxy_key: ${{secrets.PROXY_KEY}}
|
||||||
|
|
||||||
|
# Serveur web
|
||||||
|
host: ${{secrets.HOST}}
|
||||||
|
port : ${{secrets.PORT}}
|
||||||
|
username : ${{secrets.USER}}
|
||||||
|
key: ${{secrets.KEY}}
|
||||||
|
|
||||||
|
script_stop: true
|
||||||
|
|
||||||
|
# See https://github.com/ae-utbm/sith3/wiki/GitHub-Actions#deployment-action
|
||||||
|
script: |
|
||||||
|
export PATH="$HOME/.poetry/bin:$PATH"
|
||||||
|
pushd ${{secrets.SITH_PATH}}
|
||||||
|
|
||||||
|
git pull
|
||||||
|
poetry update
|
||||||
|
poetry run ./manage.py migrate
|
||||||
|
echo "yes" | poetry run ./manage.py collectstatic
|
||||||
|
poetry run ./manage.py compilestatic
|
||||||
|
poetry run ./manage.py compilemessages
|
||||||
|
|
||||||
|
sudo systemctl restart uwsgi
|
||||||
|
|
||||||
|
sentry:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: production
|
||||||
|
timeout-minutes: 30
|
||||||
|
needs: deployment
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Sentry Release
|
||||||
|
uses: getsentry/action-release@v1.2.0
|
||||||
|
env:
|
||||||
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||||
|
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||||
|
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
||||||
|
with:
|
||||||
|
environment: production
|
83
.github/workflows/unittests.yml
vendored
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
name: Sith3 CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
unittests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# Skip unit testing if no diff on .py files
|
||||||
|
- name: Check file diff
|
||||||
|
uses: technote-space/get-diff-action@v6
|
||||||
|
id: git-diff
|
||||||
|
with:
|
||||||
|
PATTERNS: |
|
||||||
|
**/*.py
|
||||||
|
|
||||||
|
- name: Set up python
|
||||||
|
if: steps.git-diff.outputs.diff
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.8'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
if: steps.git-diff.outputs.diff
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install gettext libxapian-dev libgraphviz-dev
|
||||||
|
|
||||||
|
- name: Install poetry
|
||||||
|
if: steps.git-diff.outputs.diff
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install poetry
|
||||||
|
|
||||||
|
- name: Checking pyproject.toml syntax
|
||||||
|
if: steps.git-diff.outputs.diff
|
||||||
|
run: poetry check
|
||||||
|
|
||||||
|
- name: Install project
|
||||||
|
if: steps.git-diff.outputs.diff
|
||||||
|
run: poetry install -E testing
|
||||||
|
|
||||||
|
- name: Setup xapian index
|
||||||
|
if: steps.git-diff.outputs.diff
|
||||||
|
run: |
|
||||||
|
mkdir -p /dev/shm/search_indexes
|
||||||
|
ln -s /dev/shm/search_indexes sith/search_indexes
|
||||||
|
|
||||||
|
- name: Setup project
|
||||||
|
if: steps.git-diff.outputs.diff
|
||||||
|
run: poetry run ./manage.py compilemessages
|
||||||
|
|
||||||
|
- name: Launch tests and generate coverage report
|
||||||
|
if: steps.git-diff.outputs.diff
|
||||||
|
run: |
|
||||||
|
poetry run coverage run ./manage.py test
|
||||||
|
poetry run coverage report
|
||||||
|
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Set up python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.8'
|
||||||
|
|
||||||
|
- name: Install black
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install black==22.6.0
|
||||||
|
|
||||||
|
- name: Check linting
|
||||||
|
run: black --check .
|
3
.gitignore
vendored
@ -4,6 +4,8 @@ db.sqlite3
|
|||||||
*.mo
|
*.mo
|
||||||
*__pycache__*
|
*__pycache__*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
pyrightconfig.json
|
||||||
|
dist/
|
||||||
.vscode/
|
.vscode/
|
||||||
env/
|
env/
|
||||||
doc/html
|
doc/html
|
||||||
@ -13,3 +15,4 @@ 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 .
|
|
18
.mailmap
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
Code <gregoire.duvauchelle@utbm.fr>
|
||||||
|
Cyl <labetowiez@aol.fr>
|
||||||
|
Juste <maaxleblanc@gmail.com>
|
||||||
|
Krophil <pierre.brunet@krophil.fr>
|
||||||
|
Lo-J <renaudg779@gmail.com>
|
||||||
|
Nabos <gnikwo@hotmail.com>
|
||||||
|
Och <francescowitz68@gmail.com>
|
||||||
|
Partoo <joqaste@gmail.com>
|
||||||
|
Skia <skia@hya.sk> <lordbanana25@mailoo.org>
|
||||||
|
Skia <skia@hya.sk> <skia@libskia.so>
|
||||||
|
Sli <klmp200@klmp200.net> <antoine@bartuccio.fr>
|
||||||
|
Soldat <ryan-68@live.fr>
|
||||||
|
Terre <jbaptiste.lenglet+git@gmail.com>
|
||||||
|
Vial <robin.trioux@utbm.fr>
|
||||||
|
Zar <antoine.charmeau@utbm.fr> <antoine.charmeau@laposte.net>
|
||||||
|
root <root@localhost.localdomain>
|
||||||
|
tleb <tleb@openmailbox.org> <theo.lebrun@live.fr>
|
||||||
|
tleb <tleb@openmailbox.org> <theo.lebrun@utbm.fr>
|
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
@ -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
@ -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 !
|
133
README.md
@ -1,104 +1,41 @@
|
|||||||
[](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/889796155523874847?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:
|
<ul>
|
||||||
|
<li>
|
||||||
```bash
|
<p align="justify">
|
||||||
sudo apt install libmysqlclient-dev libssl-dev libjpeg-dev zlib1g-dev python3-dev libffi-dev python3-dev libgraphviz-dev pkg-config python3-xapian gettext
|
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>
|
||||||
On macos, you will need homebrew
|
<li>
|
||||||
|
<p align="justify">
|
||||||
```bash
|
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.
|
||||||
brew install xapian
|
</p>
|
||||||
```
|
</li>
|
||||||
|
<li>
|
||||||
If it doesn't work it's because it need [this pull request](https://github.com/Homebrew/homebrew-core/pull/34835) to be validated.
|
<p align="justify">
|
||||||
|
Keep in mind that this documentation is thought to be read in order.
|
||||||
The development is done with sqlite, but it is advised to set a more robust DBMS for production (Postgresql for example)
|
</p>
|
||||||
|
</li>
|
||||||
### Get started
|
</ul>
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
> This project is licenced under GNU GPL, see the LICENSE file at the top of the repository for more details.
|
||||||
|
|
||||||
|
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
|
|
||||||
|
|
@ -4,6 +4,7 @@ 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):
|
||||||
@ -243,6 +244,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 +269,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,6 +2,7 @@
|
|||||||
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):
|
||||||
@ -22,6 +23,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 +33,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 +58,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 +68,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 +78,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(
|
||||||
|
@ -29,6 +29,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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -22,12 +22,12 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
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
|
||||||
@ -110,7 +110,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")
|
||||||
@ -136,9 +141,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:
|
||||||
@ -203,7 +216,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)
|
||||||
@ -263,7 +280,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 +303,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 +312,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 +320,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 +352,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:
|
||||||
@ -487,6 +512,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 +544,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:
|
||||||
|
@ -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 %}
|
||||||
|
@ -23,9 +23,9 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
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 (
|
||||||
@ -110,6 +110,9 @@ class JournalTest(TestCase):
|
|||||||
class OperationTest(TestCase):
|
class OperationTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
call_command("populate")
|
call_command("populate")
|
||||||
|
self.tomorrow_formatted = (date.today() + timedelta(days=1)).strftime(
|
||||||
|
"%d/%m/%Y"
|
||||||
|
)
|
||||||
self.journal = GeneralJournal.objects.filter(id=1).first()
|
self.journal = GeneralJournal.objects.filter(id=1).first()
|
||||||
self.skia = User.objects.filter(username="skia").first()
|
self.skia = User.objects.filter(username="skia").first()
|
||||||
at = AccountingType(
|
at = AccountingType(
|
||||||
@ -158,7 +161,7 @@ class OperationTest(TestCase):
|
|||||||
"target_type": "OTHER",
|
"target_type": "OTHER",
|
||||||
"target_id": "",
|
"target_id": "",
|
||||||
"target_label": "Le fantome de la nuit",
|
"target_label": "Le fantome de la nuit",
|
||||||
"date": "04/12/2020",
|
"date": self.tomorrow_formatted,
|
||||||
"mode": "CASH",
|
"mode": "CASH",
|
||||||
"cheque_number": "",
|
"cheque_number": "",
|
||||||
"invoice": "",
|
"invoice": "",
|
||||||
@ -191,7 +194,7 @@ class OperationTest(TestCase):
|
|||||||
"target_type": "OTHER",
|
"target_type": "OTHER",
|
||||||
"target_id": "",
|
"target_id": "",
|
||||||
"target_label": "Le fantome de la nuit",
|
"target_label": "Le fantome de la nuit",
|
||||||
"date": "04/12/2020",
|
"date": self.tomorrow_formatted,
|
||||||
"mode": "CASH",
|
"mode": "CASH",
|
||||||
"cheque_number": "",
|
"cheque_number": "",
|
||||||
"invoice": "",
|
"invoice": "",
|
||||||
@ -218,7 +221,7 @@ class OperationTest(TestCase):
|
|||||||
"target_type": "OTHER",
|
"target_type": "OTHER",
|
||||||
"target_id": "",
|
"target_id": "",
|
||||||
"target_label": "Le fantome du jour",
|
"target_label": "Le fantome du jour",
|
||||||
"date": "04/12/2020",
|
"date": self.tomorrow_formatted,
|
||||||
"mode": "CASH",
|
"mode": "CASH",
|
||||||
"cheque_number": "",
|
"cheque_number": "",
|
||||||
"invoice": "",
|
"invoice": "",
|
||||||
@ -245,7 +248,7 @@ class OperationTest(TestCase):
|
|||||||
"target_type": "OTHER",
|
"target_type": "OTHER",
|
||||||
"target_id": "",
|
"target_id": "",
|
||||||
"target_label": "Le fantome de l'aurore",
|
"target_label": "Le fantome de l'aurore",
|
||||||
"date": "04/12/2020",
|
"date": self.tomorrow_formatted,
|
||||||
"mode": "CASH",
|
"mode": "CASH",
|
||||||
"cheque_number": "",
|
"cheque_number": "",
|
||||||
"invoice": "",
|
"invoice": "",
|
||||||
@ -272,30 +275,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>""",
|
||||||
)
|
)
|
||||||
|
@ -22,131 +22,133 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.urls import re_path
|
||||||
|
|
||||||
from accounting.views import *
|
from accounting.views import *
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Accounting types
|
# Accounting types
|
||||||
url(
|
re_path(
|
||||||
r"^simple_type$",
|
r"^simple_type$",
|
||||||
SimplifiedAccountingTypeListView.as_view(),
|
SimplifiedAccountingTypeListView.as_view(),
|
||||||
name="simple_type_list",
|
name="simple_type_list",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^simple_type/create$",
|
r"^simple_type/create$",
|
||||||
SimplifiedAccountingTypeCreateView.as_view(),
|
SimplifiedAccountingTypeCreateView.as_view(),
|
||||||
name="simple_type_new",
|
name="simple_type_new",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^simple_type/(?P<type_id>[0-9]+)/edit$",
|
r"^simple_type/(?P<type_id>[0-9]+)/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"),
|
re_path(r"^type$", AccountingTypeListView.as_view(), name="type_list"),
|
||||||
url(r"^type/create$", AccountingTypeCreateView.as_view(), name="type_new"),
|
re_path(r"^type/create$", AccountingTypeCreateView.as_view(), name="type_new"),
|
||||||
url(
|
re_path(
|
||||||
r"^type/(?P<type_id>[0-9]+)/edit$",
|
r"^type/(?P<type_id>[0-9]+)/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"),
|
re_path(r"^$", BankAccountListView.as_view(), name="bank_list"),
|
||||||
url(r"^bank/create$", BankAccountCreateView.as_view(), name="bank_new"),
|
re_path(r"^bank/create$", BankAccountCreateView.as_view(), name="bank_new"),
|
||||||
url(
|
re_path(
|
||||||
r"^bank/(?P<b_account_id>[0-9]+)$",
|
r"^bank/(?P<b_account_id>[0-9]+)$",
|
||||||
BankAccountDetailView.as_view(),
|
BankAccountDetailView.as_view(),
|
||||||
name="bank_details",
|
name="bank_details",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^bank/(?P<b_account_id>[0-9]+)/edit$",
|
r"^bank/(?P<b_account_id>[0-9]+)/edit$",
|
||||||
BankAccountEditView.as_view(),
|
BankAccountEditView.as_view(),
|
||||||
name="bank_edit",
|
name="bank_edit",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^bank/(?P<b_account_id>[0-9]+)/delete$",
|
r"^bank/(?P<b_account_id>[0-9]+)/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"),
|
re_path(r"^club/create$", ClubAccountCreateView.as_view(), name="club_new"),
|
||||||
url(
|
re_path(
|
||||||
r"^club/(?P<c_account_id>[0-9]+)$",
|
r"^club/(?P<c_account_id>[0-9]+)$",
|
||||||
ClubAccountDetailView.as_view(),
|
ClubAccountDetailView.as_view(),
|
||||||
name="club_details",
|
name="club_details",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^club/(?P<c_account_id>[0-9]+)/edit$",
|
r"^club/(?P<c_account_id>[0-9]+)/edit$",
|
||||||
ClubAccountEditView.as_view(),
|
ClubAccountEditView.as_view(),
|
||||||
name="club_edit",
|
name="club_edit",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^club/(?P<c_account_id>[0-9]+)/delete$",
|
r"^club/(?P<c_account_id>[0-9]+)/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"),
|
re_path(r"^journal/create$", JournalCreateView.as_view(), name="journal_new"),
|
||||||
url(
|
re_path(
|
||||||
r"^journal/(?P<j_id>[0-9]+)$",
|
r"^journal/(?P<j_id>[0-9]+)$",
|
||||||
JournalDetailView.as_view(),
|
JournalDetailView.as_view(),
|
||||||
name="journal_details",
|
name="journal_details",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^journal/(?P<j_id>[0-9]+)/edit$",
|
r"^journal/(?P<j_id>[0-9]+)/edit$",
|
||||||
JournalEditView.as_view(),
|
JournalEditView.as_view(),
|
||||||
name="journal_edit",
|
name="journal_edit",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^journal/(?P<j_id>[0-9]+)/delete$",
|
r"^journal/(?P<j_id>[0-9]+)/delete$",
|
||||||
JournalDeleteView.as_view(),
|
JournalDeleteView.as_view(),
|
||||||
name="journal_delete",
|
name="journal_delete",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^journal/(?P<j_id>[0-9]+)/statement/nature$",
|
r"^journal/(?P<j_id>[0-9]+)/statement/nature$",
|
||||||
JournalNatureStatementView.as_view(),
|
JournalNatureStatementView.as_view(),
|
||||||
name="journal_nature_statement",
|
name="journal_nature_statement",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^journal/(?P<j_id>[0-9]+)/statement/person$",
|
r"^journal/(?P<j_id>[0-9]+)/statement/person$",
|
||||||
JournalPersonStatementView.as_view(),
|
JournalPersonStatementView.as_view(),
|
||||||
name="journal_person_statement",
|
name="journal_person_statement",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^journal/(?P<j_id>[0-9]+)/statement/accounting$",
|
r"^journal/(?P<j_id>[0-9]+)/statement/accounting$",
|
||||||
JournalAccountingStatementView.as_view(),
|
JournalAccountingStatementView.as_view(),
|
||||||
name="journal_accounting_statement",
|
name="journal_accounting_statement",
|
||||||
),
|
),
|
||||||
# Operations
|
# Operations
|
||||||
url(
|
re_path(
|
||||||
r"^operation/create/(?P<j_id>[0-9]+)$",
|
r"^operation/create/(?P<j_id>[0-9]+)$",
|
||||||
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"),
|
re_path(
|
||||||
url(
|
r"^operation/(?P<op_id>[0-9]+)$", OperationEditView.as_view(), name="op_edit"
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
r"^operation/(?P<op_id>[0-9]+)/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"),
|
re_path(r"^company/list$", CompanyListView.as_view(), name="co_list"),
|
||||||
url(r"^company/create$", CompanyCreateView.as_view(), name="co_new"),
|
re_path(r"^company/create$", CompanyCreateView.as_view(), name="co_new"),
|
||||||
url(r"^company/(?P<co_id>[0-9]+)$", CompanyEditView.as_view(), name="co_edit"),
|
re_path(r"^company/(?P<co_id>[0-9]+)$", CompanyEditView.as_view(), name="co_edit"),
|
||||||
# Labels
|
# Labels
|
||||||
url(r"^label/new$", LabelCreateView.as_view(), name="label_new"),
|
re_path(r"^label/new$", LabelCreateView.as_view(), name="label_new"),
|
||||||
url(
|
re_path(
|
||||||
r"^label/(?P<clubaccount_id>[0-9]+)$",
|
r"^label/(?P<clubaccount_id>[0-9]+)$",
|
||||||
LabelListView.as_view(),
|
LabelListView.as_view(),
|
||||||
name="label_list",
|
name="label_list",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^label/(?P<label_id>[0-9]+)/edit$", LabelEditView.as_view(), name="label_edit"
|
r"^label/(?P<label_id>[0-9]+)/edit$", LabelEditView.as_view(), name="label_edit"
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^label/(?P<label_id>[0-9]+)/delete$",
|
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"),
|
re_path(r"^refound/account$", RefoundAccountView.as_view(), name="refound_account"),
|
||||||
]
|
]
|
||||||
|
@ -24,8 +24,8 @@
|
|||||||
|
|
||||||
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 +496,7 @@ class OperationCreateView(CanCreateMixin, CreateView):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add journal to the context """
|
"""Add journal to the context"""
|
||||||
kwargs = super(OperationCreateView, self).get_context_data(**kwargs)
|
kwargs = super(OperationCreateView, self).get_context_data(**kwargs)
|
||||||
if self.journal:
|
if self.journal:
|
||||||
kwargs["object"] = self.journal
|
kwargs["object"] = self.journal
|
||||||
@ -514,7 +514,7 @@ class OperationEditView(CanEditMixin, UpdateView):
|
|||||||
template_name = "accounting/operation_edit.jinja"
|
template_name = "accounting/operation_edit.jinja"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add journal to the context """
|
"""Add journal to the context"""
|
||||||
kwargs = super(OperationEditView, self).get_context_data(**kwargs)
|
kwargs = super(OperationEditView, self).get_context_data(**kwargs)
|
||||||
kwargs["object"] = self.object.journal
|
kwargs["object"] = self.object.journal
|
||||||
return kwargs
|
return kwargs
|
||||||
@ -735,7 +735,7 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
|||||||
return statement
|
return statement
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add infos to the context """
|
"""Add infos to the context"""
|
||||||
kwargs = super(JournalNatureStatementView, self).get_context_data(**kwargs)
|
kwargs = super(JournalNatureStatementView, self).get_context_data(**kwargs)
|
||||||
kwargs["statement"] = self.big_statement()
|
kwargs["statement"] = self.big_statement()
|
||||||
return kwargs
|
return kwargs
|
||||||
@ -774,7 +774,7 @@ class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
|||||||
return sum(self.statement(movement_type).values())
|
return sum(self.statement(movement_type).values())
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add journal to the context """
|
"""Add journal to the context"""
|
||||||
kwargs = super(JournalPersonStatementView, self).get_context_data(**kwargs)
|
kwargs = super(JournalPersonStatementView, self).get_context_data(**kwargs)
|
||||||
kwargs["credit_statement"] = self.statement("CREDIT")
|
kwargs["credit_statement"] = self.statement("CREDIT")
|
||||||
kwargs["debit_statement"] = self.statement("DEBIT")
|
kwargs["debit_statement"] = self.statement("DEBIT")
|
||||||
@ -804,7 +804,7 @@ class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView)
|
|||||||
return statement
|
return statement
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add journal to the context """
|
"""Add journal to the context"""
|
||||||
kwargs = super(JournalAccountingStatementView, self).get_context_data(**kwargs)
|
kwargs = super(JournalAccountingStatementView, self).get_context_data(**kwargs)
|
||||||
kwargs["statement"] = self.statement()
|
kwargs["statement"] = self.statement()
|
||||||
return kwargs
|
return kwargs
|
||||||
|
26
api/urls.py
@ -22,35 +22,37 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
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"),
|
||||||
]
|
]
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
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
|
||||||
@ -46,7 +46,7 @@ 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/)
|
||||||
@ -77,3 +77,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 *
|
||||||
|
@ -25,7 +25,6 @@
|
|||||||
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
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
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
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ class CounterViewSet(RightModelViewSet):
|
|||||||
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/)
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
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
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
|
|||||||
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)
|
||||||
@ -105,7 +105,7 @@ 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 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)
|
||||||
@ -114,7 +114,7 @@ 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 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)
|
||||||
@ -125,7 +125,7 @@ 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)
|
||||||
|
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)
|
||||||
|
]
|
||||||
|
)
|
@ -26,7 +26,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
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ class UserViewSet(RightModelViewSet):
|
|||||||
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)
|
||||||
|
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
|
@ -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,28 @@ class MailingForm(forms.Form):
|
|||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
class SellingsFormBase(forms.Form):
|
class SellingsForm(forms.Form):
|
||||||
begin_date = forms.DateTimeField(
|
begin_date = TzAwareDateTimeField(label=_("Begin date"), required=False)
|
||||||
["%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):
|
||||||
"""
|
"""
|
||||||
|
@ -3,6 +3,7 @@ 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):
|
||||||
@ -90,7 +91,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,6 +3,7 @@ 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):
|
||||||
@ -18,6 +19,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 +36,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 +48,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(
|
||||||
|
@ -3,6 +3,7 @@ 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):
|
||||||
@ -14,6 +15,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,
|
||||||
|
@ -5,6 +5,7 @@ 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):
|
||||||
@ -51,12 +52,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 +89,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 +98,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):
|
||||||
@ -31,7 +32,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(
|
||||||
|
@ -3,6 +3,7 @@ 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):
|
||||||
@ -14,6 +15,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",
|
||||||
|
@ -26,10 +26,10 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.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 +46,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,
|
||||||
@ -75,7 +77,10 @@ class Club(models.Model):
|
|||||||
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 +96,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"]
|
||||||
@ -183,8 +190,8 @@ class Club(models.Model):
|
|||||||
name=settings.SITH_MAIN_MEMBERS_GROUP
|
name=settings.SITH_MAIN_MEMBERS_GROUP
|
||||||
).first()
|
).first()
|
||||||
self.make_home()
|
self.make_home()
|
||||||
self.home.edit_groups = [board]
|
self.home.edit_groups.set([board])
|
||||||
self.home.view_groups = [member, subscribers]
|
self.home.view_groups.set([member, subscribers])
|
||||||
self.home.save()
|
self.home.save()
|
||||||
self.make_page()
|
self.make_page()
|
||||||
|
|
||||||
@ -261,9 +268,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)
|
||||||
@ -317,7 +330,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 +352,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):
|
||||||
@ -409,6 +431,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 +439,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)
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
{% from 'core/macros.jinja' import user_profile_link %}
|
{% from 'core/macros.jinja' import user_profile_link, paginate %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h3>{% trans %}Sellings{% endtrans %}</h3>
|
<h3>{% trans %}Sellings{% endtrans %}</h3>
|
||||||
<form action="" method="get">
|
<form id="form" action="?page=1" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form }}
|
{{ form }}
|
||||||
<p><input type="submit" value="{% trans %}Show{% endtrans %}" /></p>
|
<p><input type="submit" value="{% trans %}Show{% endtrans %}" /></p>
|
||||||
@ -28,7 +28,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for s in result %}
|
{% for s in paginated_result %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ s.date|localtime|date(DATETIME_FORMAT) }} {{ s.date|localtime|time(DATETIME_FORMAT) }}</td>
|
<td>{{ s.date|localtime|date(DATETIME_FORMAT) }} {{ s.date|localtime|time(DATETIME_FORMAT) }}</td>
|
||||||
<td>{{ s.counter }}</td>
|
<td>{{ s.counter }}</td>
|
||||||
@ -53,6 +53,14 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function formPagination(link){
|
||||||
|
$("form").attr("action", link.href);
|
||||||
|
link.href = "javascript:void(0)"; // block link action
|
||||||
|
$("form").submit();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{{ paginate(paginated_result, paginator, "formPagination(this)") }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for widget in form_mailing_removal.subwidgets %}
|
{% for widget in form_mailing_removal.subwidgets %}
|
||||||
{% set user = ms[widget.data.value][0] %}
|
{% set user = ms[widget.data.value.value][0] %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ user.get_username }}</td>
|
<td>{{ user.get_username }}</td>
|
||||||
<td>{{ user.get_email }}</td>
|
<td>{{ user.get_email }}</td>
|
||||||
|
@ -25,8 +25,8 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import timezone, html
|
from django.utils import timezone, html
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.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.core.exceptions import ValidationError, NON_FIELD_ERRORS
|
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
|
||||||
|
|
||||||
@ -161,10 +161,10 @@ class ClubTest(TestCase):
|
|||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id})
|
reverse("club:club_members", kwargs={"club_id": self.bdf.id})
|
||||||
)
|
)
|
||||||
self.assertTrue(response.status_code == 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTrue(
|
self.assertIn(
|
||||||
"""Richard Batsbak</a></td>\\n <td>Vice-Pr\\xc3\\xa9sident</td>"""
|
"""Richard Batsbak</a></td>\n <td>Vice-Président⸱e</td>""",
|
||||||
in str(response.content)
|
response.content.decode(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_create_add_user_to_club_from_richard_fail(self):
|
def test_create_add_user_to_club_from_richard_fail(self):
|
||||||
@ -180,7 +180,7 @@ class ClubTest(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertTrue(response.status_code == 200)
|
self.assertTrue(response.status_code == 200)
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
"<li>Vous n'avez pas la permission de faire cela</li>"
|
"<li>Vous n'avez pas la permission de faire cela</li>"
|
||||||
in str(response.content)
|
in str(response.content)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -369,14 +369,15 @@ class ClubTest(TestCase):
|
|||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse("club:club_members", kwargs={"club_id": self.bdf.id})
|
reverse("club:club_members", kwargs={"club_id": self.bdf.id})
|
||||||
)
|
)
|
||||||
self.assertTrue(response.status_code == 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
content = str(response.content)
|
content = response.content.decode()
|
||||||
self.assertTrue(
|
self.assertIn(
|
||||||
"Richard Batsbak</a></td>\\n <td>Curieux</td>" in content
|
"Richard Batsbak</a></td>\n <td>Curieux⸱euse</td>",
|
||||||
|
content,
|
||||||
)
|
)
|
||||||
self.assertTrue(
|
self.assertIn(
|
||||||
"S' Kia</a></td>\\n <td>Responsable info</td>"
|
"S' Kia</a></td>\n <td>Responsable info</td>",
|
||||||
in content
|
content,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -641,7 +642,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 +651,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")
|
||||||
|
61
club/urls.py
@ -23,80 +23,93 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.urls import re_path
|
||||||
|
|
||||||
from club.views import *
|
from club.views import *
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r"^$", ClubListView.as_view(), name="club_list"),
|
re_path(r"^$", ClubListView.as_view(), name="club_list"),
|
||||||
url(r"^new$", ClubCreateView.as_view(), name="club_new"),
|
re_path(r"^new$", ClubCreateView.as_view(), name="club_new"),
|
||||||
url(r"^stats$", ClubStatView.as_view(), name="club_stats"),
|
re_path(r"^stats$", ClubStatView.as_view(), name="club_stats"),
|
||||||
url(r"^(?P<club_id>[0-9]+)/$", ClubView.as_view(), name="club_view"),
|
re_path(r"^(?P<club_id>[0-9]+)/$", ClubView.as_view(), name="club_view"),
|
||||||
url(
|
re_path(
|
||||||
r"^(?P<club_id>[0-9]+)/rev/(?P<rev_id>[0-9]+)/$",
|
r"^(?P<club_id>[0-9]+)/rev/(?P<rev_id>[0-9]+)/$",
|
||||||
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"),
|
re_path(
|
||||||
url(r"^(?P<club_id>[0-9]+)/edit$", ClubEditView.as_view(), name="club_edit"),
|
r"^(?P<club_id>[0-9]+)/hist$", ClubPageHistView.as_view(), name="club_hist"
|
||||||
url(
|
),
|
||||||
|
re_path(r"^(?P<club_id>[0-9]+)/edit$", ClubEditView.as_view(), name="club_edit"),
|
||||||
|
re_path(
|
||||||
r"^(?P<club_id>[0-9]+)/edit/page$",
|
r"^(?P<club_id>[0-9]+)/edit/page$",
|
||||||
ClubPageEditView.as_view(),
|
ClubPageEditView.as_view(),
|
||||||
name="club_edit_page",
|
name="club_edit_page",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^(?P<club_id>[0-9]+)/members$", ClubMembersView.as_view(), name="club_members"
|
r"^(?P<club_id>[0-9]+)/members$", ClubMembersView.as_view(), name="club_members"
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^(?P<club_id>[0-9]+)/elderlies$",
|
r"^(?P<club_id>[0-9]+)/elderlies$",
|
||||||
ClubOldMembersView.as_view(),
|
ClubOldMembersView.as_view(),
|
||||||
name="club_old_members",
|
name="club_old_members",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^(?P<club_id>[0-9]+)/sellings$",
|
r"^(?P<club_id>[0-9]+)/sellings$",
|
||||||
ClubSellingView.as_view(),
|
ClubSellingView.as_view(),
|
||||||
name="club_sellings",
|
name="club_sellings",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^(?P<club_id>[0-9]+)/sellings/csv$",
|
r"^(?P<club_id>[0-9]+)/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"),
|
re_path(
|
||||||
url(r"^(?P<club_id>[0-9]+)/tools$", ClubToolsView.as_view(), name="tools"),
|
r"^(?P<club_id>[0-9]+)/prop$", ClubEditPropView.as_view(), name="club_prop"
|
||||||
url(r"^(?P<club_id>[0-9]+)/mailing$", ClubMailingView.as_view(), name="mailing"),
|
),
|
||||||
url(
|
re_path(r"^(?P<club_id>[0-9]+)/tools$", ClubToolsView.as_view(), name="tools"),
|
||||||
|
re_path(
|
||||||
|
r"^(?P<club_id>[0-9]+)/mailing$", ClubMailingView.as_view(), name="mailing"
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
r"^(?P<mailing_id>[0-9]+)/mailing/generate$",
|
r"^(?P<mailing_id>[0-9]+)/mailing/generate$",
|
||||||
MailingAutoGenerationView.as_view(),
|
MailingAutoGenerationView.as_view(),
|
||||||
name="mailing_generate",
|
name="mailing_generate",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^(?P<mailing_id>[0-9]+)/mailing/delete$",
|
r"^(?P<mailing_id>[0-9]+)/mailing/delete$",
|
||||||
MailingDeleteView.as_view(),
|
MailingDeleteView.as_view(),
|
||||||
name="mailing_delete",
|
name="mailing_delete",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^(?P<mailing_subscription_id>[0-9]+)/mailing/delete/subscription$",
|
r"^(?P<mailing_subscription_id>[0-9]+)/mailing/delete/subscription$",
|
||||||
MailingSubscriptionDeleteView.as_view(),
|
MailingSubscriptionDeleteView.as_view(),
|
||||||
name="mailing_subscription_delete",
|
name="mailing_subscription_delete",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^membership/(?P<membership_id>[0-9]+)/set_old$",
|
r"^membership/(?P<membership_id>[0-9]+)/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"),
|
re_path(
|
||||||
url(
|
r"^membership/(?P<membership_id>[0-9]+)/delete$",
|
||||||
|
MembershipDeleteView.as_view(),
|
||||||
|
name="membership_delete",
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r"^(?P<club_id>[0-9]+)/poster$", PosterListView.as_view(), name="poster_list"
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
r"^(?P<club_id>[0-9]+)/poster/create$",
|
r"^(?P<club_id>[0-9]+)/poster/create$",
|
||||||
PosterCreateView.as_view(),
|
PosterCreateView.as_view(),
|
||||||
name="poster_create",
|
name="poster_create",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^(?P<club_id>[0-9]+)/poster/(?P<poster_id>[0-9]+)/edit$",
|
r"^(?P<club_id>[0-9]+)/poster/(?P<poster_id>[0-9]+)/edit$",
|
||||||
PosterEditView.as_view(),
|
PosterEditView.as_view(),
|
||||||
name="poster_edit",
|
name="poster_edit",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^(?P<club_id>[0-9]+)/poster/(?P<poster_id>[0-9]+)/delete$",
|
r"^(?P<club_id>[0-9]+)/poster/(?P<poster_id>[0-9]+)/delete$",
|
||||||
PosterDeleteView.as_view(),
|
PosterDeleteView.as_view(),
|
||||||
name="poster_delete",
|
name="poster_delete",
|
||||||
|
189
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):
|
||||||
@ -318,7 +328,7 @@ class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView):
|
|||||||
current_tab = "elderlies"
|
current_tab = "elderlies"
|
||||||
|
|
||||||
|
|
||||||
class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailView):
|
class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
||||||
"""
|
"""
|
||||||
Sellings of a club
|
Sellings of a club
|
||||||
"""
|
"""
|
||||||
@ -327,21 +337,35 @@ class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailView):
|
|||||||
pk_url_kwarg = "club_id"
|
pk_url_kwarg = "club_id"
|
||||||
template_name = "club/club_sellings.jinja"
|
template_name = "club/club_sellings.jinja"
|
||||||
current_tab = "sellings"
|
current_tab = "sellings"
|
||||||
|
form_class = SellingsForm
|
||||||
|
paginate_by = 70
|
||||||
|
|
||||||
def get_form_class(self):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
kwargs = {
|
try:
|
||||||
"product": forms.ModelChoiceField(
|
self.asked_page = int(request.GET.get("page", 1))
|
||||||
self.object.products.order_by("name").all(),
|
except ValueError:
|
||||||
label=_("Product"),
|
raise Http404
|
||||||
required=False,
|
return super(ClubSellingView, self).dispatch(request, *args, **kwargs)
|
||||||
)
|
|
||||||
}
|
def get_form_kwargs(self):
|
||||||
return type("SellingsForm", (SellingsFormBase,), kwargs)
|
kwargs = super(ClubSellingView, self).get_form_kwargs()
|
||||||
|
kwargs["club"] = self.object
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
return self.get(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs = super(ClubSellingView, self).get_context_data(**kwargs)
|
kwargs = super(ClubSellingView, self).get_context_data(**kwargs)
|
||||||
form = self.get_form_class()(self.request.GET)
|
|
||||||
qs = Selling.objects.filter(club=self.object)
|
qs = Selling.objects.filter(club=self.object)
|
||||||
|
|
||||||
|
kwargs["result"] = qs[:0]
|
||||||
|
kwargs["paginated_result"] = kwargs["result"]
|
||||||
|
kwargs["total"] = 0
|
||||||
|
kwargs["total_quantity"] = 0
|
||||||
|
kwargs["benefit"] = 0
|
||||||
|
|
||||||
|
form = self.get_form()
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
if not len([v for v in form.cleaned_data.values() if v is not None]):
|
if not len([v for v in form.cleaned_data.values() if v is not None]):
|
||||||
qs = Selling.objects.filter(id=-1)
|
qs = Selling.objects.filter(id=-1)
|
||||||
@ -349,19 +373,36 @@ class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailView):
|
|||||||
qs = qs.filter(date__gte=form.cleaned_data["begin_date"])
|
qs = qs.filter(date__gte=form.cleaned_data["begin_date"])
|
||||||
if form.cleaned_data["end_date"]:
|
if form.cleaned_data["end_date"]:
|
||||||
qs = qs.filter(date__lte=form.cleaned_data["end_date"])
|
qs = qs.filter(date__lte=form.cleaned_data["end_date"])
|
||||||
if form.cleaned_data["counter"]:
|
|
||||||
qs = qs.filter(counter=form.cleaned_data["counter"])
|
if form.cleaned_data["counters"]:
|
||||||
if form.cleaned_data["product"]:
|
qs = qs.filter(counter__in=form.cleaned_data["counters"])
|
||||||
qs = qs.filter(product__id=form.cleaned_data["product"].id)
|
|
||||||
|
selected_products = []
|
||||||
|
if form.cleaned_data["products"]:
|
||||||
|
selected_products.extend(form.cleaned_data["products"])
|
||||||
|
if form.cleaned_data["archived_products"]:
|
||||||
|
selected_products.extend(form.cleaned_data["archived_products"])
|
||||||
|
|
||||||
|
if len(selected_products) > 0:
|
||||||
|
qs = qs.filter(product__in=selected_products)
|
||||||
|
|
||||||
kwargs["result"] = qs.all().order_by("-id")
|
kwargs["result"] = qs.all().order_by("-id")
|
||||||
kwargs["total"] = sum([s.quantity * s.unit_price for s in qs.all()])
|
kwargs["total"] = sum([s.quantity * s.unit_price for s in kwargs["result"]])
|
||||||
kwargs["total_quantity"] = sum([s.quantity for s in qs.all()])
|
total_quantity = qs.all().aggregate(Sum("quantity"))
|
||||||
kwargs["benefit"] = kwargs["total"] - sum(
|
if total_quantity["quantity__sum"]:
|
||||||
[s.product.purchase_price for s in qs.exclude(product=None)]
|
kwargs["total_quantity"] = total_quantity["quantity__sum"]
|
||||||
|
benefit = (
|
||||||
|
qs.exclude(product=None).all().aggregate(Sum("product__purchase_price"))
|
||||||
)
|
)
|
||||||
else:
|
if benefit["product__purchase_price__sum"]:
|
||||||
kwargs["result"] = qs[:0]
|
kwargs["benefit"] = benefit["product__purchase_price__sum"]
|
||||||
kwargs["form"] = form
|
|
||||||
|
kwargs["paginator"] = Paginator(kwargs["result"], self.paginate_by)
|
||||||
|
try:
|
||||||
|
kwargs["paginated_result"] = kwargs["paginator"].page(self.asked_page)
|
||||||
|
except InvalidPage:
|
||||||
|
raise Http404
|
||||||
|
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
@ -370,16 +411,46 @@ class ClubSellingCSVView(ClubSellingView):
|
|||||||
Generate sellings in csv for a given period
|
Generate sellings in csv for a given period
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
class StreamWriter:
|
||||||
import csv
|
"""Implements a file-like interface for streaming the CSV"""
|
||||||
|
|
||||||
|
def write(self, value):
|
||||||
|
"""Write the value by returning it, instead of storing in a buffer."""
|
||||||
|
return value
|
||||||
|
|
||||||
|
def write_selling(self, selling):
|
||||||
|
row = [selling.date, selling.counter]
|
||||||
|
if selling.seller:
|
||||||
|
row.append(selling.seller.get_display_name())
|
||||||
|
else:
|
||||||
|
row.append("")
|
||||||
|
if selling.customer:
|
||||||
|
row.append(selling.customer.user.get_display_name())
|
||||||
|
else:
|
||||||
|
row.append("")
|
||||||
|
row = row + [
|
||||||
|
selling.label,
|
||||||
|
selling.quantity,
|
||||||
|
selling.quantity * selling.unit_price,
|
||||||
|
selling.get_payment_method_display(),
|
||||||
|
]
|
||||||
|
if selling.product:
|
||||||
|
row.append(selling.product.selling_price)
|
||||||
|
row.append(selling.product.purchase_price)
|
||||||
|
row.append(selling.product.selling_price - selling.product.purchase_price)
|
||||||
|
else:
|
||||||
|
row = row + ["", "", ""]
|
||||||
|
return row
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
|
||||||
response = HttpResponse(content_type="text/csv")
|
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
name = _("Sellings") + "_" + self.object.name + ".csv"
|
|
||||||
response["Content-Disposition"] = "filename=" + name
|
|
||||||
kwargs = self.get_context_data(**kwargs)
|
kwargs = self.get_context_data(**kwargs)
|
||||||
|
|
||||||
|
# Use the StreamWriter class instead of request for streaming
|
||||||
|
pseudo_buffer = self.StreamWriter()
|
||||||
writer = csv.writer(
|
writer = csv.writer(
|
||||||
response, delimiter=";", lineterminator="\n", quoting=csv.QUOTE_ALL
|
pseudo_buffer, delimiter=";", lineterminator="\n", quoting=csv.QUOTE_ALL
|
||||||
)
|
)
|
||||||
|
|
||||||
writer.writerow([_t("Quantity"), kwargs["total_quantity"]])
|
writer.writerow([_t("Quantity"), kwargs["total_quantity"]])
|
||||||
@ -400,29 +471,17 @@ class ClubSellingCSVView(ClubSellingView):
|
|||||||
_t("Benefit"),
|
_t("Benefit"),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
for o in kwargs["result"]:
|
|
||||||
row = [o.date, o.counter]
|
# Stream response
|
||||||
if o.seller:
|
response = StreamingHttpResponse(
|
||||||
row.append(o.seller.get_display_name())
|
(
|
||||||
else:
|
writer.writerow(self.write_selling(selling))
|
||||||
row.append("")
|
for selling in kwargs["result"]
|
||||||
if o.customer:
|
),
|
||||||
row.append(o.customer.user.get_display_name())
|
content_type="text/csv",
|
||||||
else:
|
)
|
||||||
row.append("")
|
name = _("Sellings") + "_" + self.object.name + ".csv"
|
||||||
row = row + [
|
response["Content-Disposition"] = "filename=" + name
|
||||||
o.label,
|
|
||||||
o.quantity,
|
|
||||||
o.quantity * o.unit_price,
|
|
||||||
o.get_payment_method_display(),
|
|
||||||
]
|
|
||||||
if o.product:
|
|
||||||
row.append(o.product.selling_price)
|
|
||||||
row.append(o.product.purchase_price)
|
|
||||||
row.append(o.product.selling_price - o.product.purchase_price)
|
|
||||||
else:
|
|
||||||
row = row + ["", "", ""]
|
|
||||||
writer.writerow(row)
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@ -451,7 +510,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 +552,19 @@ class MembershipSetOldView(CanEditMixin, DetailView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MembershipDeleteView(UserIsRootMixin, DeleteView):
|
||||||
|
"""
|
||||||
|
Delete a membership (for admins only)
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = Membership
|
||||||
|
pk_url_kwarg = "membership_id"
|
||||||
|
template_name = "core/delete_confirm.jinja"
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse_lazy("core:user_clubs", kwargs={"user_id": self.object.user.id})
|
||||||
|
|
||||||
|
|
||||||
class ClubStatView(TemplateView):
|
class ClubStatView(TemplateView):
|
||||||
template_name = "club/stats.jinja"
|
template_name = "club/stats.jinja"
|
||||||
|
|
||||||
@ -574,7 +646,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(
|
||||||
|
@ -3,6 +3,7 @@ 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):
|
||||||
@ -50,6 +51,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 +60,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 +105,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,6 +3,7 @@ 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):
|
||||||
@ -56,6 +57,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 +66,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 +75,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,6 +4,7 @@ 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):
|
||||||
@ -48,12 +49,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,
|
||||||
|
12
com/migrations/0006_remove_sith_index_page.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# -*- 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")]
|
105
com/models.py
@ -26,11 +26,11 @@
|
|||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.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
|
||||||
|
|
||||||
@ -45,7 +45,6 @@ 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="")
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
@ -62,6 +61,11 @@ NEWS_TYPES = [
|
|||||||
("CALL", _("Call")),
|
("CALL", _("Call")),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
WEEKMAIL_TYPE = [
|
||||||
|
("WEEKMAIL", _("Weekmail")),
|
||||||
|
("INVITATION", _("Invitation")),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class News(models.Model):
|
class News(models.Model):
|
||||||
"""The news class"""
|
"""The news class"""
|
||||||
@ -72,13 +76,22 @@ 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):
|
||||||
@ -139,7 +152,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 +168,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)
|
||||||
@ -158,11 +183,18 @@ class Weekmail(models.Model):
|
|||||||
protip = models.TextField(_("protip"), blank=True)
|
protip = models.TextField(_("protip"), blank=True)
|
||||||
conclusion = models.TextField(_("conclusion"), blank=True)
|
conclusion = models.TextField(_("conclusion"), blank=True)
|
||||||
sent = models.BooleanField(_("sent"), default=False)
|
sent = models.BooleanField(_("sent"), default=False)
|
||||||
|
type = models.CharField(
|
||||||
|
_("type"), max_length=16, choices=WEEKMAIL_TYPE, default="WEEKMAIL"
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["-id"]
|
ordering = ["-id"]
|
||||||
|
|
||||||
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,20 +216,50 @@ 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 switch_type(self):
|
||||||
|
"""
|
||||||
|
Switch the type of weekmail we are sending :
|
||||||
|
- a simple weekmail
|
||||||
|
- or an invitation
|
||||||
|
"""
|
||||||
|
if self.type == "INVITATION":
|
||||||
|
self.type = "WEEKMAIL"
|
||||||
|
else:
|
||||||
|
self.type = "INVITATION"
|
||||||
|
|
||||||
def render_html(self):
|
def render_html(self):
|
||||||
|
"""
|
||||||
|
Renders an HTML version of the mail with images and fancy CSS.
|
||||||
|
"""
|
||||||
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.
|
||||||
|
"""
|
||||||
|
if self.type == "INVITATION":
|
||||||
|
return (
|
||||||
|
"http://" + settings.SITH_URL + static("com/img/invitation_bannerP22.png")
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
"http://" + settings.SITH_URL + static("com/img/weekmail_bannerV2P22.png")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_footer(self):
|
def get_footer(self):
|
||||||
return "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)
|
||||||
@ -208,15 +270,25 @@ class Weekmail(models.Model):
|
|||||||
|
|
||||||
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)
|
||||||
|
|
||||||
@ -249,7 +321,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 +340,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):
|
||||||
|
@ -6,58 +6,13 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="news">
|
{% if user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %}
|
||||||
{% if user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %}
|
<div id="news_admin">
|
||||||
<div id="news_admin">
|
|
||||||
<a href="{{ url('com:news_admin_list') }}">{% trans %}Administrate news{% endtrans %}</a>
|
<a href="{{ url('com:news_admin_list') }}">{% trans %}Administrate news{% endtrans %}</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div id="right_column" class="news_column">
|
|
||||||
<div id="agenda">
|
|
||||||
<div id="agenda_title">{% trans %}Agenda{% endtrans %}</div>
|
|
||||||
<div id="agenda_content">
|
|
||||||
{% for d in NewsDate.objects.filter(end_date__gte=timezone.now(),
|
|
||||||
news__is_moderated=True, news__type__in=["WEEKLY",
|
|
||||||
"EVENT"]).order_by('start_date', 'end_date') %}
|
|
||||||
<div class="agenda_item">
|
|
||||||
<div class="agenda_date">
|
|
||||||
<strong>{{ d.start_date|localtime|date('D d M Y') }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="agenda_time">
|
|
||||||
<span>{{ d.start_date|localtime|time(DATETIME_FORMAT) }}</span> -
|
|
||||||
<span>{{ d.end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong><a href="{{ url('com:news_detail', news_id=d.news.id) }}">{{ d.news.title }}</a></strong>
|
|
||||||
<a href="{{ d.news.club.get_absolute_url() }}">{{ d.news.club }}</a>
|
|
||||||
</div>
|
|
||||||
<div class="agenda_item_content">{{ d.news.summary|markdown }}</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="birthdays">
|
|
||||||
<div id="birthdays_title">{% trans %}Birthdays{% endtrans %}</div>
|
|
||||||
<div id="birthdays_content">
|
|
||||||
<ul class="birthdays_year">
|
|
||||||
{% for d in birthdays.dates('date_of_birth', 'year', 'DESC') %}
|
|
||||||
<li>
|
|
||||||
{% trans age=timezone.now().year - d.year %}{{ age }} year old{% endtrans %}
|
|
||||||
<ul>
|
|
||||||
{% for u in birthdays.filter(date_of_birth__year=d.year) %}
|
|
||||||
<li><a href="{{ u.get_absolute_url() }}">{{ u.get_short_name() }}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
<div id="news">
|
||||||
<div id="left_column" class="news_column">
|
<div id="left_column" class="news_column">
|
||||||
|
|
||||||
{% for news in object_list.filter(type="NOTICE") %}
|
{% for news in object_list.filter(type="NOTICE") %}
|
||||||
@ -67,8 +22,7 @@
|
|||||||
</section>
|
</section>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% for news in object_list.filter(dates__start_date__lte=timezone.now(),
|
{% for news in object_list.filter(dates__start_date__lte=timezone.now(), dates__end_date__gte=timezone.now(), type="CALL") %}
|
||||||
dates__end_date__gte=timezone.now(), type="CALL") %}
|
|
||||||
<section class="news_call">
|
<section class="news_call">
|
||||||
<h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
|
<h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
|
||||||
<div class="news_date">
|
<div class="news_date">
|
||||||
@ -81,8 +35,7 @@
|
|||||||
</section>
|
</section>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% set events_dates = NewsDate.objects.filter(end_date__gte=timezone.now(), start_date__lte=timezone.now()+timedelta(days=5),
|
{% set events_dates = NewsDate.objects.filter(end_date__gte=timezone.now(), start_date__lte=timezone.now()+timedelta(days=5), news__type="EVENT", news__is_moderated=True).datetimes('start_date', 'day') %}
|
||||||
news__type="EVENT", news__is_moderated=True).datetimes('start_date', 'day') %}
|
|
||||||
<h3>{% trans %}Events today and the next few days{% endtrans %}</h3>
|
<h3>{% trans %}Events today and the next few days{% endtrans %}</h3>
|
||||||
{% if events_dates %}
|
{% if events_dates %}
|
||||||
{% for d in events_dates %}
|
{% for d in events_dates %}
|
||||||
@ -145,6 +98,58 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="right_column" class="news_column">
|
||||||
|
<div id="agenda">
|
||||||
|
<div id="agenda_title">{% trans %}Agenda{% endtrans %}</div>
|
||||||
|
<div id="agenda_content">
|
||||||
|
{% for d in NewsDate.objects.filter(end_date__gte=timezone.now(),
|
||||||
|
news__is_moderated=True, news__type__in=["WEEKLY",
|
||||||
|
"EVENT"]).order_by('start_date', 'end_date') %}
|
||||||
|
<div class="agenda_item">
|
||||||
|
<div class="agenda_date">
|
||||||
|
<strong>{{ d.start_date|localtime|date('D d M Y') }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="agenda_time">
|
||||||
|
<span>{{ d.start_date|localtime|time(DATETIME_FORMAT) }}</span> -
|
||||||
|
<span>{{ d.end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong><a href="{{ url('com:news_detail', news_id=d.news.id) }}">{{ d.news.title }}</a></strong>
|
||||||
|
<a href="{{ d.news.club.get_absolute_url() }}">{{ d.news.club }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="agenda_item_content">{{ d.news.summary|markdown }}</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="birthdays">
|
||||||
|
<div id="birthdays_title">{% trans %}Birthdays{% endtrans %}</div>
|
||||||
|
<div id="birthdays_content">
|
||||||
|
{% if user.is_subscribed %}
|
||||||
|
{# Cache request for 1 hour #}
|
||||||
|
{% cache 3600 "birthdays" %}
|
||||||
|
<ul class="birthdays_year">
|
||||||
|
{% for d in birthdays.dates('date_of_birth', 'year', 'DESC') %}
|
||||||
|
<li>
|
||||||
|
{% trans age=timezone.now().year - d.year %}{{ age }} year old{% endtrans %}
|
||||||
|
<ul>
|
||||||
|
{% for u in birthdays.filter(date_of_birth__year=d.year) %}
|
||||||
|
<li><a href="{{ u.get_absolute_url() }}">{{ u.get_short_name() }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endcache %}
|
||||||
|
{% else %}
|
||||||
|
<p>{% trans %}You need an up to date subscription to access this content{% endtrans %}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -36,8 +36,8 @@
|
|||||||
<div class="name">{{ poster.name }}</div>
|
<div class="name">{{ poster.name }}</div>
|
||||||
<div class="image"><img src="{{ poster.file.url }}"></img></div>
|
<div class="image"><img src="{{ poster.file.url }}"></img></div>
|
||||||
<div class="dates">
|
<div class="dates">
|
||||||
<div class="begin">{{ poster.date_begin | date("d/M/Y H:m") }}</div>
|
<div class="begin">{{ poster.date_begin | localtime | date("d/M/Y H:m") }}</div>
|
||||||
<div class="end">{{ poster.date_end | date("d/M/Y H:m") }}</div>
|
<div class="end">{{ poster.date_end | localtime | date("d/M/Y H:m") }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% if app == "com" %}
|
{% if app == "com" %}
|
||||||
<a class="edit" href="{{ url(app + ":poster_edit", poster.id) }}">{% trans %}Edit{% endtrans %}</a>
|
<a class="edit" href="{{ url(app + ":poster_edit", poster.id) }}">{% trans %}Edit{% endtrans %}</a>
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
<p><a href="{{ url('com:weekmail_preview') }}">{% trans %}Preview{% endtrans %}</a></p>
|
<p><a href="{{ url('com:weekmail_preview') }}">{% trans %}Preview{% endtrans %}</a></p>
|
||||||
<p><a href="{{ url('com:weekmail_preview') }}?send=true">{% trans %}Send{% endtrans %}</a></p>
|
<p><a href="{{ url('com:weekmail_preview') }}?send=true">{% trans %}Send{% endtrans %}</a></p>
|
||||||
<p><a href="{{ url('com:weekmail_article') }}">{% trans %}New article{% endtrans %}</a></p>
|
<p><a href="{{ url('com:weekmail_article') }}">{% trans %}New article{% endtrans %}</a></p>
|
||||||
|
<p><a href="{{ url('com:weekmail') }}" onclick="{{weekmail.switch_type()}}">{% trans %}Switch invitation/weekmail{% endtrans %}</a></p>
|
||||||
<h4>{% trans %}Articles in no weekmail yet{% endtrans %}</h4>
|
<h4>{% trans %}Articles in no weekmail yet{% endtrans %}</h4>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -7,15 +7,33 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<a href="{{ url('com:weekmail') }}">{% trans %}Back{% endtrans %}</a>
|
<a href="{{ url('com:weekmail') }}">{% trans %}Back{% endtrans %}</a>
|
||||||
{% if request.GET['send'] %}
|
{% if bad_recipients %}
|
||||||
<p>{% trans %}Are you sure you want to send this weekmail?{% endtrans %}</p>
|
<p>
|
||||||
{% if request.LANGUAGE_CODE != settings.LANGUAGE_CODE[:2] %}
|
<span class="important">
|
||||||
<p><strong>{% trans %}Warning: you are sending the weekmail in another language than the default one!{% endtrans %}</strong></p>
|
{% trans %}The following recipients were refused by the SMTP:{% endtrans %}
|
||||||
{% endif %}
|
</span>
|
||||||
<form method="post" action="">
|
<ul>
|
||||||
|
{% for r in bad_recipients.keys() %}
|
||||||
|
<li>{{ r }}</li>
|
||||||
|
{% 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 %}
|
{% csrf_token %}
|
||||||
<button type="submit" name="send" value="validate">{% trans %}Send{% endtrans %}</button>
|
<button type="submit" name="send" value="validate">{% trans %}Send{% endtrans %}</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<hr>
|
<hr>
|
||||||
{{ weekmail_rendered|safe }}
|
{{ weekmail_rendered|safe }}
|
||||||
|
64
com/tests.py
@ -24,12 +24,37 @@
|
|||||||
|
|
||||||
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.translation import gettext as _
|
||||||
|
|
||||||
|
|
||||||
from core.models import User, RealGroup
|
from core.models import User, RealGroup
|
||||||
|
|
||||||
|
|
||||||
|
class ComAlertTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
call_command("populate")
|
||||||
|
|
||||||
|
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 setUp(self):
|
||||||
|
call_command("populate")
|
||||||
|
|
||||||
|
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):
|
def setUp(self):
|
||||||
call_command("populate")
|
call_command("populate")
|
||||||
@ -37,7 +62,7 @@ class ComTest(TestCase):
|
|||||||
self.com_group = RealGroup.objects.filter(
|
self.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]
|
self.skia.groups.set([self.com_group])
|
||||||
self.skia.save()
|
self.skia.save()
|
||||||
self.client.login(username=self.skia.username, password="plop")
|
self.client.login(username=self.skia.username, password="plop")
|
||||||
|
|
||||||
@ -54,9 +79,11 @@ class ComTest(TestCase):
|
|||||||
)
|
)
|
||||||
r = self.client.get(reverse("core:index"))
|
r = self.client.get(reverse("core:index"))
|
||||||
self.assertTrue(r.status_code == 200)
|
self.assertTrue(r.status_code == 200)
|
||||||
self.assertTrue(
|
self.assertContains(
|
||||||
"""<div id="alert_box">\\n <div class="markdown"><h3>ALERTE!</h3>\\n<p><strong>Caaaataaaapuuuulte!!!!</strong></p>"""
|
r,
|
||||||
in str(r.content)
|
"""<div id="alert_box">
|
||||||
|
<div class="markdown"><h3>ALERTE!</h3>
|
||||||
|
<p><strong>Caaaataaaapuuuulte!!!!</strong></p>""",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_info_msg(self):
|
def test_info_msg(self):
|
||||||
@ -70,7 +97,28 @@ 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")
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
67
com/urls.py
@ -22,98 +22,103 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.urls import re_path
|
||||||
|
|
||||||
from com.views import *
|
from com.views import *
|
||||||
from club.views import MailingDeleteView
|
from club.views import MailingDeleteView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r"^sith/edit/alert$", AlertMsgEditView.as_view(), name="alert_edit"),
|
re_path(r"^sith/edit/alert$", AlertMsgEditView.as_view(), name="alert_edit"),
|
||||||
url(r"^sith/edit/info$", InfoMsgEditView.as_view(), name="info_edit"),
|
re_path(r"^sith/edit/info$", InfoMsgEditView.as_view(), name="info_edit"),
|
||||||
url(r"^sith/edit/index$", IndexEditView.as_view(), name="index_edit"),
|
re_path(
|
||||||
url(
|
|
||||||
r"^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"),
|
re_path(r"^weekmail$", WeekmailEditView.as_view(), name="weekmail"),
|
||||||
url(r"^weekmail/preview$", WeekmailPreviewView.as_view(), name="weekmail_preview"),
|
re_path(
|
||||||
url(
|
r"^weekmail/preview$", WeekmailPreviewView.as_view(), name="weekmail_preview"
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
r"^weekmail/new_article$",
|
r"^weekmail/new_article$",
|
||||||
WeekmailArticleCreateView.as_view(),
|
WeekmailArticleCreateView.as_view(),
|
||||||
name="weekmail_article",
|
name="weekmail_article",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^weekmail/article/(?P<article_id>[0-9]+)/delete$",
|
r"^weekmail/article/(?P<article_id>[0-9]+)/delete$",
|
||||||
WeekmailArticleDeleteView.as_view(),
|
WeekmailArticleDeleteView.as_view(),
|
||||||
name="weekmail_article_delete",
|
name="weekmail_article_delete",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^weekmail/article/(?P<article_id>[0-9]+)/edit$",
|
r"^weekmail/article/(?P<article_id>[0-9]+)/edit$",
|
||||||
WeekmailArticleEditView.as_view(),
|
WeekmailArticleEditView.as_view(),
|
||||||
name="weekmail_article_edit",
|
name="weekmail_article_edit",
|
||||||
),
|
),
|
||||||
url(r"^news$", NewsListView.as_view(), name="news_list"),
|
re_path(r"^news$", NewsListView.as_view(), name="news_list"),
|
||||||
url(r"^news/admin$", NewsAdminListView.as_view(), name="news_admin_list"),
|
re_path(r"^news/admin$", NewsAdminListView.as_view(), name="news_admin_list"),
|
||||||
url(r"^news/create$", NewsCreateView.as_view(), name="news_new"),
|
re_path(r"^news/create$", NewsCreateView.as_view(), name="news_new"),
|
||||||
url(
|
re_path(
|
||||||
r"^news/(?P<news_id>[0-9]+)/delete$",
|
r"^news/(?P<news_id>[0-9]+)/delete$",
|
||||||
NewsDeleteView.as_view(),
|
NewsDeleteView.as_view(),
|
||||||
name="news_delete",
|
name="news_delete",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^news/(?P<news_id>[0-9]+)/moderate$",
|
r"^news/(?P<news_id>[0-9]+)/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"),
|
re_path(
|
||||||
url(r"^news/(?P<news_id>[0-9]+)$", NewsDetailView.as_view(), name="news_detail"),
|
r"^news/(?P<news_id>[0-9]+)/edit$", NewsEditView.as_view(), name="news_edit"
|
||||||
url(r"^mailings$", MailingListAdminView.as_view(), name="mailing_admin"),
|
),
|
||||||
url(
|
re_path(
|
||||||
|
r"^news/(?P<news_id>[0-9]+)$", NewsDetailView.as_view(), name="news_detail"
|
||||||
|
),
|
||||||
|
re_path(r"^mailings$", MailingListAdminView.as_view(), name="mailing_admin"),
|
||||||
|
re_path(
|
||||||
r"^mailings/(?P<mailing_id>[0-9]+)/moderate$",
|
r"^mailings/(?P<mailing_id>[0-9]+)/moderate$",
|
||||||
MailingModerateView.as_view(),
|
MailingModerateView.as_view(),
|
||||||
name="mailing_moderate",
|
name="mailing_moderate",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^mailings/(?P<mailing_id>[0-9]+)/delete$",
|
r"^mailings/(?P<mailing_id>[0-9]+)/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"),
|
re_path(r"^poster$", PosterListView.as_view(), name="poster_list"),
|
||||||
url(r"^poster/create$", PosterCreateView.as_view(), name="poster_create"),
|
re_path(r"^poster/create$", PosterCreateView.as_view(), name="poster_create"),
|
||||||
url(
|
re_path(
|
||||||
r"^poster/(?P<poster_id>[0-9]+)/edit$",
|
r"^poster/(?P<poster_id>[0-9]+)/edit$",
|
||||||
PosterEditView.as_view(),
|
PosterEditView.as_view(),
|
||||||
name="poster_edit",
|
name="poster_edit",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^poster/(?P<poster_id>[0-9]+)/delete$",
|
r"^poster/(?P<poster_id>[0-9]+)/delete$",
|
||||||
PosterDeleteView.as_view(),
|
PosterDeleteView.as_view(),
|
||||||
name="poster_delete",
|
name="poster_delete",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^poster/moderate$",
|
r"^poster/moderate$",
|
||||||
PosterModerateListView.as_view(),
|
PosterModerateListView.as_view(),
|
||||||
name="poster_moderate_list",
|
name="poster_moderate_list",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^poster/(?P<object_id>[0-9]+)/moderate$",
|
r"^poster/(?P<object_id>[0-9]+)/moderate$",
|
||||||
PosterModerateView.as_view(),
|
PosterModerateView.as_view(),
|
||||||
name="poster_moderate",
|
name="poster_moderate",
|
||||||
),
|
),
|
||||||
url(r"^screen$", ScreenListView.as_view(), name="screen_list"),
|
re_path(r"^screen$", ScreenListView.as_view(), name="screen_list"),
|
||||||
url(r"^screen/create$", ScreenCreateView.as_view(), name="screen_create"),
|
re_path(r"^screen/create$", ScreenCreateView.as_view(), name="screen_create"),
|
||||||
url(
|
re_path(
|
||||||
r"^screen/(?P<screen_id>[0-9]+)/slideshow$",
|
r"^screen/(?P<screen_id>[0-9]+)/slideshow$",
|
||||||
ScreenSlideshowView.as_view(),
|
ScreenSlideshowView.as_view(),
|
||||||
name="screen_slideshow",
|
name="screen_slideshow",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^screen/(?P<screen_id>[0-9]+)/edit$",
|
r"^screen/(?P<screen_id>[0-9]+)/edit$",
|
||||||
ScreenEditView.as_view(),
|
ScreenEditView.as_view(),
|
||||||
name="screen_edit",
|
name="screen_edit",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^screen/(?P<screen_id>[0-9]+)/delete$",
|
r"^screen/(?P<screen_id>[0-9]+)/delete$",
|
||||||
ScreenDeleteView.as_view(),
|
ScreenDeleteView.as_view(),
|
||||||
name="screen_delete",
|
name="screen_delete",
|
||||||
|
82
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")}
|
||||||
)
|
)
|
||||||
@ -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(
|
||||||
@ -437,23 +415,36 @@ class NewsDetailView(CanViewMixin, DetailView):
|
|||||||
# Weekmail
|
# Weekmail
|
||||||
|
|
||||||
|
|
||||||
class WeekmailPreviewView(ComTabsMixin, CanEditPropMixin, DetailView):
|
class WeekmailPreviewView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, DetailView):
|
||||||
model = Weekmail
|
model = Weekmail
|
||||||
template_name = "com/weekmail_preview.jinja"
|
template_name = "com/weekmail_preview.jinja"
|
||||||
success_url = reverse_lazy("com:weekmail")
|
success_url = reverse_lazy("com:weekmail")
|
||||||
current_tab = "weekmail"
|
current_tab = "weekmail"
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
self.bad_recipients = []
|
||||||
|
return super(WeekmailPreviewView, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
try:
|
|
||||||
if request.POST["send"] == "validate":
|
if request.POST["send"] == "validate":
|
||||||
|
try:
|
||||||
self.object.send()
|
self.object.send()
|
||||||
return HttpResponseRedirect(
|
return HttpResponseRedirect(
|
||||||
reverse("com:weekmail") + "?qn_weekmail_send_success"
|
reverse("com:weekmail") + "?qn_weekmail_send_success"
|
||||||
)
|
)
|
||||||
except:
|
except SMTPRecipientsRefused as e:
|
||||||
pass
|
self.bad_recipients = e.recipients
|
||||||
return super(WeekmailEditView, self).get(request, *args, **kwargs)
|
elif request.POST["send"] == "clean":
|
||||||
|
try:
|
||||||
|
self.object.send() # This should fail
|
||||||
|
except SMTPRecipientsRefused as e:
|
||||||
|
users = User.objects.filter(email__in=e.recipients.keys())
|
||||||
|
for u in users:
|
||||||
|
u.preferences.receive_weekmail = False
|
||||||
|
u.preferences.save()
|
||||||
|
self.quick_notif_list += ["qn_success"]
|
||||||
|
return super(WeekmailPreviewView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
def get_object(self, queryset=None):
|
||||||
return self.model.objects.filter(sent=False).order_by("-id").first()
|
return self.model.objects.filter(sent=False).order_by("-id").first()
|
||||||
@ -462,6 +453,7 @@ class WeekmailPreviewView(ComTabsMixin, CanEditPropMixin, DetailView):
|
|||||||
"""Add rendered weekmail"""
|
"""Add rendered weekmail"""
|
||||||
kwargs = super(WeekmailPreviewView, self).get_context_data(**kwargs)
|
kwargs = super(WeekmailPreviewView, self).get_context_data(**kwargs)
|
||||||
kwargs["weekmail_rendered"] = self.object.render_html()
|
kwargs["weekmail_rendered"] = self.object.render_html()
|
||||||
|
kwargs["bad_recipients"] = self.bad_recipients
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
@ -538,7 +530,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
|
|||||||
return super(WeekmailEditView, self).get(request, *args, **kwargs)
|
return super(WeekmailEditView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add orphan articles """
|
"""Add orphan articles"""
|
||||||
kwargs = super(WeekmailEditView, self).get_context_data(**kwargs)
|
kwargs = super(WeekmailEditView, self).get_context_data(**kwargs)
|
||||||
kwargs["orphans"] = WeekmailArticle.objects.filter(weekmail=None)
|
kwargs["orphans"] = WeekmailArticle.objects.filter(weekmail=None)
|
||||||
return kwargs
|
return kwargs
|
||||||
@ -750,7 +742,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
|
||||||
|
|
||||||
|
@ -21,5 +21,3 @@
|
|||||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
default_app_config = "core.apps.SithConfig"
|
|
||||||
|
107
core/management/commands/check_front.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import re
|
||||||
|
from subprocess import PIPE, Popen, TimeoutExpired
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
# see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
|
||||||
|
# added "v?"
|
||||||
|
semver_regex = re.compile(
|
||||||
|
"""^v?(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Checks the front dependencies are up to date."
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
deps = settings.SITH_FRONT_DEP_VERSIONS
|
||||||
|
|
||||||
|
processes = dict(
|
||||||
|
(url, create_process(url))
|
||||||
|
for url in deps.keys()
|
||||||
|
if parse_semver(deps[url]) is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
for url, process in processes.items():
|
||||||
|
try:
|
||||||
|
stdout, stderr = process.communicate(timeout=15)
|
||||||
|
except TimeoutExpired:
|
||||||
|
process.kill()
|
||||||
|
self.stderr.write(self.style.WARNING("{}: timeout".format(url)))
|
||||||
|
continue
|
||||||
|
# error, notice, warning
|
||||||
|
|
||||||
|
stdout = stdout.decode("utf-8")
|
||||||
|
stderr = stderr.decode("utf-8")
|
||||||
|
|
||||||
|
if stderr != "":
|
||||||
|
self.stderr.write(self.style.WARNING(stderr.strip()))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# get all tags, parse them as semvers and find the biggest
|
||||||
|
tags = list_tags(stdout)
|
||||||
|
tags = map(parse_semver, tags)
|
||||||
|
tags = filter(lambda tag: tag is not None, tags)
|
||||||
|
latest_version = max(tags)
|
||||||
|
|
||||||
|
# cannot fail as those which fail are filtered in the processes dict creation
|
||||||
|
current_version = parse_semver(deps[url])
|
||||||
|
assert current_version is not None
|
||||||
|
|
||||||
|
if latest_version == current_version:
|
||||||
|
msg = "{}: {}".format(url, semver_to_s(current_version))
|
||||||
|
self.stdout.write(self.style.SUCCESS(msg))
|
||||||
|
else:
|
||||||
|
msg = "{}: {} < {}".format(
|
||||||
|
url, semver_to_s(current_version), semver_to_s(latest_version)
|
||||||
|
)
|
||||||
|
self.stdout.write(self.style.ERROR(msg))
|
||||||
|
|
||||||
|
|
||||||
|
def create_process(url):
|
||||||
|
"""Spawn a "git ls-remote --tags" child process."""
|
||||||
|
return Popen(["git", "ls-remote", "--tags", url], stdout=PIPE, stderr=PIPE)
|
||||||
|
|
||||||
|
|
||||||
|
def list_tags(s):
|
||||||
|
"""Parses "git ls-remote --tags" output. Takes a string."""
|
||||||
|
tag_prefix = "refs/tags/"
|
||||||
|
|
||||||
|
for line in s.strip().split("\n"):
|
||||||
|
# an example line could be:
|
||||||
|
# "1f41e2293f9c3c1962d2d97afa666207b98a222a\trefs/tags/foo"
|
||||||
|
parts = line.split("\t")
|
||||||
|
|
||||||
|
# check we have a commit ID (SHA-1 hash) and a tag name
|
||||||
|
assert len(parts) == 2
|
||||||
|
assert len(parts[0]) == 40
|
||||||
|
assert parts[1].startswith(tag_prefix)
|
||||||
|
|
||||||
|
# avoid duplicates (a peeled tag will appear twice: as "name" and as "name^{}")
|
||||||
|
if not parts[1].endswith("^{}"):
|
||||||
|
yield parts[1][len(tag_prefix) :]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_semver(s):
|
||||||
|
"""
|
||||||
|
Turns a semver string into a 3-tuple or None if the parsing failed, it is a
|
||||||
|
prerelease or it has build metadata.
|
||||||
|
|
||||||
|
See https://semver.org
|
||||||
|
"""
|
||||||
|
m = semver_regex.match(s)
|
||||||
|
|
||||||
|
if (
|
||||||
|
m is None
|
||||||
|
or m.group("prerelease") is not None
|
||||||
|
or m.group("buildmetadata") is not None
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return (int(m.group("major")), int(m.group("minor")), int(m.group("patch")))
|
||||||
|
|
||||||
|
|
||||||
|
def semver_to_s(t):
|
||||||
|
"""Expects a 3-tuple with ints and turns it into a string of type "1.2.3"."""
|
||||||
|
return "{}.{}.{}".format(t[0], t[1], t[2])
|
44
core/management/commands/compilemessages.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding:utf-8 -*
|
||||||
|
#
|
||||||
|
# Copyright 2019
|
||||||
|
# - Sli <antoine@bartuccio.fr>
|
||||||
|
#
|
||||||
|
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
||||||
|
# http://ae.utbm.fr.
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License a published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
|
||||||
|
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
from django.core.management.commands import compilemessages
|
||||||
|
|
||||||
|
|
||||||
|
class Command(compilemessages.Command):
|
||||||
|
"""
|
||||||
|
Wrap call to compilemessages to avoid building whole env
|
||||||
|
"""
|
||||||
|
|
||||||
|
help = """
|
||||||
|
The usage is the same as the real compilemessages
|
||||||
|
but it goes into the sith dir first
|
||||||
|
"""
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
|
||||||
|
os.chdir("sith")
|
||||||
|
super(Command, self).handle(*args, **options)
|
92
core/management/commands/documentation.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding:utf-8 -*
|
||||||
|
#
|
||||||
|
# Copyright 2019
|
||||||
|
# - Sli <antoine@bartuccio.fr>
|
||||||
|
#
|
||||||
|
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
||||||
|
# http://ae.utbm.fr.
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License a published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
|
||||||
|
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import signal
|
||||||
|
|
||||||
|
from http.server import test, CGIHTTPRequestHandler
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.utils import autoreload
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Generate Sphinx documentation and launch basic server"
|
||||||
|
|
||||||
|
default_addr = "127.0.0.1"
|
||||||
|
default_port = "8080"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
"addrport", nargs="?", help="Optional port number, or ipaddr:port"
|
||||||
|
)
|
||||||
|
|
||||||
|
def build_documentation(self):
|
||||||
|
os.chdir(os.path.join(self.project_dir, "doc"))
|
||||||
|
err = os.system("make html")
|
||||||
|
|
||||||
|
if err != 0:
|
||||||
|
self.stdout.write("A build error occured")
|
||||||
|
|
||||||
|
def start_server(self, **kwargs):
|
||||||
|
os.chdir(os.path.join(self.project_dir, "doc", "_build/html"))
|
||||||
|
addr = self.default_addr
|
||||||
|
port = self.default_port
|
||||||
|
if kwargs["addrport"]:
|
||||||
|
addrport = kwargs["addrport"].split(":")
|
||||||
|
|
||||||
|
addr = addrport[0]
|
||||||
|
|
||||||
|
if len(addrport) > 1:
|
||||||
|
port = addrport[1]
|
||||||
|
|
||||||
|
if not port.isnumeric():
|
||||||
|
self.stdout.write("%s is not a valid port" % (port,))
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
test(HandlerClass=CGIHTTPRequestHandler, port=int(port), bind=addr)
|
||||||
|
|
||||||
|
def build_and_start_server(self, **kwargs):
|
||||||
|
self.build_documentation()
|
||||||
|
self.start_server(**kwargs)
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
self.project_dir = os.getcwd()
|
||||||
|
|
||||||
|
signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
|
||||||
|
try:
|
||||||
|
if os.environ.get(autoreload.DJANGO_AUTORELOAD_ENV) == "true":
|
||||||
|
reloader = autoreload.get_reloader()
|
||||||
|
reloader.watch_dir(os.path.join(self.project_dir, "doc"), "**/*.rst")
|
||||||
|
autoreload.logger.info(
|
||||||
|
"Watching for file changes with %s", reloader.__class__.__name__
|
||||||
|
)
|
||||||
|
autoreload.start_django(reloader, self.build_and_start_server, **kwargs)
|
||||||
|
else:
|
||||||
|
exit_code = autoreload.restart_with_reloader()
|
||||||
|
sys.exit(exit_code)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
@ -142,18 +142,18 @@ class Command(BaseCommand):
|
|||||||
g.save()
|
g.save()
|
||||||
c = Counter(id=b[0], name=b[1], club=bar_club, type="BAR")
|
c = Counter(id=b[0], name=b[1], club=bar_club, type="BAR")
|
||||||
c.save()
|
c.save()
|
||||||
c.edit_groups = [g]
|
g.editable_counters.add(c)
|
||||||
c.save()
|
g.save()
|
||||||
self.reset_index("counter")
|
self.reset_index("counter")
|
||||||
Counter(name="Eboutic", club=main_club, type="EBOUTIC").save()
|
Counter(name="Eboutic", club=main_club, type="EBOUTIC").save()
|
||||||
Counter(name="AE", club=main_club, type="OFFICE").save()
|
Counter(name="AE", club=main_club, type="OFFICE").save()
|
||||||
|
|
||||||
home_root.view_groups = [
|
home_root.view_groups.set(
|
||||||
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first()
|
[Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first()]
|
||||||
]
|
)
|
||||||
club_root.view_groups = [
|
club_root.view_groups.set(
|
||||||
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first()
|
[Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first()]
|
||||||
]
|
)
|
||||||
home_root.save()
|
home_root.save()
|
||||||
club_root.save()
|
club_root.save()
|
||||||
|
|
||||||
@ -163,7 +163,7 @@ class Command(BaseCommand):
|
|||||||
p = Page(name="Index")
|
p = Page(name="Index")
|
||||||
p.set_lock(root)
|
p.set_lock(root)
|
||||||
p.save()
|
p.save()
|
||||||
p.view_groups = [settings.SITH_GROUP_PUBLIC_ID]
|
p.view_groups.set([settings.SITH_GROUP_PUBLIC_ID])
|
||||||
p.set_lock(root)
|
p.set_lock(root)
|
||||||
p.save()
|
p.save()
|
||||||
PageRev(
|
PageRev(
|
||||||
@ -178,7 +178,7 @@ Welcome to the wiki page!
|
|||||||
p = Page(name="services")
|
p = Page(name="services")
|
||||||
p.set_lock(root)
|
p.set_lock(root)
|
||||||
p.save()
|
p.save()
|
||||||
p.view_groups = [settings.SITH_GROUP_PUBLIC_ID]
|
p.view_groups.set([settings.SITH_GROUP_PUBLIC_ID])
|
||||||
p.set_lock(root)
|
p.set_lock(root)
|
||||||
PageRev(
|
PageRev(
|
||||||
page=p,
|
page=p,
|
||||||
@ -297,9 +297,13 @@ Welcome to the wiki page!
|
|||||||
counter.view_groups = [
|
counter.view_groups = [
|
||||||
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id
|
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id
|
||||||
]
|
]
|
||||||
counter.groups = [
|
counter.groups.set(
|
||||||
Group.objects.filter(id=settings.SITH_GROUP_COUNTER_ADMIN_ID).first().id
|
[
|
||||||
|
Group.objects.filter(id=settings.SITH_GROUP_COUNTER_ADMIN_ID)
|
||||||
|
.first()
|
||||||
|
.id
|
||||||
]
|
]
|
||||||
|
)
|
||||||
counter.save()
|
counter.save()
|
||||||
# Adding user Comptable
|
# Adding user Comptable
|
||||||
comptable = User(
|
comptable = User(
|
||||||
@ -316,11 +320,13 @@ Welcome to the wiki page!
|
|||||||
comptable.view_groups = [
|
comptable.view_groups = [
|
||||||
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id
|
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id
|
||||||
]
|
]
|
||||||
comptable.groups = [
|
comptable.groups.set(
|
||||||
|
[
|
||||||
Group.objects.filter(id=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
|
Group.objects.filter(id=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
|
||||||
.first()
|
.first()
|
||||||
.id
|
.id
|
||||||
]
|
]
|
||||||
|
)
|
||||||
comptable.save()
|
comptable.save()
|
||||||
# Adding user Guy
|
# Adding user Guy
|
||||||
u = User(
|
u = User(
|
||||||
@ -359,11 +365,11 @@ Welcome to the wiki page!
|
|||||||
PageRev(
|
PageRev(
|
||||||
page=p, title="Aide sur la syntaxe", author=skia, content=rm.read()
|
page=p, title="Aide sur la syntaxe", author=skia, content=rm.read()
|
||||||
).save()
|
).save()
|
||||||
p.view_groups = [settings.SITH_GROUP_PUBLIC_ID]
|
p.view_groups.set([settings.SITH_GROUP_PUBLIC_ID])
|
||||||
p.save(force_lock=True)
|
p.save(force_lock=True)
|
||||||
p = Page(name="Services")
|
p = Page(name="Services")
|
||||||
p.save(force_lock=True)
|
p.save(force_lock=True)
|
||||||
p.view_groups = [settings.SITH_GROUP_PUBLIC_ID]
|
p.view_groups.set([settings.SITH_GROUP_PUBLIC_ID])
|
||||||
p.save(force_lock=True)
|
p.save(force_lock=True)
|
||||||
PageRev(
|
PageRev(
|
||||||
page=p,
|
page=p,
|
||||||
@ -377,13 +383,6 @@ Welcome to the wiki page!
|
|||||||
|
|
||||||
""",
|
""",
|
||||||
).save()
|
).save()
|
||||||
# Adding README
|
|
||||||
p = Page(name="README")
|
|
||||||
p.save(force_lock=True)
|
|
||||||
p.view_groups = [settings.SITH_GROUP_PUBLIC_ID]
|
|
||||||
p.save(force_lock=True)
|
|
||||||
with open(os.path.join(root_path) + "/README.md", "r") as rm:
|
|
||||||
PageRev(page=p, title="README", author=skia, content=rm.read()).save()
|
|
||||||
|
|
||||||
# Subscription
|
# Subscription
|
||||||
default_subscription = "un-semestre"
|
default_subscription = "un-semestre"
|
||||||
@ -612,6 +611,7 @@ Welcome to the wiki page!
|
|||||||
mde.products.add(cons)
|
mde.products.add(cons)
|
||||||
mde.products.add(dcons)
|
mde.products.add(dcons)
|
||||||
mde.sellers.add(skia)
|
mde.sellers.add(skia)
|
||||||
|
|
||||||
mde.save()
|
mde.save()
|
||||||
|
|
||||||
eboutic = Counter.objects.filter(name="Eboutic").first()
|
eboutic = Counter.objects.filter(name="Eboutic").first()
|
||||||
@ -849,9 +849,9 @@ Welcome to the wiki page!
|
|||||||
)
|
)
|
||||||
comunity.set_password("plop")
|
comunity.set_password("plop")
|
||||||
comunity.save()
|
comunity.save()
|
||||||
comunity.groups = [
|
comunity.groups.set(
|
||||||
Group.objects.filter(name="Communication admin").first().id
|
[Group.objects.filter(name="Communication admin").first().id]
|
||||||
]
|
)
|
||||||
comunity.save()
|
comunity.save()
|
||||||
Membership(
|
Membership(
|
||||||
user=comunity,
|
user=comunity,
|
||||||
@ -869,7 +869,7 @@ Welcome to the wiki page!
|
|||||||
)
|
)
|
||||||
tutu.set_password("plop")
|
tutu.set_password("plop")
|
||||||
tutu.save()
|
tutu.save()
|
||||||
tutu.groups = [settings.SITH_GROUP_PEDAGOGY_ADMIN_ID]
|
tutu.groups.set([settings.SITH_GROUP_PEDAGOGY_ADMIN_ID])
|
||||||
tutu.save()
|
tutu.save()
|
||||||
|
|
||||||
# Adding subscription for sli
|
# Adding subscription for sli
|
||||||
@ -936,6 +936,7 @@ Welcome to the wiki page!
|
|||||||
# Add barman to counter
|
# Add barman to counter
|
||||||
c = Counter.objects.get(id=2)
|
c = Counter.objects.get(id=2)
|
||||||
c.sellers.add(User.objects.get(pk=krophil.pk))
|
c.sellers.add(User.objects.get(pk=krophil.pk))
|
||||||
|
mde.sellers.add(sli)
|
||||||
c.save()
|
c.save()
|
||||||
|
|
||||||
# Create an election
|
# Create an election
|
||||||
@ -1113,6 +1114,7 @@ Welcome to the wiki page!
|
|||||||
manager="Laurent HEYBERGER",
|
manager="Laurent HEYBERGER",
|
||||||
semester=settings.SITH_PEDAGOGY_UV_SEMESTER[3][0],
|
semester=settings.SITH_PEDAGOGY_UV_SEMESTER[3][0],
|
||||||
language=settings.SITH_PEDAGOGY_UV_LANGUAGE[0][0],
|
language=settings.SITH_PEDAGOGY_UV_LANGUAGE[0][0],
|
||||||
|
department=settings.SITH_PROFILE_DEPARTMENTS[-2][0],
|
||||||
credits=5,
|
credits=5,
|
||||||
title="Participation dans une association étudiante",
|
title="Participation dans une association étudiante",
|
||||||
objectives="* Permettre aux étudiants de réaliser, pendant un semestre, un projet culturel ou associatif et de le valoriser.",
|
objectives="* Permettre aux étudiants de réaliser, pendant un semestre, un projet culturel ou associatif et de le valoriser.",
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from mistune import Renderer, InlineGrammar, InlineLexer, Markdown, escape, escape_link
|
from mistune import Renderer, InlineGrammar, InlineLexer, Markdown, escape, escape_link
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
class SithRenderer(Renderer):
|
class SithRenderer(Renderer):
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
|
import threading
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.functional import SimpleLazyObject
|
from django.utils.functional import SimpleLazyObject
|
||||||
from django.contrib.auth import get_user
|
from django.contrib.auth import get_user
|
||||||
@ -37,8 +38,8 @@ AnonymousUser = getattr(importlib.import_module(module), klass)
|
|||||||
def get_cached_user(request):
|
def get_cached_user(request):
|
||||||
if not hasattr(request, "_cached_user"):
|
if not hasattr(request, "_cached_user"):
|
||||||
user = get_user(request)
|
user = get_user(request)
|
||||||
if user.is_anonymous():
|
if user.is_anonymous:
|
||||||
user = AnonymousUser(request)
|
user = AnonymousUser()
|
||||||
|
|
||||||
request._cached_user = user
|
request._cached_user = user
|
||||||
|
|
||||||
@ -49,8 +50,31 @@ class AuthenticationMiddleware(DjangoAuthenticationMiddleware):
|
|||||||
def process_request(self, request):
|
def process_request(self, request):
|
||||||
assert hasattr(request, "session"), (
|
assert hasattr(request, "session"), (
|
||||||
"The Django authentication middleware requires session middleware "
|
"The Django authentication middleware requires session middleware "
|
||||||
"to be installed. Edit your MIDDLEWARE_CLASSES setting to insert "
|
"to be installed. Edit your MIDDLEWARE setting to insert "
|
||||||
"'django.contrib.sessions.middleware.SessionMiddleware' before "
|
"'django.contrib.sessions.middleware.SessionMiddleware' before "
|
||||||
"'account.middleware.AuthenticationMiddleware'."
|
"'account.middleware.AuthenticationMiddleware'."
|
||||||
)
|
)
|
||||||
request.user = SimpleLazyObject(lambda: get_cached_user(request))
|
request.user = SimpleLazyObject(lambda: get_cached_user(request))
|
||||||
|
|
||||||
|
|
||||||
|
_threadlocal = threading.local()
|
||||||
|
|
||||||
|
|
||||||
|
def get_signal_request():
|
||||||
|
"""
|
||||||
|
!!! Do not use if your operation is asynchronus !!!
|
||||||
|
Allow to access current request in signals
|
||||||
|
This is a hack that looks into the thread
|
||||||
|
Mainly used for log purpose
|
||||||
|
"""
|
||||||
|
|
||||||
|
return getattr(_threadlocal, "request", None)
|
||||||
|
|
||||||
|
|
||||||
|
class SignalRequestMiddleware:
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
setattr(_threadlocal, "request", request)
|
||||||
|
return self.get_response(request)
|
||||||
|
@ -8,6 +8,7 @@ import django.core.validators
|
|||||||
import core.models
|
import core.models
|
||||||
import phonenumber_field.modelfields
|
import phonenumber_field.modelfields
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@ -276,6 +277,7 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"group_ptr",
|
"group_ptr",
|
||||||
models.OneToOneField(
|
models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
parent_link=True,
|
parent_link=True,
|
||||||
serialize=False,
|
serialize=False,
|
||||||
@ -329,6 +331,7 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"owner_group",
|
"owner_group",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
default=1,
|
default=1,
|
||||||
related_name="owned_page",
|
related_name="owned_page",
|
||||||
verbose_name="owner group",
|
verbose_name="owner group",
|
||||||
@ -390,10 +393,19 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"author",
|
"author",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
to=settings.AUTH_USER_MODEL, related_name="page_rev"
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
related_name="page_rev",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"page",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="core.Page",
|
||||||
|
related_name="revisions",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("page", models.ForeignKey(to="core.Page", related_name="revisions")),
|
|
||||||
],
|
],
|
||||||
options={"ordering": ["date"]},
|
options={"ordering": ["date"]},
|
||||||
),
|
),
|
||||||
@ -420,7 +432,9 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"user",
|
"user",
|
||||||
models.OneToOneField(
|
models.OneToOneField(
|
||||||
to=settings.AUTH_USER_MODEL, related_name="preferences"
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
related_name="preferences",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -469,6 +483,7 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"owner",
|
"owner",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
verbose_name="owner",
|
verbose_name="owner",
|
||||||
to=settings.AUTH_USER_MODEL,
|
to=settings.AUTH_USER_MODEL,
|
||||||
related_name="owned_files",
|
related_name="owned_files",
|
||||||
@ -477,6 +492,7 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"parent",
|
"parent",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
null=True,
|
null=True,
|
||||||
related_name="children",
|
related_name="children",
|
||||||
verbose_name="parent",
|
verbose_name="parent",
|
||||||
@ -512,6 +528,7 @@ class Migration(migrations.Migration):
|
|||||||
model_name="user",
|
model_name="user",
|
||||||
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",
|
related_name="home_of",
|
||||||
|
@ -3,6 +3,7 @@ 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):
|
||||||
@ -21,6 +22,7 @@ class Migration(migrations.Migration):
|
|||||||
model_name="page",
|
model_name="page",
|
||||||
name="lock_user",
|
name="lock_user",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
verbose_name="lock user",
|
verbose_name="lock user",
|
||||||
default=None,
|
default=None,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
@ -4,6 +4,7 @@ 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.utils.timezone
|
import django.utils.timezone
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@ -48,7 +49,9 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"user",
|
"user",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
related_name="notifications", to=settings.AUTH_USER_MODEL
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="notifications",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -3,6 +3,7 @@ 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):
|
||||||
@ -14,6 +15,7 @@ class Migration(migrations.Migration):
|
|||||||
model_name="sithfile",
|
model_name="sithfile",
|
||||||
name="moderator",
|
name="moderator",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
related_name="moderated_files",
|
related_name="moderated_files",
|
||||||
verbose_name="owner",
|
verbose_name="owner",
|
||||||
default=0,
|
default=0,
|
||||||
|
@ -3,6 +3,7 @@ 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):
|
||||||
@ -14,6 +15,7 @@ class Migration(migrations.Migration):
|
|||||||
model_name="sithfile",
|
model_name="sithfile",
|
||||||
name="moderator",
|
name="moderator",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
related_name="moderated_files",
|
related_name="moderated_files",
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
|
@ -3,6 +3,7 @@ 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):
|
||||||
@ -35,7 +36,9 @@ class Migration(migrations.Migration):
|
|||||||
model_name="preferences",
|
model_name="preferences",
|
||||||
name="user",
|
name="user",
|
||||||
field=models.OneToOneField(
|
field=models.OneToOneField(
|
||||||
related_name="_preferences", to=settings.AUTH_USER_MODEL
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="_preferences",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -4,6 +4,7 @@ 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.utils.timezone
|
import django.utils.timezone
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@ -33,7 +34,9 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"user",
|
"user",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
related_name="gifts", to=settings.AUTH_USER_MODEL
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="gifts",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import core.models
|
import core.models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@ -14,6 +15,7 @@ class Migration(migrations.Migration):
|
|||||||
model_name="page",
|
model_name="page",
|
||||||
name="owner_group",
|
name="owner_group",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
verbose_name="owner group",
|
verbose_name="owner group",
|
||||||
default=core.models.Page.get_default_owner_group,
|
default=core.models.Page.get_default_owner_group,
|
||||||
related_name="owned_page",
|
related_name="owned_page",
|
||||||
|
40
core/migrations/0030_auto_20190704_1500.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.20 on 2019-07-04 13:00
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("core", "0029_auto_20180426_2013")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="notification",
|
||||||
|
name="type",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("POSTER_MODERATION", "A new poster needs to be moderated"),
|
||||||
|
("MAILING_MODERATION", "A new mailing list needs to be moderated"),
|
||||||
|
(
|
||||||
|
"PEDAGOGY_MODERATION",
|
||||||
|
"A new pedagogy comment has been signaled for moderation",
|
||||||
|
),
|
||||||
|
("NEWS_MODERATION", "There are %s fresh news to be moderated"),
|
||||||
|
("FILE_MODERATION", "New files to be moderated"),
|
||||||
|
(
|
||||||
|
"SAS_MODERATION",
|
||||||
|
"There are %s pictures to be moderated in the SAS",
|
||||||
|
),
|
||||||
|
("NEW_PICTURES", "You've been identified on some pictures"),
|
||||||
|
("REFILLING", "You just refilled of %s €"),
|
||||||
|
("SELLING", "You just bought %s"),
|
||||||
|
("GENERIC", "You have a notification"),
|
||||||
|
],
|
||||||
|
default="GENERIC",
|
||||||
|
max_length=32,
|
||||||
|
verbose_name="type",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
27
core/migrations/0031_auto_20190906_1615.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.24 on 2019-09-06 14:15
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("core", "0030_auto_20190704_1500")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="sithfile",
|
||||||
|
name="is_folder",
|
||||||
|
field=models.BooleanField(
|
||||||
|
db_index=True, default=True, verbose_name="is folder"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="sithfile",
|
||||||
|
name="is_in_sas",
|
||||||
|
field=models.BooleanField(
|
||||||
|
db_index=True, default=False, verbose_name="is in the SAS"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
20
core/migrations/0032_auto_20190909_0043.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.24 on 2019-09-08 22:43
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("core", "0031_auto_20190906_1615")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="notification",
|
||||||
|
name="viewed",
|
||||||
|
field=models.BooleanField(
|
||||||
|
db_index=True, default=False, verbose_name="viewed"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
27
core/migrations/0033_auto_20191006_0049.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Generated by Django 2.2.6 on 2019-10-05 22:49
|
||||||
|
|
||||||
|
import django.contrib.auth.models
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("core", "0032_auto_20190909_0043")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="page",
|
||||||
|
options={
|
||||||
|
"permissions": (
|
||||||
|
(
|
||||||
|
"change_prop_page",
|
||||||
|
"Can change the page's properties (groups, ...)",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterModelManagers(
|
||||||
|
name="group",
|
||||||
|
managers=[("objects", django.contrib.auth.models.GroupManager())],
|
||||||
|
),
|
||||||
|
]
|
51
core/migrations/0034_operationlog.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Generated by Django 2.2.6 on 2019-11-14 15:10
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("core", "0033_auto_20191006_0049"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="OperationLog",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("date", models.DateTimeField(auto_now_add=True, verbose_name="date")),
|
||||||
|
("label", models.CharField(max_length=255, verbose_name="label")),
|
||||||
|
(
|
||||||
|
"operation_type",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("SELLING_DELETION", "Selling deletion"),
|
||||||
|
("REFILLING_DELETION", "Refilling deletion"),
|
||||||
|
],
|
||||||
|
max_length=40,
|
||||||
|
verbose_name="operation type",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"operator",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="logs",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
24
core/migrations/0035_auto_20200216_1743.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 2.2.10 on 2020-02-16 16:43
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("core", "0034_operationlog"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="sex",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[("MAN", "Man"), ("WOMAN", "Woman")],
|
||||||
|
max_length=10,
|
||||||
|
null=True,
|
||||||
|
verbose_name="sex",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
28
core/migrations/0036_auto_20211001_0248.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 2.2.24 on 2021-10-01 00:48
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("core", "0035_auto_20200216_1743")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="pronouns",
|
||||||
|
field=models.CharField(default="", max_length=64, verbose_name="pronouns"),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="sex",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[("MAN", "Man"), ("WOMAN", "Woman"), ("OTHER", "Other")],
|
||||||
|
max_length=10,
|
||||||
|
null=True,
|
||||||
|
verbose_name="sex",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
18
core/migrations/0037_auto_20211105_1708.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2.24 on 2021-11-05 16:08
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("core", "0036_auto_20211001_0248")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="pronouns",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, default="", max_length=64, verbose_name="pronouns"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
116
core/models.py
@ -34,11 +34,11 @@ from django.contrib.auth.models import (
|
|||||||
GroupManager as AuthGroupManager,
|
GroupManager as AuthGroupManager,
|
||||||
AnonymousUser as AuthAnonymousUser,
|
AnonymousUser as AuthAnonymousUser,
|
||||||
)
|
)
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.core.exceptions import ValidationError, PermissionDenied
|
from django.core.exceptions import ValidationError, PermissionDenied
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||||
@ -65,11 +65,19 @@ class MetaGroupManager(AuthGroupManager):
|
|||||||
|
|
||||||
|
|
||||||
class Group(AuthGroup):
|
class Group(AuthGroup):
|
||||||
|
"""
|
||||||
|
Implement both RealGroups and Meta groups
|
||||||
|
|
||||||
|
Groups are sorted by their is_meta property
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: If False, this is a RealGroup
|
||||||
is_meta = models.BooleanField(
|
is_meta = models.BooleanField(
|
||||||
_("meta group status"),
|
_("meta group status"),
|
||||||
default=False,
|
default=False,
|
||||||
help_text=_("Whether a group is a meta group or not"),
|
help_text=_("Whether a group is a meta group or not"),
|
||||||
)
|
)
|
||||||
|
#: Description of the group
|
||||||
description = models.CharField(_("description"), max_length=60)
|
description = models.CharField(_("description"), max_length=60)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -83,6 +91,15 @@ class Group(AuthGroup):
|
|||||||
|
|
||||||
|
|
||||||
class MetaGroup(Group):
|
class MetaGroup(Group):
|
||||||
|
"""
|
||||||
|
MetaGroups are dynamically created groups.
|
||||||
|
Generaly used with clubs where creating a club creates two groups:
|
||||||
|
|
||||||
|
* club-SITH_BOARD_SUFFIX
|
||||||
|
* club-SITH_MEMBER_SUFFIX
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: Assign a manager in a way that MetaGroup.objects only return groups with is_meta=False
|
||||||
objects = MetaGroupManager()
|
objects = MetaGroupManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -94,6 +111,12 @@ class MetaGroup(Group):
|
|||||||
|
|
||||||
|
|
||||||
class RealGroup(Group):
|
class RealGroup(Group):
|
||||||
|
"""
|
||||||
|
RealGroups are created by the developer.
|
||||||
|
Most of the time they match a number in settings to be easily used for permissions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: Assign a manager in a way that MetaGroup.objects only return groups with is_meta=True
|
||||||
objects = RealGroupManager()
|
objects = RealGroupManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -202,9 +225,11 @@ class User(AbstractBaseUser):
|
|||||||
sex = models.CharField(
|
sex = models.CharField(
|
||||||
_("sex"),
|
_("sex"),
|
||||||
max_length=10,
|
max_length=10,
|
||||||
choices=[("MAN", _("Man")), ("WOMAN", _("Woman"))],
|
null=True,
|
||||||
default="MAN",
|
blank=True,
|
||||||
|
choices=[("MAN", _("Man")), ("WOMAN", _("Woman")), ("OTHER", _("Other"))],
|
||||||
)
|
)
|
||||||
|
pronouns = models.CharField(_("pronouns"), max_length=64, blank=True, default="")
|
||||||
tshirt_size = models.CharField(
|
tshirt_size = models.CharField(
|
||||||
_("tshirt size"),
|
_("tshirt size"),
|
||||||
max_length=5,
|
max_length=5,
|
||||||
@ -389,6 +414,20 @@ class User(AbstractBaseUser):
|
|||||||
.has_rights_in_club(self)
|
.has_rights_in_club(self)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def can_read_subscription_history(self):
|
||||||
|
if self.is_root or self.is_board_member:
|
||||||
|
return True
|
||||||
|
|
||||||
|
from club.models import Club
|
||||||
|
|
||||||
|
for club in Club.objects.filter(
|
||||||
|
id__in=settings.SITH_CAN_READ_SUBSCRIPTION_HISTORY
|
||||||
|
).all():
|
||||||
|
if club.has_rights_in_club(self):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def can_create_subscription(self):
|
def can_create_subscription(self):
|
||||||
from club.models import Club
|
from club.models import Club
|
||||||
@ -659,13 +698,17 @@ class User(AbstractBaseUser):
|
|||||||
|
|
||||||
|
|
||||||
class AnonymousUser(AuthAnonymousUser):
|
class AnonymousUser(AuthAnonymousUser):
|
||||||
def __init__(self, request):
|
def __init__(self):
|
||||||
super(AnonymousUser, self).__init__()
|
super(AnonymousUser, self).__init__()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def can_create_subscription(self):
|
def can_create_subscription(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_read_subscription_history(self):
|
||||||
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def was_subscribed(self):
|
def was_subscribed(self):
|
||||||
return False
|
return False
|
||||||
@ -743,7 +786,9 @@ class AnonymousUser(AuthAnonymousUser):
|
|||||||
|
|
||||||
|
|
||||||
class Preferences(models.Model):
|
class Preferences(models.Model):
|
||||||
user = models.OneToOneField(User, related_name="_preferences")
|
user = models.OneToOneField(
|
||||||
|
User, related_name="_preferences", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
receive_weekmail = models.BooleanField(
|
receive_weekmail = models.BooleanField(
|
||||||
_("do you want to receive the weekmail"), default=False
|
_("do you want to receive the weekmail"), default=False
|
||||||
)
|
)
|
||||||
@ -777,7 +822,12 @@ def get_thumbnail_directory(instance, filename):
|
|||||||
class SithFile(models.Model):
|
class SithFile(models.Model):
|
||||||
name = models.CharField(_("file name"), max_length=256, blank=False)
|
name = models.CharField(_("file name"), max_length=256, blank=False)
|
||||||
parent = models.ForeignKey(
|
parent = models.ForeignKey(
|
||||||
"self", related_name="children", verbose_name=_("parent"), null=True, blank=True
|
"self",
|
||||||
|
related_name="children",
|
||||||
|
verbose_name=_("parent"),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
file = models.FileField(
|
file = models.FileField(
|
||||||
upload_to=get_directory,
|
upload_to=get_directory,
|
||||||
@ -800,14 +850,19 @@ class SithFile(models.Model):
|
|||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
owner = models.ForeignKey(User, related_name="owned_files", verbose_name=_("owner"))
|
owner = models.ForeignKey(
|
||||||
|
User,
|
||||||
|
related_name="owned_files",
|
||||||
|
verbose_name=_("owner"),
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
edit_groups = models.ManyToManyField(
|
edit_groups = models.ManyToManyField(
|
||||||
Group, related_name="editable_files", verbose_name=_("edit group"), blank=True
|
Group, related_name="editable_files", verbose_name=_("edit group"), blank=True
|
||||||
)
|
)
|
||||||
view_groups = models.ManyToManyField(
|
view_groups = models.ManyToManyField(
|
||||||
Group, related_name="viewable_files", verbose_name=_("view group"), blank=True
|
Group, related_name="viewable_files", verbose_name=_("view group"), blank=True
|
||||||
)
|
)
|
||||||
is_folder = models.BooleanField(_("is folder"), default=True)
|
is_folder = models.BooleanField(_("is folder"), default=True, db_index=True)
|
||||||
mime_type = models.CharField(_("mime type"), max_length=30)
|
mime_type = models.CharField(_("mime type"), max_length=30)
|
||||||
size = models.IntegerField(_("size"), default=0)
|
size = models.IntegerField(_("size"), default=0)
|
||||||
date = models.DateTimeField(_("date"), default=timezone.now)
|
date = models.DateTimeField(_("date"), default=timezone.now)
|
||||||
@ -818,10 +873,11 @@ class SithFile(models.Model):
|
|||||||
verbose_name=_("owner"),
|
verbose_name=_("owner"),
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
asked_for_removal = models.BooleanField(_("asked for removal"), default=False)
|
asked_for_removal = models.BooleanField(_("asked for removal"), default=False)
|
||||||
is_in_sas = models.BooleanField(
|
is_in_sas = models.BooleanField(
|
||||||
_("is in the SAS"), default=False
|
_("is in the SAS"), default=False, db_index=True
|
||||||
) # Allows to query this flag, updated at each call to save()
|
) # Allows to query this flag, updated at each call to save()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -935,8 +991,8 @@ class SithFile(models.Model):
|
|||||||
def copy_rights(self):
|
def copy_rights(self):
|
||||||
"""Copy, if possible, the rights of the parent folder"""
|
"""Copy, if possible, the rights of the parent folder"""
|
||||||
if self.parent is not None:
|
if self.parent is not None:
|
||||||
self.edit_groups = self.parent.edit_groups.all()
|
self.edit_groups.set(self.parent.edit_groups.all())
|
||||||
self.view_groups = self.parent.view_groups.all()
|
self.view_groups.set(self.parent.view_groups.all())
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def move_to(self, parent):
|
def move_to(self, parent):
|
||||||
@ -1133,6 +1189,7 @@ class Page(models.Model):
|
|||||||
related_name="owned_page",
|
related_name="owned_page",
|
||||||
verbose_name=_("owner group"),
|
verbose_name=_("owner group"),
|
||||||
default=get_default_owner_group,
|
default=get_default_owner_group,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
edit_groups = models.ManyToManyField(
|
edit_groups = models.ManyToManyField(
|
||||||
Group, related_name="editable_page", verbose_name=_("edit group"), blank=True
|
Group, related_name="editable_page", verbose_name=_("edit group"), blank=True
|
||||||
@ -1147,6 +1204,7 @@ class Page(models.Model):
|
|||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
default=None,
|
default=None,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
lock_timeout = models.DateTimeField(
|
lock_timeout = models.DateTimeField(
|
||||||
_("lock_timeout"), null=True, blank=True, default=None
|
_("lock_timeout"), null=True, blank=True, default=None
|
||||||
@ -1156,7 +1214,6 @@ class Page(models.Model):
|
|||||||
unique_together = ("name", "parent")
|
unique_together = ("name", "parent")
|
||||||
permissions = (
|
permissions = (
|
||||||
("change_prop_page", "Can change the page's properties (groups, ...)"),
|
("change_prop_page", "Can change the page's properties (groups, ...)"),
|
||||||
("view_page", "Can view the page"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -1347,8 +1404,8 @@ class PageRev(models.Model):
|
|||||||
title = models.CharField(_("page title"), max_length=255, blank=True)
|
title = models.CharField(_("page title"), max_length=255, blank=True)
|
||||||
content = models.TextField(_("page content"), blank=True)
|
content = models.TextField(_("page content"), blank=True)
|
||||||
date = models.DateTimeField(_("date"), auto_now=True)
|
date = models.DateTimeField(_("date"), auto_now=True)
|
||||||
author = models.ForeignKey(User, related_name="page_rev")
|
author = models.ForeignKey(User, related_name="page_rev", on_delete=models.CASCADE)
|
||||||
page = models.ForeignKey(Page, related_name="revisions")
|
page = models.ForeignKey(Page, related_name="revisions", on_delete=models.CASCADE)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["date"]
|
ordering = ["date"]
|
||||||
@ -1386,14 +1443,16 @@ class PageRev(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Notification(models.Model):
|
class Notification(models.Model):
|
||||||
user = models.ForeignKey(User, related_name="notifications")
|
user = models.ForeignKey(
|
||||||
|
User, related_name="notifications", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
url = models.CharField(_("url"), max_length=255)
|
url = models.CharField(_("url"), max_length=255)
|
||||||
param = models.CharField(_("param"), max_length=128, default="")
|
param = models.CharField(_("param"), max_length=128, default="")
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
_("type"), max_length=32, choices=settings.SITH_NOTIFICATIONS, default="GENERIC"
|
_("type"), max_length=32, choices=settings.SITH_NOTIFICATIONS, default="GENERIC"
|
||||||
)
|
)
|
||||||
date = models.DateTimeField(_("date"), default=timezone.now)
|
date = models.DateTimeField(_("date"), default=timezone.now)
|
||||||
viewed = models.BooleanField(_("viewed"), default=False)
|
viewed = models.BooleanField(_("viewed"), default=False, db_index=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.param:
|
if self.param:
|
||||||
@ -1422,7 +1481,7 @@ class Notification(models.Model):
|
|||||||
class Gift(models.Model):
|
class Gift(models.Model):
|
||||||
label = models.CharField(_("label"), max_length=255)
|
label = models.CharField(_("label"), max_length=255)
|
||||||
date = models.DateTimeField(_("date"), default=timezone.now)
|
date = models.DateTimeField(_("date"), default=timezone.now)
|
||||||
user = models.ForeignKey(User, related_name="gifts")
|
user = models.ForeignKey(User, related_name="gifts", on_delete=models.CASCADE)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s - %s" % (self.translated_label, self.date.strftime("%d %b %Y"))
|
return "%s - %s" % (self.translated_label, self.date.strftime("%d %b %Y"))
|
||||||
@ -1438,3 +1497,24 @@ class Gift(models.Model):
|
|||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
return user.is_board_member or user.is_root
|
return user.is_board_member or user.is_root
|
||||||
|
|
||||||
|
|
||||||
|
class OperationLog(models.Model):
|
||||||
|
"""
|
||||||
|
General purpose log object to register operations
|
||||||
|
"""
|
||||||
|
|
||||||
|
date = models.DateTimeField(_("date"), auto_now_add=True)
|
||||||
|
label = models.CharField(_("label"), max_length=255)
|
||||||
|
operator = models.ForeignKey(
|
||||||
|
User, related_name="logs", on_delete=models.SET_NULL, null=True
|
||||||
|
)
|
||||||
|
operation_type = models.CharField(
|
||||||
|
_("operation type"), max_length=40, choices=settings.SITH_LOG_OPERATION_TYPE
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_owned_by(self, user):
|
||||||
|
return user.is_root
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s - %s - %s" % (self.operation_type, self.label, self.operator)
|
||||||
|
@ -25,9 +25,9 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sass
|
import sass
|
||||||
|
from urllib.parse import urljoin
|
||||||
from django.utils.encoding import force_bytes, iri_to_uri
|
from django.utils.encoding import force_bytes, iri_to_uri
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.utils.six.moves.urllib.parse import urljoin
|
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from core.scss.storage import ScssFileStorage, find_file
|
from core.scss.storage import ScssFileStorage, find_file
|
||||||
|
@ -34,6 +34,7 @@ from forum.models import ForumMessage, ForumMessageMeta
|
|||||||
class UserIndex(indexes.SearchIndex, indexes.Indexable):
|
class UserIndex(indexes.SearchIndex, indexes.Indexable):
|
||||||
text = indexes.CharField(document=True, use_template=True)
|
text = indexes.CharField(document=True, use_template=True)
|
||||||
auto = indexes.EdgeNgramField(use_template=True)
|
auto = indexes.EdgeNgramField(use_template=True)
|
||||||
|
last_update = indexes.DateTimeField(model_attr="last_update")
|
||||||
|
|
||||||
def get_model(self):
|
def get_model(self):
|
||||||
return User
|
return User
|
||||||
@ -45,6 +46,9 @@ class UserIndex(indexes.SearchIndex, indexes.Indexable):
|
|||||||
def get_updated_field(self):
|
def get_updated_field(self):
|
||||||
return "last_update"
|
return "last_update"
|
||||||
|
|
||||||
|
def prepare_auto(self, obj):
|
||||||
|
return self.prepared_data["auto"].strip()[:245]
|
||||||
|
|
||||||
|
|
||||||
class IndexSignalProcessor(signals.BaseSignalProcessor):
|
class IndexSignalProcessor(signals.BaseSignalProcessor):
|
||||||
def setup(self):
|
def setup(self):
|
||||||
|
BIN
core/static/com/img/invitation_bannerP22.png
Normal file
After Width: | Height: | Size: 273 KiB |
BIN
core/static/com/img/weekmail_bannerA19.jpg
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
core/static/com/img/weekmail_bannerV1P22.png
Normal file
After Width: | Height: | Size: 275 KiB |
BIN
core/static/com/img/weekmail_bannerV2P22.png
Normal file
After Width: | Height: | Size: 275 KiB |
BIN
core/static/com/img/weekmail_footerA19.jpg
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
core/static/com/img/weekmail_footerP22.png
Normal file
After Width: | Height: | Size: 242 KiB |
4
core/static/core/easymde/easymde.min.css
vendored
Normal file → Executable file
4
core/static/core/easymde/easymde.min.js
vendored
BIN
core/static/core/img/promo_20.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
core/static/core/img/promo_21.png
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
core/static/core/img/promo_22.png
Normal file
After Width: | Height: | Size: 55 KiB |
@ -1,782 +0,0 @@
|
|||||||
/**
|
|
||||||
* @author zhixin wen <wenzhixin2010@gmail.com>
|
|
||||||
* @version 1.2.1
|
|
||||||
*
|
|
||||||
* http://wenzhixin.net.cn/p/multiple-select/
|
|
||||||
*/
|
|
||||||
|
|
||||||
(function ($) {
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// it only does '%s', and return '' when arguments are undefined
|
|
||||||
var sprintf = function (str) {
|
|
||||||
var args = arguments,
|
|
||||||
flag = true,
|
|
||||||
i = 1;
|
|
||||||
|
|
||||||
str = str.replace(/%s/g, function () {
|
|
||||||
var arg = args[i++];
|
|
||||||
|
|
||||||
if (typeof arg === 'undefined') {
|
|
||||||
flag = false;
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return arg;
|
|
||||||
});
|
|
||||||
return flag ? str : '';
|
|
||||||
};
|
|
||||||
|
|
||||||
var removeDiacritics = function (str) {
|
|
||||||
var defaultDiacriticsRemovalMap = [
|
|
||||||
{'base':'A', 'letters':/[\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F]/g},
|
|
||||||
{'base':'AA','letters':/[\uA732]/g},
|
|
||||||
{'base':'AE','letters':/[\u00C6\u01FC\u01E2]/g},
|
|
||||||
{'base':'AO','letters':/[\uA734]/g},
|
|
||||||
{'base':'AU','letters':/[\uA736]/g},
|
|
||||||
{'base':'AV','letters':/[\uA738\uA73A]/g},
|
|
||||||
{'base':'AY','letters':/[\uA73C]/g},
|
|
||||||
{'base':'B', 'letters':/[\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181]/g},
|
|
||||||
{'base':'C', 'letters':/[\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E]/g},
|
|
||||||
{'base':'D', 'letters':/[\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779]/g},
|
|
||||||
{'base':'DZ','letters':/[\u01F1\u01C4]/g},
|
|
||||||
{'base':'Dz','letters':/[\u01F2\u01C5]/g},
|
|
||||||
{'base':'E', 'letters':/[\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E]/g},
|
|
||||||
{'base':'F', 'letters':/[\u0046\u24BB\uFF26\u1E1E\u0191\uA77B]/g},
|
|
||||||
{'base':'G', 'letters':/[\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E]/g},
|
|
||||||
{'base':'H', 'letters':/[\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D]/g},
|
|
||||||
{'base':'I', 'letters':/[\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197]/g},
|
|
||||||
{'base':'J', 'letters':/[\u004A\u24BF\uFF2A\u0134\u0248]/g},
|
|
||||||
{'base':'K', 'letters':/[\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2]/g},
|
|
||||||
{'base':'L', 'letters':/[\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780]/g},
|
|
||||||
{'base':'LJ','letters':/[\u01C7]/g},
|
|
||||||
{'base':'Lj','letters':/[\u01C8]/g},
|
|
||||||
{'base':'M', 'letters':/[\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C]/g},
|
|
||||||
{'base':'N', 'letters':/[\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4]/g},
|
|
||||||
{'base':'NJ','letters':/[\u01CA]/g},
|
|
||||||
{'base':'Nj','letters':/[\u01CB]/g},
|
|
||||||
{'base':'O', 'letters':/[\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C]/g},
|
|
||||||
{'base':'OI','letters':/[\u01A2]/g},
|
|
||||||
{'base':'OO','letters':/[\uA74E]/g},
|
|
||||||
{'base':'OU','letters':/[\u0222]/g},
|
|
||||||
{'base':'P', 'letters':/[\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754]/g},
|
|
||||||
{'base':'Q', 'letters':/[\u0051\u24C6\uFF31\uA756\uA758\u024A]/g},
|
|
||||||
{'base':'R', 'letters':/[\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782]/g},
|
|
||||||
{'base':'S', 'letters':/[\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784]/g},
|
|
||||||
{'base':'T', 'letters':/[\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786]/g},
|
|
||||||
{'base':'TZ','letters':/[\uA728]/g},
|
|
||||||
{'base':'U', 'letters':/[\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244]/g},
|
|
||||||
{'base':'V', 'letters':/[\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245]/g},
|
|
||||||
{'base':'VY','letters':/[\uA760]/g},
|
|
||||||
{'base':'W', 'letters':/[\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72]/g},
|
|
||||||
{'base':'X', 'letters':/[\u0058\u24CD\uFF38\u1E8A\u1E8C]/g},
|
|
||||||
{'base':'Y', 'letters':/[\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE]/g},
|
|
||||||
{'base':'Z', 'letters':/[\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762]/g},
|
|
||||||
{'base':'a', 'letters':/[\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250]/g},
|
|
||||||
{'base':'aa','letters':/[\uA733]/g},
|
|
||||||
{'base':'ae','letters':/[\u00E6\u01FD\u01E3]/g},
|
|
||||||
{'base':'ao','letters':/[\uA735]/g},
|
|
||||||
{'base':'au','letters':/[\uA737]/g},
|
|
||||||
{'base':'av','letters':/[\uA739\uA73B]/g},
|
|
||||||
{'base':'ay','letters':/[\uA73D]/g},
|
|
||||||
{'base':'b', 'letters':/[\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253]/g},
|
|
||||||
{'base':'c', 'letters':/[\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184]/g},
|
|
||||||
{'base':'d', 'letters':/[\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A]/g},
|
|
||||||
{'base':'dz','letters':/[\u01F3\u01C6]/g},
|
|
||||||
{'base':'e', 'letters':/[\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD]/g},
|
|
||||||
{'base':'f', 'letters':/[\u0066\u24D5\uFF46\u1E1F\u0192\uA77C]/g},
|
|
||||||
{'base':'g', 'letters':/[\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F]/g},
|
|
||||||
{'base':'h', 'letters':/[\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265]/g},
|
|
||||||
{'base':'hv','letters':/[\u0195]/g},
|
|
||||||
{'base':'i', 'letters':/[\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131]/g},
|
|
||||||
{'base':'j', 'letters':/[\u006A\u24D9\uFF4A\u0135\u01F0\u0249]/g},
|
|
||||||
{'base':'k', 'letters':/[\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3]/g},
|
|
||||||
{'base':'l', 'letters':/[\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747]/g},
|
|
||||||
{'base':'lj','letters':/[\u01C9]/g},
|
|
||||||
{'base':'m', 'letters':/[\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F]/g},
|
|
||||||
{'base':'n', 'letters':/[\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5]/g},
|
|
||||||
{'base':'nj','letters':/[\u01CC]/g},
|
|
||||||
{'base':'o', 'letters':/[\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275]/g},
|
|
||||||
{'base':'oi','letters':/[\u01A3]/g},
|
|
||||||
{'base':'ou','letters':/[\u0223]/g},
|
|
||||||
{'base':'oo','letters':/[\uA74F]/g},
|
|
||||||
{'base':'p','letters':/[\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755]/g},
|
|
||||||
{'base':'q','letters':/[\u0071\u24E0\uFF51\u024B\uA757\uA759]/g},
|
|
||||||
{'base':'r','letters':/[\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783]/g},
|
|
||||||
{'base':'s','letters':/[\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B]/g},
|
|
||||||
{'base':'t','letters':/[\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787]/g},
|
|
||||||
{'base':'tz','letters':/[\uA729]/g},
|
|
||||||
{'base':'u','letters':/[\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289]/g},
|
|
||||||
{'base':'v','letters':/[\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C]/g},
|
|
||||||
{'base':'vy','letters':/[\uA761]/g},
|
|
||||||
{'base':'w','letters':/[\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73]/g},
|
|
||||||
{'base':'x','letters':/[\u0078\u24E7\uFF58\u1E8B\u1E8D]/g},
|
|
||||||
{'base':'y','letters':/[\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF]/g},
|
|
||||||
{'base':'z','letters':/[\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763]/g}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (var i = 0; i < defaultDiacriticsRemovalMap.length; i++) {
|
|
||||||
str = str.replace(defaultDiacriticsRemovalMap[i].letters, defaultDiacriticsRemovalMap[i].base);
|
|
||||||
}
|
|
||||||
|
|
||||||
return str;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
function MultipleSelect($el, options) {
|
|
||||||
var that = this,
|
|
||||||
name = $el.attr('name') || options.name || '';
|
|
||||||
|
|
||||||
this.options = options;
|
|
||||||
|
|
||||||
// hide select element
|
|
||||||
this.$el = $el.hide();
|
|
||||||
|
|
||||||
// label element
|
|
||||||
this.$label = this.$el.closest('label');
|
|
||||||
if (this.$label.length === 0 && this.$el.attr('id')) {
|
|
||||||
this.$label = $(sprintf('label[for="%s"]', this.$el.attr('id').replace(/:/g, '\\:')));
|
|
||||||
}
|
|
||||||
|
|
||||||
// restore class and title from select element
|
|
||||||
this.$parent = $(sprintf(
|
|
||||||
'<div class="ms-parent %s" %s/>',
|
|
||||||
$el.attr('class') || '',
|
|
||||||
sprintf('title="%s"', $el.attr('title'))));
|
|
||||||
|
|
||||||
// add placeholder to choice button
|
|
||||||
this.$choice = $(sprintf([
|
|
||||||
'<button type="button" class="ms-choice">',
|
|
||||||
'<span class="placeholder">%s</span>',
|
|
||||||
'<div></div>',
|
|
||||||
'</button>'
|
|
||||||
].join(''),
|
|
||||||
this.options.placeholder));
|
|
||||||
|
|
||||||
// default position is bottom
|
|
||||||
this.$drop = $(sprintf('<div class="ms-drop %s"%s></div>',
|
|
||||||
this.options.position,
|
|
||||||
sprintf(' style="width: %s"', this.options.dropWidth)));
|
|
||||||
|
|
||||||
this.$el.after(this.$parent);
|
|
||||||
this.$parent.append(this.$choice);
|
|
||||||
this.$parent.append(this.$drop);
|
|
||||||
|
|
||||||
if (this.$el.prop('disabled')) {
|
|
||||||
this.$choice.addClass('disabled');
|
|
||||||
}
|
|
||||||
this.$parent.css('width',
|
|
||||||
this.options.width ||
|
|
||||||
this.$el.css('width') ||
|
|
||||||
this.$el.outerWidth() + 20);
|
|
||||||
|
|
||||||
this.selectAllName = 'data-name="selectAll' + name + '"';
|
|
||||||
this.selectGroupName = 'data-name="selectGroup' + name + '"';
|
|
||||||
this.selectItemName = 'data-name="selectItem' + name + '"';
|
|
||||||
|
|
||||||
if (!this.options.keepOpen) {
|
|
||||||
$(document).click(function (e) {
|
|
||||||
if ($(e.target)[0] === that.$choice[0] ||
|
|
||||||
$(e.target).parents('.ms-choice')[0] === that.$choice[0]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (($(e.target)[0] === that.$drop[0] ||
|
|
||||||
$(e.target).parents('.ms-drop')[0] !== that.$drop[0] && e.target !== $el[0]) &&
|
|
||||||
that.options.isOpen) {
|
|
||||||
that.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MultipleSelect.prototype = {
|
|
||||||
constructor: MultipleSelect,
|
|
||||||
|
|
||||||
init: function () {
|
|
||||||
var that = this,
|
|
||||||
$ul = $('<ul></ul>');
|
|
||||||
|
|
||||||
this.$drop.html('');
|
|
||||||
|
|
||||||
if (this.options.filter) {
|
|
||||||
this.$drop.append([
|
|
||||||
'<div class="ms-search">',
|
|
||||||
'<input type="text" autocomplete="off" autocorrect="off" autocapitilize="off" spellcheck="false">',
|
|
||||||
'</div>'].join('')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.options.selectAll && !this.options.single) {
|
|
||||||
$ul.append([
|
|
||||||
'<li class="ms-select-all">',
|
|
||||||
'<label>',
|
|
||||||
sprintf('<input type="checkbox" %s /> ', this.selectAllName),
|
|
||||||
this.options.selectAllDelimiter[0],
|
|
||||||
this.options.selectAllText,
|
|
||||||
this.options.selectAllDelimiter[1],
|
|
||||||
'</label>',
|
|
||||||
'</li>'
|
|
||||||
].join(''));
|
|
||||||
}
|
|
||||||
|
|
||||||
$.each(this.$el.children(), function (i, elm) {
|
|
||||||
$ul.append(that.optionToHtml(i, elm));
|
|
||||||
});
|
|
||||||
$ul.append(sprintf('<li class="ms-no-results">%s</li>', this.options.noMatchesFound));
|
|
||||||
this.$drop.append($ul);
|
|
||||||
|
|
||||||
this.$drop.find('ul').css('max-height', this.options.maxHeight + 'px');
|
|
||||||
this.$drop.find('.multiple').css('width', this.options.multipleWidth + 'px');
|
|
||||||
|
|
||||||
this.$searchInput = this.$drop.find('.ms-search input');
|
|
||||||
this.$selectAll = this.$drop.find('input[' + this.selectAllName + ']');
|
|
||||||
this.$selectGroups = this.$drop.find('input[' + this.selectGroupName + ']');
|
|
||||||
this.$selectItems = this.$drop.find('input[' + this.selectItemName + ']:enabled');
|
|
||||||
this.$disableItems = this.$drop.find('input[' + this.selectItemName + ']:disabled');
|
|
||||||
this.$noResults = this.$drop.find('.ms-no-results');
|
|
||||||
|
|
||||||
this.events();
|
|
||||||
this.updateSelectAll(true);
|
|
||||||
this.update(true);
|
|
||||||
|
|
||||||
if (this.options.isOpen) {
|
|
||||||
this.open();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
optionToHtml: function (i, elm, group, groupDisabled) {
|
|
||||||
var that = this,
|
|
||||||
$elm = $(elm),
|
|
||||||
classes = $elm.attr('class') || '',
|
|
||||||
title = sprintf('title="%s"', $elm.attr('title')),
|
|
||||||
multiple = this.options.multiple ? 'multiple' : '',
|
|
||||||
disabled,
|
|
||||||
type = this.options.single ? 'radio' : 'checkbox';
|
|
||||||
|
|
||||||
if ($elm.is('option')) {
|
|
||||||
var value = $elm.val(),
|
|
||||||
text = that.options.textTemplate($elm),
|
|
||||||
selected = $elm.prop('selected'),
|
|
||||||
style = sprintf('style="%s"', this.options.styler(value)),
|
|
||||||
$el;
|
|
||||||
|
|
||||||
disabled = groupDisabled || $elm.prop('disabled');
|
|
||||||
|
|
||||||
$el = $([
|
|
||||||
sprintf('<li class="%s %s" %s %s>', multiple, classes, title, style),
|
|
||||||
sprintf('<label class="%s">', disabled ? 'disabled' : ''),
|
|
||||||
sprintf('<input type="%s" %s%s%s%s>',
|
|
||||||
type, this.selectItemName,
|
|
||||||
selected ? ' checked="checked"' : '',
|
|
||||||
disabled ? ' disabled="disabled"' : '',
|
|
||||||
sprintf(' data-group="%s"', group)),
|
|
||||||
sprintf('<span>%s</span>', text),
|
|
||||||
'</label>',
|
|
||||||
'</li>'
|
|
||||||
].join(''));
|
|
||||||
$el.find('input').val(value);
|
|
||||||
return $el;
|
|
||||||
}
|
|
||||||
if ($elm.is('optgroup')) {
|
|
||||||
var label = that.options.labelTemplate($elm),
|
|
||||||
$group = $('<div/>');
|
|
||||||
|
|
||||||
group = 'group_' + i;
|
|
||||||
disabled = $elm.prop('disabled');
|
|
||||||
|
|
||||||
$group.append([
|
|
||||||
'<li class="group">',
|
|
||||||
sprintf('<label class="optgroup %s" data-group="%s">', disabled ? 'disabled' : '', group),
|
|
||||||
this.options.hideOptgroupCheckboxes || this.options.single ? '' :
|
|
||||||
sprintf('<input type="checkbox" %s %s>',
|
|
||||||
this.selectGroupName, disabled ? 'disabled="disabled"' : ''),
|
|
||||||
label,
|
|
||||||
'</label>',
|
|
||||||
'</li>'
|
|
||||||
].join(''));
|
|
||||||
|
|
||||||
$.each($elm.children(), function (i, elm) {
|
|
||||||
$group.append(that.optionToHtml(i, elm, group, disabled));
|
|
||||||
});
|
|
||||||
return $group.html();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
events: function () {
|
|
||||||
var that = this,
|
|
||||||
toggleOpen = function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
that[that.options.isOpen ? 'close' : 'open']();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.$label) {
|
|
||||||
this.$label.off('click').on('click', function (e) {
|
|
||||||
if (e.target.nodeName.toLowerCase() !== 'label' || e.target !== this) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
toggleOpen(e);
|
|
||||||
if (!that.options.filter || !that.options.isOpen) {
|
|
||||||
that.focus();
|
|
||||||
}
|
|
||||||
e.stopPropagation(); // Causes lost focus otherwise
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$choice.off('click').on('click', toggleOpen)
|
|
||||||
.off('focus').on('focus', this.options.onFocus)
|
|
||||||
.off('blur').on('blur', this.options.onBlur);
|
|
||||||
|
|
||||||
this.$parent.off('keydown').on('keydown', function (e) {
|
|
||||||
switch (e.which) {
|
|
||||||
case 27: // esc key
|
|
||||||
that.close();
|
|
||||||
that.$choice.focus();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$searchInput.off('keydown').on('keydown',function (e) {
|
|
||||||
// Ensure shift-tab causes lost focus from filter as with clicking away
|
|
||||||
if (e.keyCode === 9 && e.shiftKey) {
|
|
||||||
that.close();
|
|
||||||
}
|
|
||||||
}).off('keyup').on('keyup', function (e) {
|
|
||||||
// enter or space
|
|
||||||
// Avoid selecting/deselecting if no choices made
|
|
||||||
if (that.options.filterAcceptOnEnter && (e.which === 13 || e.which == 32) && that.$searchInput.val()) {
|
|
||||||
that.$selectAll.click();
|
|
||||||
that.close();
|
|
||||||
that.focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
that.filter();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$selectAll.off('click').on('click', function () {
|
|
||||||
var checked = $(this).prop('checked'),
|
|
||||||
$items = that.$selectItems.filter(':visible');
|
|
||||||
|
|
||||||
if ($items.length === that.$selectItems.length) {
|
|
||||||
that[checked ? 'checkAll' : 'uncheckAll']();
|
|
||||||
} else { // when the filter option is true
|
|
||||||
that.$selectGroups.prop('checked', checked);
|
|
||||||
$items.prop('checked', checked);
|
|
||||||
that.options[checked ? 'onCheckAll' : 'onUncheckAll']();
|
|
||||||
that.update();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.$selectGroups.off('click').on('click', function () {
|
|
||||||
var group = $(this).parent().attr('data-group'),
|
|
||||||
$items = that.$selectItems.filter(':visible'),
|
|
||||||
$children = $items.filter(sprintf('[data-group="%s"]', group)),
|
|
||||||
checked = $children.length !== $children.filter(':checked').length;
|
|
||||||
|
|
||||||
$children.prop('checked', checked);
|
|
||||||
that.updateSelectAll();
|
|
||||||
that.update();
|
|
||||||
that.options.onOptgroupClick({
|
|
||||||
label: $(this).parent().text(),
|
|
||||||
checked: checked,
|
|
||||||
children: $children.get(),
|
|
||||||
instance: that
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this.$selectItems.off('click').on('click', function () {
|
|
||||||
that.updateSelectAll();
|
|
||||||
that.update();
|
|
||||||
that.updateOptGroupSelect();
|
|
||||||
that.options.onClick({
|
|
||||||
label: $(this).parent().text(),
|
|
||||||
value: $(this).val(),
|
|
||||||
checked: $(this).prop('checked'),
|
|
||||||
instance: that
|
|
||||||
});
|
|
||||||
|
|
||||||
if (that.options.single && that.options.isOpen && !that.options.keepOpen) {
|
|
||||||
that.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (that.options.single) {
|
|
||||||
var clickedVal = $(this).val();
|
|
||||||
that.$selectItems.filter(function() {
|
|
||||||
return $(this).val() !== clickedVal;
|
|
||||||
}).each(function() {
|
|
||||||
$(this).prop('checked', false);
|
|
||||||
});
|
|
||||||
that.update();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
open: function () {
|
|
||||||
if (this.$choice.hasClass('disabled')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.options.isOpen = true;
|
|
||||||
this.$choice.find('>div').addClass('open');
|
|
||||||
this.$drop[this.animateMethod('show')]();
|
|
||||||
|
|
||||||
// fix filter bug: no results show
|
|
||||||
this.$selectAll.parent().show();
|
|
||||||
this.$noResults.hide();
|
|
||||||
|
|
||||||
// Fix #77: 'All selected' when no options
|
|
||||||
if (!this.$el.children().length) {
|
|
||||||
this.$selectAll.parent().hide();
|
|
||||||
this.$noResults.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.options.container) {
|
|
||||||
var offset = this.$drop.offset();
|
|
||||||
this.$drop.appendTo($(this.options.container));
|
|
||||||
this.$drop.offset({
|
|
||||||
top: offset.top,
|
|
||||||
left: offset.left
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.options.filter) {
|
|
||||||
this.$searchInput.val('');
|
|
||||||
this.$searchInput.focus();
|
|
||||||
this.filter();
|
|
||||||
}
|
|
||||||
this.options.onOpen();
|
|
||||||
},
|
|
||||||
|
|
||||||
close: function () {
|
|
||||||
this.options.isOpen = false;
|
|
||||||
this.$choice.find('>div').removeClass('open');
|
|
||||||
this.$drop[this.animateMethod('hide')]();
|
|
||||||
if (this.options.container) {
|
|
||||||
this.$parent.append(this.$drop);
|
|
||||||
this.$drop.css({
|
|
||||||
'top': 'auto',
|
|
||||||
'left': 'auto'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.options.onClose();
|
|
||||||
},
|
|
||||||
|
|
||||||
animateMethod: function (method) {
|
|
||||||
var methods = {
|
|
||||||
show: {
|
|
||||||
fade: 'fadeIn',
|
|
||||||
slide: 'slideDown'
|
|
||||||
},
|
|
||||||
hide: {
|
|
||||||
fade: 'fadeOut',
|
|
||||||
slide: 'slideUp'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return methods[method][this.options.animate] || method;
|
|
||||||
},
|
|
||||||
|
|
||||||
update: function (isInit) {
|
|
||||||
var selects = this.options.displayValues ? this.getSelects() : this.getSelects('text'),
|
|
||||||
$span = this.$choice.find('>span'),
|
|
||||||
sl = selects.length;
|
|
||||||
|
|
||||||
if (sl === 0) {
|
|
||||||
$span.addClass('placeholder').html(this.options.placeholder);
|
|
||||||
} else if (this.options.allSelected && sl === this.$selectItems.length + this.$disableItems.length) {
|
|
||||||
$span.removeClass('placeholder').html(this.options.allSelected);
|
|
||||||
} else if (this.options.ellipsis && sl > this.options.minimumCountSelected) {
|
|
||||||
$span.removeClass('placeholder').text(selects.slice(0, this.options.minimumCountSelected)
|
|
||||||
.join(this.options.delimiter) + '...');
|
|
||||||
} else if (this.options.countSelected && sl > this.options.minimumCountSelected) {
|
|
||||||
$span.removeClass('placeholder').html(this.options.countSelected
|
|
||||||
.replace('#', selects.length)
|
|
||||||
.replace('%', this.$selectItems.length + this.$disableItems.length));
|
|
||||||
} else {
|
|
||||||
$span.removeClass('placeholder').text(selects.join(this.options.delimiter));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.options.addTitle) {
|
|
||||||
$span.prop('title', this.getSelects('text'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// set selects to select
|
|
||||||
this.$el.val(this.getSelects()).trigger('change');
|
|
||||||
|
|
||||||
// add selected class to selected li
|
|
||||||
this.$drop.find('li').removeClass('selected');
|
|
||||||
this.$drop.find('input:checked').each(function () {
|
|
||||||
$(this).parents('li').first().addClass('selected');
|
|
||||||
});
|
|
||||||
|
|
||||||
// trigger <select> change event
|
|
||||||
if (!isInit) {
|
|
||||||
this.$el.trigger('change');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateSelectAll: function (isInit) {
|
|
||||||
var $items = this.$selectItems;
|
|
||||||
|
|
||||||
if (!isInit) {
|
|
||||||
$items = $items.filter(':visible');
|
|
||||||
}
|
|
||||||
this.$selectAll.prop('checked', $items.length &&
|
|
||||||
$items.length === $items.filter(':checked').length);
|
|
||||||
if (!isInit && this.$selectAll.prop('checked')) {
|
|
||||||
this.options.onCheckAll();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateOptGroupSelect: function () {
|
|
||||||
var $items = this.$selectItems.filter(':visible');
|
|
||||||
$.each(this.$selectGroups, function (i, val) {
|
|
||||||
var group = $(val).parent().attr('data-group'),
|
|
||||||
$children = $items.filter(sprintf('[data-group="%s"]', group));
|
|
||||||
$(val).prop('checked', $children.length &&
|
|
||||||
$children.length === $children.filter(':checked').length);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
//value or text, default: 'value'
|
|
||||||
getSelects: function (type) {
|
|
||||||
var that = this,
|
|
||||||
texts = [],
|
|
||||||
values = [];
|
|
||||||
this.$drop.find(sprintf('input[%s]:checked', this.selectItemName)).each(function () {
|
|
||||||
texts.push($(this).parents('li').first().text());
|
|
||||||
values.push($(this).val());
|
|
||||||
});
|
|
||||||
|
|
||||||
if (type === 'text' && this.$selectGroups.length) {
|
|
||||||
texts = [];
|
|
||||||
this.$selectGroups.each(function () {
|
|
||||||
var html = [],
|
|
||||||
text = $.trim($(this).parent().text()),
|
|
||||||
group = $(this).parent().data('group'),
|
|
||||||
$children = that.$drop.find(sprintf('[%s][data-group="%s"]', that.selectItemName, group)),
|
|
||||||
$selected = $children.filter(':checked');
|
|
||||||
|
|
||||||
if (!$selected.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.push('[');
|
|
||||||
html.push(text);
|
|
||||||
if ($children.length > $selected.length) {
|
|
||||||
var list = [];
|
|
||||||
$selected.each(function () {
|
|
||||||
list.push($(this).parent().text());
|
|
||||||
});
|
|
||||||
html.push(': ' + list.join(', '));
|
|
||||||
}
|
|
||||||
html.push(']');
|
|
||||||
texts.push(html.join(''));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return type === 'text' ? texts : values;
|
|
||||||
},
|
|
||||||
|
|
||||||
setSelects: function (values) {
|
|
||||||
var that = this;
|
|
||||||
this.$selectItems.prop('checked', false);
|
|
||||||
this.$disableItems.prop('checked', false);
|
|
||||||
$.each(values, function (i, value) {
|
|
||||||
that.$selectItems.filter(sprintf('[value="%s"]', value)).prop('checked', true);
|
|
||||||
that.$disableItems.filter(sprintf('[value="%s"]', value)).prop('checked', true);
|
|
||||||
});
|
|
||||||
this.$selectAll.prop('checked', this.$selectItems.length ===
|
|
||||||
this.$selectItems.filter(':checked').length + this.$disableItems.filter(':checked').length);
|
|
||||||
|
|
||||||
$.each(that.$selectGroups, function (i, val) {
|
|
||||||
var group = $(val).parent().attr('data-group'),
|
|
||||||
$children = that.$selectItems.filter('[data-group="' + group + '"]');
|
|
||||||
$(val).prop('checked', $children.length &&
|
|
||||||
$children.length === $children.filter(':checked').length);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
enable: function () {
|
|
||||||
this.$choice.removeClass('disabled');
|
|
||||||
},
|
|
||||||
|
|
||||||
disable: function () {
|
|
||||||
this.$choice.addClass('disabled');
|
|
||||||
},
|
|
||||||
|
|
||||||
checkAll: function () {
|
|
||||||
this.$selectItems.prop('checked', true);
|
|
||||||
this.$selectGroups.prop('checked', true);
|
|
||||||
this.$selectAll.prop('checked', true);
|
|
||||||
this.update();
|
|
||||||
this.options.onCheckAll();
|
|
||||||
},
|
|
||||||
|
|
||||||
uncheckAll: function () {
|
|
||||||
this.$selectItems.prop('checked', false);
|
|
||||||
this.$selectGroups.prop('checked', false);
|
|
||||||
this.$selectAll.prop('checked', false);
|
|
||||||
this.update();
|
|
||||||
this.options.onUncheckAll();
|
|
||||||
},
|
|
||||||
|
|
||||||
focus: function () {
|
|
||||||
this.$choice.focus();
|
|
||||||
this.options.onFocus();
|
|
||||||
},
|
|
||||||
|
|
||||||
blur: function () {
|
|
||||||
this.$choice.blur();
|
|
||||||
this.options.onBlur();
|
|
||||||
},
|
|
||||||
|
|
||||||
refresh: function () {
|
|
||||||
this.init();
|
|
||||||
},
|
|
||||||
|
|
||||||
filter: function () {
|
|
||||||
var that = this,
|
|
||||||
text = $.trim(this.$searchInput.val()).toLowerCase();
|
|
||||||
|
|
||||||
if (text.length === 0) {
|
|
||||||
this.$selectAll.parent().show();
|
|
||||||
this.$selectItems.parent().show();
|
|
||||||
this.$disableItems.parent().show();
|
|
||||||
this.$selectGroups.parent().show();
|
|
||||||
this.$noResults.hide();
|
|
||||||
} else {
|
|
||||||
this.$selectItems.each(function () {
|
|
||||||
var $parent = $(this).parent();
|
|
||||||
$parent[removeDiacritics($parent.text().toLowerCase()).indexOf(removeDiacritics(text)) < 0 ? 'hide' : 'show']();
|
|
||||||
});
|
|
||||||
this.$disableItems.parent().hide();
|
|
||||||
this.$selectGroups.each(function () {
|
|
||||||
var $parent = $(this).parent();
|
|
||||||
var group = $parent.attr('data-group'),
|
|
||||||
$items = that.$selectItems.filter(':visible');
|
|
||||||
$parent[$items.filter(sprintf('[data-group="%s"]', group)).length ? 'show' : 'hide']();
|
|
||||||
});
|
|
||||||
|
|
||||||
//Check if no matches found
|
|
||||||
if (this.$selectItems.parent().filter(':visible').length) {
|
|
||||||
this.$selectAll.parent().show();
|
|
||||||
this.$noResults.hide();
|
|
||||||
} else {
|
|
||||||
this.$selectAll.parent().hide();
|
|
||||||
this.$noResults.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.updateOptGroupSelect();
|
|
||||||
this.updateSelectAll();
|
|
||||||
this.options.onFilter(text);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$.fn.multipleSelect = function () {
|
|
||||||
var option = arguments[0],
|
|
||||||
args = arguments,
|
|
||||||
|
|
||||||
value,
|
|
||||||
allowedMethods = [
|
|
||||||
'getSelects', 'setSelects',
|
|
||||||
'enable', 'disable',
|
|
||||||
'open', 'close',
|
|
||||||
'checkAll', 'uncheckAll',
|
|
||||||
'focus', 'blur',
|
|
||||||
'refresh', 'close'
|
|
||||||
];
|
|
||||||
|
|
||||||
this.each(function () {
|
|
||||||
var $this = $(this),
|
|
||||||
data = $this.data('multipleSelect'),
|
|
||||||
options = $.extend({}, $.fn.multipleSelect.defaults,
|
|
||||||
$this.data(), typeof option === 'object' && option);
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
data = new MultipleSelect($this, options);
|
|
||||||
$this.data('multipleSelect', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof option === 'string') {
|
|
||||||
if ($.inArray(option, allowedMethods) < 0) {
|
|
||||||
throw 'Unknown method: ' + option;
|
|
||||||
}
|
|
||||||
value = data[option](args[1]);
|
|
||||||
} else {
|
|
||||||
data.init();
|
|
||||||
if (args[1]) {
|
|
||||||
value = data[args[1]].apply(data, [].slice.call(args, 2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return typeof value !== 'undefined' ? value : this;
|
|
||||||
};
|
|
||||||
|
|
||||||
$.fn.multipleSelect.defaults = {
|
|
||||||
name: '',
|
|
||||||
isOpen: false,
|
|
||||||
placeholder: '',
|
|
||||||
selectAll: true,
|
|
||||||
selectAllDelimiter: ['[', ']'],
|
|
||||||
minimumCountSelected: 3,
|
|
||||||
ellipsis: false,
|
|
||||||
multiple: false,
|
|
||||||
multipleWidth: 80,
|
|
||||||
single: false,
|
|
||||||
filter: false,
|
|
||||||
width: undefined,
|
|
||||||
dropWidth: undefined,
|
|
||||||
maxHeight: 250,
|
|
||||||
container: null,
|
|
||||||
position: 'bottom',
|
|
||||||
keepOpen: false,
|
|
||||||
animate: 'none', // 'none', 'fade', 'slide'
|
|
||||||
displayValues: false,
|
|
||||||
delimiter: ', ',
|
|
||||||
addTitle: false,
|
|
||||||
filterAcceptOnEnter: false,
|
|
||||||
hideOptgroupCheckboxes: false,
|
|
||||||
|
|
||||||
selectAllText: 'Tout sélectionner',
|
|
||||||
allSelected: 'Tout sélectionné',
|
|
||||||
countSelected: '# sur % sélectionnés',
|
|
||||||
noMatchesFound: 'Introuvable',
|
|
||||||
|
|
||||||
styler: function () {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
textTemplate: function ($elm) {
|
|
||||||
return $elm.html();
|
|
||||||
},
|
|
||||||
labelTemplate: function ($elm) {
|
|
||||||
return $elm.attr('label');
|
|
||||||
},
|
|
||||||
|
|
||||||
onOpen: function () {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
onClose: function () {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
onCheckAll: function () {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
onUncheckAll: function () {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
onFocus: function () {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
onBlur: function () {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
onOptgroupClick: function () {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
onClick: function () {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
onFilter: function () {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})(jQuery);
|
|
@ -38,7 +38,21 @@ $( function() {
|
|||||||
$("#quick_notif li").click(function () {
|
$("#quick_notif li").click(function () {
|
||||||
$(this).hide();
|
$(this).hide();
|
||||||
})
|
})
|
||||||
} );
|
});
|
||||||
|
|
||||||
|
function createQuickNotif(msg) {
|
||||||
|
const el = document.createElement('li')
|
||||||
|
el.textContent = msg
|
||||||
|
el.addEventListener('click', () => el.parentNode.removeChild(el))
|
||||||
|
document.getElementById('quick_notif').appendChild(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteQuickNotifs() {
|
||||||
|
const el = document.getElementById('quick_notif')
|
||||||
|
while (el.firstChild) {
|
||||||
|
el.removeChild(el.firstChild)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function display_notif() {
|
function display_notif() {
|
||||||
$('#header_notif').toggle().parent().toggleClass("white");
|
$('#header_notif').toggle().parent().toggleClass("white");
|
||||||
|
1
core/static/core/js/vue.global.prod.js
Normal file
@ -1,191 +0,0 @@
|
|||||||
/**
|
|
||||||
* @author zhixin wen <wenzhixin2010@gmail.com>
|
|
||||||
*/
|
|
||||||
|
|
||||||
.ms-parent {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-choice {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 26px;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid #aaa;
|
|
||||||
text-align: left;
|
|
||||||
white-space: nowrap;
|
|
||||||
line-height: 26px;
|
|
||||||
color: #444;
|
|
||||||
text-decoration: none;
|
|
||||||
-webkit-border-radius: 4px;
|
|
||||||
-moz-border-radius: 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-choice.disabled {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
background-image: none;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-choice > span {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 20px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
display: block;
|
|
||||||
padding-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-choice > span.placeholder {
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-choice > div {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 20px;
|
|
||||||
height: 25px;
|
|
||||||
background: url('multiple-select.png') left top no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-choice > div.open {
|
|
||||||
background: url('multiple-select.png') right top no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-drop {
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
display: none;
|
|
||||||
margin-top: -1px;
|
|
||||||
padding: 0;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1000;
|
|
||||||
background: #fff;
|
|
||||||
color: #000;
|
|
||||||
border: 1px solid #aaa;
|
|
||||||
-webkit-border-radius: 4px;
|
|
||||||
-moz-border-radius: 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-drop.bottom {
|
|
||||||
top: 100%;
|
|
||||||
-webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
|
|
||||||
-moz-box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
|
|
||||||
box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-drop.top {
|
|
||||||
bottom: 100%;
|
|
||||||
-webkit-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
|
|
||||||
-moz-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
|
|
||||||
box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-search {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0;
|
|
||||||
min-height: 26px;
|
|
||||||
padding: 4px;
|
|
||||||
position: relative;
|
|
||||||
white-space: nowrap;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 10000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-search input {
|
|
||||||
width: 100%;
|
|
||||||
height: auto !important;
|
|
||||||
min-height: 24px;
|
|
||||||
padding: 0 20px 0 5px;
|
|
||||||
margin: 0;
|
|
||||||
outline: 0;
|
|
||||||
font-family: sans-serif;
|
|
||||||
font-size: 1em;
|
|
||||||
border: 1px solid #aaa;
|
|
||||||
-webkit-border-radius: 0;
|
|
||||||
-moz-border-radius: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
-webkit-box-shadow: none;
|
|
||||||
-moz-box-shadow: none;
|
|
||||||
box-shadow: none;
|
|
||||||
background: #fff url('multiple-select.png') no-repeat 100% -22px;
|
|
||||||
background: url('multiple-select.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee));
|
|
||||||
background: url('multiple-select.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%);
|
|
||||||
background: url('multiple-select.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%);
|
|
||||||
background: url('multiple-select.png') no-repeat 100% -22px, -o-linear-gradient(bottom, white 85%, #eeeeee 99%);
|
|
||||||
background: url('multiple-select.png') no-repeat 100% -22px, -ms-linear-gradient(top, #ffffff 85%, #eeeeee 99%);
|
|
||||||
background: url('multiple-select.png') no-repeat 100% -22px, linear-gradient(top, #ffffff 85%, #eeeeee 99%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-search, .ms-search input {
|
|
||||||
-webkit-box-sizing: border-box;
|
|
||||||
-khtml-box-sizing: border-box;
|
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
-ms-box-sizing: border-box;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-drop ul {
|
|
||||||
overflow: auto;
|
|
||||||
margin: 0;
|
|
||||||
padding: 5px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-drop ul > li {
|
|
||||||
list-style: none;
|
|
||||||
display: list-item;
|
|
||||||
background-image: none;
|
|
||||||
position: static;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-drop ul > li .disabled {
|
|
||||||
opacity: .35;
|
|
||||||
filter: Alpha(Opacity=35);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-drop ul > li.multiple {
|
|
||||||
display: block;
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-drop ul > li.group {
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-drop ul > li.multiple label {
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-drop ul > li label {
|
|
||||||
font-weight: normal;
|
|
||||||
display: block;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-drop ul > li label.optgroup {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-drop input[type="checkbox"] {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-drop .ms-no-results {
|
|
||||||
display: none;
|
|
||||||
}
|
|
Before Width: | Height: | Size: 3.3 KiB |
@ -28,7 +28,13 @@ $twitblue: hsl(206, 82%, 63%);
|
|||||||
|
|
||||||
$shadow-color: rgb(223, 223, 223);
|
$shadow-color: rgb(223, 223, 223);
|
||||||
|
|
||||||
$background-bouton-color: hsl(0, 0%, 90%);
|
$background-button-color: hsl(0, 0%, 95%);
|
||||||
|
|
||||||
|
/*--------------------------MEDIA QUERY HELPERS------------------------*/
|
||||||
|
$small-devices: 576px;
|
||||||
|
$medium-devices: 768px;
|
||||||
|
$large-devices: 992px;
|
||||||
|
$extra-large-devices: 1200px;
|
||||||
|
|
||||||
/*--------------------------------GENERAL------------------------------*/
|
/*--------------------------------GENERAL------------------------------*/
|
||||||
|
|
||||||
@ -41,10 +47,11 @@ body {
|
|||||||
input[type=button], input[type=submit], input[type=reset],input[type=file] {
|
input[type=button], input[type=submit], input[type=reset],input[type=file] {
|
||||||
border: none;
|
border: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: $background-bouton-color;
|
background-color: $background-button-color;
|
||||||
padding: 10px;
|
padding: 0.4em;
|
||||||
|
margin: 0.1em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 16px;
|
font-size: 1.2em;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: $shadow-color 0px 0px 1px;
|
box-shadow: $shadow-color 0px 0px 1px;
|
||||||
@ -56,9 +63,10 @@ input[type=button], input[type=submit], input[type=reset],input[type=file] {
|
|||||||
button{
|
button{
|
||||||
border: none;
|
border: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: $background-bouton-color;
|
background-color: $background-button-color;
|
||||||
padding: 10px;
|
padding: 0.4em;
|
||||||
font-size: 14px;
|
margin: 0.1em;
|
||||||
|
font-size: 1.18em;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
box-shadow: $shadow-color 0px 0px 1px;
|
box-shadow: $shadow-color 0px 0px 1px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -69,24 +77,26 @@ button{
|
|||||||
input,textarea[type=text],[type=number]{
|
input,textarea[type=text],[type=number]{
|
||||||
border: none;
|
border: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: $background-bouton-color;
|
background-color: $background-button-color;
|
||||||
padding: 7px;
|
padding: 0.4em;
|
||||||
font-size: 16px;
|
margin: 0.1em;
|
||||||
|
font-size: 1.2em;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
max-width: 95%;
|
||||||
}
|
}
|
||||||
textarea{
|
textarea{
|
||||||
border: none;
|
border: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: $background-bouton-color;
|
background-color: $background-button-color;
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
font-size: 16px;
|
font-size: 1.2em;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
select{
|
select{
|
||||||
border: none;
|
border: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 15px;
|
font-size: 1.2em;
|
||||||
background-color: $background-bouton-color;
|
background-color: $background-button-color;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -124,9 +134,10 @@ a {
|
|||||||
|
|
||||||
#header_language_chooser {
|
#header_language_chooser {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.2em;
|
top: 2em;
|
||||||
right: 0.5em;
|
left: 0.5em;
|
||||||
width: 3%;
|
width: 3%;
|
||||||
|
min-width: 2.2em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
input {
|
input {
|
||||||
display: block;
|
display: block;
|
||||||
@ -151,9 +162,6 @@ header {
|
|||||||
border-radius: 0px 0px 10px 10px;
|
border-radius: 0px 0px 10px 10px;
|
||||||
|
|
||||||
#header_logo {
|
#header_logo {
|
||||||
display: inline-block;
|
|
||||||
flex: none;
|
|
||||||
background-size: 100% 100%;
|
|
||||||
background-color: $white-color;
|
background-color: $white-color;
|
||||||
padding: 0.2em;
|
padding: 0.2em;
|
||||||
border-radius: 0px 0px 0px 9px;
|
border-radius: 0px 0px 0px 9px;
|
||||||
@ -163,11 +171,19 @@ header {
|
|||||||
margin: 0px;
|
margin: 0px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 70%;
|
||||||
|
max-height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#header_connect_links {
|
#header_connect_links {
|
||||||
margin: 0.6em 0.6em 0em auto;
|
margin: 0.6em 0.6em 0em auto;
|
||||||
|
padding: 0.2em;
|
||||||
color: $white-color;
|
color: $white-color;
|
||||||
form {
|
form {
|
||||||
display: inline;
|
display: inline;
|
||||||
@ -184,6 +200,7 @@ header {
|
|||||||
#header_bar {
|
#header_bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: auto;
|
flex: auto;
|
||||||
|
flex-wrap: wrap;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@ -197,7 +214,6 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#header_bars_infos {
|
#header_bars_infos {
|
||||||
width: 35ch;
|
|
||||||
flex: initial;
|
flex: initial;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
margin: 0.2em 0.2em;
|
margin: 0.2em 0.2em;
|
||||||
@ -207,12 +223,15 @@ header {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
flex: auto;
|
flex: auto;
|
||||||
margin: 0.8em 0em;
|
margin: 0.8em 0em;
|
||||||
|
input {
|
||||||
|
width: 14ch;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#header_user_links {
|
#header_user_links {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 120ch;
|
|
||||||
flex: initial;
|
flex: initial;
|
||||||
|
flex-wrap: wrap;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
margin: 0em;
|
margin: 0em;
|
||||||
div {
|
div {
|
||||||
@ -281,42 +300,34 @@ header {
|
|||||||
|
|
||||||
#info_boxes {
|
#info_boxes {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
margin: 1em auto;
|
margin: 1em auto;
|
||||||
p {
|
|
||||||
margin: 0px;
|
|
||||||
padding: 7px;
|
|
||||||
}
|
|
||||||
#alert_box, #info_box {
|
#alert_box, #info_box {
|
||||||
font-size: 14px;
|
flex: 49%;
|
||||||
display: inline-block;
|
font-size: 0.9em;
|
||||||
flex: auto;
|
margin: 0.2em;
|
||||||
padding: 2px;
|
border-radius: 0.6em;
|
||||||
margin: 0.2em 1.5%;
|
.markdown {
|
||||||
min-width: 10%;
|
margin: 0.5em;
|
||||||
max-width: 46%;
|
}
|
||||||
min-height: 20px;
|
|
||||||
&:before {
|
&:before {
|
||||||
float: left;
|
font-family: FontAwesome;
|
||||||
|
font-size: 4em;
|
||||||
|
float: right;
|
||||||
margin: 0.2em;
|
margin: 0.2em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#info_box {
|
#info_box {
|
||||||
border-radius: 10px;
|
|
||||||
background: $primary-neutral-light-color;
|
background: $primary-neutral-light-color;
|
||||||
&:before {
|
&:before {
|
||||||
font-family: FontAwesome;
|
|
||||||
font-size: 4em;
|
|
||||||
content: "\f05a";
|
content: "\f05a";
|
||||||
color: hsl(210, 100%, 56%);
|
color: hsl(210, 100%, 56%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#alert_box {
|
#alert_box {
|
||||||
border-radius: 10px;
|
|
||||||
background: $second-color;
|
background: $second-color;
|
||||||
&:before {
|
&:before {
|
||||||
font-family: FontAwesome;
|
|
||||||
font-size: 4em;
|
|
||||||
content: "\f06a";
|
content: "\f06a";
|
||||||
color: $white-color;
|
color: $white-color;
|
||||||
}
|
}
|
||||||
@ -339,7 +350,7 @@ header {
|
|||||||
a {
|
a {
|
||||||
flex: auto;
|
flex: auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px;
|
padding: 1.5em;
|
||||||
color: $white-color;
|
color: $white-color;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
@ -452,6 +463,8 @@ header {
|
|||||||
|
|
||||||
/*---------------------------------NEWS--------------------------------*/
|
/*---------------------------------NEWS--------------------------------*/
|
||||||
#news {
|
#news {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
.news_column {
|
.news_column {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
@ -461,11 +474,13 @@ header {
|
|||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
#right_column {
|
#right_column {
|
||||||
width: 20%;
|
flex: 20%;
|
||||||
float: right;
|
float: right;
|
||||||
|
margin: 0.2em;
|
||||||
}
|
}
|
||||||
#left_column {
|
#left_column {
|
||||||
width: 79%;
|
flex: 79%;
|
||||||
|
margin: 0.2em;
|
||||||
h3 {
|
h3 {
|
||||||
background: $second-color;
|
background: $second-color;
|
||||||
box-shadow: $shadow-color 1px 1px 1px;
|
box-shadow: $shadow-color 1px 1px 1px;
|
||||||
@ -478,6 +493,11 @@ header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@media screen and (max-width: $small-devices){
|
||||||
|
#left_column, #right_column {
|
||||||
|
flex: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* AGENDA/BIRTHDAYS */
|
/* AGENDA/BIRTHDAYS */
|
||||||
#agenda,#birthdays {
|
#agenda,#birthdays {
|
||||||
@ -685,6 +705,12 @@ header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $small-devices){
|
||||||
|
#page {
|
||||||
|
width: 98%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#news_details {
|
#news_details {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
@ -717,7 +743,7 @@ header {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 16px;
|
font-size: 1.2em;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
float: right;
|
float: right;
|
||||||
display: block;
|
display: block;
|
||||||
@ -1105,33 +1131,36 @@ u, .underline {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
#basket {
|
|
||||||
width: 40%;
|
|
||||||
background: $primary-neutral-light-color;
|
|
||||||
float: right;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#products {
|
|
||||||
width: 90%;
|
|
||||||
margin: 0px auto;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bar_ui {
|
#bar_ui {
|
||||||
float: left;
|
padding: 0.4em;
|
||||||
min-width: 57%;
|
display: flex;
|
||||||
}
|
flex-wrap: wrap;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
|
||||||
#user_info_container {}
|
#products {
|
||||||
|
flex-basis: 100%;
|
||||||
|
margin: 0.2em;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
#user_info {
|
#click_form {
|
||||||
float: right;
|
flex: auto;
|
||||||
padding: 5px;
|
margin: 0.2em;
|
||||||
width: 40%;
|
}
|
||||||
margin: 0px auto;
|
|
||||||
|
#user_info {
|
||||||
|
flex: auto;
|
||||||
|
padding: 0.5em;
|
||||||
|
margin: 0.2em;
|
||||||
|
height: 100%;
|
||||||
background: $secondary-neutral-light-color;
|
background: $secondary-neutral-light-color;
|
||||||
|
img {
|
||||||
|
max-width: 70%;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*-----------------------------USER PROFILE----------------------------*/
|
/*-----------------------------USER PROFILE----------------------------*/
|
||||||
@ -1163,6 +1192,7 @@ u, .underline {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
img {
|
img {
|
||||||
|
width: 5em;
|
||||||
margin: 0.5em;
|
margin: 0.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1206,6 +1236,11 @@ u, .underline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@media screen and (max-width: $small-devices){
|
||||||
|
#user_profile_infos, #user_profile_pictures {
|
||||||
|
flex-basis: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1406,6 +1441,7 @@ textarea {
|
|||||||
.search_bar {
|
.search_bar {
|
||||||
margin: 10px 0px;
|
margin: 10px 0px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
height: 20p;
|
height: 20p;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@ -1545,6 +1581,7 @@ footer {
|
|||||||
color: $white-color;
|
color: $white-color;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
background-color: $primary-neutral-dark-color;
|
background-color: $primary-neutral-dark-color;
|
||||||
box-shadow: $shadow-color 0px 0px 15px;
|
box-shadow: $shadow-color 0px 0px 15px;
|
||||||
a {
|
a {
|
||||||
@ -1564,7 +1601,6 @@ footer {
|
|||||||
form {
|
form {
|
||||||
margin: 0px auto;
|
margin: 0px auto;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
width: 60%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
@ -1668,3 +1704,512 @@ label {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --------------------------------------pedagogy-----------------------------------*/
|
||||||
|
|
||||||
|
$pedagogy-blue: #1bb9ea;
|
||||||
|
$pedagogy-orange: #ea7900;
|
||||||
|
$pedagogy-hover-blue: #0e97ce;
|
||||||
|
$pedagogy-light-blue: #caf0ff;
|
||||||
|
$pedagogy-white-text: #f0f0f0;
|
||||||
|
|
||||||
|
.pedagogy {
|
||||||
|
&.star-not-checked {
|
||||||
|
color : #f7f7f7;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
&.star-checked {
|
||||||
|
color: $pedagogy-orange;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.grade-without-star {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $large-devices){
|
||||||
|
&.star-not-checked {
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
&.star-checked {
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $small-devices){
|
||||||
|
&.grade-without-star {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
&.grade-with-star {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#dynamic_view {
|
||||||
|
font-size: 1.1em;
|
||||||
|
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
|
td {
|
||||||
|
text-align: center;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#search_form {
|
||||||
|
|
||||||
|
.search-form-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto;
|
||||||
|
grid-template-rows: auto auto auto;
|
||||||
|
grid-template-areas:
|
||||||
|
"action-bar action-bar"
|
||||||
|
"search-bar search-bar"
|
||||||
|
"radio-department radio-department"
|
||||||
|
"radio-credit-type radio-semester";
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
grid-area: action-bar;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
grid-area: search-bar;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 200px;
|
||||||
|
grid-template-rows: auto;
|
||||||
|
grid-template-areas: "search-bar-input search-bar-button";
|
||||||
|
|
||||||
|
@media screen and (max-width: $medium-devices){
|
||||||
|
grid-template-columns: auto auto;
|
||||||
|
grid-template-rows: auto;
|
||||||
|
grid-template-areas: "search-bar-input search-bar-button";
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $small-devices){
|
||||||
|
grid-template-columns: auto;
|
||||||
|
grid-template-rows: auto;
|
||||||
|
grid-template-areas: "search-bar-input";
|
||||||
|
|
||||||
|
.search-bar-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar-input {
|
||||||
|
grid-area: search-bar-input;
|
||||||
|
background: $pedagogy-light-blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar-button {
|
||||||
|
grid-area: search-bar-button;
|
||||||
|
background: $pedagogy-orange;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-department {
|
||||||
|
grid-area: radio-department;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-credit-type {
|
||||||
|
grid-area: radio-credit-type;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-semester {
|
||||||
|
grid-area: radio-semester;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-guide input[type="radio"],input[type="checkbox"] {
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
.radio-guide {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.radio-guide label {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: $pedagogy-blue;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.radio-guide input[type="radio"]:checked + label {
|
||||||
|
background-color: $pedagogy-orange;
|
||||||
|
}
|
||||||
|
.radio-guide input[type="checkbox"]:checked + label {
|
||||||
|
background-color: $pedagogy-orange;
|
||||||
|
}
|
||||||
|
.radio-guide label:hover {
|
||||||
|
background-color: $pedagogy-hover-blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#uv_detail {
|
||||||
|
color: #062f38;
|
||||||
|
|
||||||
|
.uv-quick-info-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 20% 20% 20% 20% auto;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
grid-template-areas:
|
||||||
|
"hours-cm hours-td hours-tp hours-te hours-the"
|
||||||
|
"department credit-type semester . ." ;
|
||||||
|
}
|
||||||
|
|
||||||
|
.department {
|
||||||
|
grid-area: department;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credit-type {
|
||||||
|
grid-area: credit-type;
|
||||||
|
}
|
||||||
|
|
||||||
|
.semester {
|
||||||
|
grid-area: semester;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hours-cm {
|
||||||
|
grid-area: hours-cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hours-td {
|
||||||
|
grid-area: hours-td;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hours-tp {
|
||||||
|
grid-area: hours-tp;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hours-te {
|
||||||
|
grid-area: hours-te;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hours-the {
|
||||||
|
grid-area: hours-the;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leave_comment_not_allowed {
|
||||||
|
p {
|
||||||
|
text-align: center;
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#leave_comment {
|
||||||
|
.leave-comment-grid-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 270px auto;
|
||||||
|
grid-template-rows: 100%;
|
||||||
|
grid-template-areas: "stars comment";
|
||||||
|
|
||||||
|
@media screen and (max-width: $large-devices){
|
||||||
|
grid-template-columns: 100%;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
grid-template-areas:
|
||||||
|
"stars"
|
||||||
|
"comment";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-accordion-content {
|
||||||
|
background-color: $white-color;
|
||||||
|
border-color: $pedagogy-orange;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-stars {
|
||||||
|
grid-area: stars;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-comment {
|
||||||
|
grid-area: comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-accordion-header {
|
||||||
|
background-color: $pedagogy-orange;
|
||||||
|
color: $pedagogy-white-text;
|
||||||
|
clip-path: polygon(0 0%, 0 100%, 30% 100%, 33% 0);
|
||||||
|
|
||||||
|
@media screen and (max-width: $large-devices){
|
||||||
|
clip-path: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-accordion-header-icon {
|
||||||
|
color: $pedagogy-white-text;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-stars {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="submit"] {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.uv-details-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 150px 100px auto;
|
||||||
|
grid-template-rows: 156px 1fr;
|
||||||
|
grid-template-areas:
|
||||||
|
"grade grade-stars uv-infos"
|
||||||
|
". . uv-infos";
|
||||||
|
|
||||||
|
@media screen and (max-width: $large-devices){
|
||||||
|
grid-template-columns: 50% 50%;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
grid-template-areas:
|
||||||
|
"grade grade-stars"
|
||||||
|
"uv-infos uv-infos";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade {
|
||||||
|
grid-area: grade;
|
||||||
|
color: $pedagogy-white-text;
|
||||||
|
background-color: $pedagogy-blue;
|
||||||
|
padding-right: 10px;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
text-align: right;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-stars {
|
||||||
|
grid-area: grade-stars;
|
||||||
|
color: $pedagogy-white-text;
|
||||||
|
background-color: $pedagogy-blue;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uv-infos {
|
||||||
|
grid-area: uv-infos;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 300px auto;
|
||||||
|
grid-template-rows: auto auto auto;
|
||||||
|
grid-template-areas:
|
||||||
|
"grade-block comment"
|
||||||
|
"grade-block info"
|
||||||
|
"comment-end-bar comment-end-bar";
|
||||||
|
margin-bottom: 30px;
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
@media screen and (max-width: $large-devices){
|
||||||
|
grid-template-columns: auto;
|
||||||
|
grid-template-rows: auto auto auto auto;
|
||||||
|
grid-template-areas:
|
||||||
|
"grade-block"
|
||||||
|
"comment"
|
||||||
|
"info"
|
||||||
|
"comment-end-bar"
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-block {
|
||||||
|
grid-area: grade-block;
|
||||||
|
width: 300px;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 150px 150px;
|
||||||
|
grid-template-rows: 156px auto;
|
||||||
|
grid-template-areas:
|
||||||
|
"grade-type grade-stars"
|
||||||
|
"grade-extension grade-extension";
|
||||||
|
grid-gap: 15px;
|
||||||
|
|
||||||
|
clip-path: polygon(0 0, 0 100%, 100% 100%, 100% 30px, 270px 0);
|
||||||
|
align-items: start;
|
||||||
|
|
||||||
|
background-color: $pedagogy-blue;
|
||||||
|
|
||||||
|
@media screen and (max-width: $large-devices){
|
||||||
|
grid-template-columns: 50% auto;
|
||||||
|
grid-template-rows: auto;
|
||||||
|
grid-template-areas:"grade-type grade-stars";
|
||||||
|
width: auto;
|
||||||
|
clip-path: none;
|
||||||
|
align-content: space-evenly;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-extension {
|
||||||
|
grid-area: grade-extension;
|
||||||
|
background-color: $pedagogy-blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-type {
|
||||||
|
grid-area: grade-type;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
color: $pedagogy-white-text;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-stars {
|
||||||
|
grid-area: grade-stars;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment {
|
||||||
|
grid-area: comment;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
grid-template-areas:
|
||||||
|
"anchor"
|
||||||
|
"markdown";
|
||||||
|
|
||||||
|
@media screen and (max-width: $large-devices){
|
||||||
|
border-left: solid;
|
||||||
|
border-right: solid;
|
||||||
|
border-color: $pedagogy-blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anchor {
|
||||||
|
grid-area: anchor;
|
||||||
|
text-align: right;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown {
|
||||||
|
grid-area: markdown;
|
||||||
|
|
||||||
|
min-height: 139px;
|
||||||
|
margin-top: 0px;
|
||||||
|
margin-right: 0px;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: justify;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
grid-area: info;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
|
||||||
|
@media screen and (max-width: $large-devices){
|
||||||
|
border-left: solid;
|
||||||
|
border-right: solid;
|
||||||
|
border-color: $pedagogy-blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-reported {
|
||||||
|
color: red;
|
||||||
|
float: left;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-end-bar {
|
||||||
|
grid-area: comment-end-bar;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 33% auto auto;
|
||||||
|
grid-template-rows: 2.5em;
|
||||||
|
grid-template-areas: "author date report";
|
||||||
|
|
||||||
|
background-color: $pedagogy-blue;
|
||||||
|
margin-top: -1px;
|
||||||
|
|
||||||
|
@media screen and (max-width: $large-devices){
|
||||||
|
grid-template-columns: auto;
|
||||||
|
grid-template-rows: auto auto auto;
|
||||||
|
grid-template-areas:
|
||||||
|
"report"
|
||||||
|
"date"
|
||||||
|
"author";
|
||||||
|
margin-top: 0px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author {
|
||||||
|
grid-area: author;
|
||||||
|
|
||||||
|
padding-top: 6px;
|
||||||
|
padding-left: 20px;
|
||||||
|
|
||||||
|
background-color: $pedagogy-orange;
|
||||||
|
clip-path: polygon(0 10px, 0 100%, 350px 200%, 300px 10px);
|
||||||
|
|
||||||
|
@media screen and (max-width: $large-devices){
|
||||||
|
clip-path: none;
|
||||||
|
padding: 0px;
|
||||||
|
padding-bottom: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $pedagogy-white-text;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: $pedagogy-hover-blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
grid-area: date;
|
||||||
|
color: $pedagogy-white-text;
|
||||||
|
|
||||||
|
@media screen and (max-width: $large-devices){
|
||||||
|
padding-bottom: 7px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.report {
|
||||||
|
grid-area: report;
|
||||||
|
justify-self: right;
|
||||||
|
padding-right: 30px;
|
||||||
|
padding-left: 30px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $pedagogy-white-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: $pedagogy-hover-blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $large-devices){
|
||||||
|
text-align: center;
|
||||||
|
justify-self: inherit;
|
||||||
|
padding-bottom: 7px;
|
||||||
|
background-color: $white-color;
|
||||||
|
|
||||||
|
border-left: solid;
|
||||||
|
border-right: solid;
|
||||||
|
border-color: $pedagogy-blue;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $black-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
281
core/static/election/election.scss
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
$padding: 1.5rem;
|
||||||
|
$padding_smaller: .5rem;
|
||||||
|
$gap: .25rem;
|
||||||
|
$border: .01rem solid black;
|
||||||
|
$min_col_width: 100px;
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: red !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: $gap;
|
||||||
|
|
||||||
|
> input,
|
||||||
|
> label {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.election_vote {
|
||||||
|
overflow-x: scroll !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.election_table {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
>.lists {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
>tr {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
>.column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: $padding;
|
||||||
|
border: $border;
|
||||||
|
border-collapse: collapse;
|
||||||
|
position: relative;
|
||||||
|
min-width: $min_col_width;
|
||||||
|
|
||||||
|
>a{
|
||||||
|
margin-left: $padding;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 25%;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: absolute;
|
||||||
|
right: $gap;
|
||||||
|
top: $gap;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
>.role {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
>tr {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
background-color: lightgrey;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: lightgrey;
|
||||||
|
}
|
||||||
|
|
||||||
|
>.role_title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 0;
|
||||||
|
padding: $padding;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
|
||||||
|
>.role_text {
|
||||||
|
>h4 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
>p {
|
||||||
|
margin-top: .5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
>.role_buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: $gap;
|
||||||
|
|
||||||
|
> button,
|
||||||
|
> button > i,
|
||||||
|
> a {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background-color: #e9e9e9;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 25%;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:hover > i {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> button {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> button[disabled] {
|
||||||
|
background-color: #eee;
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
>i,
|
||||||
|
&:hover,
|
||||||
|
&:hover > i {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
>.list_per_role {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
border: $border;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: $padding_smaller;
|
||||||
|
margin: 0;
|
||||||
|
min-width: $min_col_width;
|
||||||
|
|
||||||
|
>.candidates {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: $gap;
|
||||||
|
|
||||||
|
>.candidate {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
list-style-type: none;
|
||||||
|
gap: $gap;
|
||||||
|
|
||||||
|
>input[type="radio"]:checked + label,
|
||||||
|
>input[type="checkbox"]:checked + label {
|
||||||
|
background-color: lightgray;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
>figure>.edit_btns>a:hover{
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
>label>figure,
|
||||||
|
>figure {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: $gap;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
>img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
>figcaption {
|
||||||
|
h5 {
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
q {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
>.edit_btns {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
top: $gap;
|
||||||
|
right: $gap;
|
||||||
|
gap: $gap;
|
||||||
|
|
||||||
|
> a {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background-color: #e9e9e9;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 25%;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #d8d8d8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.election_details {
|
||||||
|
margin: .5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
border: none;
|
||||||
|
color: black;
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
padding: 0.4em;
|
||||||
|
margin: 0.1em;
|
||||||
|
font-size: 1.18em;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: #dfdfdf 0px 0px 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: black;
|
||||||
|
background: #d4d4d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_send {
|
||||||
|
background-color: #59aee2;
|
||||||
|
&:hover {
|
||||||
|
background-color: rgb(130, 186, 235);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|