1
0
mirror of https://gitlab.crans.org/mediatek/med.git synced 2025-07-04 11:32:15 +02:00

Compare commits

..

182 Commits

Author SHA1 Message Date
48c056b210 Harden Django project configuration
Set session and CSRF cookies as secure for production.
Set HSTS header to let browser remember HTTPS for 1 year.
2022-03-09 12:30:18 +01:00
cf544dc596 Fix game research 2021-11-14 16:41:38 +01:00
c0521005ef Don't put current date as default value for memberships 2021-11-14 16:24:38 +01:00
09c61091d5 Put empty strings instead of None as default values 2021-11-14 16:16:17 +01:00
145806b5ce Merge branch 'note' into 'main'
Note Kfet integration

See merge request mediatek/med!7
2021-11-14 15:57:40 +01:00
49898143ce expires in may be not small 2021-11-14 15:51:45 +01:00
fe55a2a5ea Don't hardcode OAuth2 parameters 2021-11-14 15:22:11 +01:00
cdcb743b55 Fix membership filters 2021-11-14 15:09:04 +01:00
e75f04b530 Don't test to add users since registration is disabled 2021-11-14 14:32:28 +01:00
34766257b3 Linting 2021-11-14 14:28:13 +01:00
d0877f5cdc Translate borrows 2021-11-14 14:26:41 +01:00
aceb0d893c Users can log in iff they are or were a member, don't display the database to everyone 2021-11-04 15:04:57 +01:00
41435a6838 Store access token id in session data 2021-11-04 14:25:35 +01:00
d3c2441111 Get memberships from Note Kfet 2021-11-04 14:23:03 +01:00
d036ea6f27 Get or create a user and query Note Kfet to get data 2021-11-04 14:13:17 +01:00
e2aa645bbf Start implementation of OAuth client 2021-11-04 11:29:03 +01:00
faf697d3cf Drop unusued models. Keep migration script, but this is now unusable 2021-11-02 12:55:03 +01:00
b0a1602ea2 Merge branch 'drop_old_structure' into 'main'
Suppression de l'ancienne structure

See merge request mediatek/med!6
2021-11-02 12:54:17 +01:00
d2ad52c15a Drop unusued models. Keep migration script, but this is now unusable 2021-11-02 12:51:25 +01:00
8c6828564c Merge branch 'medium' into 'main'
Migration des données

See merge request mediatek/med!5
2021-11-02 12:42:46 +01:00
d75250f436 Linting 2021-11-02 12:39:53 +01:00
c424c7c040 Give ISBN to each borrowable object, even if it does not have one 2021-11-02 12:32:31 +01:00
a01d480dd2 Show games in Admin menu 2021-11-02 12:30:44 +01:00
079ade9bbb Fix ISBN scraper 2021-10-26 15:35:47 +02:00
4928b555b7 Fix missing translations 2021-10-26 15:16:00 +02:00
ae0d1a080e Fix borrowable autocompletion 2021-10-26 15:07:37 +02:00
1e6e033cdd Hide borrowable section 2021-10-26 15:07:29 +02:00
d0805ebe8a Migrate old-format into new format 2021-10-26 15:00:50 +02:00
6789b9e3ac Don't mix names 2021-10-26 14:25:21 +02:00
54f8198b86 Keep displaying models in Django-Admin main menu 2021-10-26 11:42:32 +02:00
e3bab2389c Replace old models by new models to update DB structure 2021-10-26 11:37:32 +02:00
39e345ee67 Install Django Polymorphic to use polymorphic models 2021-10-23 19:20:32 +02:00
0b3701f01f Fix index page 2021-10-23 19:20:20 +02:00
1ab06af5c7 Merge branch 'small-cleanup' into 'main'
Small cleanup

See merge request mediatek/med!4
2021-10-23 19:09:04 +02:00
2c4aacfc08 Destroy Django 1.11 functions 2021-10-23 19:03:59 +02:00
70018f0043 Destroy Django 1.11 functions 2021-10-23 19:00:45 +02:00
b638add396 Fix translations 2021-10-23 18:46:41 +02:00
9f5807e3b5 Translate game fields into english 2021-10-23 18:39:51 +02:00
7396cfc017 Fix tests 2021-10-23 18:33:51 +02:00
52ecd59bf6 Translate models in english 2021-10-23 18:31:03 +02:00
2a17a32d4c No more manage memberships, we will use NK20 2021-10-23 14:49:44 +02:00
7f24e5c1bf Install python3-requests 2021-10-23 14:03:58 +02:00
69a8080050 Drop Python 3.7 and 3.8 support 2021-10-23 14:01:08 +02:00
a78d0b4904 Django Extensions is fun 2021-10-23 13:59:24 +02:00
15fc5cd73f Cleanup dependencies 2021-10-23 13:54:08 +02:00
94c6c73615 Fix Bedetheque
Signed-off-by: club-med <club-med@zamokv5.crans.org>
2021-10-01 12:58:45 +02:00
33a7e5adc6 Update Bedetheque scraper 2021-04-29 13:51:52 +02:00
1a96b1a2aa For an unknown reason, button inputs are not sent as POST parameters 2021-04-29 13:51:24 +02:00
e7dfaf8b8b Merge branch '2021' into 'master'
Prepare the website for 2021

See merge request mediatek/med!3
2021-01-01 16:45:14 +01:00
74f453637a Linting 2020-12-28 23:12:27 +01:00
b8ccb40ded Màj Buster 2020-12-28 23:00:01 +01:00
574233acd0 Fix CI, add django22-py39 CI step 2020-12-28 22:58:04 +01:00
746d5cc816 Prepare the website for 2021 2020-12-27 18:22:52 +01:00
4de83344a7 Export games 2020-10-26 15:13:19 +01:00
a64d600645 Index page 2020-10-26 14:44:25 +01:00
22408b16c7 Capitalize title 2020-10-26 14:38:03 +01:00
6abbaaf75d Buggy markdown 2020-10-26 14:33:52 +01:00
9afb750be6 Use _meta field instead of class 2020-10-26 14:29:55 +01:00
997c2eac50 Use _meta field instead of class 2020-10-26 14:29:11 +01:00
be2094b263 Revues are treated separately 2020-10-26 14:27:43 +01:00
cb9cd8f9b6 Add script to export the database into a user-friendly website 2020-10-26 14:21:27 +01:00
9dd2a142b7 Better publish date 2020-10-03 15:55:56 +02:00
dcba832549 Too much 404... 2020-10-03 15:51:25 +02:00
aa51a40cf6 Wrong use of ISBNDB API 2020-10-03 15:47:22 +02:00
263b4cff77 Incorrect use of Authorization header 2020-10-03 15:41:49 +02:00
91aeb28c3a Incorrect use of Authorization header 2020-10-03 15:40:33 +02:00
3af19c0f27 Temporary use ISBNDB to scrap data 2020-10-03 15:36:28 +02:00
80c520d76c If no day of month, set the first day of the month 2020-10-03 15:15:35 +02:00
665f7a2875 Ensure that the fetched roman ISBN is the queried ISBN 2020-10-03 15:10:43 +02:00
c1098577e1 Ignore future media import errors 2020-10-03 15:05:05 +02:00
e09b503ee1 Remove Des from title 2020-10-03 11:03:22 +02:00
9c53d89ad3 Get only the ten first characters for the date 2020-10-02 17:42:56 +02:00
8c8692b8d2 Better roman import 2020-10-02 17:12:02 +02:00
3edc3ffa02 Normalize before removing wrong letters 2020-10-02 16:59:58 +02:00
8fa724e848 Allow to have a R in front of subtitles 2020-10-02 16:43:04 +02:00
838fcecb56 Normalize after replacing UN 2020-10-02 16:20:24 +02:00
8b097dc4e0 Normalize after replacing L' 2020-10-02 13:54:16 +02:00
6985e39130 Add button to generate a side identifier 2020-10-02 13:12:55 +02:00
6f60de1838 Socket need to be accessible by apache 2020-09-28 21:31:50 +02:00
9ecd876923 Add script to regenerate side identifiers 2020-09-28 10:51:21 +02:00
be76bf4857 Add "De" as unwanted title prefix 2020-09-26 11:45:31 +02:00
4198ea8a72 Use icontains rather than iregex in search field 2020-09-25 15:10:27 +02:00
7ed6b9712b Add present field 2020-09-25 14:20:17 +02:00
57659acc93 Add find medium link in explore dropdown 2020-09-25 11:20:53 +02:00
a06ae5c9b9 Merge remote-tracking branch 'origin/master' into master
# Conflicts:
#	media/admin.py
#	users/admin.py
#	users/views.py
2020-09-25 10:28:39 +02:00
796c985ffb Add page to search a media by its ISBN 2020-09-25 10:27:43 +02:00
dc4cb56dd0 More recent Pillow are fine 2020-09-23 20:56:57 +02:00
b2f0ee0b44 Ubuntu is Python 3.8 2020-09-23 20:51:42 +02:00
35ecc2800f Remove uwsgi from testing 2020-09-23 20:47:24 +02:00
73615afa77 It's better with tox 2020-09-23 20:42:01 +02:00
8d76bd255a Fix syntax of gitlabci 2020-09-23 20:38:26 +02:00
5e3003720f Docutils is useless for testing 2020-09-23 20:33:49 +02:00
952a3ddddf Do not force install mysqlclient 2020-09-23 20:32:23 +02:00
91b361d7a6 Do not make string nullable in database 2020-09-23 20:31:25 +02:00
9a7304f573 Add __str__ on Adhesion model 2020-09-23 20:21:55 +02:00
8d20b14cbb Remove additional newline in imports 2020-09-23 20:19:31 +02:00
ad33d33e6c Test project on Debian and Ubuntu 2020-09-23 20:14:44 +02:00
d1e9693647 Fix systemd service unit 2020-09-23 18:50:50 +02:00
df1a1cb5de Update README with systemd service 2020-09-23 18:00:05 +02:00
47292feab2 Systemd service file 2020-09-23 17:57:46 +02:00
52af84b146 Upgrade for Debian Buster 2020-09-23 17:37:55 +02:00
a6db8a37e7 Merge branch 'cartons' into 'master'
Cartons

See merge request mediatek/med!2
2020-09-23 16:29:02 +02:00
4409911659 Merge branch 'cartons' of https://gitlab.crans.org/mediatek/med into cartons 2020-08-13 08:18:30 +02:00
0147c5b42c Revert CAS to make it works on Zamok 2020-08-13 08:18:15 +02:00
e63d8630cc Import 45 RPM vinyles 2020-05-24 17:47:37 +02:00
44abcaf202 Import romans without any ISBN 2020-05-24 16:01:05 +02:00
963ff25506 Add revues 2020-05-24 15:37:57 +02:00
3977ab9ec3 Fix CD import 2020-05-23 16:42:59 +02:00
28eac94312 Store RPM in vinyles 2020-05-23 14:31:22 +02:00
e2d4a80dba Store RPM in vinyles 2020-05-23 14:23:39 +02:00
02b81016b8 Import CDs 2020-05-22 22:17:17 +02:00
d88fccb51d Import vinyles 2020-05-22 22:15:37 +02:00
50f3cf39c1 Import future media 2020-05-22 21:49:09 +02:00
bed5912f54 Cat ISBN list to future medias 2020-05-22 21:37:02 +02:00
20cb710af5 Romans were missing 2020-05-22 21:08:44 +02:00
4ab2e9df57 Forgot migrations 2020-05-22 18:06:50 +02:00
054865cd41 Ajout des CD et des vinyles 2020-05-22 18:04:41 +02:00
001f40a033 Fix synthax 2020-05-21 17:07:50 +02:00
82efeba272 Split comic strips and mangas 2020-05-21 16:56:41 +02:00
1657f5c42c Update translations 2020-05-12 17:43:14 +02:00
1c8d5750bb Fix linters 2020-05-12 17:34:53 +02:00
ea30cdec6e Add a model to store future media list. 2020-05-12 17:33:32 +02:00
adbaf66401 Scrap from Google books API 2020-05-12 17:08:40 +02:00
43b3b5ccfe Fix CI 2020-05-12 15:09:06 +02:00
aa9b69f2d6 Fix openlibrary scrap, fix linters 2020-05-12 14:56:31 +02:00
8e39f6039e Prepare CAS authentication 2020-02-22 18:11:50 +01:00
10417242f4 Suspension temporaire des checksums invalides 2020-02-20 17:32:50 +01:00
4c55bdd200 Le Fetch ouvre ensuite la page pour entrer un nouveau média (code moins sale) 2020-02-20 13:52:50 +01:00
0c9b3c4d5f Le Fetch ouvre ensuite la page pour entrer un nouveau média (code sans doute temporaire) 2020-02-20 13:42:22 +01:00
39c3a59838 Allow linter to fail 2020-02-18 21:34:12 +01:00
4b07ddda23 Django security upgrade 2020-02-12 07:57:49 +01:00
98b38cd7a4 Fix entrypoint for Django 2.2 2020-02-11 21:39:08 +01:00
1bf9668315 Ignore venv and tox when compiling translations 2020-02-11 21:28:57 +01:00
1b848eede9 Remove redoc 2020-02-11 21:25:30 +01:00
698ae42c9d Drop old login algorithm 2020-02-11 21:17:41 +01:00
5b86781881 Fix indentation 2020-02-11 21:12:00 +01:00
b69ded4115 FUCK LINTERS TEST, FUCK PEP 8 2020-02-10 21:30:23 +01:00
ff224d20cd FUCK LINTERS TEST (ERDNAXE C'EST RELOUUU) 2020-02-10 21:21:32 +01:00
2753f700a6 FUCK LINTERS TEST 2020-02-10 21:12:12 +01:00
343ab02874 Fixed publish date when there is only the year 2020-02-10 19:33:34 +01:00
92dc21f014 Remove (AUT) fields from titles 2020-02-10 18:23:13 +01:00
47ce447aad Œ => OE 2020-02-10 18:18:42 +01:00
0548e34568 Subtitles don't need <span> fields 2020-02-10 18:16:19 +01:00
5b2dd84115 Docker 2020-02-10 17:51:49 +01:00
3d81977dbd Side identifiers cannot begin with le/la/les/l'/the 2020-02-10 17:51:09 +01:00
53ea1288c1 ISBN is unique 2020-02-10 17:41:05 +01:00
ef710bf964 Fix author sort 2020-02-10 17:35:35 +01:00
71a8aa065b Fix author sort 2020-02-10 17:35:07 +01:00
70045d4e2d Python 3.5 support 2020-02-10 17:29:10 +01:00
ea821483d0 Add author note 2020-02-10 17:27:30 +01:00
11f0eff4d4 Clean code 2020-02-10 17:07:06 +01:00
8dbf0494c2 The last name of the author is considered for the side identifier 2020-02-10 16:48:57 +01:00
dc23ac0396 Remove special chars from side identifiers 2020-02-10 16:47:05 +01:00
e4d1ed852f Fix page numbers 2020-02-10 16:32:13 +01:00
939efe01a0 Less complexity for download_data_openlibrary 2020-02-10 12:08:48 +01:00
778e3239a4 Less complexity for download_data_openlibrary 2020-02-10 12:03:40 +01:00
d3a4a246d9 Clean code 2020-02-10 11:47:59 +01:00
7fd8e92371 Fix authors & openlibrary scrap 2020-02-10 11:29:26 +01:00
c7d804d9bf One line was too long 2020-02-10 05:06:24 +01:00
ac8d91ac5e Correct test 2020-02-10 05:03:53 +01:00
f3f9c70de9 Pipelines 2020-02-10 04:59:39 +01:00
f082716895 The name of an author is the second word (if no ,) 2020-02-10 04:21:41 +01:00
3d62973634 Generate side identifier 2020-02-10 04:17:50 +01:00
6cd7f883b9 Scrap author and illustrator (may not work for some books) 2020-02-10 03:58:52 +01:00
552d2b8f0e Required fields are not checked when the ISBN is typed 2020-02-10 03:08:08 +01:00
a52c9f5cb3 Working htaccess 2020-02-09 21:47:24 +01:00
47bf025145 IPV6 Barcode scanner 2020-02-09 18:18:02 +01:00
ac866cc0ba Remove Clef Model 2020-02-09 18:17:26 +01:00
87063e267e Remove Clef Model 2020-02-09 17:55:51 +01:00
0f0e5fcd25 Blame MySQL 2020-02-09 17:30:13 +01:00
a5c560307a Better way to create DB 2020-02-09 16:59:18 +01:00
5623607e4f Add MySQL to requirements 2020-02-09 16:41:43 +01:00
4b553386b0 Add static files to htaccess 2020-02-09 16:14:47 +01:00
754b9632c3 README Apache 2020-02-09 16:07:37 +01:00
3fe0dfbc02 How to kill server 2020-02-09 15:45:49 +01:00
d7d68609d1 No input when collecting statics 2020-02-09 15:29:05 +01:00
3337c70a21 Fix entrypoint 2020-02-09 15:26:34 +01:00
32dbf748a1 Use sane defaults for development 2020-02-09 15:09:18 +01:00
5c3c8eed8e Do not cover dead code 2020-02-09 15:08:52 +01:00
8a13f87a9e Clarify Django commands 2020-02-09 15:08:27 +01:00
4d8d54e7de Add uwsgi configuration 2020-02-09 14:18:39 +01:00
6f780c3f27 Remove sporz app 2020-02-09 14:07:39 +01:00
88 changed files with 3516 additions and 1258 deletions

View File

@ -3,9 +3,7 @@ source =
logs
med
media
search
static
templates
theme
users
omit =

3
.gitignore vendored
View File

@ -33,8 +33,9 @@ coverage
# Local data
settings_local.py
static_files/*
static/*
*.log
*.pid
# Virtualenv
env/

View File

@ -1,26 +1,24 @@
image: python:3.6
stages:
- test
- quality-assurance
before_script:
- pip install tox
python35:
image: python:3.5
py39-django22:
stage: test
script: tox -e py35
python36:
image: python:3.6
stage: test
script: tox -e py36
python37:
image: python:3.7
stage: test
script: tox -e py37
image: debian:bullseye
before_script:
- >
apt-get update &&
apt-get install --no-install-recommends -y
python3-django python3-django-polymorphic python3-django-reversion
python3-djangorestframework python3-docutils python3-requests tox
script: tox -e py39
linters:
stage: test
stage: quality-assurance
image: debian:bullseye
before_script:
- apt-get update && apt-get install -y tox
script: tox -e linters
# Be nice to new contributors, but please use `tox`
allow_failure: true

View File

@ -11,36 +11,75 @@ Elle permet de gérer les medias, bd, jeux, emprunts, ainsi que les adhérents d
Ce projet est sous la licence GNU public license v3.0.
## Développement
## Installation
Après avoir installé un environnement Django,
### Développement
On peut soit développer avec Docker, soit utiliser un VirtualEnv.
Dans le cas du VirtualEnv,
```bash
python3 -m venv venv
. venv/bin/activate
pip install -r requirements.txt
./manage.py compilemessages
./manage.py makemigrations
./manage.py migrate
./manage.py collectstatic
./manage.py runserver
```
## Configuration d'une base MySQL
### Production
Sur le serveur mysql ou postgresl, il est nécessaire de créer une base de donnée med,
ainsi qu'un user med et un mot de passe associé.
Vous pouvez soit utiliser Docker, soit configurer manuellement le serveur.
Voici les étapes à éxecuter pour mysql :
#### Mise en place du projet sur Zamok
```SQL
CREATE DATABASE med;
CREATE USER 'med'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON med.* TO 'med'@'localhost';
FLUSH PRIVILEGES;
Pour mettre en place le projet sans droits root,
on va créer un socket uwsgi dans le répertoire personnel de l'utilisateur `club-med`
puis on va dire à Apache2 d'utiliser ce socket avec un `.htaccess`.
```bash
git clone https://gitlab.crans.org/mediatek/med.git django-med
chmod go-rwx -R django-med
python3 -m venv venv --system-site-packages
. venv/bin/activate
pip install -r requirements.txt
pip install mysqlclient~=1.4.4 # si base MySQL
pip install uwsgi~=2.0.18 # si production
./entrypoint.sh # lance en shell
```
Et pour postgresql :
Pour lancer le serveur au démarrage de Zamok, on suit les instructions dans `django-med.service`.
Pour reverse-proxyfier le serveur derrière Apache, on place dans `~/www/.htaccess` :
```apache
RewriteEngine On
# UWSGI socket
RewriteRule ^django.wsgi/(.*)$ unix:/home/c/club-med/django-med/uwsgi.sock|fcgi://localhost/ [P,NE,L]
# When not a file and not starting with django.wsgi, then forward to UWSGI
RewriteCond %{REQUEST_URI} !^/django.wsgi/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ /django.wsgi/$1 [QSA,L]
```
Il est néanmoins une mauvaise idée de faire de la production sur SQLite,
on configure donc ensuite Django et une base de données.
#### Configuration d'une base de données
Sur le serveur MySQL ou PostgreSQL, il est nécessaire de créer une base de donnée med,
ainsi qu'un user med et un mot de passe associé.
Voici les étapes à executer pour PostgreSQL :
```SQL
CREATE DATABASE med;
CREATE USER med WITH PASSWORD 'password';
GRANT ALL PRIVILEGES ON DATABASE med TO med;
CREATE DATABASE "club-med";
CREATE USER "club-med" WITH PASSWORD 'MY-STRONG-PASSWORD';
GRANT ALL PRIVILEGES ON DATABASE "club-med" TO "club-med";
```
## Exemple de groupes de droits
@ -51,14 +90,6 @@ bureau
media | Can add borrowed item
media | Can change borrowed item
media | Can delete borrowed item
users | Can view adhesion
users | Can add adhesion
users | Can change adhesion
users | Can delete adhesion
users | Can view clef
users | Can add clef
users | Can change clef
users | Can delete clef
users | Can view user
users | Can add user
users | Can change user
@ -83,7 +114,6 @@ keyholder
media | Can change borrowed item
media | Can delete borrowed item
users | Can view user
users | Can view clef
users (default group for everyone)
media | Can view author

22
django-med.service Normal file
View File

@ -0,0 +1,22 @@
# Copy to ~/.config/systemd/user/django-med.service then
# systemctl --user daemon-reload
# systemctl --user start django-med.service
[Unit]
Description=Mediatek Django project
After=syslog.target
[Service]
WorkingDirectory=/home/c/club-med/django-med
Environment="PATH=/home/c/club-med/django-med/venv/bin"
ExecStart=/home/c/club-med/django-med/entrypoint.sh
Restart=on-failure
KillSignal=SIGQUIT
Type=notify
StandardError=syslog
NotifyAccess=all
StandardOutput=append:/home/c/club-med/django-med/service.log
StandardError=append:/home/c/club-med/django-med/service_error.log
[Install]
WantedBy=multi-user.target

View File

@ -1,8 +1,21 @@
#!/bin/bash
python manage.py compilemessages
python manage.py makemigrations
sleep 2
python manage.py migrate
# This will launch the Django project as a fastcgi socket
# then Apache or NGINX will be able to use that socket
# TODO: use uwsgi in production
python manage.py runserver 0.0.0.0:8000
# Option "-i" will be only available in Django 3.0+, but it does not support Python 3.5
#python manage.py compilemessages -i ".tox" -i "venv"
python manage.py compilemessages
# Wait for database (docker)
sleep 2
python manage.py migrate
python manage.py collectstatic --no-input
# harakiri parameter respawns processes taking more than 20 seconds
# max-requests parameter respawns processes after serving 5000 requests
# vacuum parameter cleans up when stopped
uwsgi --socket "$HOME/www/uwsgi.sock" --chmod-socket=666 --master --plugins python3 \
--module med.wsgi:application --env DJANGO_SETTINGS_MODULE=med.settings \
--processes 4 --harakiri=20 --max-requests=5000 --vacuum \
--static-map /static="$(pwd)/static" --protocol=fastcgi

View File

@ -3,7 +3,6 @@
from django.test import TestCase
from django.urls import reverse
from users.models import User
"""

View File

@ -9,7 +9,6 @@ from django.db.models import Count
from django.shortcuts import render
from django.utils.translation import gettext_lazy as _
from reversion.models import Revision
from med.settings import PAGINATION_NUMBER
from users.models import User

View File

@ -7,8 +7,7 @@ from django.contrib.auth.admin import Group, GroupAdmin
from django.contrib.sites.admin import Site, SiteAdmin
from django.utils.translation import gettext_lazy as _
from django.views.decorators.cache import never_cache
from media.models import Emprunt
from media.models import Borrow
class DatabaseAdmin(AdminSite):
@ -23,8 +22,8 @@ class DatabaseAdmin(AdminSite):
# User is always authenticated
# Get currently borrowed items
user_borrowed = Emprunt.objects.filter(user=request.user,
date_rendu=None)
user_borrowed = Borrow.objects.filter(user=request.user,
given_back=None)
response.context_data["borrowed_items"] = user_borrowed
return response

View File

@ -1,84 +0,0 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import binascii
import hashlib
import os
from base64 import decodestring
from base64 import encodestring
from collections import OrderedDict
from django.contrib.auth import hashers
ALGO_NAME = "{SSHA}"
ALGO_LEN = len(ALGO_NAME + "$")
DIGEST_LEN = 20
def make_secret(password):
salt = os.urandom(4)
h = hashlib.sha1(password.encode())
h.update(salt)
return ALGO_NAME + "$" + encodestring(h.digest() + salt).decode()[:-1]
def check_password(challenge_password, password):
challenge_bytes = decodestring(challenge_password[ALGO_LEN:].encode())
digest = challenge_bytes[:DIGEST_LEN]
salt = challenge_bytes[DIGEST_LEN:]
hr = hashlib.sha1(password.encode())
hr.update(salt)
valid_password = True
# La comparaison est volontairement en temps constant
# (pour éviter les timing-attacks)
for i, j in zip(digest, hr.digest()):
valid_password &= i == j
return valid_password
class SSHAPasswordHasher(hashers.BasePasswordHasher):
"""
SSHA password hashing to allow for LDAP auth compatibility
"""
algorithm = ALGO_NAME
def encode(self, password, salt, iterations=None):
"""
Hash and salt the given password using SSHA algorithm
salt is overridden
"""
assert password is not None
return make_secret(password)
def verify(self, password, encoded):
"""
Check password against encoded using SSHA algorithm
"""
assert encoded.startswith(self.algorithm)
return check_password(encoded, password)
def safe_summary(self, encoded):
"""
Provides a safe summary ofthe password
"""
assert encoded.startswith(self.algorithm)
hash = encoded[ALGO_LEN:]
hash = binascii.hexlify(decodestring(hash.encode())).decode()
return OrderedDict([
('algorithm', self.algorithm),
('iterations', 0),
('salt', hashers.mask_hash(hash[2 * DIGEST_LEN:], show=2)),
('hash', hashers.mask_hash(hash[:2 * DIGEST_LEN])),
])
def harden_runtime(self, password, encoded):
"""
Method implemented to shut up BasePasswordHasher warning
As we are not using multiple iterations the method is pretty useless
"""
pass

View File

@ -16,7 +16,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SECRET_KEY = 'CHANGE_ME_IN_LOCAL_SETTINGS!'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
DEBUG = True
ADMINS = (
# ('Admin', 'webmaster@example.com'),
@ -26,6 +26,16 @@ SITE_ID = 1
ALLOWED_HOSTS = ['127.0.0.1']
# Use secure cookies in production
SESSION_COOKIE_SECURE = not DEBUG
CSRF_COOKIE_SECURE = not DEBUG
# Remember HTTPS for 1 year
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Application definition
INSTALLED_APPS = [
@ -35,6 +45,8 @@ INSTALLED_APPS = [
# External apps
'reversion',
'rest_framework',
'django_extensions',
'polymorphic',
# Django contrib
'django.contrib.admin',
@ -45,13 +57,13 @@ INSTALLED_APPS = [
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_filters',
# Med apps
'users',
'med',
'media',
'logs',
'sporz',
]
MIDDLEWARE = [
@ -145,7 +157,7 @@ USE_TZ = True
# Don't put anything in this directory yourself; store your static files
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
# Example: "/var/www/example.com/static/"
STATIC_ROOT = os.path.join(BASE_DIR, 'static_files')
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
# URL prefix for static files.
# Example: "http://example.com/static/", "http://static.example.com/"
@ -153,6 +165,8 @@ STATIC_URL = '/static/'
# Django REST Framework
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
'DEFAULT_PERMISSION_CLASSES': [
'med.permissions.DjangoViewModelPermissions',
]
@ -161,19 +175,28 @@ REST_FRAMEWORK = {
# Med configuration
PAGINATION_NUMBER = 25
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
'med.login.SSHAPasswordHasher',
]
AUTH_USER_MODEL = 'users.User'
MAX_EMPRUNT = 5 # Max emprunts
NOTE_KFET_URL = 'https://note.crans.org'
NOTE_KFET_CLIENT_ID = 'CHANGE_ME'
NOTE_KFET_CLIENT_SECRET = 'CHANGE_ME'
NOTE_KFET_SCOPES = '1_1 2_1 48_1'
try:
from .settings_local import *
except ImportError:
pass
AUTHLIB_OAUTH_CLIENTS = {
'notekfet': {
'client_id': f'{NOTE_KFET_CLIENT_ID}',
'client_secret': f'{NOTE_KFET_CLIENT_SECRET}',
'access_token_url': f'{NOTE_KFET_URL}/o/token/',
'refresh_token_url': f'{NOTE_KFET_URL}/o/token/',
'authorize_url': f'{NOTE_KFET_URL}/o/authorize/',
'userinfo_endpoint': f'{NOTE_KFET_URL}/api/me/',
'client_kwargs': {
'scope': NOTE_KFET_SCOPES,
}
}
}

View File

@ -33,21 +33,15 @@ DEBUG = True
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'med',
'USER': 'med',
'NAME': 'club-med',
'USER': 'club-med',
'PASSWORD': 'password_to_store_in_env',
'HOST': 'db',
'PORT': '',
}
}
# or MySQL database for Zamok
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.mysql',
# 'NAME': 'club-med',
# 'USER': 'club-med',
# 'PASSWORD': 'CHANGE ME !!!',
# 'HOST': 'localhost',
# },
# }
NOTE_KFET_URL = 'https://note.crans.org'
NOTE_KFET_CLIENT_ID = 'CHANGE_ME'
NOTE_KFET_CLIENT_SECRET = 'CHANGE_ME'
NOTE_KFET_SCOPES = '1_1 2_1 48_1'

View File

@ -2,12 +2,10 @@
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.decorators import login_required
from django.contrib.auth.views import PasswordResetView
from django.urls import include, path
from django.views.generic import RedirectView, TemplateView
from django.views.generic import RedirectView
from rest_framework import routers
from rest_framework.schemas import get_schema_view
import media.views
import users.views
@ -15,15 +13,21 @@ from .admin import admin_site
# API router
router = routers.DefaultRouter()
router.register(r'authors', media.views.AuteurViewSet)
router.register(r'media', media.views.MediaViewSet)
router.register(r'borrowed_items', media.views.EmpruntViewSet)
router.register(r'games', media.views.JeuViewSet)
router.register(r'authors', media.views.AuthorViewSet)
router.register(r'media/comic', media.views.ComicViewSet)
router.register(r'media/manga', media.views.MangaViewSet)
router.register(r'media/cd', media.views.CDViewSet)
router.register(r'media/vinyl', media.views.VinylViewSet)
router.register(r'media/novel', media.views.NovelViewSet)
router.register(r'media/review', media.views.ReviewViewSet)
router.register(r'media/future', media.views.FutureMediumViewSet)
router.register(r'borrowed_items', media.views.BorrowViewSet)
router.register(r'games', media.views.GameViewSet)
router.register(r'users', users.views.UserViewSet)
router.register(r'groups', users.views.GroupViewSet)
urlpatterns = [
path('', media.views.index, name='index'),
path('', media.views.IndexView.as_view(), name='index'),
# Include project routers
path('users/', include('users.urls')),
@ -33,10 +37,6 @@ urlpatterns = [
# REST API
path('api/', include(router.urls)),
path('api-auth/', include('rest_framework.urls')),
path('openapi', login_required(get_schema_view()), name='openapi-schema'),
path('redoc/',
login_required(TemplateView.as_view(template_name='redoc.html')),
name='redoc'),
# Include Django Contrib and Core routers
path('accounts/password_reset/', PasswordResetView.as_view(),

View File

@ -2,22 +2,34 @@
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import ugettext_lazy as _
from polymorphic.admin import PolymorphicChildModelAdmin, \
PolymorphicParentModelAdmin
from med.admin import admin_site
from reversion.admin import VersionAdmin
from med.admin import admin_site
from .forms import MediaAdminForm
from .models import Auteur, Emprunt, Jeu, Media
from .models import Author, Borrow, Borrowable, CD, Comic, FutureMedium, \
Game, Manga, Novel, Review, Vinyl
class AuteurAdmin(VersionAdmin):
class AuthorAdmin(VersionAdmin):
list_display = ('name',)
search_fields = ('name',)
class MediaAdmin(VersionAdmin):
class BorrowableAdmin(PolymorphicParentModelAdmin):
search_fields = ('title',)
child_models = (CD, Comic, Manga, Novel, Review, Vinyl,)
def get_model_perms(self, request):
# We don't want that the borrowable items appear directly in
# main menu, but we still want search borrowable items.
return {}
class MediumAdmin(VersionAdmin, PolymorphicChildModelAdmin):
list_display = ('__str__', 'authors_list', 'side_identifier', 'isbn',
'external_link')
search_fields = ('title', 'authors__name', 'side_identifier', 'subtitle',
@ -25,6 +37,7 @@ class MediaAdmin(VersionAdmin):
autocomplete_fields = ('authors',)
date_hierarchy = 'publish_date'
form = MediaAdminForm
show_in_index = True
def authors_list(self, obj):
return ", ".join([a.name for a in obj.authors.all()])
@ -60,30 +73,61 @@ class MediaAdmin(VersionAdmin):
extra_context=extra_context)
class EmpruntAdmin(VersionAdmin):
list_display = ('media', 'user', 'date_emprunt', 'date_rendu',
'permanencier_emprunt', 'permanencier_rendu_custom')
search_fields = ('media__title', 'media__side_identifier',
'user__username', 'date_emprunt', 'date_rendu')
date_hierarchy = 'date_emprunt'
autocomplete_fields = ('media', 'user', 'permanencier_emprunt',
'permanencier_rendu')
class FutureMediumAdmin(VersionAdmin):
list_display = ('isbn',)
search_fields = ('isbn',)
def permanencier_rendu_custom(self, obj):
def changeform_view(self, request, object_id=None, form_url='',
extra_context=None):
"""
Show a button if item has not been returned yet
We use _continue for ISBN fetching, so remove continue button
"""
if obj.permanencier_rendu:
return obj.permanencier_rendu
else:
return format_html(
'<a class="button" href="{}">{}</a>',
reverse('media:retour-emprunt', args=[obj.pk]),
_('Turn back')
)
extra_context = extra_context or {}
extra_context['show_save_and_continue'] = False
extra_context['show_save'] = False
return super().changeform_view(request, object_id, form_url,
extra_context=extra_context)
permanencier_rendu_custom.short_description = _('given back to')
permanencier_rendu_custom.allow_tags = True
class CDAdmin(VersionAdmin, PolymorphicChildModelAdmin):
list_display = ('title', 'authors_list', 'side_identifier',)
search_fields = ('title', 'authors__name', 'side_identifier',)
autocomplete_fields = ('authors',)
show_in_index = True
def authors_list(self, obj):
return ", ".join([a.name for a in obj.authors.all()])
authors_list.short_description = _('authors')
class VinylAdmin(VersionAdmin, PolymorphicChildModelAdmin):
list_display = ('title', 'authors_list', 'side_identifier', 'rpm',)
search_fields = ('title', 'authors__name', 'side_identifier', 'rpm',)
autocomplete_fields = ('authors',)
show_in_index = True
def authors_list(self, obj):
return ", ".join([a.name for a in obj.authors.all()])
authors_list.short_description = _('authors')
class ReviewAdmin(VersionAdmin, PolymorphicChildModelAdmin):
list_display = ('__str__', 'number', 'year', 'month', 'day', 'double',)
search_fields = ('title', 'number', 'year',)
show_in_index = True
class BorrowAdmin(VersionAdmin):
list_display = ('borrowable', 'user', 'borrow_date', 'borrowed_with',
'given_back_to')
search_fields = ('borrowable__isbn', 'borrowable__title',
'borrowable__medium__side_identifier',
'user__username', 'borrow_date', 'given_back')
date_hierarchy = 'borrow_date'
autocomplete_fields = ('borrowable', 'user', 'borrowed_with',
'given_back_to')
def add_view(self, request, form_url='', extra_context=None):
"""
@ -91,19 +135,27 @@ class EmpruntAdmin(VersionAdmin):
"""
# Make GET data mutable
data = request.GET.copy()
data['permanencier_emprunt'] = request.user
data['borrowed_with'] = request.user
request.GET = data
return super().add_view(request, form_url, extra_context)
class JeuAdmin(VersionAdmin):
list_display = ('name', 'proprietaire', 'duree', 'nombre_joueurs_min',
'nombre_joueurs_max', 'comment')
search_fields = ('name', 'proprietaire__username', 'duree', 'comment')
autocomplete_fields = ('proprietaire',)
class GameAdmin(VersionAdmin, PolymorphicChildModelAdmin):
list_display = ('title', 'owner', 'duration', 'players_min',
'players_max', 'comment', 'isbn')
search_fields = ('isbn', 'title', 'owner__username', 'duration', 'comment')
autocomplete_fields = ('owner',)
show_in_index = True
admin_site.register(Auteur, AuteurAdmin)
admin_site.register(Media, MediaAdmin)
admin_site.register(Emprunt, EmpruntAdmin)
admin_site.register(Jeu, JeuAdmin)
admin_site.register(Author, AuthorAdmin)
admin_site.register(Borrowable, BorrowableAdmin)
admin_site.register(Comic, MediumAdmin)
admin_site.register(Manga, MediumAdmin)
admin_site.register(Novel, MediumAdmin)
admin_site.register(CD, CDAdmin)
admin_site.register(Vinyl, VinylAdmin)
admin_site.register(Review, ReviewAdmin)
admin_site.register(FutureMedium, FutureMediumAdmin)
admin_site.register(Borrow, BorrowAdmin)
admin_site.register(Game, GameAdmin)

View File

@ -1,15 +1,86 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# Copyright (C) 2017-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import json
import os
import re
import unicodedata
from urllib.error import HTTPError
import urllib.request
from django.core.exceptions import ValidationError
from django.db.models import QuerySet
from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from .models import Author, Comic
from .scraper import BedetequeScraper
def generate_side_identifier(title, authors, subtitle=None):
if isinstance(authors, QuerySet):
authors = list(authors)
title_normalized = title.upper()
title_normalized = title_normalized.replace('', '\'')
title_normalized = re.sub(r'^DE ', '', title_normalized)
title_normalized = re.sub(r'^DES ', '', title_normalized)
title_normalized = re.sub(r'^LE ', '', title_normalized)
title_normalized = re.sub(r'^LA ', '', title_normalized)
title_normalized = re.sub(r'^LES ', '', title_normalized)
title_normalized = re.sub(r'^L\'', '', title_normalized)
title_normalized = re.sub(r'^UN ', '', title_normalized)
title_normalized = re.sub(r'^UNE ', '', title_normalized)
title_normalized = re.sub(r'^THE ', '', title_normalized)
title_normalized = re.sub(r'Œ', 'OE', title_normalized)
title_normalized = title_normalized.replace(' ', '')
title_normalized = ''.join(
char
for char in unicodedata.normalize(
'NFKD', title_normalized.casefold())
if all(not unicodedata.category(char).startswith(cat)
for cat in {'M', 'P', 'Z', 'C'}) or char == ' '
).casefold().upper()
title_normalized = re.sub("[^A-Z0-9$]", "", title_normalized)
authors = authors.copy()
def sort(author):
return "{:042d}".format(-author.note) + author.name.split(" ")[-1]\
+ ".{:042d}".format(author.pk)
authors.sort(key=sort)
primary_author = authors[0]
author_name = primary_author.name.upper()
if ',' not in author_name and ' ' in author_name:
author_name = author_name.split(' ')[-1]
author_name = ''.join(
char for char in unicodedata.normalize('NFKD', author_name.casefold())
if all(not unicodedata.category(char).startswith(cat)
for cat in {'M', 'P', 'Z', 'C'}) or char == ' '
).casefold().upper()
author_name = re.sub("[^A-Z]", "", author_name)
side_identifier = "{:.3} {:.3}".format(author_name, title_normalized, )
if subtitle:
subtitle = re.sub(r'</span>', '', subtitle)
subtitle = re.sub(r'<span.*>', '', subtitle)
start = subtitle.split(' ')[0].replace('.', '')
start = re.sub("^R?", "", start)
if start.isnumeric():
side_identifier += " {:0>2}".format(start, )
# Normalize side identifier, in order to remove accents
side_identifier = ''.join(
char for char in unicodedata.normalize('NFKD',
side_identifier.casefold())
if all(not unicodedata.category(char).startswith(cat)
for cat in {'M', 'P', 'Z', 'C'})
or char == ' ').casefold().upper()
return side_identifier
class MediaAdminForm(ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -17,6 +88,46 @@ class MediaAdminForm(ModelForm):
if isbn_field:
isbn_field.widget.template_name = "media/isbn_button.html"
isbn_field.widget.attrs.update({'autofocus': 'autofocus'})
side_identifier_field = self.fields.get('side_identifier')
if side_identifier_field and self.instance and self.instance.pk:
instance = self.instance
title, authors, subtitle = instance.title,\
instance.authors.all(), None
if hasattr(instance, "subtitle"):
subtitle = instance.subtitle
side_identifier_field.widget.attrs.update(
{'data-generated-side-identifier':
generate_side_identifier(title, authors, subtitle)})
side_identifier_field.widget.template_name =\
"media/generate_side_identifier.html"
def download_data_isbndb(self, isbn):
api_url = "https://api2.isbndb.com/book/" + str(isbn)\
+ "?Authorization=" + os.getenv("ISBNDB_KEY", "")
req = urllib.request.Request(api_url)
req.add_header("Authorization", os.getenv("ISBNDB_KEY", ""))
try:
with urllib.request.urlopen(req) as url:
data: dict = json.loads(url.read().decode())["book"]
except HTTPError:
return False
print(data)
data.setdefault("title", "")
data.setdefault("date_published", "1970-01-01")
data.setdefault("pages", 0)
data.setdefault("authors", [])
data.setdefault("image", "")
self.cleaned_data["title"] = data["title"]
self.cleaned_data["publish_date"] = data["date_published"][:10]
while len(self.cleaned_data["publish_date"]) == 4 \
or len(self.cleaned_data["publish_date"]) == 7:
self.cleaned_data["publish_date"] += "-01"
self.cleaned_data["number_of_pages"] = data["pages"]
self.cleaned_data["authors"] = \
list(Author.objects.get_or_create(name=author_name)[0]
for author_name in data["authors"])
self.cleaned_data["external_url"] = data["image"]
return True
def download_data_bedeteque(self, isbn):
"""
@ -28,10 +139,69 @@ class MediaAdminForm(ModelForm):
if not r:
return False
# If results, then take the most accurate
data = scraper.scrap_bd_info(r[0])
data = scraper.scrap_comic_info(r[0])
self.cleaned_data.update(data)
return True
def download_data_google(self, isbn):
"""
Download data from google books
:return True if success
"""
api_url = "https://www.googleapis.com/books/v1/volumes?q=ISBN:{}"\
.format(isbn)
with urllib.request.urlopen(api_url) as url:
data = json.loads(url.read().decode())
if data and data['totalItems']:
fetched_item = None
for item in data['items']:
for identifiers in item["volumeInfo"]["industryIdentifiers"]:
if identifiers["identifier"] == isbn:
fetched_item = item
break
if fetched_item:
break
if not fetched_item:
return False
# Fill the data
self.parse_data_google(fetched_item)
return True
return False
def parse_data_google(self, data):
info = data['volumeInfo']
self.cleaned_data['external_url'] = info['canonicalVolumeLink']
if 'title' in info:
self.cleaned_data['title'] = info['title']
if 'subtitle' in data:
self.cleaned_data['subtitle'] = info['subtitle']
if 'pageCount' in info:
self.cleaned_data['number_of_pages'] = \
info['pageCount']
elif not self.cleaned_data['number_of_pages']:
self.cleaned_data['number_of_pages'] = 0
if 'publishedDate' in info:
if "-" not in info["publishedDate"]:
info["publishedDate"] += "-01-01"
elif len(info["publishedDate"]) == 7:
info["publishedDate"] += "-01"
self.cleaned_data['publish_date'] = info['publishedDate'][:10]
if 'authors' not in self.cleaned_data \
or not self.cleaned_data['authors']:
self.cleaned_data['authors'] = list()
if 'authors' in info:
for author in info['authors']:
author_obj = Author.objects.get_or_create(
name=author)[0]
self.cleaned_data['authors'].append(author_obj)
print(self.cleaned_data)
def download_data_openlibrary(self, isbn):
"""
Download data from openlibrary
@ -41,33 +211,146 @@ class MediaAdminForm(ModelForm):
"&format=json&jscmd=data".format(isbn)
with urllib.request.urlopen(api_url) as url:
data = json.loads(url.read().decode())
if data and data['ISBN:' + isbn]:
data = data['ISBN:' + isbn]
if 'url' in data:
# Fill the data
self.cleaned_data['external_url'] = data['url']
if 'title' in data:
self.cleaned_data['title'] = data['title']
if 'subtitle' in data:
self.cleaned_data['subtitle'] = data['subtitle']
if 'number_of_pages' in data:
self.cleaned_data['number_of_pages'] = \
data['number_of_pages']
self.parse_data_openlibrary(data)
return True
return False
def parse_data_openlibrary(self, data):
self.cleaned_data['external_url'] = data['url']
if 'title' in data:
self.cleaned_data['title'] = data['title']
if 'subtitle' in data:
self.cleaned_data['subtitle'] = data['subtitle']
if 'number_of_pages' in data:
self.cleaned_data['number_of_pages'] = \
data['number_of_pages']
elif not self.cleaned_data['number_of_pages']:
self.cleaned_data['number_of_pages'] = 0
if 'publish_date' in data:
months = ['January', 'February', "March", "April", "Mai",
"June", "July", "August", "September",
"October", "November", "December"]
split = data['publish_date'].replace(',', '').split(' ')
if len(split) == 1:
self.cleaned_data['publish_date'] = split[0] + "-01-01"
else:
month_to_number = dict(
Jan="01",
Feb="02",
Mar="03",
Apr="04",
May="05",
Jun="06",
Jul="07",
Aug="08",
Sep="09",
Oct="10",
Nov="11",
Dec="12",
)
if split[0][:3] in month_to_number:
self.cleaned_data['publish_date']\
= split[2] + "-" \
+ month_to_number[split[0][:3]] + "-" + split[1]
else:
self.cleaned_data['publish_date'] = "{}-{:02d}-{:02d}" \
.format(split[2], months.index(split[0])
+ 1, int(split[1]), )
if 'authors' not in self.cleaned_data \
or not self.cleaned_data['authors']:
self.cleaned_data['authors'] = list()
if 'authors' in data:
for author in data['authors']:
author_obj = Author.objects.get_or_create(
name=author['name'])[0]
self.cleaned_data['authors'].append(author_obj)
def clean(self):
"""
If user fetch ISBN data, then download data before validating the form
"""
# TODO implement authors, side_identifier
if "_continue" in self.request.POST:
isbn = self.cleaned_data.get('isbn')
if isbn:
# ISBN is present, try with bedeteque
scrap_result = self.download_data_bedeteque(isbn)
if not scrap_result:
# Try with OpenLibrary
self.download_data_openlibrary(isbn)
super().clean()
return super().clean()
if "_isbn" in self.data\
or "_isbn_addanother" in self.data:
isbn = self.cleaned_data.get('isbn')
if "_isbn_addanother" in self.data:
self.data = self.data.copy()
self.data['_addanother'] = 42
self.request.POST = self.data
if isbn:
scrap_result = self.download_data_isbndb(isbn)
if not scrap_result:
# ISBN is present, try with bedeteque
scrap_result = self.download_data_bedeteque(isbn)
if not scrap_result:
# Try with Google
scrap_result = self.download_data_google(isbn)
if not scrap_result:
# Try with OpenLibrary
if not self.download_data_openlibrary(isbn):
self.add_error('isbn',
_("This ISBN is not found."))
return self.cleaned_data
if self.cleaned_data['title']:
self.cleaned_data['title'] = re.sub(
r'\(AUT\) ',
'',
self.cleaned_data['title']
)
if self.cleaned_data['authors']:
side_identifier = generate_side_identifier(
self.cleaned_data["title"],
self.cleaned_data["authors"],
self.cleaned_data["subtitle"],
)
self.cleaned_data['side_identifier'] = side_identifier
return self.cleaned_data
def _clean_fields(self):
# First clean ISBN field
isbn_field = self.fields['isbn']
isbn = isbn_field.widget.value_from_datadict(
self.data, self.files, self.add_prefix('isbn'))
isbn = isbn_field.clean(isbn)
self.cleaned_data['isbn'] = isbn
for name, field in self.fields.items():
# value_from_datadict() gets the data from the data dictionaries.
# Each widget type knows how to retrieve its own data, because some
# widgets split data over several HTML fields.
if field.disabled:
value = self.get_initial_for_field(field, name)
else:
value = field.widget.value_from_datadict(
self.data, self.files, self.add_prefix(name))
try:
# We don't want to check a field when we enter an ISBN.
if "isbn" not in self.data \
or not self.cleaned_data.get('isbn'):
value = field.clean(value)
self.cleaned_data[name] = value
if hasattr(self, 'clean_%s' % name):
value = getattr(self, 'clean_%s' % name)()
self.cleaned_data[name] = value
except ValidationError as e:
self.add_error(name, e)
class Meta:
model = Comic
fields = ('isbn', 'title', 'subtitle', 'external_url',
'side_identifier', 'authors', 'number_of_pages',
'publish_date', 'present', )

View File

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-16 14:00+0200\n"
"POT-Creation-Date: 2021-11-14 14:25+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -13,154 +13,294 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: admin.py:32 models.py:24 models.py:56
#: admin.py:46 admin.py:102 admin.py:114 models.py:30 models.py:85
msgid "authors"
msgstr "auteurs"
#: admin.py:42
#: admin.py:56
msgid "external url"
msgstr "URL externe"
#: admin.py:82
msgid "Turn back"
msgstr "Rendre"
#: admin.py:85 models.py:112
msgid "given back to"
msgstr "rendu à"
#: fields.py:17
msgid "ISBN-10 or ISBN-13"
msgstr "ISBN-10 ou ISBN-13"
#: models.py:16 models.py:136
msgid "name"
msgstr "nom"
#: forms.py:302
msgid "This ISBN is not found."
msgstr "L'ISBN n'a pas été trouvé."
#: models.py:23
msgid "author"
msgstr "auteur"
#: management/commands/migrate_to_new_format.py:57 models.py:156
msgid "CDs"
msgstr "CDs"
#: models.py:30
msgid "ISBN"
msgstr "ISBN"
#: management/commands/migrate_to_new_format.py:57 models.py:155
msgid "CD"
msgstr "CD"
#: models.py:31
msgid "You may be able to scan it from a bar code."
msgstr "Peut souvent être scanné à partir du code barre."
#: management/commands/migrate_to_new_format.py:73 models.py:149
msgid "vinyls"
msgstr "vinyles"
#: models.py:36
msgid "title"
msgstr "titre"
#: management/commands/migrate_to_new_format.py:73 models.py:148
msgid "vinyl"
msgstr "vinyle"
#: models.py:40
msgid "subtitle"
msgstr "sous-titre"
#: management/commands/migrate_to_new_format.py:91 models.py:196
msgid "reviews"
msgstr "revues"
#: models.py:46
msgid "external URL"
msgstr "URL externe"
#: management/commands/migrate_to_new_format.py:91 models.py:195
msgid "review"
msgstr "revue"
#: models.py:51
msgid "side identifier"
msgstr "côte"
#: models.py:59
msgid "number of pages"
msgstr "nombre de pages"
#: models.py:64
msgid "publish date"
msgstr "date de publication"
#: models.py:76
msgid "medium"
msgstr "medium"
#: models.py:77
msgid "media"
msgstr "media"
#: models.py:89
msgid "borrower"
msgstr "emprunteur"
#: models.py:92
msgid "borrowed on"
msgstr "emprunté le"
#: models.py:97
msgid "given back on"
msgstr "rendu le"
#: models.py:103
msgid "borrowed with"
msgstr "emprunté avec"
#: models.py:104
msgid "The keyholder that registered this borrowed item."
msgstr "Le permanencier qui enregistre cet emprunt."
#: models.py:113
msgid "The keyholder to whom this item was given back."
msgstr "Le permanencier à qui l'emprunt a été rendu."
#: models.py:120
msgid "borrowed item"
msgstr "emprunt"
#: models.py:121
msgid "borrowed items"
msgstr "emprunts"
#: models.py:141
msgid "owner"
msgstr "propriétaire"
#: models.py:146
msgid "duration"
msgstr "durée"
#: models.py:150
msgid "minimum number of players"
msgstr "nombre minimum de joueurs"
#: models.py:154
msgid "maximum number of players"
msgstr "nombre maximum de joueurs"
#: models.py:160
msgid "comment"
msgstr "commentaire"
#: models.py:167
msgid "game"
msgstr "jeu"
#: models.py:168
#: management/commands/migrate_to_new_format.py:111 models.py:315
msgid "games"
msgstr "jeux"
#: templates/media/isbn_button.html:3
msgid "Fetch data"
msgstr "Télécharger les données"
#: management/commands/migrate_to_new_format.py:111 models.py:314
msgid "game"
msgstr "jeu"
#: validators.py:20
#: models.py:17
msgid "name"
msgstr "nom"
#: models.py:22
msgid "note"
msgstr "note"
#: models.py:29
msgid "author"
msgstr "auteur"
#: models.py:36 models.py:202
msgid "ISBN"
msgstr "ISBN"
#: models.py:37 models.py:203
msgid "You may be able to scan it from a bar code."
msgstr "Peut souvent être scanné à partir du code barre."
#: models.py:45
msgid "title"
msgstr "titre"
#: models.py:49 models.py:220
msgid "present"
msgstr "présent"
#: models.py:50 models.py:221
msgid "Tell that the medium is present in the Mediatek."
msgstr "Indique que le medium est présent à la Mediatek."
#: models.py:68
msgid "borrowable"
msgstr "empruntable"
#: models.py:69
msgid "borrowables"
msgstr "empruntables"
#: models.py:74
msgid "external URL"
msgstr "URL externe"
#: models.py:79
msgid "side identifier"
msgstr "côte"
#: models.py:89
msgid "medium"
msgstr "medium"
#: models.py:90
msgid "media"
msgstr "media"
#: models.py:95
msgid "subtitle"
msgstr "sous-titre"
#: models.py:101
msgid "number of pages"
msgstr "nombre de pages"
#: models.py:107
msgid "publish date"
msgstr "date de publication"
#: models.py:113
msgid "book"
msgstr "livre"
#: models.py:114
msgid "books"
msgstr "livres"
#: models.py:119
msgid "comic"
msgstr "BD"
#: models.py:120
msgid "comics"
msgstr "BDs"
#: models.py:126
msgid "manga"
msgstr "manga"
#: models.py:127
msgid "mangas"
msgstr "mangas"
#: models.py:133
msgid "novel"
msgstr "roman"
#: models.py:134
msgid "novels"
msgstr "romans"
#: models.py:140
msgid "rounds per minute"
msgstr "tours par minute"
#: models.py:142
msgid "33 RPM"
msgstr "33 TPM"
#: models.py:143
msgid "45 RPM"
msgstr "45 TPM"
#: models.py:162
msgid "number"
msgstr "nombre"
#: models.py:166
msgid "year"
msgstr "année"
#: models.py:173
msgid "month"
msgstr "mois"
#: models.py:180
msgid "day"
msgstr "jour"
#: models.py:187
msgid "double"
msgstr "double"
#: models.py:210
msgid "type"
msgstr "type"
#: models.py:212
msgid "Comic"
msgstr "BD"
#: models.py:213
msgid "Manga"
msgstr "Manga"
#: models.py:214
msgid "Roman"
msgstr "Roman"
#: models.py:226
msgid "future medium"
msgstr "medium à importer"
#: models.py:227
msgid "future media"
msgstr "medias à importer"
#: models.py:237
msgid "object"
msgstr "objet"
#: models.py:242
msgid "borrower"
msgstr "emprunteur"
#: models.py:245
msgid "borrowed on"
msgstr "emprunté le"
#: models.py:250
msgid "given back on"
msgstr "rendu le"
#: models.py:256
msgid "borrowed with"
msgstr "emprunté avec"
#: models.py:257
msgid "The keyholder that registered this borrowed item."
msgstr "Le permanencier qui enregistre cet emprunt."
#: models.py:265
msgid "given back to"
msgstr "rendu à"
#: models.py:266
msgid "The keyholder to whom this item was given back."
msgstr "Le permanencier à qui l'emprunt a été rendu."
#: models.py:273
msgid "borrowed item"
msgstr "emprunt"
#: models.py:274
msgid "borrowed items"
msgstr "emprunts"
#: models.py:289
msgid "owner"
msgstr "propriétaire"
#: models.py:294
msgid "duration"
msgstr "durée"
#: models.py:298
msgid "minimum number of players"
msgstr "nombre minimum de joueurs"
#: models.py:302
msgid "maximum number of players"
msgstr "nombre maximum de joueurs"
#: models.py:307
msgid "comment"
msgstr "commentaire"
#: templates/media/generate_side_identifier.html:3
msgid "Generate side identifier"
msgstr "Générer la cote"
#: templates/media/isbn_button.html:3
msgid "Fetch data and add another"
msgstr "Télécharger les données et ajouter un nouveau medium"
#: templates/media/isbn_button.html:4
msgid "Fetch only"
msgstr "Télécharger les données seulement"
#: validators.py:18
msgid "Invalid ISBN: Not a string"
msgstr "ISBN invalide : ce n'est pas une chaîne de caractères"
#: validators.py:23
#: validators.py:21
msgid "Invalid ISBN: Wrong length"
msgstr "ISBN invalide : mauvaise longueur"
#: validators.py:26
msgid "Invalid ISBN: Failed checksum"
msgstr "ISBN invalide : mauvais checksum"
#: validators.py:29
#: validators.py:27
msgid "Invalid ISBN: Only upper case allowed"
msgstr "ISBN invalide : seulement les majuscules sont autorisées"
#: views.py:41
#: views.py:25
msgid "Welcome to the Mediatek database"
msgstr "Bienvenue sur la base de données de la Mediatek"

View File

View File

@ -0,0 +1,99 @@
from django.core.management import BaseCommand
from media.models import Comic, CD, Manga, Review, Novel, Vinyl, Game
class Command(BaseCommand):
"""
Extract the database into a user-friendly website written in Markdown.
"""
def add_arguments(self, parser):
parser.add_argument('--directory', '-d', type=str, default='.',
help="Directory where mkdocs is running.")
def handle(self, *args, **options):
directory = options["directory"]
with open(directory + "/docs/index.md", "w") as f:
f.write("# Media de la Mediatek\n\n\n")
f.write("Ce site répertorie l'intégralité des media présents "
"à la Mediatek de l'ENS Paris-Saclay.\n")
for model_class, file_name in [(Comic, "bd.md"), (Manga, "mangas.md"),
(Novel, "romans.md"),
(CD, "cd.md"), (Vinyl, "vinyles.md")]:
self.process_model_class(model_class, file_name, f, directory)
# Traitement différent pour les revues
with open(directory + "/docs/revues.md", "w") as f:
f.write("# Revues\n\n\n")
titles = list(set(obj["title"] for obj in
Review.objects.values("title").distinct().all()))
titles.sort()
for title in titles:
f.write(f"## {title}\n\n\n")
for medium in Review.objects.filter(title=title)\
.order_by("number").all():
f.write(f"### Numéro {medium.number}\n\n\n")
if medium.double:
f.write("Double revue\n\n")
if medium.year:
f.write(f"Année : {medium.year}\n\n")
if medium.month:
f.write(f"Mois : {medium.month}\n\n")
if medium.day:
f.write(f"Jour : {medium.day}\n\n")
f.write("\n\n\n")
# Traitement différent pour les jeux
with open(directory + "/docs/jeux.md", "w") as f:
f.write("# Jeux\n\n\n")
for game in Game.objects.order_by("name").all():
f.write(f"## {game.name}\n\n\n")
f.write(f"Durée : {game.duration}\n\n")
f.write(f"Nombre de joueurs : {game.players_min} "
f"- {game.players_max}\n\n")
if game.owner.username != "Med":
f.write(f"Propriétaire : {game.owner.username}\n\n")
if game.comment:
f.write(f"Commentaire : {game.comment}\n\n")
f.write("\n\n\n")
def process_model_class(self, model_class, file_name, f, directory):
with open(directory + "/docs/" + file_name, "w") as f:
f.write("# " + str(model_class._meta.verbose_name_plural)
.capitalize() + "\n\n\n")
titles = list(set(obj["title"] for obj in model_class.objects
.values("title").distinct().all()))
titles.sort()
for title in titles:
f.write(f"## {title}\n\n\n")
for medium in model_class.objects.filter(title=title) \
.order_by("side_identifier").all():
if hasattr(medium, "subtitle"):
f.write(f"### {medium.subtitle}\n\n\n")
if hasattr(medium, "isbn"):
f.write(f"ISBN : {medium.isbn}\n\n")
f.write(f"Cote : {medium.side_identifier}\n\n")
f.write("Auteurs : " + ", ".join(
author.name for author in medium.authors.all())
+ "\n\n")
if hasattr(medium, "number_of_pages"):
f.write(f"Nombre de pages : "
f"{medium.number_of_pages}\n\n")
if hasattr(medium, "rpm"):
f.write(f"Tours par minute : "
f"{medium.rpm}\n\n")
if hasattr(medium, "publish_date"):
f.write(f"Publié le : "
f"{medium.publish_date}\n\n")
if hasattr(medium, "external_url"):
f.write(f"Lien : [{medium.external_url}]"
f"({medium.external_url})\n\n")
f.write("\n\n\n")

View File

@ -0,0 +1,50 @@
from argparse import FileType
from sys import stdin
from django.core.management import BaseCommand
from media.models import Author, CD
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('input', nargs='?',
type=FileType('r'),
default=stdin,
help="CD to be imported.")
def handle(self, *args, **options):
file = options["input"]
cds = []
for line in file:
cds.append(line[:-1].split('|', 2))
print("Registering", len(cds), "CDs")
imported = 0
for cd in cds:
if len(cd) != 3:
continue
title = cd[0]
side = cd[1]
authors_str = cd[2].split('|')
authors = [Author.objects.get_or_create(name=author)[0]
for author in authors_str]
cd, created = CD.objects.get_or_create(
title=title,
side_identifier=side,
)
cd.authors.set(authors)
cd.save()
if not created:
self.stderr.write(self.style.WARNING(
"One CD was already imported. Skipping..."))
else:
self.stdout.write(self.style.SUCCESS(
"CD imported"))
imported += 1
self.stdout.write(self.style.SUCCESS(
"{count} CDs imported".format(count=imported)))

View File

@ -0,0 +1,52 @@
from random import random
from time import sleep
from django.core.management import BaseCommand
from media.forms import MediaAdminForm
from media.models import Comic, FutureMedium, Manga, Novel
class Command(BaseCommand):
def handle(self, *args, **options):
for future_medium in FutureMedium.objects.all():
isbn = future_medium.isbn
type_str = future_medium.type
if type_str == 'bd':
cl = Comic
elif type_str == 'manga':
cl = Manga
elif type_str == 'roman':
cl = Novel
else:
self.stderr.write(self.style.WARNING(
"Unknown medium type: {type}. Ignoring..."
.format(type=type_str)))
continue
if cl.objects.filter(isbn=isbn).exists():
self.stderr.write(self.style.WARNING(
f"ISBN {isbn} for type {type_str} already exists, "
f"remove it"))
future_medium.delete()
continue
form = MediaAdminForm(instance=cl(),
data={"isbn": isbn, "_isbn": True, })
# Don't DDOS any website
sleep(5 + (4 * random() - 1))
try:
form.full_clean()
if hasattr(form.instance, "subtitle") and \
not form.instance.subtitle:
form.instance.subtitle = ""
form.save()
future_medium.delete()
self.stdout.write(self.style.SUCCESS(
"Medium with ISBN {isbn} successfully imported"
.format(isbn=isbn)))
except Exception as e:
self.stderr.write(self.style.WARNING(
"An error occured while importing ISBN {isbn}: {error}"
.format(isbn=isbn,
error=str(e.__class__) + "(" + str(e) + ")")))

View File

@ -0,0 +1,92 @@
from argparse import FileType
from sys import stdin
from django.core.exceptions import ValidationError
from django.core.management import BaseCommand
from media.models import Comic, FutureMedium, Manga, Novel
from media.validators import isbn_validator
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('--media-type',
type=str,
default='bd',
choices=[
'bd',
'manga',
'roman',
],
help="Type of media to be "
"imported.")
parser.add_argument('input', nargs='?',
type=FileType('r'),
default=stdin,
help="ISBN to be imported.")
def handle(self, *args, **options):
type_str = options["media_type"]
media_classes = [Comic, Manga, Novel, FutureMedium]
file = options["input"]
isbns = []
for line in file:
isbns.append(line[:-1])
print("Registering", len(isbns), "ISBN")
imported = 0
not_imported = []
for isbn in isbns:
if not isbn:
continue
try:
if not isbn_validator(isbn):
raise ValidationError(
"This ISBN is invalid for an unknown reason")
except ValidationError as e:
self.stderr.write(self.style.ERROR(
"The following ISBN is invalid:"
" {isbn}, reason: {reason}. Ignoring...".format(
isbn=isbn, reason=e.message)))
isbn_exists = False
for cl in media_classes:
if cl.objects.filter(isbn=isbn).exists():
isbn_exists = True
medium = cl.objects.get(isbn=isbn)
self.stderr.write(self.style.WARNING(
("Warning: ISBN {isbn} already exists, and is "
+ "registered as type {type}: {name}. Ignoring...")
.format(isbn=isbn,
name=str(medium),
type=str(cl._meta.verbose_name))))
not_imported.append(medium)
break
if isbn_exists:
continue
FutureMedium.objects.create(isbn=isbn, type=type_str)
self.stdout.write(self.style.SUCCESS("ISBN {isbn} imported"
.format(isbn=isbn)))
imported += 1
self.stdout.write(self.style.SUCCESS("{count} media imported"
.format(count=imported)))
with open('not_imported_media.csv', 'w') as f:
f.write("isbn|type|title\n")
for medium in not_imported:
if not hasattr(medium, 'title') or not medium.title:
medium.title = ''
f.write(medium.isbn + "|"
+ str(medium._meta.verbose_name)
+ "|" + medium.title + "\n")
self.stderr.write(self.style.WARNING(("{count} media already "
+ "imported").format(
count=len(not_imported))))

View File

@ -0,0 +1,50 @@
from argparse import FileType
from sys import stdin
from django.core.management import BaseCommand
from media.models import Author, Comic
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('input', nargs='?',
type=FileType('r'),
default=stdin,
help="Marvel comic to be imported.")
def handle(self, *args, **options):
file = options["input"]
revues = []
for line in file:
revues.append(line[:-1].split('|', 2))
print("Registering", len(revues), "Marvel comics")
imported = 0
for revue in revues:
if len(revue) != 3:
continue
title = revue[0]
number = revue[1]
authors = [Author.objects.get_or_create(name=n)[0]
for n in revue[2].split('|')]
bd = Comic.objects.create(
title=title,
subtitle=number,
side_identifier="{:.3} {:.3} {:0>2}"
.format(authors[0].name.upper(),
title.upper(),
number),
)
bd.authors.set(authors)
bd.save()
self.stdout.write(self.style.SUCCESS(
"Comic imported"))
imported += 1
self.stdout.write(self.style.SUCCESS(
"{count} comics imported".format(count=imported)))

View File

@ -0,0 +1,46 @@
from argparse import FileType
from sys import stdin
from django.core.management import BaseCommand
from media.forms import generate_side_identifier
from media.models import Novel, Author
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('input', nargs='?',
type=FileType('r'),
default=stdin,
help="Revues to be imported.")
def handle(self, *args, **options):
file = options["input"]
romans = []
for line in file:
romans.append(line[:-1].split('|'))
print("Registering", len(romans), "romans")
imported = 0
for book in romans:
if len(book) != 2:
continue
title = book[1]
authors = [Author.objects.get_or_create(name=n)[0]
for n in book[0].split(';')]
side_identifier = generate_side_identifier(title, authors)
roman = Novel.objects.create(
title=title,
side_identifier=side_identifier,
)
roman.authors.set(authors)
roman.save()
self.stdout.write(self.style.SUCCESS(
"Roman imported"))
imported += 1
self.stdout.write(self.style.SUCCESS(
"{count} romans imported".format(count=imported)))

View File

@ -0,0 +1,58 @@
from argparse import FileType
from sys import stdin
from django.core.management import BaseCommand
from media.models import Review
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('input', nargs='?',
type=FileType('r'),
default=stdin,
help="Revues to be imported.")
def handle(self, *args, **options):
file = options["input"]
revues = []
for line in file:
revues.append(line[:-1].split('|'))
print("Registering", len(revues), "revues")
imported = 0
for revue in revues:
if len(revue) != 5:
continue
title = revue[0]
number = revue[1]
day = revue[2]
if not day:
day = None
month = revue[3]
if not month:
month = None
year = revue[4]
if not year:
year = None
revue, created = Review.objects.get_or_create(
title=title,
number=number.replace('*', ''),
year=year,
month=month,
day=day,
double=number.endswith('*'),
)
if not created:
self.stderr.write(self.style.WARNING(
"One revue was already imported. Skipping..."))
else:
self.stdout.write(self.style.SUCCESS(
"Revue imported"))
imported += 1
self.stdout.write(self.style.SUCCESS(
"{count} revues imported".format(count=imported)))

View File

@ -0,0 +1,58 @@
from argparse import FileType
from sys import stdin
from django.core.management import BaseCommand
from media.models import Author, Vinyl
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('input', nargs='?',
type=FileType('r'),
default=stdin,
help="Vinyle to be imported.")
parser.add_argument('--rpm',
type=int,
default=45,
help="RPM of the imported vinyles.")
def handle(self, *args, **options):
rpm = options["rpm"]
file = options["input"]
vinyles = []
for line in file:
vinyles.append(line[:-1].split('|', 2))
print("Registering", len(vinyles), "vinyles")
imported = 0
for vinyle in vinyles:
if len(vinyle) != 3:
continue
side = vinyle[0]
title = vinyle[1 if rpm == 33 else 2]
authors_str = vinyle[2 if rpm == 33 else 1]\
.split('|' if rpm == 33 else ';')
authors = [Author.objects.get_or_create(name=author)[0]
for author in authors_str]
vinyle, created = Vinyl.objects.get_or_create(
title=title,
side_identifier=side,
rpm=rpm,
)
vinyle.authors.set(authors)
vinyle.save()
if not created:
self.stderr.write(self.style.WARNING(
"One vinyle was already imported. Skipping..."))
else:
self.stdout.write(self.style.SUCCESS(
"Vinyle imported"))
imported += 1
self.stdout.write(self.style.SUCCESS(
"{count} vinyles imported".format(count=imported)))

View File

@ -0,0 +1,133 @@
from django.core.management import BaseCommand
from django.db import transaction
from django.utils.translation import gettext_lazy as _
from media.models import CD, Comic, Game, Manga, Novel, Review, Vinyl
from tqdm import tqdm
class Command(BaseCommand):
"""
Convert old format into new format
"""
def add_arguments(self, parser):
parser.add_argument('--doit', action='store_true',
help="Actually do the mogration.")
@transaction.atomic
def handle(self, *args, **options): # noqa: C901
self.stderr.write(self.style.WARNING(
"Old data structure has been deleted. This script won't work "
"anymore (and is now useless)"))
from media.models import OldCD, OldComic, OldGame, OldManga, OldNovel,\
OldReview, OldVinyl
# Migrate books
for old_book_class, book_class in [(OldComic, Comic),
(OldManga, Manga),
(OldNovel, Novel)]:
name = book_class._meta.verbose_name
name_plural = book_class._meta.verbose_name_plural
for book in tqdm(old_book_class.objects.all(),
desc=name_plural, unit=str(name)):
try:
new_book = book_class.objects.create(
isbn=book.isbn,
title=book.title,
subtitle=book.subtitle,
external_url=book.external_url,
side_identifier=book.side_identifier,
number_of_pages=book.number_of_pages,
publish_date=book.publish_date,
present=book.present,
)
new_book.authors.set(book.authors.all())
new_book.save()
except Exception:
self.stderr.write(f"There was an error with {name} "
f"{book} ({book.pk})")
raise
self.stdout.write(f"{book_class.objects.count()} {name_plural} "
"migrated")
# Migrate CDs
for cd in tqdm(OldCD.objects.all(),
desc=_("CDs"), unit=str(_("CD"))):
try:
new_cd = CD.objects.create(
title=cd.title,
present=cd.present,
)
new_cd.authors.set(cd.authors.all())
new_cd.save()
except Exception:
self.stderr.write(f"There was an error with {cd} ({cd.pk})")
raise
self.stdout.write(f"{CD.objects.count()} {_('CDs')} migrated")
# Migrate vinyls
for vinyl in tqdm(OldVinyl.objects.all(),
desc=_("vinyls"), unit=str(_("vinyl"))):
try:
new_vinyl = Vinyl.objects.create(
title=vinyl.title,
present=vinyl.present,
rpm=vinyl.rpm,
)
new_vinyl.authors.set(vinyl.authors.all())
new_vinyl.save()
except Exception:
self.stderr.write(f"There was an error with {vinyl} "
f"({vinyl.pk})")
raise
self.stdout.write(f"{Vinyl.objects.count()} {_('vinyls')} migrated")
# Migrate reviews
for review in tqdm(OldReview.objects.all(),
desc=_("reviews"), unit=str(_("review"))):
try:
Review.objects.create(
title=review.title,
number=review.number,
year=review.year,
month=review.month,
day=review.day,
double=review.double,
present=review.present,
)
except Exception:
self.stderr.write(f"There was an error with {review} "
f"({review.pk})")
raise
self.stdout.write(f"{Review.objects.count()} {_('reviews')} migrated")
# Migrate games
for game in tqdm(OldGame.objects.all(),
desc=_("games"), unit=str(_("game"))):
try:
Game.objects.create(
title=game.title,
owner=game.owner,
duration=game.duration,
players_min=game.players_min,
players_max=game.players_max,
comment=game.comment,
)
except Exception:
self.stderr.write(f"There was an error with {game} "
f"({game.pk})")
raise
self.stdout.write(f"{Game.objects.count()} {_('games')} migrated")
if not options['doit']:
self.stdout.write(self.style.WARNING(
"Warning: Data were't saved. Please use --doit option "
"to really perform the migration."
))
exit(1)

View File

@ -0,0 +1,68 @@
from django.core.management import BaseCommand
from django.db import transaction
from media.forms import generate_side_identifier
from media.models import Comic, Manga, Novel
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('--type', '-t',
type=str,
default='bd',
choices=['bd', 'manga', 'roman'],
help="Type of medium where the "
"sides need to be regenerated.")
parser.add_argument('--noninteractivemode', '-ni', action="store_true",
help="Disable the interaction mode and replace "
"existing side identifiers.")
parser.add_argument('--no-commit', '-nc', action="store_true",
help="Only show modifications, don't commit "
"them to database.")
@transaction.atomic
def handle(self, *args, **options):
t = options["type"]
medium_class = None
if t == "bd":
medium_class = Comic
elif t == "manga":
medium_class = Manga
elif t == "roman":
medium_class = Novel
interactive_mode = not options["noninteractivemode"]
replaced = 0
for obj in medium_class.objects.all():
current_side_identifier = obj.side_identifier
if not obj.authors.all():
self.stdout.write(str(obj))
subtitle = obj.subtitle if hasattr(obj, "subtitle") else None
generated_side_identifier = generate_side_identifier(
obj.title, obj.authors.all(), subtitle)
if current_side_identifier != generated_side_identifier:
answer = 'y'
if interactive_mode:
answer = ''
while answer != 'y' and answer != 'n':
answer = input(f"For medium {obj}, current side: "
f"{current_side_identifier}, "
f"generated side: "
f"{generated_side_identifier}, "
f"would you like to replace ? [y/n]")\
.lower()[0]
if answer == 'y':
self.stdout.write(self.style.WARNING(
f"Replace side of {obj} from {current_side_identifier}"
f" to {generated_side_identifier}..."))
obj.side_identifier = generated_side_identifier
if not options["no_commit"]:
obj.save()
replaced += 1
if replaced:
self.stdout.write(self.style.SUCCESS(
f"{replaced} side identifiers were replaced."))
else:
self.stdout.write(self.style.WARNING("Nothing changed."))

View File

@ -0,0 +1,61 @@
from time import sleep
from django.core.management import BaseCommand
from media.forms import MediaAdminForm
from media.models import Comic, Manga
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('--view-only', action="store_true",
help="Display only modifications. "
+ "Only useful for debug.")
def handle(self, *args, **options):
converted = 0
for media in Comic.objects.all():
if media.pk < 3400:
continue
# We sleep 5 seconds to avoid a ban from Bedetheque
sleep(5)
self.stdout.write(str(media))
form = MediaAdminForm(instance=media,
data={"isbn": media.isbn, "_isbn": True, })
form.full_clean()
if "format" not in form.cleaned_data:
self.stdout.write("Format not specified."
" Assume it is a comic strip.")
continue
format = form.cleaned_data["format"]
self.stdout.write("Format: {}".format(format))
if not options["view_only"]:
if format == "manga":
self.stdout.write(self.style.WARNING(
"This media is a manga. "
"Transfer it into a new object..."))
manga = Manga.objects.create(
isbn=media.isbn,
title=media.title,
subtitle=media.subtitle,
external_url=media.external_url,
side_identifier=media.side_identifier,
number_of_pages=media.number_of_pages,
publish_date=media.publish_date,
)
manga.authors.set(media.authors.all())
manga.save()
self.stdout.write(self.style.SUCCESS(
"Manga successfully saved. Deleting old medium..."))
media.delete()
self.stdout.write(self.style.SUCCESS("Medium deleted"))
converted += 1
self.stdout.write(self.style.SUCCESS(
"Successfully saved {:d} mangas".format(converted)))

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2020-02-10 16:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('media', '0024_auto_20190816_1356'),
]
operations = [
migrations.AddField(
model_name='auteur',
name='note',
field=models.IntegerField(default=0, verbose_name='note'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 2.2.4 on 2020-02-10 16:40
from django.db import migrations
import media.fields
import media.validators
class Migration(migrations.Migration):
dependencies = [
('media', '0025_auteur_note'),
]
operations = [
migrations.AlterField(
model_name='media',
name='isbn',
field=media.fields.ISBNField(blank=True, help_text='You may be able to scan it from a bar code.', max_length=28, null=True, unique=True, validators=[media.validators.isbn_validator], verbose_name='ISBN'),
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 2.2.10 on 2020-05-12 15:23
from django.db import migrations, models
import media.fields
import media.validators
class Migration(migrations.Migration):
dependencies = [
('media', '0026_auto_20200210_1740'),
]
operations = [
migrations.CreateModel(
name='FutureMedia',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('isbn', media.fields.ISBNField(blank=True, help_text='You may be able to scan it from a bar code.', max_length=28, null=True, unique=True, validators=[media.validators.isbn_validator], verbose_name='ISBN')),
],
options={
'verbose_name': 'future medium',
'verbose_name_plural': 'future media',
},
),
]

View File

@ -0,0 +1,34 @@
# Generated by Django 2.2.10 on 2020-05-21 14:28
from django.db import migrations, models
import media.fields
import media.validators
class Migration(migrations.Migration):
dependencies = [
('media', '0027_futuremedia'),
]
operations = [
migrations.CreateModel(
name='Manga',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('isbn', media.fields.ISBNField(blank=True, help_text='You may be able to scan it from a bar code.', max_length=28, null=True, unique=True, validators=[media.validators.isbn_validator], verbose_name='ISBN')),
('title', models.CharField(max_length=255, verbose_name='title')),
('subtitle', models.CharField(blank=True, max_length=255, null=True, verbose_name='subtitle')),
('external_url', models.URLField(blank=True, null=True, verbose_name='external URL')),
('side_identifier', models.CharField(max_length=255, verbose_name='side identifier')),
('number_of_pages', models.PositiveIntegerField(blank=True, null=True, verbose_name='number of pages')),
('publish_date', models.DateField(blank=True, null=True, verbose_name='publish date')),
('authors', models.ManyToManyField(to='media.Auteur', verbose_name='authors')),
],
options={
'verbose_name': 'medium',
'verbose_name_plural': 'media',
'ordering': ['title', 'subtitle'],
},
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 2.2.10 on 2020-05-21 14:59
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('media', '0028_manga'),
]
operations = [
migrations.AlterModelOptions(
name='manga',
options={'ordering': ['title', 'subtitle'], 'verbose_name': 'manga', 'verbose_name_plural': 'mangas'},
),
]

View File

@ -0,0 +1,49 @@
# Generated by Django 2.2.10 on 2020-05-22 15:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('media', '0029_auto_20200521_1659'),
]
operations = [
migrations.RenameModel(
old_name='Media',
new_name='BD',
),
migrations.AlterModelOptions(
name='manga',
options={},
),
migrations.CreateModel(
name='Vinyle',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255, verbose_name='title')),
('side_identifier', models.CharField(max_length=255, verbose_name='side identifier')),
('authors', models.ManyToManyField(to='media.Auteur', verbose_name='authors')),
],
options={
'verbose_name': 'vinyle',
'verbose_name_plural': 'vinyles',
'ordering': ['title'],
},
),
migrations.CreateModel(
name='CD',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255, verbose_name='title')),
('side_identifier', models.CharField(max_length=255, verbose_name='side identifier')),
('authors', models.ManyToManyField(to='media.Auteur', verbose_name='authors')),
],
options={
'verbose_name': 'CD',
'verbose_name_plural': 'CDs',
'ordering': ['title'],
},
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 2.2.10 on 2020-05-22 15:58
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('media', '0030_auto_20200522_1757'),
]
operations = [
migrations.AlterModelOptions(
name='bd',
options={'ordering': ['title', 'subtitle'], 'verbose_name': 'BD', 'verbose_name_plural': 'BDs'},
),
]

View File

@ -0,0 +1,38 @@
# Generated by Django 2.2.10 on 2020-05-22 19:07
from django.db import migrations, models
import media.fields
import media.validators
class Migration(migrations.Migration):
dependencies = [
('media', '0031_auto_20200522_1758'),
]
operations = [
migrations.AlterModelOptions(
name='manga',
options={'ordering': ['title'], 'verbose_name': 'manga', 'verbose_name_plural': 'mangas'},
),
migrations.CreateModel(
name='Roman',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('isbn', media.fields.ISBNField(blank=True, help_text='You may be able to scan it from a bar code.', max_length=28, null=True, unique=True, validators=[media.validators.isbn_validator], verbose_name='ISBN')),
('title', models.CharField(max_length=255, verbose_name='title')),
('subtitle', models.CharField(blank=True, max_length=255, null=True, verbose_name='subtitle')),
('external_url', models.URLField(blank=True, null=True, verbose_name='external URL')),
('side_identifier', models.CharField(max_length=255, verbose_name='side identifier')),
('number_of_pages', models.PositiveIntegerField(blank=True, null=True, verbose_name='number of pages')),
('publish_date', models.DateField(blank=True, null=True, verbose_name='publish date')),
('authors', models.ManyToManyField(to='media.Auteur', verbose_name='authors')),
],
options={
'verbose_name': 'roman',
'verbose_name_plural': 'romans',
'ordering': ['title', 'subtitle'],
},
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.10 on 2020-05-22 19:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('media', '0032_auto_20200522_2107'),
]
operations = [
migrations.AddField(
model_name='futuremedia',
name='type',
field=models.CharField(choices=[('bd', 'BD'), ('manga', 'Manga'), ('roman', 'Roman')], default='bd', max_length=8, verbose_name='type'),
preserve_default=False,
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.10 on 2020-05-23 12:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('media', '0033_futuremedia_type'),
]
operations = [
migrations.AddField(
model_name='vinyle',
name='rpm',
field=models.PositiveIntegerField(choices=[(33, '33 RPM'), (45, '45 RPM')], default=45, verbose_name='rounds per minute'),
preserve_default=False,
),
]

View File

@ -0,0 +1,29 @@
# Generated by Django 2.2.10 on 2020-05-24 12:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('media', '0034_vinyle_rpm'),
]
operations = [
migrations.CreateModel(
name='Revue',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255, verbose_name='title')),
('number', models.PositiveIntegerField(verbose_name='number')),
('year', models.PositiveIntegerField(verbose_name='year')),
('month', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='month')),
('day', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='day')),
],
options={
'verbose_name': 'revue',
'verbose_name_plural': 'revues',
'ordering': ['title', 'number'],
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.10 on 2020-05-24 13:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('media', '0035_revue'),
]
operations = [
migrations.AlterField(
model_name='revue',
name='year',
field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='year'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.10 on 2020-05-24 13:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('media', '0036_auto_20200524_1500'),
]
operations = [
migrations.AddField(
model_name='revue',
name='double',
field=models.BooleanField(default=False, verbose_name='double'),
),
]

View File

@ -0,0 +1,55 @@
# Generated by Django 2.2.12 on 2020-09-23 18:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('media', '0037_revue_double'),
]
operations = [
migrations.AlterField(
model_name='bd',
name='external_url',
field=models.URLField(blank=True, default='', verbose_name='external URL'),
preserve_default=False,
),
migrations.AlterField(
model_name='bd',
name='subtitle',
field=models.CharField(blank=True, default='', max_length=255, verbose_name='subtitle'),
preserve_default=False,
),
migrations.AlterField(
model_name='jeu',
name='comment',
field=models.CharField(blank=True, default='', max_length=255, verbose_name='comment'),
preserve_default=False,
),
migrations.AlterField(
model_name='manga',
name='external_url',
field=models.URLField(blank=True, default='', verbose_name='external URL'),
preserve_default=False,
),
migrations.AlterField(
model_name='manga',
name='subtitle',
field=models.CharField(blank=True, default='', max_length=255, verbose_name='subtitle'),
preserve_default=False,
),
migrations.AlterField(
model_name='roman',
name='external_url',
field=models.URLField(blank=True, default='', verbose_name='external URL'),
preserve_default=False,
),
migrations.AlterField(
model_name='roman',
name='subtitle',
field=models.CharField(blank=True, default='', max_length=255, verbose_name='subtitle'),
preserve_default=False,
),
]

View File

@ -0,0 +1,48 @@
# Generated by Django 2.2.16 on 2020-09-25 12:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('media', '0038_auto_20200923_2030'),
]
operations = [
migrations.AddField(
model_name='bd',
name='present',
field=models.BooleanField(default=False, help_text='Tell that the medium is present in the Mediatek.', verbose_name='present'),
),
migrations.AddField(
model_name='cd',
name='present',
field=models.BooleanField(default=False, help_text='Tell that the medium is present in the Mediatek.', verbose_name='present'),
),
migrations.AddField(
model_name='futuremedia',
name='present',
field=models.BooleanField(default=False, help_text='Tell that the medium is present in the Mediatek.', verbose_name='present'),
),
migrations.AddField(
model_name='manga',
name='present',
field=models.BooleanField(default=False, help_text='Tell that the medium is present in the Mediatek.', verbose_name='present'),
),
migrations.AddField(
model_name='revue',
name='present',
field=models.BooleanField(default=False, help_text='Tell that the medium is present in the Mediatek.', verbose_name='present'),
),
migrations.AddField(
model_name='roman',
name='present',
field=models.BooleanField(default=False, help_text='Tell that the medium is present in the Mediatek.', verbose_name='present'),
),
migrations.AddField(
model_name='vinyle',
name='present',
field=models.BooleanField(default=False, help_text='Tell that the medium is present in the Mediatek.', verbose_name='present'),
),
]

View File

@ -0,0 +1,61 @@
# Generated by Django 2.2.17 on 2021-10-23 16:30
from django.conf import settings
from django.db import migrations, models
import media.fields
import media.validators
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('media', '0039_mark_media_present'),
]
operations = [
migrations.RenameModel(
old_name='Auteur',
new_name='Author',
),
migrations.RenameModel(
old_name='Roman',
new_name='Comic',
),
migrations.RenameModel(
old_name='Jeu',
new_name='Game',
),
migrations.RenameModel(
old_name='BD',
new_name='Novel',
),
migrations.RenameModel(
old_name='Revue',
new_name='Review',
),
migrations.RenameModel(
old_name='Vinyle',
new_name='Vinyl',
),
migrations.RenameModel(
old_name='FutureMedia',
new_name='FutureMedium',
),
migrations.AlterModelOptions(
name='comic',
options={'ordering': ['title', 'subtitle'], 'verbose_name': 'comic', 'verbose_name_plural': 'comics'},
),
migrations.AlterModelOptions(
name='novel',
options={'ordering': ['title', 'subtitle'], 'verbose_name': 'novel', 'verbose_name_plural': 'novels'},
),
migrations.AlterModelOptions(
name='review',
options={'ordering': ['title', 'number'], 'verbose_name': 'review', 'verbose_name_plural': 'reviews'},
),
migrations.AlterModelOptions(
name='vinyl',
options={'ordering': ['title'], 'verbose_name': 'vinyl', 'verbose_name_plural': 'vinyls'},
),
]

View File

@ -0,0 +1,44 @@
# Generated by Django 2.2.17 on 2021-10-23 16:38
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('media', '0040_auto_20211023_1830'),
]
operations = [
migrations.RenameField(
model_name='game',
old_name='duree',
new_name='duration',
),
migrations.RenameField(
model_name='game',
old_name='proprietaire',
new_name='owner',
),
migrations.RenameField(
model_name='game',
old_name='nombre_joueurs_max',
new_name='players_max',
),
migrations.RenameField(
model_name='game',
old_name='nombre_joueurs_min',
new_name='players_min',
),
migrations.AlterField(
model_name='emprunt',
name='media',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='media.Comic'),
),
migrations.AlterField(
model_name='futuremedium',
name='type',
field=models.CharField(choices=[('bd', 'Comic'), ('manga', 'Manga'), ('roman', 'Roman')], max_length=8, verbose_name='type'),
),
]

View File

@ -0,0 +1,70 @@
# Generated by Django 2.2.17 on 2021-10-23 17:29
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('media', '0041_auto_20211023_1838'),
]
operations = [
migrations.RenameModel(
old_name='CD',
new_name='OldCD',
),
migrations.RenameModel(
old_name='Manga',
new_name='OldManga',
),
# Remove index before renaming the model
migrations.AlterField(
model_name='game',
name='owner',
field=models.ForeignKey(db_index=False, on_delete=models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='owner'),
),
migrations.RenameModel(
old_name='Game',
new_name='OldGame',
),
migrations.AlterField(
model_name='oldgame',
name='owner',
field=models.ForeignKey(db_index=True, on_delete=models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='owner'),
),
migrations.RenameField(
model_name='oldgame',
old_name='name',
new_name='title',
),
migrations.RenameModel(
old_name='Novel',
new_name='OldNovel',
),
migrations.RenameModel(
old_name='Comic',
new_name='OldComic',
),
migrations.RenameModel(
old_name='Review',
new_name='OldReview',
),
migrations.RenameModel(
old_name='Vinyl',
new_name='OldVinyl',
),
migrations.AlterModelOptions(
name='oldcomic',
options={'ordering': ['title', 'subtitle'], 'verbose_name': 'comic', 'verbose_name_plural': 'comics'},
),
migrations.AlterModelOptions(
name='oldmanga',
options={'ordering': ['title'], 'verbose_name': 'manga', 'verbose_name_plural': 'mangas'},
),
migrations.AlterModelOptions(
name='oldnovel',
options={'ordering': ['title', 'subtitle'], 'verbose_name': 'novel', 'verbose_name_plural': 'novels'},
),
]

View File

@ -0,0 +1,166 @@
# Generated by Django 2.2.17 on 2021-10-23 18:12
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import media.fields
import media.validators
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('media', '0042_auto_20211023_1929'),
]
operations = [
migrations.CreateModel(
name='Borrowable',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('isbn', media.fields.ISBNField(blank=True, help_text='You may be able to scan it from a bar code.', max_length=28, null=True, unique=True, validators=[media.validators.isbn_validator], verbose_name='ISBN')),
('title', models.CharField(max_length=255, verbose_name='title')),
('present', models.BooleanField(default=False, help_text='Tell that the medium is present in the Mediatek.', verbose_name='present')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_media.borrowable_set+', to='contenttypes.ContentType')),
],
options={
'verbose_name': 'borrowable',
'verbose_name_plural': 'borrowables',
},
),
migrations.AlterModelOptions(
name='oldgame',
options={'ordering': ['title'], 'verbose_name': 'game', 'verbose_name_plural': 'games'},
),
migrations.AlterField(
model_name='emprunt',
name='media',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='media.Borrowable'),
),
migrations.CreateModel(
name='Medium',
fields=[
('borrowable_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Borrowable')),
('external_url', models.URLField(blank=True, verbose_name='external URL')),
('side_identifier', models.CharField(max_length=255, verbose_name='side identifier')),
('authors', models.ManyToManyField(to='media.Author', verbose_name='authors')),
],
options={
'verbose_name': 'medium',
'verbose_name_plural': 'media',
},
bases=('media.borrowable',),
),
migrations.CreateModel(
name='Review',
fields=[
('borrowable_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Borrowable')),
('number', models.PositiveIntegerField(verbose_name='number')),
('year', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='year')),
('month', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='month')),
('day', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='day')),
('double', models.BooleanField(default=False, verbose_name='double')),
],
options={
'verbose_name': 'review',
'verbose_name_plural': 'reviews',
'ordering': ['title', 'number'],
},
bases=('media.borrowable',),
),
migrations.CreateModel(
name='Book',
fields=[
('medium_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Medium')),
('subtitle', models.CharField(blank=True, max_length=255, verbose_name='subtitle')),
('number_of_pages', models.PositiveIntegerField(blank=True, null=True, verbose_name='number of pages')),
('publish_date', models.DateField(blank=True, null=True, verbose_name='publish date')),
],
options={
'verbose_name': 'book',
'verbose_name_plural': 'books',
},
bases=('media.medium',),
),
migrations.CreateModel(
name='CD',
fields=[
('medium_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Medium')),
],
options={
'verbose_name': 'CD',
'verbose_name_plural': 'CDs',
'ordering': ['title'],
},
bases=('media.medium',),
),
migrations.CreateModel(
name='Vinyl',
fields=[
('medium_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Medium')),
('rpm', models.PositiveIntegerField(choices=[(33, '33 RPM'), (45, '45 RPM')], verbose_name='rounds per minute')),
],
options={
'verbose_name': 'vinyl',
'verbose_name_plural': 'vinyls',
'ordering': ['title'],
},
bases=('media.medium',),
),
migrations.CreateModel(
name='Game',
fields=[
('borrowable_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Borrowable')),
('duration', models.CharField(choices=[('-1h', '-1h'), ('1-2h', '1-2h'), ('2-3h', '2-3h'), ('3-4h', '3-4h'), ('4h+', '4h+')], max_length=255, verbose_name='duration')),
('players_min', models.IntegerField(validators=[django.core.validators.MinValueValidator(1)], verbose_name='minimum number of players')),
('players_max', models.IntegerField(validators=[django.core.validators.MinValueValidator(1)], verbose_name='maximum number of players')),
('comment', models.CharField(blank=True, max_length=255, verbose_name='comment')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='owner')),
],
options={
'verbose_name': 'game',
'verbose_name_plural': 'games',
'ordering': ['title'],
},
bases=('media.borrowable',),
),
migrations.CreateModel(
name='Comic',
fields=[
('book_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Book')),
],
options={
'verbose_name': 'comic',
'verbose_name_plural': 'comics',
'ordering': ['title', 'subtitle'],
},
bases=('media.book',),
),
migrations.CreateModel(
name='Manga',
fields=[
('book_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Book')),
],
options={
'verbose_name': 'manga',
'verbose_name_plural': 'mangas',
'ordering': ['title', 'subtitle'],
},
bases=('media.book',),
),
migrations.CreateModel(
name='Novel',
fields=[
('book_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Book')),
],
options={
'verbose_name': 'novel',
'verbose_name_plural': 'novels',
'ordering': ['title', 'subtitle'],
},
bases=('media.book',),
),
]

View File

@ -0,0 +1,58 @@
# Generated by Django 2.2.24 on 2021-11-02 11:54
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('media', '0043_auto_20211023_2012'),
]
operations = [
migrations.RemoveField(
model_name='oldcd',
name='authors',
),
migrations.RemoveField(
model_name='oldcomic',
name='authors',
),
migrations.RemoveField(
model_name='oldgame',
name='owner',
),
migrations.RemoveField(
model_name='oldmanga',
name='authors',
),
migrations.RemoveField(
model_name='oldnovel',
name='authors',
),
migrations.DeleteModel(
name='OldReview',
),
migrations.RemoveField(
model_name='oldvinyl',
name='authors',
),
migrations.DeleteModel(
name='OldCD',
),
migrations.DeleteModel(
name='OldComic',
),
migrations.DeleteModel(
name='OldGame',
),
migrations.DeleteModel(
name='OldManga',
),
migrations.DeleteModel(
name='OldNovel',
),
migrations.DeleteModel(
name='OldVinyl',
),
]

View File

@ -0,0 +1,36 @@
# Generated by Django 2.2.24 on 2021-11-14 13:23
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('media', '0044_auto_20211102_1254'),
]
operations = [
migrations.CreateModel(
name='Borrow',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('borrow_date', models.DateTimeField(verbose_name='borrowed on')),
('given_back', models.DateTimeField(blank=True, null=True, verbose_name='given back on')),
('borrowable', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='media.Borrowable', verbose_name='object')),
('borrowed_with', models.ForeignKey(help_text='The keyholder that registered this borrowed item.', on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='borrowed with')),
('given_back_to', models.ForeignKey(blank=True, help_text='The keyholder to whom this item was given back.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='given back to')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='borrower')),
],
options={
'verbose_name': 'borrowed item',
'verbose_name_plural': 'borrowed items',
'ordering': ['-borrow_date'],
},
),
migrations.DeleteModel(
name='Emprunt',
),
]

View File

@ -1,21 +1,27 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# Copyright (C) 2017-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
from django.core.validators import MinValueValidator
from django.db import models
from django.utils.translation import gettext_lazy as _
from polymorphic.models import PolymorphicModel
from .fields import ISBNField
class Auteur(models.Model):
class Author(models.Model):
name = models.CharField(
max_length=255,
unique=True,
verbose_name=_('name'),
)
note = models.IntegerField(
default=0,
verbose_name=_("note"),
)
def __str__(self):
return self.name
@ -25,88 +31,235 @@ class Auteur(models.Model):
ordering = ['name']
class Media(models.Model):
class Borrowable(PolymorphicModel):
isbn = ISBNField(
_('ISBN'),
help_text=_('You may be able to scan it from a bar code.'),
unique=True,
blank=True,
null=True,
)
title = models.CharField(
verbose_name=_('title'),
max_length=255,
verbose_name=_("title"),
)
subtitle = models.CharField(
verbose_name=_('subtitle'),
max_length=255,
blank=True,
null=True,
present = models.BooleanField(
verbose_name=_("present"),
help_text=_("Tell that the medium is present in the Mediatek."),
default=False,
)
def __str__(self):
obj = self
if obj.__class__ == Borrowable:
# Get true object instance, useful for autocompletion
obj = Borrowable.objects.get(pk=obj.pk)
title = obj.title
if hasattr(obj, 'subtitle'):
subtitle = obj.subtitle
if subtitle:
title = f"{title} : {subtitle}"
return title
class Meta:
verbose_name = _('borrowable')
verbose_name_plural = _('borrowables')
class Medium(Borrowable):
external_url = models.URLField(
verbose_name=_('external URL'),
blank=True,
null=True,
)
side_identifier = models.CharField(
verbose_name=_('side identifier'),
max_length=255,
)
authors = models.ManyToManyField(
'Auteur',
'Author',
verbose_name=_('authors'),
)
class Meta:
verbose_name = _("medium")
verbose_name_plural = _("media")
class Book(Medium):
subtitle = models.CharField(
verbose_name=_('subtitle'),
max_length=255,
blank=True,
)
number_of_pages = models.PositiveIntegerField(
verbose_name=_('number of pages'),
blank=True,
null=True,
)
publish_date = models.DateField(
verbose_name=_('publish date'),
blank=True,
null=True,
)
def __str__(self):
if self.subtitle:
return "{} : {}".format(self.title, self.subtitle)
else:
return self.title
class Meta:
verbose_name = _("medium")
verbose_name_plural = _("media")
verbose_name = _("book")
verbose_name_plural = _("books")
class Comic(Book):
class Meta:
verbose_name = _("comic")
verbose_name_plural = _("comics")
ordering = ['title', 'subtitle']
class Emprunt(models.Model):
media = models.ForeignKey(
'Media',
class Manga(Book):
class Meta:
verbose_name = _("manga")
verbose_name_plural = _("mangas")
ordering = ['title', 'subtitle']
class Novel(Book):
class Meta:
verbose_name = _("novel")
verbose_name_plural = _("novels")
ordering = ['title', 'subtitle']
class Vinyl(Medium):
rpm = models.PositiveIntegerField(
verbose_name=_('rounds per minute'),
choices=[
(33, _('33 RPM')),
(45, _('45 RPM')),
],
)
class Meta:
verbose_name = _("vinyl")
verbose_name_plural = _("vinyls")
ordering = ['title']
class CD(Medium):
class Meta:
verbose_name = _("CD")
verbose_name_plural = _("CDs")
ordering = ['title']
class Review(Borrowable):
number = models.PositiveIntegerField(
verbose_name=_('number'),
)
year = models.PositiveIntegerField(
verbose_name=_('year'),
null=True,
blank=True,
default=None,
)
month = models.PositiveIntegerField(
verbose_name=_('month'),
null=True,
blank=True,
default=None,
)
day = models.PositiveIntegerField(
verbose_name=_('day'),
null=True,
blank=True,
default=None,
)
double = models.BooleanField(
verbose_name=_('double'),
default=False,
)
def __str__(self):
return self.title + "" + str(self.number)
class Meta:
verbose_name = _("review")
verbose_name_plural = _("reviews")
ordering = ['title', 'number']
class FutureMedium(models.Model):
isbn = ISBNField(
_('ISBN'),
help_text=_('You may be able to scan it from a bar code.'),
unique=True,
blank=True,
null=True,
)
type = models.CharField(
_('type'),
choices=[
('bd', _('Comic')),
('manga', _('Manga')),
('roman', _('Roman')),
],
max_length=8,
)
present = models.BooleanField(
verbose_name=_("present"),
help_text=_("Tell that the medium is present in the Mediatek."),
default=False,
)
class Meta:
verbose_name = _("future medium")
verbose_name_plural = _("future media")
def __str__(self):
return "Future medium (ISBN: {isbn})".format(isbn=self.isbn, )
class Borrow(models.Model):
borrowable = models.ForeignKey(
'media.Borrowable',
on_delete=models.PROTECT,
verbose_name=_('object'),
)
user = models.ForeignKey(
'users.User',
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
verbose_name=_("borrower"),
)
date_emprunt = models.DateTimeField(
borrow_date = models.DateTimeField(
verbose_name=_('borrowed on'),
)
date_rendu = models.DateTimeField(
given_back = models.DateTimeField(
blank=True,
null=True,
verbose_name=_('given back on'),
)
permanencier_emprunt = models.ForeignKey(
'users.User',
borrowed_with = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name='user_permanencier_emprunt',
related_name='+',
verbose_name=_('borrowed with'),
help_text=_('The keyholder that registered this borrowed item.')
)
permanencier_rendu = models.ForeignKey(
'users.User',
given_back_to = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name='user_permanencier_rendu',
related_name='+',
blank=True,
null=True,
verbose_name=_('given back to'),
@ -114,56 +267,50 @@ class Emprunt(models.Model):
)
def __str__(self):
return str(self.media) + str(self.user)
return str(self.borrowable) + str(self.user)
class Meta:
verbose_name = _("borrowed item")
verbose_name_plural = _("borrowed items")
ordering = ['-date_emprunt']
ordering = ['-borrow_date']
class Jeu(models.Model):
DUREE = (
class Game(Borrowable):
DURATIONS = (
('-1h', '-1h'),
('1-2h', '1-2h'),
('2-3h', '2-3h'),
('3-4h', '3-4h'),
('4h+', '4h+'),
)
name = models.CharField(
max_length=255,
verbose_name=_("name"),
)
proprietaire = models.ForeignKey(
owner = models.ForeignKey(
'users.User',
on_delete=models.PROTECT,
verbose_name=_("owner"),
)
duree = models.CharField(
choices=DUREE,
duration = models.CharField(
choices=DURATIONS,
max_length=255,
verbose_name=_("duration"),
)
nombre_joueurs_min = models.IntegerField(
players_min = models.IntegerField(
validators=[MinValueValidator(1)],
verbose_name=_("minimum number of players"),
)
nombre_joueurs_max = models.IntegerField(
players_max = models.IntegerField(
validators=[MinValueValidator(1)],
verbose_name=_('maximum number of players'),
)
comment = models.CharField(
max_length=255,
blank=True,
null=True,
verbose_name=_('comment'),
)
def __str__(self):
return str(self.name)
return str(self.title)
class Meta:
verbose_name = _("game")
verbose_name_plural = _("games")
ordering = ['name']
ordering = ['title']

View File

@ -4,6 +4,7 @@
import re
import requests
from media.models import Author
class BedetequeScraper:
@ -22,7 +23,7 @@ class BedetequeScraper:
:return: CSRF token
"""
response = self.session.get(self.referer).content.decode()
regex = r'csrf_token_bedetheque\"\s*value=\"(\w*)\"'
regex = r'csrf_token_bel\"\s*value=\"(\w*)\"'
return re.search(regex, response).group(1)
def search_by_isbn(self, isbn: str) -> [str]:
@ -43,7 +44,7 @@ class BedetequeScraper:
regex = r'href=\"(https://www\.bedetheque\.com/BD.*.html)\"'
return re.findall(regex, content)
def scrap_bd_info(self, bd_url: str) -> dict:
def scrap_comic_info(self, bd_url: str) -> dict:
"""
Load BD web page and scrap data
:param bd_url: URL where to find BD data
@ -56,6 +57,9 @@ class BedetequeScraper:
regex_subtitle = r'<h2>\s*(.*)</h2>'
regex_publish_date = r'datePublished\" content=\"([\d-]*)\">'
regex_nb_of_pages = r'numberOfPages\">(\d*)</span'
regex_format = r'<label>Format : </label>Format (\w+)</li>'
regex_author = r'<span itemprop=\"author\">(((?!<).)*)</span>'
regex_illustrator = r'span itemprop=\"illustrator\">(((?!<).)*)</span'
data = {
'external_url': bd_url,
@ -73,10 +77,6 @@ class BedetequeScraper:
subtitle = subtitle.replace('<span class="numa"></span>', '')
data['subtitle'] = ' '.join(subtitle.split())
# TODO implement author
# regex_author = r'author\">([^<]*)</span'
# 'author': re.search(regex_author, content).group(1),
# Get publish date
search_publish_date = re.search(regex_publish_date, content)
if search_publish_date:
@ -86,5 +86,26 @@ class BedetequeScraper:
search_nb_pages = re.search(regex_nb_of_pages, content)
if search_nb_pages and search_nb_pages.group(1).isnumeric():
data['number_of_pages'] = search_nb_pages.group(1)
elif 'number_of_pages' not in data:
data['number_of_pages'] = 0
# Get format of the book
search_format = re.search(regex_format, content)
if search_format:
data['format'] = search_format.group(1).lower()
# Get author and illustrator
author = re.search(regex_author, content)
if 'author' not in data:
data['authors'] = list()
if author:
author_obj = Author.objects.get_or_create(
name=author.group(1))[0]
data['authors'].append(author_obj)
illustrator = re.search(regex_illustrator, content)
if illustrator:
author_obj = Author.objects.get_or_create(
name=illustrator.group(1))[0]
data['authors'].append(author_obj)
return data

View File

@ -1,31 +1,64 @@
from rest_framework import serializers
from .models import Auteur, Emprunt, Jeu, Media
from .models import Author, Borrow, CD, Comic, FutureMedium, Manga, Game, \
Novel, Review, Vinyl
class AuteurSerializer(serializers.HyperlinkedModelSerializer):
class AuthorSerializer(serializers.ModelSerializer):
class Meta:
model = Auteur
model = Author
fields = ['url', 'name']
class MediaSerializer(serializers.HyperlinkedModelSerializer):
class ComicSerializer(serializers.ModelSerializer):
class Meta:
model = Media
fields = ['url', 'isbn', 'title', 'subtitle', 'external_url',
'side_identifier', 'authors', 'number_of_pages',
'publish_date']
model = Comic
fields = '__all__'
class EmpruntSerializer(serializers.HyperlinkedModelSerializer):
class MangaSerializer(serializers.ModelSerializer):
class Meta:
model = Emprunt
fields = ['url', 'media', 'user', 'date_emprunt', 'date_rendu',
'permanencier_emprunt', 'permanencier_rendu']
model = Manga
fields = '__all__'
class JeuSerializer(serializers.HyperlinkedModelSerializer):
class CDSerializer(serializers.ModelSerializer):
class Meta:
model = Jeu
fields = ['url', 'name', 'proprietaire', 'duree', 'nombre_joueurs_min',
'nombre_joueurs_max', 'comment']
model = CD
fields = '__all__'
class VinylSerializer(serializers.ModelSerializer):
class Meta:
model = Vinyl
fields = '__all__'
class NovelSerializer(serializers.ModelSerializer):
class Meta:
model = Novel
fields = '__all__'
class ReviewSerializer(serializers.ModelSerializer):
class Meta:
model = Review
fields = '__all__'
class FutureMediumSerializer(serializers.ModelSerializer):
class Meta:
model = FutureMedium
fields = '__all__'
class BorrowSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Borrow
fields = '__all__'
class GameSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Game
fields = '__all__'

View File

@ -0,0 +1,149 @@
{% extends "base.html" %}
{% block content %}
<form id="form" action="#" onsubmit="searchISBN()">
<label for="isbn" id="id_isbn_label">ISBN :</label>
<input type="text" id="isbn" autofocus>
<input type="hidden" id="old-isbn">
<input type="submit" id="isbn_search">
<input type="checkbox" id="mark_as_present" checked onchange="document.getElementById('isbn').focus()" />
<label for="mark_as_present">Marquer automatiquement comme présent si trouvé et que je cherche par ISBN</label>
</form>
<ul id="result"></ul>
{% endblock %}
{% block extrajavascript %}
<script>
function markAsPresent(type, id, present=true) {
let request = new XMLHttpRequest();
request.open("GET", "/media/mark-as-present/" + type + "/" + id + "/" + (present ? "" : "?absent=1"), true);
request.onload = function() {
document.getElementById("isbn").value = document.getElementById("old-isbn").value;
searchISBN();
};
request.send();
}
function searchISBN() {
let isbn = document.getElementById("isbn").value;
let result_div = document.getElementById("result");
let markAsPresent = document.getElementById("mark_as_present").checked;
result_div.innerHTML = "<li id='recap-isbn'>Recherche : " + isbn + "</li>";
document.getElementById("isbn").value = "";
document.getElementById("old-isbn").value = isbn;
document.getElementById("isbn").focus();
let bd_request = new XMLHttpRequest();
bd_request.open('GET', '/api/media/comic/?search=' + isbn, true);
bd_request.onload = function () {
let data = JSON.parse(this.response);
data.results.forEach(comic => {
let present = comic.present;
if (markAsPresent && isbn === comic.isbn) {
present = true;
let presentRequest = new XMLHttpRequest();
presentRequest.open("GET", "/media/mark-as-present/bd/" + comic.id + "/", true);
presentRequest.send();
}
result_div.innerHTML += "<li id='comic_" + comic.id + "'>" +
"<a href='/database/media/comic/" + comic.id + "/change/'>BD : "
+ comic.title + (comic.subtitle ? " - " + comic.subtitle : "") + "</a>"
+ (present ? " (<a class='absent' href='#' onclick=\"markAsPresent('bd', " + comic.id + ", false)\">marquer comme absent</a>)"
: " (absent, <a class='present' href='#' onclick=\"markAsPresent('bd', " + comic.id + ")\">marquer comme présent</a>)") + "</li>";
});
}
bd_request.send();
let manga_request = new XMLHttpRequest();
manga_request.open('GET', '/api/media/manga/?search=' + isbn, true);
manga_request.onload = function () {
let data = JSON.parse(this.response);
data.results.forEach(manga => {
let present = manga.present;
if (markAsPresent && isbn === manga.isbn) {
present = true;
let presentRequest = new XMLHttpRequest();
presentRequest.open("GET", "/media/mark-as-present/manga/" + manga.id + "/", true);
presentRequest.send();
}
result_div.innerHTML += "<li id='manga_" + manga.id + "'>" +
"<a href='/database/media/manga/" + manga.id + "/change/'>Manga : "
+ manga.title + (manga.subtitle ? " - " + manga.subtitle : "") + "</a>"
+ (present ? " (<a class='absent' href='#' onclick=\"markAsPresent('manga', " + manga.id + ", false)\">marquer comme absent</a>)"
: " (absent, <a class='present' href='#' onclick=\"markAsPresent('manga', " + manga.id + ")\">marquer comme présent</a>)") + "</li>";
});
}
manga_request.send();
let cd_request = new XMLHttpRequest();
cd_request.open('GET', '/api/media/cd/?search=' + isbn, true);
cd_request.onload = function () {
let data = JSON.parse(this.response);
data.results.forEach(cd => {
let present = cd.present;
result_div.innerHTML += "<li id='cd_" + cd.id + "'>" +
"<a href='/database/media/cd/" + cd.id + "/change/'>CD : " + cd.title + "</a>"
+ (present ? " (<a class='absent' href='#' onclick=\"markAsPresent('cd', " + cd.id + ", false)\">marquer comme absent</a>)"
: " (absent, <a class='present' href='#' onclick=\"markAsPresent('cd', " + cd.id + ")\">marquer comme présent</a>)") + "</li>";
});
}
cd_request.send();
let vinyle_request = new XMLHttpRequest();
vinyle_request.open('GET', '/api/media/vinyl/?search=' + isbn, true);
vinyle_request.onload = function () {
let data = JSON.parse(this.response);
data.results.forEach(vinyl => {
let present = markAsPresent || vinyl.present;
result_div.innerHTML += "<li id='vinyl_" + vinyl.id + "'>" +
"<a href='/database/media/vinyl/" + vinyl.id + "/change/'>Vinyle : " + vinyl.title + "</a>"
+ (present ? " (<a class='absent' href='#' onclick=\"markAsPresent('vinyl', " + vinyl.id + ", false)\">marquer comme absent</a>)"
: " (absent, <a class='present' href='#' onclick=\"markAsPresent('vinyl', " + vinyl.id + ")\">marquer comme présent</a>)") + "</li>";
});
}
vinyle_request.send();
let roman_request = new XMLHttpRequest();
roman_request.open('GET', '/api/media/novel/?search=' + isbn, true);
roman_request.onload = function () {
let data = JSON.parse(this.response);
data.results.forEach(novel => {
let present = novel.present;
if (markAsPresent && isbn === novel.isbn) {
present = true;
let presentRequest = new XMLHttpRequest();
presentRequest.open("GET", "/media/mark-as-present/novel/" + novel.id + "/", true);
presentRequest.send();
}
result_div.innerHTML += "<li id='roman_" + novel.id + "'>" +
"<a href='/database/media/roman/" + novel.id + "/change/'>Roman : " + novel.title + "</a>"
+ (present ? " (<a class='absent' href='#' onclick=\"markAsPresent('novel', " + novel.id + ", false)\">marquer comme absent</a>)"
: " (absent, <a class='present' href='#' onclick=\"markAsPresent('novel', " + novel.id + ")\">marquer comme présent</a>)") + "</li>";
});
}
roman_request.send();
let future_request = new XMLHttpRequest();
future_request.open('GET', '/api/media/future/?search=' + isbn, true);
future_request.onload = function () {
let data = JSON.parse(this.response);
data.results.forEach(future => {
let present = future.present;
if (markAsPresent && isbn === future.isbn) {
present = true;
let presentRequest = new XMLHttpRequest();
presentRequest.open("GET", "/media/mark-as-present/future/" + bd.id + "/", true);
presentRequest.send();
}
result_div.innerHTML += "<li id='future_" + future.id + "'>" +
"<a href='/database/media/future/" + future.id + "/change/'>Medium non traité : " + future.title + "</a>"
+ (present ? " (<a class='absent' href='#' onclick=\"markAsPresent('future', " + future.id + ", false)\">marquer comme absent</a>)"
: " (absent, <a class='present' href='#' onclick=\"markAsPresent('future', " + future.id + ")\">marquer comme présent</a>)") + "</li>";
});
}
future_request.send();
}
</script>
{% endblock %}

View File

@ -0,0 +1,3 @@
{% load i18n %}
{% include "django/forms/widgets/input.html" %}
<a href="#" class="button" onclick="document.getElementById('{{ widget.attrs.id }}').value = document.getElementById('{{ widget.attrs.id }}').getAttribute('data-generated-side-identifier')">{% trans "Generate side identifier" %}</a>

View File

@ -1,3 +1,4 @@
{% load i18n %}
{% include "django/forms/widgets/input.html" %}
<input type="submit" value="{% trans "Fetch data" %}" name="_continue">
<input type="submit" value="{% trans "Fetch data and add another" %}" name="_isbn_addanother" onclick="form.submit()">
<input type="submit" value="{% trans "Fetch only" %}" name="_isbn" onclick="form.submit()">

View File

@ -3,8 +3,7 @@
from django.test import TestCase
from django.urls import reverse
from media.models import Auteur, Media
from media.models import Author, Comic
from users.models import User
"""
@ -22,44 +21,44 @@ class TemplateTests(TestCase):
self.client.force_login(self.user)
# Create an author
self.dummy_author = Auteur.objects.create(name="Test author")
self.dummy_author = Author.objects.create(name="Test author")
# Create media
self.dummy_media1 = Media.objects.create(
self.dummy_bd1 = Comic.objects.create(
title="Test media",
side_identifier="T M",
)
self.dummy_media1.authors.add(self.dummy_author)
self.dummy_media2 = Media.objects.create(
self.dummy_bd1.authors.add(self.dummy_author)
self.dummy_bd2 = Comic.objects.create(
title="Test media bis",
side_identifier="T M 2",
external_url="https://example.com/",
)
self.dummy_media2.authors.add(self.dummy_author)
self.dummy_bd2.authors.add(self.dummy_author)
def test_media_media_changelist(self):
response = self.client.get(reverse('admin:media_media_changelist'))
def test_comic_bd_changelist(self):
response = self.client.get(reverse('admin:media_comic_changelist'))
self.assertEqual(response.status_code, 200)
def test_media_media_add(self):
response = self.client.get(reverse('admin:media_media_add'))
def test_comic_bd_add(self):
response = self.client.get(reverse('admin:media_comic_add'))
self.assertEqual(response.status_code, 200)
def test_media_isbn_download(self):
def test_comic_isbn_download(self):
data = {
'_continue': True,
'_isbn': True,
'isbn': "0316358525",
}
response = self.client.post(reverse(
'admin:media_media_change',
args=[self.dummy_media1.id],
'admin:media_comic_change',
args=[self.dummy_bd1.id],
), data=data)
self.assertEqual(response.status_code, 302)
def test_comic_borrow_changelist(self):
response = self.client.get(reverse('admin:media_borrow_changelist'))
self.assertEqual(response.status_code, 200)
def test_media_emprunt_changelist(self):
response = self.client.get(reverse('admin:media_emprunt_changelist'))
self.assertEqual(response.status_code, 200)
def test_media_emprunt_add(self):
response = self.client.get(reverse('admin:media_emprunt_add'))
def test_comic_borrow_add(self):
response = self.client.get(reverse('admin:media_borrow_add'))
self.assertEqual(response.status_code, 200)

View File

@ -2,12 +2,32 @@
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf.urls import url
from django.urls import path
from . import views
app_name = 'media'
urlpatterns = [
url(r'^retour_emprunt/(?P<empruntid>[0-9]+)$', views.retour_emprunt,
name='retour-emprunt'),
path('find/', views.FindMediumView.as_view(), name="find"),
path('mark-as-present/comic/<int:pk>/',
views.MarkComicAsPresent.as_view(),
name="mark_comic_as_present"),
path('mark-as-present/manga/<int:pk>/',
views.MarkMangaAsPresent.as_view(),
name="mark_manga_as_present"),
path('mark-as-present/cd/<int:pk>/',
views.MarkCDAsPresent.as_view(),
name="mark_cd_as_present"),
path('mark-as-present/vinyl/<int:pk>/',
views.MarkVinylAsPresent.as_view(),
name="mark_vinyle_as_present"),
path('mark-as-present/novel/<int:pk>/',
views.MarkNovelAsPresent.as_view(),
name="mark_novel_as_present"),
path('mark-as-present/review/<int:pk>/',
views.MarkReviewAsPresent.as_view(),
name="mark_review_as_present"),
path('mark-as-present/future/<int:pk>/',
views.MarkFutureAsPresent.as_view(),
name="mark_future_as_present"),
]

View File

@ -8,7 +8,6 @@ Based on https://github.com/secnot/django-isbn-field
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from stdnum import isbn
def isbn_validator(raw_isbn):
@ -21,8 +20,8 @@ def isbn_validator(raw_isbn):
if len(isbn_to_check) != 10 and len(isbn_to_check) != 13:
raise ValidationError(_('Invalid ISBN: Wrong length'))
if not isbn.is_valid(isbn_to_check):
raise ValidationError(_('Invalid ISBN: Failed checksum'))
# if not isbn.is_valid(isbn_to_check):
# raise ValidationError(_('Invalid ISBN: Failed checksum'))
if isbn_to_check != isbn_to_check.upper():
raise ValidationError(_('Invalid ISBN: Only upper case allowed'))

View File

@ -2,76 +2,174 @@
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required
from django.db import transaction
from django.shortcuts import redirect, render
from django.utils import timezone
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse
from django_filters.rest_framework import DjangoFilterBackend
from django.shortcuts import redirect
from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView, DetailView
from rest_framework import viewsets
from reversion import revisions as reversion
from rest_framework.filters import SearchFilter
from .models import Auteur, Emprunt, Jeu, Media
from .serializers import AuteurSerializer, EmpruntSerializer, \
JeuSerializer, MediaSerializer
from .models import Author, Borrow, CD, Comic, FutureMedium, Game, Manga,\
Novel, Review, Vinyl
from .serializers import AuthorSerializer, BorrowSerializer, ComicSerializer, \
CDSerializer, FutureMediumSerializer, GameSerializer, MangaSerializer, \
NovelSerializer, ReviewSerializer, VinylSerializer
@login_required
@permission_required('media.change_emprunt')
def retour_emprunt(request, empruntid):
try:
emprunt_instance = Emprunt.objects.get(pk=empruntid)
except Emprunt.DoesNotExist:
messages.error(request, u"Entrée inexistante")
return redirect("admin:media_emprunt_changelist")
with transaction.atomic(), reversion.create_revision():
emprunt_instance.permanencier_rendu = request.user
emprunt_instance.date_rendu = timezone.now()
emprunt_instance.save()
reversion.set_user(request.user)
messages.success(request, "Retour enregistré")
return redirect("admin:media_emprunt_changelist")
def index(request):
class IndexView(TemplateView):
"""
Home page which redirect to admin when logged in
"""
if request.user.is_authenticated:
return redirect('admin:index')
else:
return render(request, 'admin/index.html', {
'title': _('Welcome to the Mediatek database'),
})
extra_context = {'title': _('Welcome to the Mediatek database')}
template_name = 'admin/index.html'
def dispatch(self, request, *args, **kwargs):
if request.user.is_authenticated:
return redirect('admin:index')
return super().dispatch(request, *args, **kwargs)
class AuteurViewSet(viewsets.ModelViewSet):
class FindMediumView(LoginRequiredMixin, TemplateView):
template_name = "media/find_medium.html"
class MarkMediumAsPresent(LoginRequiredMixin, DetailView):
def dispatch(self, request, *args, **kwargs):
object = self.get_object()
object.present = not request.GET.get("absent", False)
object.save()
return HttpResponse("", content_type=204)
class MarkComicAsPresent(MarkMediumAsPresent):
model = Comic
class MarkMangaAsPresent(MarkMediumAsPresent):
model = Manga
class MarkCDAsPresent(MarkMediumAsPresent):
model = CD
class MarkVinylAsPresent(MarkMediumAsPresent):
model = Vinyl
class MarkNovelAsPresent(MarkMediumAsPresent):
model = Novel
class MarkReviewAsPresent(MarkMediumAsPresent):
model = Review
class MarkFutureAsPresent(MarkMediumAsPresent):
model = FutureMedium
class AuthorViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows authors to be viewed or edited.
"""
queryset = Auteur.objects.all()
serializer_class = AuteurSerializer
queryset = Author.objects.all()
serializer_class = AuthorSerializer
class MediaViewSet(viewsets.ModelViewSet):
class ComicViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows media to be viewed or edited.
"""
queryset = Media.objects.all()
serializer_class = MediaSerializer
queryset = Comic.objects.all()
serializer_class = ComicSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ["isbn", "side_identifier"]
search_fields = ["=isbn", "title", "subtitle", "side_identifier",
"authors__name"]
class EmpruntViewSet(viewsets.ModelViewSet):
class MangaViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows media to be viewed or edited.
"""
queryset = Manga.objects.all()
serializer_class = MangaSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ["isbn", "side_identifier"]
search_fields = ["=isbn", "title", "subtitle", "side_identifier",
"authors__name"]
class CDViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows media to be viewed or edited.
"""
queryset = CD.objects.all()
serializer_class = CDSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ["side_identifier"]
search_fields = ["title", "side_identifier", "authors__name"]
class VinylViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows media to be viewed or edited.
"""
queryset = Vinyl.objects.all()
serializer_class = VinylSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ["side_identifier", "rpm"]
search_fields = ["title", "side_identifier", "authors__name"]
class NovelViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows media to be viewed or edited.
"""
queryset = Novel.objects.all()
serializer_class = NovelSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ["isbn", "side_identifier", "number_of_pages"]
search_fields = ["=isbn", "title", "subtitle", "side_identifier",
"authors__name"]
class ReviewViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows media to be viewed or edited.
"""
queryset = Review.objects.all()
serializer_class = ReviewSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ["number", "year", "month", "day", "double"]
search_fields = ["title"]
class FutureMediumViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows media to be viewed or edited.
"""
queryset = FutureMedium.objects.all()
serializer_class = FutureMediumSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ["isbn"]
search_fields = ["=isbn"]
class BorrowViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows borrowed items to be viewed or edited.
"""
queryset = Emprunt.objects.all()
serializer_class = EmpruntSerializer
queryset = Borrow.objects.all()
serializer_class = BorrowSerializer
class JeuViewSet(viewsets.ModelViewSet):
class GameViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows games to be viewed or edited.
"""
queryset = Jeu.objects.all()
serializer_class = JeuSerializer
queryset = Game.objects.all()
serializer_class = GameSerializer

View File

@ -1,12 +1,9 @@
Django==2.2.4
docutils==0.14
Pillow==5.4.1
pytz==2019.1
six==1.12.0
sqlparse==0.2.4
django-reversion==3.0.3
python-stdnum==1.10
djangorestframework==3.9.2
pyyaml==3.13
coreapi==2.3.3
psycopg2
authlib~=0.15
docutils~=0.16 # for Django-admin docs
Django~=2.2
django-filter~=2.4
django-polymorphic~=3.0
django-reversion~=3.0
djangorestframework~=3.12
django_extensions~=3.0
requests~=2.25 # for scrapping

View File

@ -1,5 +0,0 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'sporz.apps.SporzConfig'

View File

@ -1,76 +0,0 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.db.models import Q
from med.admin import admin_site
from .models import GameSave, Player
class PlayerInline(admin.TabularInline):
model = Player
# Do not always show extra players
extra = 0
min_num = 5
class GameSaveAdmin(admin.ModelAdmin):
inlines = [PlayerInline, ]
list_display = ('__str__', 'game_master', 'game_has_ended')
date_hierarchy = 'created_at'
autocomplete_fields = ('game_master',)
def has_change_permission(self, request, obj=None):
"""
If user is game master then authorize edit
"""
if obj and obj.game_master == request.user:
return True
return super().has_change_permission(request, obj)
def has_delete_permission(self, request, obj=None):
"""
If user is game master then authorize deletion
"""
if obj and obj.game_master == request.user:
return True
return super().has_delete_permission(request, obj)
def add_view(self, request, form_url='', extra_context=None):
"""
Autoselect game master when creating a new game
"""
# Make GET data mutable
data = request.GET.copy()
data['game_master'] = request.user
request.GET = data
return super().add_view(request, form_url, extra_context)
def formfield_for_foreignkey(self, db_field, request, **kwargs):
"""
Authorize game master change only if user can see all users
"""
if db_field.name == 'game_master':
if not request.user.has_perm('users.view_user'):
kwargs['queryset'] = get_user_model().objects.filter(
username=request.user.username)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
def get_queryset(self, request):
"""
List all game save only if user has view permission
else, list only own games and ended games
"""
queryset = super().get_queryset(request)
if request.user.has_perm('sporz.view_gamesave'):
return queryset
return queryset.filter(
Q(game_master=request.user) | Q(game_has_ended=True)
)
admin_site.register(GameSave, GameSaveAdmin)

View File

@ -1,11 +0,0 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class SporzConfig(AppConfig):
name = 'sporz'
verbose_name = _('Sporz game assitant')

View File

@ -1,134 +0,0 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-15 11:29+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: apps.py:11
msgid "Sporz game assitant"
msgstr "Assitant au jeu Sporz"
#: models.py:13
msgid "created at"
msgstr "créé le"
#: models.py:20
msgid "game master"
msgstr "maître du jeu"
#: models.py:21
msgid "Game master can edit and delete this game save."
msgstr "Le maître du jeu peut éditer et supprimer cette sauvegarde."
#: models.py:24
msgid "current round"
msgstr "tour actuel"
#: models.py:28
msgid "game has ended"
msgstr "la partie est finie"
#: models.py:29
msgid "If true, then everyone will be able to see the game."
msgstr "Quand cette case est cochée, tout le monde pourra voir le récapitulatif."
#: models.py:36 models.py:115
msgid "players"
msgstr "joueurs"
#: models.py:40
msgid "game save"
msgstr "sauvegarde de jeu"
#: models.py:41
msgid "game saves"
msgstr "sauvegardes de jeu"
#: models.py:58
msgid "Base astronaut"
msgstr "Astronaute de base"
#: models.py:59
msgid "Base mutant"
msgstr "Mutant de base"
#: models.py:60
msgid "Healer"
msgstr "Médecin"
#: models.py:61
msgid "Psychologist"
msgstr "Psychologue"
#: models.py:62
msgid "Geno-technician"
msgstr "Geno-technicien"
#: models.py:63
msgid "Computer scientist"
msgstr "Informaticien"
#: models.py:64
msgid "Hacker"
msgstr "Hackeur"
#: models.py:65
msgid "Spy"
msgstr "Espion"
#: models.py:66
msgid "Detective"
msgstr "Enquêteur"
#: models.py:67
msgid "Traitor"
msgstr "Traître"
#: models.py:75
msgid "Neutral"
msgstr "Neutre"
#: models.py:76
msgid "Host"
msgstr "Hôte"
#: models.py:77
msgid "Immunized"
msgstr "Immunisé"
#: models.py:83
msgid "game"
msgstr "jeu"
#: models.py:87
msgid "name"
msgstr "nom"
#: models.py:94
msgid "user"
msgstr "utilisateur"
#: models.py:95
msgid "Optionnal mapping to an user."
msgstr "Lien optionnel à un utilisateur."
#: models.py:103
msgid "genotype"
msgstr "génotype"
#: models.py:107
msgid "infected"
msgstr "infecté"
#: models.py:114
msgid "player"
msgstr "joueur"

View File

@ -1,51 +0,0 @@
# Generated by Django 2.2.4 on 2019-08-15 09:31
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='GameSave',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='created at')),
('current_round', models.PositiveSmallIntegerField(default=1, verbose_name='current round')),
('game_has_ended', models.BooleanField(help_text='If true, then everyone will be able to see the game.', verbose_name='game has ended')),
('game_master', models.ForeignKey(help_text='Game master can edit and delete this game save.', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='game master')),
],
options={
'verbose_name': 'game save',
'verbose_name_plural': 'game saves',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='Player',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=150, verbose_name='name')),
('role', models.CharField(choices=[('BA', 'Base astronaut'), ('BM', 'Base mutant'), ('HE', 'Healer'), ('PS', 'Psychologist'), ('GE', 'Geno-technician'), ('CO', 'Computer scientist'), ('HA', 'Hacker'), ('SP', 'Spy'), ('DE', 'Detective'), ('TR', 'Traitor')], default='BA', max_length=2)),
('genotype', models.NullBooleanField(choices=[(None, 'Neutral'), (False, 'Host'), (True, 'Immunized')], verbose_name='genotype')),
('infected', models.BooleanField(verbose_name='infected')),
('game', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sporz.GameSave', verbose_name='game')),
('user', models.ForeignKey(blank=True, help_text='Optionnal mapping to an user.', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'player',
'verbose_name_plural': 'players',
'ordering': ['user__username'],
'unique_together': {('game', 'name')},
},
),
]

View File

@ -1,117 +0,0 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
class GameSave(models.Model):
created_at = models.DateTimeField(
verbose_name=_('created at'),
default=timezone.now,
editable=False,
)
game_master = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
verbose_name=_('game master'),
help_text=_('Game master can edit and delete this game save.'),
)
current_round = models.PositiveSmallIntegerField(
verbose_name=_('current round'),
default=1,
)
game_has_ended = models.BooleanField(
verbose_name=_('game has ended'),
help_text=_('If true, then everyone will be able to see the game.'),
)
def __str__(self):
return "{} ({} {})".format(
self.created_at.strftime("%b %d %Y %H:%M:%S"),
len(self.player_set.all()),
_("players"),
)
class Meta:
verbose_name = _("game save")
verbose_name_plural = _("game saves")
ordering = ['-created_at']
class Player(models.Model):
# Player roles
BASE_ASTRONAUT = 'BA'
BASE_MUTANT = 'BM'
HEALER = 'HE'
PSYCHOLOGIST = 'PS'
GENO_TECHNICIAN = 'GE'
COMPUTER_SCIENTIST = 'CO'
HACKER = 'HA'
SPY = 'SP'
DETECTIVE = 'DE'
TRAITOR = 'TR'
ROLES = [
(BASE_ASTRONAUT, _('Base astronaut')),
(BASE_MUTANT, _("Base mutant")),
(HEALER, _("Healer")),
(PSYCHOLOGIST, _("Psychologist")),
(GENO_TECHNICIAN, _("Geno-technician")),
(COMPUTER_SCIENTIST, _("Computer scientist")),
(HACKER, _("Hacker")),
(SPY, _("Spy")),
(DETECTIVE, _("Detective")),
(TRAITOR, _("Traitor")),
]
# Genotypes
NEUTRAL = None
HOST = False
IMMUNIZED = True
GENOTYPES = [
(NEUTRAL, _("Neutral")),
(HOST, _("Host")),
(IMMUNIZED, _("Immunized"))
]
game = models.ForeignKey(
GameSave,
on_delete=models.CASCADE,
verbose_name=_('game'),
)
name = models.CharField(
max_length=150,
verbose_name=_('name'),
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
blank=True,
null=True,
verbose_name=_('user'),
help_text=_('Optionnal mapping to an user.'),
)
role = models.CharField(
max_length=2,
choices=ROLES,
default=BASE_ASTRONAUT,
)
genotype = models.NullBooleanField(
verbose_name=_('genotype'),
choices=GENOTYPES,
)
infected = models.BooleanField(
verbose_name=_('infected'),
)
def __str__(self):
return str(self.name)
class Meta:
verbose_name = _("player")
verbose_name_plural = _("players")
ordering = ['user__username']
unique_together = ['game', 'name']

View File

@ -1,19 +0,0 @@
#!/bin/bash
# This will launch the Django project as a UWSGI socket
# then Apache or NGINX will be able to use that socket
PROJECT_PATH="$(pwd)"
# Official Django configuration
uwsgi_python3 --chdir=$PROJECT_PATH \
--module=med.wsgi:application \
--env DJANGO_SETTINGS_MODULE=med.settings \
--master --pidfile=$PROJECT_PATH/uwsgi.pid \
--socket=$PROJECT_PATH/uwsgi.sock \
--processes=5 \
--chmod-socket=600 \
--harakiri=20 \
--max-requests=5000 \
--vacuum \
--daemonize=$PROJECT_PATH/uwsgi.log \
--protocol=fastcgi

View File

@ -30,6 +30,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<span class="dropdown">
<a href="{% url 'admin:index' %}">{% trans 'Explore database' %}</a>
<span class="dropdown-content">
<a href="{% url "media:find" %}">Recherche ...</a>
{% for app in available_apps %}
{% for model in app.models %}
{% if model.admin_url %}
@ -53,7 +54,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if user.is_authenticated %}
<a href="{% url 'logout' %}">{% trans 'Log out' %}</a>
{% else %}
<a href="{% url 'login' %}">{% trans 'Log in' %}</a>
<a href="{% url 'users:login' %}">{% trans 'Log in' %}</a>
{% endif %}
{% endblock %}
</div>
@ -95,9 +96,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
</noscript>
</form>
<p>
Mediatek 2017-2020 &mdash;
<a href="mailto:club-med@crans.org">Nous contactez</a> &mdash;
<a href="{% url "redoc" %}">Explorer l'API</a>
Mediatek 2017-2021 &mdash;
<a href="mailto:club-med@crans.org">Nous contacter</a> &mdash;
<a href="{% url "api-root" %}">Explorer l'API</a>
</p>
</div>
{% endif %}
@ -115,4 +116,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
});
}
</script>
{% block extrajavascript %}{% endblock %}
{% endblock %}

View File

@ -56,9 +56,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
<h3>
{% trans 'My profile' %}
<small><a class="changelink" href="{% url 'users:edit-info' %}">
{% trans 'Edit' %}
</a></small>
</h3>
<ul>
<li><strong>{% trans 'username' %}</strong> : {{ user.username }}</li>
@ -67,10 +64,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
<li><strong>{% trans 'date joined' %}</strong> : {{ user.date_joined }}</li>
<li><strong>{% trans 'last login' %}</strong> : {{ user.last_login }}</li>
<li><strong>{% trans 'address' %}</strong> : {{ user.address }}</li>
<li><strong>{% trans 'phone number' %}</strong> : {{ user.telephone }}</li>
<li><strong>{% trans 'phone number' %}</strong> : {{ user.phone_number }}</li>
<li><strong>{% trans 'groups' %}</strong> : {% for g in user.groups.all %}{{ g.name }} {% endfor %}
</li>
<li><strong>{% trans 'maximum borrowed' %}</strong> : {{ user.maxemprunt }}</li>
<li>
<strong>{% trans 'membership for current year' %}</strong> :
{% if user.is_member %}
@ -84,8 +80,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
<h3>{% trans 'Current borrowed items' %}</h3>
{% if borrowed_items %}
<ul>
{% for emprunt in borrowed_items %}
<li>{{ emprunt.media }} ({% trans 'since' %} {{ emprunt.date_emprunt }})</li>
{% for borrow in borrowed_items %}
<li>{{ borrow.object }} ({% trans 'since' %} {{ borrow.borrow_date }})</li>
{% endfor %}
</ul>
{% else %}

View File

@ -1,12 +0,0 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n static %}
{% block coltype %}nopadding{% endblock %}
{% block content %}
<redoc spec-url='{% url "openapi-schema" %}'></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"></script>
{% endblock %}

View File

@ -1,6 +1,8 @@
from http.server import BaseHTTPRequestHandler, HTTPServer
import os
import socket
from time import sleep
"""
GetBlue Android parameters
@ -20,16 +22,25 @@ class Server(BaseHTTPRequestHandler):
def do_GET(self):
self._set_headers()
isbn = self.path[7:-24]
if not isbn.isnumeric():
print("Mauvais ISBN.")
return
print("Hey j'ai un ISBN :", isbn)
os.system("xdotool type " + isbn)
os.system("xdotool key KP_Enter")
sleep(1)
os.system("xdotool click 1")
def do_HEAD(self):
self._set_headers()
class HTTPServerV6(HTTPServer):
address_family = socket.AF_INET6
if __name__ == "__main__":
server_address = ('', 8080)
httpd = HTTPServer(server_address, Server)
server_address = ('::', 8080)
httpd = HTTPServerV6(server_address, Server)
print('Starting httpd...')
httpd.serve_forever()

22
tox.ini
View File

@ -1,38 +1,30 @@
[tox]
envlist = py35,py36,py37,linters
envlist = py37,py38,py39,linters
skipsdist = True
[testenv]
basepython = python3
sitepackages = True
deps =
-r{toxinidir}/requirements.txt
coverage
commands =
./manage.py makemigrations
coverage run ./manage.py test {posargs}
coverage run --omit='*migrations*' ./manage.py test {posargs}
coverage report -m
[testenv:pre-commit]
deps = pre-commit
commands =
pre-commit run --all-files --show-diff-on-failure
[testenv:linters]
deps =
-r{toxinidir}/requirements.txt
flake8
flake8-colors
flake8-django
flake8-import-order
flake8-typing-imports
pep8-naming
pyflakes
pylint
commands =
flake8 logs media search users
pylint .
flake8 logs media users
[flake8]
ignore = D203, W503, E203, I100, I201, I202
ignore = W503, I100, I101
exclude =
.tox,
.git,
@ -44,7 +36,7 @@ exclude =
.cache,
.eggs,
*migrations*
max-complexity = 10
max-complexity = 15
import-order-style = google
application-import-names = flake8
format = ${cyan}%(path)s${reset}:${yellow_bold}%(row)d${reset}:${green_bold}%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s

View File

@ -3,29 +3,14 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin
from django.contrib import messages
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import PasswordResetForm
from django.urls import reverse
from django.utils.html import format_html
from django.utils import timezone
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from reversion.admin import VersionAdmin
from med.admin import admin_site
from .forms import UserCreationAdminForm
from .models import Adhesion, Clef, User
class ClefAdmin(VersionAdmin):
list_display = ('name', 'owner', 'comment')
ordering = ('name',)
search_fields = ('name', 'owner__username', 'comment')
autocomplete_fields = ('owner',)
class AdhesionAdmin(VersionAdmin):
list_display = ('starting_in', 'ending_in')
autocomplete_fields = ('members',)
from .models import User
class IsMemberFilter(admin.SimpleListFilter):
@ -38,12 +23,12 @@ class IsMemberFilter(admin.SimpleListFilter):
)
def queryset(self, request, queryset):
value = self.value()
if value == 'Yes':
# Get current membership year and list all members
last_adh_year = Adhesion.objects.all().order_by('starting_in') \
.reverse().first()
return last_adh_year.members
if self.parameter_name in request.GET:
queryset = queryset.filter(
membership__date_start__lte=timezone.now(),
membership__date_end__gte=timezone.now(),
).distinct()
return queryset
@ -52,62 +37,32 @@ class UserAdmin(VersionAdmin, BaseUserAdmin):
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {'fields': ('first_name', 'last_name', 'email',
'telephone', 'address', 'comment')}),
'phone_number', 'address',
'comment')}),
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions',
'maxemprunt')}),
'groups', 'user_permissions')}),
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
)
list_display = ('username', 'email', 'first_name', 'last_name',
'maxemprunt', 'is_member', 'is_staff')
'is_member', 'is_staff')
list_filter = (IsMemberFilter, 'is_staff', 'is_superuser', 'is_active',
'groups')
# Customize required initial fields
add_form_template = 'admin/change_form.html'
add_form = UserCreationAdminForm
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ("username", "email", "first_name", "last_name",
"address", "telephone"),
}),
)
def save_model(self, request, obj, form, change):
"""
On creation, send a password init mail
"""
super().save_model(request, obj, form, change)
if not change:
# Virtually fill the password reset form
password_reset = PasswordResetForm(data={'email': obj.email})
if password_reset.is_valid():
password_reset.save(request=request,
use_https=request.is_secure())
messages.success(request, _("An email to set the password"
" was sent."))
else:
messages.error(request, _("The email is invalid."))
def has_add_permission(self, request):
# Only add users through Note Kfet login
return False
def is_member(self, obj):
"""
Get current membership year and check if user is there
"""
last_adh_year = Adhesion.objects.all().order_by('starting_in') \
.reverse().first()
is_member = last_adh_year and obj in last_adh_year.members.all()
if is_member:
return format_html(
if obj.is_member:
return mark_safe(
'<img src="/static/admin/img/icon-yes.svg" alt="True">'
)
else:
return format_html(
'<img src="/static/admin/img/icon-no.svg" alt="False"> '
'<a class="button" href="{}">{}</a>',
reverse('users:adherer', args=[obj.pk]),
_('Adhere')
return mark_safe(
'<img src="/static/admin/img/icon-no.svg" alt="False">'
)
is_member.short_description = _('is member')
@ -115,5 +70,3 @@ class UserAdmin(VersionAdmin, BaseUserAdmin):
admin_site.register(User, UserAdmin)
admin_site.register(Adhesion, AdhesionAdmin)
admin_site.register(Clef, ClefAdmin)

View File

@ -1,57 +0,0 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django import forms
from django.contrib.auth.forms import UsernameField
from django.core.validators import MinLengthValidator
from django.forms import ModelForm
from .models import User
class PassForm(forms.Form):
passwd1 = forms.CharField(
label=u'Nouveau mot de passe',
max_length=255,
validators=[MinLengthValidator(8)],
widget=forms.PasswordInput,
)
passwd2 = forms.CharField(
label=u'Saisir à nouveau le mot de passe',
max_length=255,
validators=[MinLengthValidator(8)],
widget=forms.PasswordInput
)
class BaseInfoForm(ModelForm):
class Meta:
model = User
fields = [
'username',
'email',
'first_name',
'last_name',
'address',
'telephone',
]
class UserCreationAdminForm(ModelForm):
"""
A form that creates a user, with no privileges,
from the given information.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['email'].required = True
self.fields['first_name'].required = True
self.fields['last_name'].required = True
class Meta:
model = User
fields = ("username", "email", "first_name", "last_name", "address",
"telephone")
field_classes = {'username': UsernameField}

View File

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-10 16:20+0200\n"
"POT-Creation-Date: 2020-02-20 13:51+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -13,39 +13,39 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: admin.py:32
#: admin.py:25
msgid "membership status"
msgstr "statut adhérent"
#: admin.py:37
#: admin.py:30
msgid "Yes"
msgstr "Oui"
#: admin.py:54
#: admin.py:47
msgid "Personal info"
msgstr ""
msgstr "Informations personnelles"
#: admin.py:56
#: admin.py:49
msgid "Permissions"
msgstr ""
#: admin.py:59
#: admin.py:52
msgid "Important dates"
msgstr ""
msgstr "Dates importantes"
#: admin.py:89
#: admin.py:82
msgid "An email to set the password was sent."
msgstr "Un mail pour initialiser le mot de passe a été envoyé."
#: admin.py:92
#: admin.py:85
msgid "The email is invalid."
msgstr "L'adresse mail est invalide."
#: admin.py:111
#: admin.py:103
msgid "Adhere"
msgstr "Adhérer"
#: admin.py:114
#: admin.py:106
msgid "is member"
msgstr "statut adhérent"
@ -69,7 +69,7 @@ msgstr "emprunts maximal"
msgid "Maximal amount of simultaneous borrowed item authorized."
msgstr "Nombre maximal d'objets empruntés en même temps."
#: models.py:33 models.py:67
#: models.py:33
msgid "comment"
msgstr "commentaire"
@ -82,46 +82,30 @@ msgid "date joined"
msgstr ""
#: models.py:55
msgid "name"
msgstr "nom"
#: models.py:62
msgid "owner"
msgstr "propriétaire"
#: models.py:74
msgid "key"
msgstr "clé"
#: models.py:75
msgid "keys"
msgstr "clés"
#: models.py:80
msgid "starting in"
msgstr "commence en"
#: models.py:81
#: models.py:56
msgid "Year in which the membership year starts."
msgstr "Année dans laquelle la plage d'adhésion commence."
#: models.py:85
#: models.py:60
msgid "ending in"
msgstr "finie en"
#: models.py:86
#: models.py:61
msgid "Year in which the membership year ends."
msgstr "Année dans laquelle la plage d'adhésion finie."
#: models.py:91
#: models.py:66
msgid "members"
msgstr "adhérents"
#: models.py:96
#: models.py:71
msgid "membership year"
msgstr "année d'adhésion"
#: models.py:97
#: models.py:72
msgid "membership years"
msgstr "années d'adhésion"
@ -133,6 +117,18 @@ msgstr ""
msgid "Save"
msgstr ""
#: views.py:40
#: views.py:43
msgid "Edit user profile"
msgstr "Editer le profil utilisateur"
#~ msgid "name"
#~ msgstr "nom"
#~ msgid "owner"
#~ msgstr "propriétaire"
#~ msgid "key"
#~ msgstr "clé"
#~ msgid "keys"
#~ msgstr "clés"

View File

@ -0,0 +1,16 @@
# Generated by Django 2.2.4 on 2020-02-09 16:54
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('users', '0039_auto_20190810_1610'),
]
operations = [
migrations.DeleteModel(
name='Clef',
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 2.2.12 on 2020-09-23 18:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0040_delete_clef'),
]
operations = [
migrations.AlterField(
model_name='user',
name='address',
field=models.CharField(blank=True, default='', max_length=255, verbose_name='address'),
preserve_default=False,
),
migrations.AlterField(
model_name='user',
name='telephone',
field=models.CharField(blank=True, default='', max_length=15, verbose_name='phone number'),
preserve_default=False,
),
]

View File

@ -0,0 +1,16 @@
# Generated by Django 2.2.17 on 2021-10-23 12:48
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('users', '0041_auto_20200923_2030'),
]
operations = [
migrations.DeleteModel(
name='Adhesion',
),
]

View File

@ -0,0 +1,31 @@
# Generated by Django 2.2.24 on 2021-11-02 15:11
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('users', '0042_delete_adhesion'),
]
operations = [
migrations.CreateModel(
name='AccessToken',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('access_token', models.CharField(max_length=32, verbose_name='access token')),
('expires_in', models.PositiveIntegerField(verbose_name='expires in')),
('scopes', models.CharField(max_length=255, verbose_name='scopes')),
('refresh_token', models.CharField(max_length=32, verbose_name='refresh token')),
('expires_at', models.DateTimeField(verbose_name='expires at')),
('owner', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='owner')),
],
options={
'verbose_name': 'access token',
'verbose_name_plural': 'access tokens',
},
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 2.2.24 on 2021-11-04 13:20
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('users', '0043_accesstoken'),
]
operations = [
migrations.CreateModel(
name='Membership',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date_start', models.DateField(auto_now_add=True, verbose_name='start date')),
('date_end', models.DateField(auto_now_add=True, verbose_name='start date')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'membership',
'verbose_name_plural': 'memberships',
},
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 2.2.24 on 2021-11-14 13:23
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('users', '0044_membership'),
]
operations = [
migrations.RenameField(
model_name='user',
old_name='telephone',
new_name='phone_number',
),
migrations.RemoveField(
model_name='user',
name='maxemprunt',
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 2.2.24 on 2021-11-14 15:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0045_auto_20211114_1423'),
]
operations = [
migrations.AlterField(
model_name='membership',
name='date_end',
field=models.DateField(verbose_name='start date'),
),
migrations.AlterField(
model_name='membership',
name='date_start',
field=models.DateField(verbose_name='start date'),
),
]

View File

@ -1,34 +1,30 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# Copyright (C) 2017-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import datetime
import requests
from authlib.integrations.django_client import OAuth
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.db.models import Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from med.settings import MAX_EMPRUNT
class User(AbstractUser):
telephone = models.CharField(
phone_number = models.CharField(
verbose_name=_('phone number'),
max_length=15,
null=True,
blank=True,
)
address = models.CharField(
verbose_name=_('address'),
max_length=255,
null=True,
blank=True,
)
maxemprunt = models.IntegerField(
verbose_name=_('maximum borrowed'),
help_text=_('Maximal amount of simultaneous borrowed item '
'authorized.'),
default=MAX_EMPRUNT,
)
comment = models.CharField(
verbose_name=_('comment'),
help_text=_('Promotion...'),
@ -36,7 +32,7 @@ class User(AbstractUser):
blank=True,
)
date_joined = models.DateTimeField(
_('date joined'),
verbose_name=_('date joined'),
default=timezone.now,
null=True,
)
@ -45,53 +41,171 @@ class User(AbstractUser):
@property
def is_member(self):
last_year = Adhesion.objects.all().order_by(
'starting_in').reverse().first()
return last_year and self in last_year.members.all()
"""
Return True if user is member of the club.
"""
return Membership.objects.filter(
user=self,
date_start__lte=timezone.now(),
date_end__gte=timezone.now()).exists()
def update_data(self, data: dict):
"""
Update user data from given dictionary.
Useful when we want to update user data from Note Kfet.
Parameters
----------
data : dict
Dictionary with user data to update.
"""
self.email = data['email'] or ''
self.first_name = data['first_name'] or ''
self.last_name = data['last_name'] or ''
self.phone_number = data['profile']['phone_number'] or ''
self.address = data['profile']['address'] or ''
self.comment = data['profile']['section'] or ''
for membership_dict in data['memberships']:
if membership_dict['club'] != 22: # Med
continue
# Add membership if not exists
Membership.objects.get_or_create(
user=self,
date_start=membership_dict['date_start'],
date_end=membership_dict['date_end'],
)
# Only members or old members are allow to connect to the website
self.is_active = Membership.objects.filter(user=self).exists()
class Clef(models.Model):
name = models.CharField(
verbose_name=_('name'),
max_length=255,
unique=True,
class Membership(models.Model):
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
verbose_name=_('user'),
)
date_start = models.DateField(
verbose_name=_('start date'),
)
date_end = models.DateField(
verbose_name=_('start date'),
)
def __str__(self):
return f'{self.user}: {self.date_start} to {self.date_end}'
class Meta:
verbose_name = _('membership')
verbose_name_plural = _('memberships')
class AccessToken(models.Model):
owner = models.ForeignKey(
'User',
on_delete=models.PROTECT,
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
null=True,
default=None,
verbose_name=_('owner'),
blank=True,
null=True,
)
comment = models.CharField(
verbose_name=_('comment'),
access_token = models.CharField(
max_length=32,
verbose_name=_('access token'),
)
expires_in = models.PositiveIntegerField(
verbose_name=_('expires in'),
)
scopes = models.CharField(
max_length=255,
null=True,
blank=True,
verbose_name=_('scopes'),
)
refresh_token = models.CharField(
max_length=32,
verbose_name=_('refresh token'),
)
expires_at = models.DateTimeField(
verbose_name=_('expires at'),
)
def refresh(self):
"""
Refresh the access token.
"""
oauth = OAuth()
oauth.register('notekfet')
# Get the OAuth client
oauth_client = oauth.notekfet._get_oauth_client()
# Actually refresh the token
token = oauth_client.refresh_token(oauth.notekfet.access_token_url,
refresh_token=self.refresh_token)
self.access_token = token['access_token']
self.expires_in = token['expires_in']
self.scopes = token['scope']
self.refresh_token = token['refresh_token']
self.expires_at = timezone.utc.fromutc(
datetime.fromtimestamp(token['expires_at'])
)
self.save()
def refresh_if_expired(self):
"""
Refresh the current token if it is invalid.
"""
if self.expires_at < timezone.now():
self.refresh()
def auth_header(self):
"""
Return HTTP header that contains the bearer access token.
Refresh the token if needed.
"""
self.refresh_if_expired()
return {'Authorization': f'Bearer {self.access_token}'}
def fetch_user(self, create_if_not_exist: bool = False):
"""
Extract information about the Note Kfet API by using the current
access token.
"""
data = requests.get(f'{settings.NOTE_KFET_URL}/api/me/',
headers=self.auth_header()).json()
username = data['username']
email = data['email']
qs = User.objects.filter(Q(username=username) | Q(email=email))
if not qs.exists():
if create_if_not_exist:
user = User.objects.create(username=username, email=email)
else:
return None
else:
user = qs.get()
# Update user data from Note Kfet
user.update_data(data)
user.save()
# Store token owner
self.owner = user
self.save()
return user
@classmethod
def get_token(cls, request):
return AccessToken.objects.get(pk=request.session['access_token_id'])
def __str__(self):
return self.access_token
class Meta:
verbose_name = _('key')
verbose_name_plural = _('keys')
class Adhesion(models.Model):
starting_in = models.IntegerField(
verbose_name=_('starting in'),
help_text=_('Year in which the membership year starts.'),
unique=True,
)
ending_in = models.IntegerField(
verbose_name=_('ending in'),
help_text=_('Year in which the membership year ends.'),
unique=True,
)
members = models.ManyToManyField(
'User',
verbose_name=_('members'),
blank=True,
)
class Meta:
verbose_name = _('membership year')
verbose_name_plural = _('membership years')
verbose_name = _('access token')
verbose_name_plural = _('access tokens')

View File

@ -8,7 +8,7 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ['url', 'username', 'first_name', 'last_name', 'email',
'groups', 'telephone', 'address', 'maxemprunt', 'comment',
'groups', 'phone_number', 'address', 'comment',
'date_joined']

View File

@ -1,10 +1,8 @@
# -*- mode: python; coding: utf-8 -*-
# SPDX-License-Identifier: GPL-3.0-or-later
from django.core import mail
from django.test import TestCase
from django.urls import reverse
from users.models import User
"""
@ -21,30 +19,6 @@ class TemplateTests(TestCase):
)
self.client.force_login(self.user)
def test_users_edit_info(self):
response = self.client.get(reverse('users:edit-info'))
self.assertEqual(response.status_code, 200)
def test_users_user_changelist(self):
response = self.client.get(reverse('admin:users_user_changelist'))
self.assertEqual(response.status_code, 200)
def test_users_user_creation_form(self):
response = self.client.get(reverse('admin:users_user_add'))
self.assertEqual(response.status_code, 200)
def test_users_user_add_init_mail(self):
"""
Test that an initialization mail is send when a new user is added
"""
data = {
'username': "test_user",
'email': "test@example.com",
'first_name': "Test",
'last_name': "User",
}
response = self.client.post(reverse(
'admin:users_user_add',
), data=data)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(response.status_code, 302)

View File

@ -8,6 +8,6 @@ from . import views
app_name = 'users'
urlpatterns = [
url(r'^edit_info/$', views.edit_info, name='edit-info'),
url(r'^adherer/(?P<userid>[0-9]+)$', views.adherer, name='adherer'),
url('login/', views.LoginView.as_view(), name='login'),
url('authorize/', views.AuthorizeView.as_view(), name='auth'),
]

View File

@ -1,68 +1,47 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import datetime
from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required
from authlib.integrations.django_client import OAuth
from django.contrib.auth import login
from django.contrib.auth.models import Group
from django.db import transaction
from django.shortcuts import redirect, render
from django.template.context_processors import csrf
from django.utils.translation import ugettext_lazy as _
from django.urls import reverse
from django.utils import timezone
from django.views.generic import RedirectView
from rest_framework import viewsets
from reversion import revisions as reversion
from users.models import User, AccessToken
from users.forms import BaseInfoForm
from users.models import Adhesion, User
from .serializers import GroupSerializer, UserSerializer
def form(ctx, template, request):
c = ctx
c.update(csrf(request))
return render(request, template, c)
class LoginView(RedirectView):
def get_redirect_url(self, *args, **kwargs):
oauth = OAuth()
oauth.register('notekfet')
redirect_url = self.request.build_absolute_uri(reverse('users:auth'))
return oauth.notekfet.authorize_redirect(self.request,
redirect_url).url
@login_required
def edit_info(request):
"""
Edite son utilisateur
"""
user = BaseInfoForm(request.POST or None, instance=request.user)
if user.is_valid():
with transaction.atomic(), reversion.create_revision():
user.save()
reversion.set_user(request.user)
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(
field for field in user.changed_data))
messages.success(request, "L'user a bien été modifié")
return redirect("index")
return form({
'form': user,
'password_change': True,
'title': _('Edit user profile'),
}, 'users/user.html', request)
@login_required
@permission_required('users.add_adhesion')
def adherer(request, userid):
try:
users = User.objects.get(pk=userid)
except User.DoesNotExist:
messages.error(request, "Utilisateur inexistant")
return redirect("admin:users_user_changelist")
adh_year = Adhesion.objects.all().order_by('starting_in').reverse().first()
if not adh_year:
messages.error(request, "Année d'adhésion non définie")
return redirect("admin:users_user_changelist")
with transaction.atomic(), reversion.create_revision():
reversion.set_user(request.user)
adh_year.members.add(users)
adh_year.save()
reversion.set_comment("Adhesion de %s" % users)
messages.success(request, "Adhesion effectuee")
return redirect("admin:users_user_changelist")
class AuthorizeView(RedirectView):
def get_redirect_url(self, *args, **kwargs):
oauth = OAuth()
oauth.register('notekfet')
token = oauth.notekfet.authorize_access_token(self.request)
token_obj = AccessToken.objects.create(
access_token=token['access_token'],
expires_in=token['expires_in'],
scopes=token['scope'],
refresh_token=token['refresh_token'],
expires_at=timezone.utc.fromutc(
datetime.fromtimestamp(token['expires_at'])),
)
user = token_obj.fetch_user(True)
self.request.session['access_token_id'] = token_obj.id
self.request.session.save()
login(self.request, user)
return reverse('index')
class UserViewSet(viewsets.ModelViewSet):