Compare commits
121 Commits
017aaa45d5
...
9dd2a142b7
Author | SHA1 | Date |
---|---|---|
Yohann D'ANELLO | 9dd2a142b7 | |
Yohann D'ANELLO | dcba832549 | |
Yohann D'ANELLO | aa51a40cf6 | |
Yohann D'ANELLO | 263b4cff77 | |
Yohann D'ANELLO | 91aeb28c3a | |
Yohann D'ANELLO | 3af19c0f27 | |
Yohann D'ANELLO | 80c520d76c | |
Yohann D'ANELLO | 665f7a2875 | |
Yohann D'ANELLO | c1098577e1 | |
Yohann D'ANELLO | e09b503ee1 | |
Yohann D'ANELLO | 9c53d89ad3 | |
Yohann D'ANELLO | 8c8692b8d2 | |
Yohann D'ANELLO | 3edc3ffa02 | |
Yohann D'ANELLO | 8fa724e848 | |
Yohann D'ANELLO | 838fcecb56 | |
Yohann D'ANELLO | 8b097dc4e0 | |
Yohann D'ANELLO | 6985e39130 | |
Alexandre Iooss | 6f60de1838 | |
Yohann D'ANELLO | 9ecd876923 | |
Yohann D'ANELLO | be76bf4857 | |
Yohann D'ANELLO | 4198ea8a72 | |
Yohann D'ANELLO | 7ed6b9712b | |
Yohann D'ANELLO | 57659acc93 | |
Yohann D'ANELLO | a06ae5c9b9 | |
Yohann D'ANELLO | 796c985ffb | |
Alexandre Iooss | dc4cb56dd0 | |
Alexandre Iooss | b2f0ee0b44 | |
Alexandre Iooss | 35ecc2800f | |
Alexandre Iooss | 73615afa77 | |
Alexandre Iooss | 8d76bd255a | |
Alexandre Iooss | 5e3003720f | |
Alexandre Iooss | 952a3ddddf | |
Alexandre Iooss | 91b361d7a6 | |
Alexandre Iooss | 9a7304f573 | |
Alexandre Iooss | 8d20b14cbb | |
Alexandre Iooss | ad33d33e6c | |
Alexandre Iooss | d1e9693647 | |
Alexandre Iooss | df1a1cb5de | |
Alexandre Iooss | 47292feab2 | |
Alexandre Iooss | 52af84b146 | |
erdnaxe | a6db8a37e7 | |
Alexandre Iooss | 4409911659 | |
Alexandre Iooss | 0147c5b42c | |
Yohann D'ANELLO | e63d8630cc | |
Yohann D'ANELLO | 44abcaf202 | |
Yohann D'ANELLO | 963ff25506 | |
Yohann D'ANELLO | 3977ab9ec3 | |
Yohann D'ANELLO | 28eac94312 | |
Yohann D'ANELLO | e2d4a80dba | |
Yohann D'ANELLO | 02b81016b8 | |
Yohann D'ANELLO | d88fccb51d | |
Yohann D'ANELLO | 50f3cf39c1 | |
Yohann D'ANELLO | bed5912f54 | |
Yohann D'ANELLO | 20cb710af5 | |
Yohann D'ANELLO | 4ab2e9df57 | |
Yohann D'ANELLO | 054865cd41 | |
Yohann D'ANELLO | 001f40a033 | |
Yohann D'ANELLO | 82efeba272 | |
Yohann D'ANELLO | 1657f5c42c | |
Yohann D'ANELLO | 1c8d5750bb | |
Yohann D'ANELLO | ea30cdec6e | |
Yohann D'ANELLO | adbaf66401 | |
Yohann D'ANELLO | 43b3b5ccfe | |
Yohann D'ANELLO | aa9b69f2d6 | |
Yohann D'ANELLO | 8e39f6039e | |
Yohann D'ANELLO | 10417242f4 | |
Yohann D'ANELLO | 4c55bdd200 | |
Yohann D'ANELLO | 0c9b3c4d5f | |
Alexandre Iooss | 39c3a59838 | |
Alexandre Iooss | 4b07ddda23 | |
Alexandre Iooss | 98b38cd7a4 | |
Alexandre Iooss | 1bf9668315 | |
Alexandre Iooss | 1b848eede9 | |
Alexandre Iooss | 698ae42c9d | |
Alexandre Iooss | 5b86781881 | |
Yohann D'ANELLO | b69ded4115 | |
Yohann D'ANELLO | ff224d20cd | |
Yohann D'ANELLO | 2753f700a6 | |
Yohann D'ANELLO | 343ab02874 | |
Yohann D'ANELLO | 92dc21f014 | |
Yohann D'ANELLO | 47ce447aad | |
Yohann D'ANELLO | 0548e34568 | |
Yohann D'ANELLO | 5b2dd84115 | |
Yohann D'ANELLO | 3d81977dbd | |
Yohann D'ANELLO | 53ea1288c1 | |
Yohann D'ANELLO | ef710bf964 | |
Yohann D'ANELLO | 71a8aa065b | |
Yohann D'ANELLO | 70045d4e2d | |
Yohann D'ANELLO | ea821483d0 | |
Yohann D'ANELLO | 11f0eff4d4 | |
Yohann D'ANELLO | 8dbf0494c2 | |
Yohann D'ANELLO | dc23ac0396 | |
Yohann D'ANELLO | e4d1ed852f | |
Yohann D'ANELLO | 939efe01a0 | |
Yohann D'ANELLO | 778e3239a4 | |
Yohann D'ANELLO | d3a4a246d9 | |
Yohann D'ANELLO | 7fd8e92371 | |
Yohann D'ANELLO | c7d804d9bf | |
Yohann D'ANELLO | ac8d91ac5e | |
Yohann D'ANELLO | f3f9c70de9 | |
Yohann D'ANELLO | f082716895 | |
Yohann D'ANELLO | 3d62973634 | |
Yohann D'ANELLO | 6cd7f883b9 | |
Yohann D'ANELLO | 552d2b8f0e | |
Alexandre Iooss | a52c9f5cb3 | |
Yohann D'ANELLO | 47bf025145 | |
Yohann D'ANELLO | ac866cc0ba | |
Yohann D'ANELLO | 87063e267e | |
Yohann D'ANELLO | 0f0e5fcd25 | |
Alexandre Iooss | a5c560307a | |
Alexandre Iooss | 5623607e4f | |
Yohann D'anello | 4b553386b0 | |
Yohann D'anello | 754b9632c3 | |
Alexandre Iooss | 3fe0dfbc02 | |
Alexandre Iooss | d7d68609d1 | |
Alexandre Iooss | 3337c70a21 | |
Alexandre Iooss | 32dbf748a1 | |
Alexandre Iooss | 5c3c8eed8e | |
Alexandre Iooss | 8a13f87a9e | |
Alexandre Iooss | 4d8d54e7de | |
Alexandre Iooss | 6f780c3f27 |
|
@ -3,9 +3,7 @@ source =
|
|||
logs
|
||||
med
|
||||
media
|
||||
search
|
||||
static
|
||||
templates
|
||||
theme
|
||||
users
|
||||
omit =
|
||||
|
|
|
@ -33,8 +33,9 @@ coverage
|
|||
|
||||
# Local data
|
||||
settings_local.py
|
||||
static_files/*
|
||||
static/*
|
||||
*.log
|
||||
*.pid
|
||||
|
||||
# Virtualenv
|
||||
env/
|
||||
|
|
|
@ -1,26 +1,37 @@
|
|||
image: python:3.6
|
||||
|
||||
stages:
|
||||
- test
|
||||
- quality-assurance
|
||||
|
||||
before_script:
|
||||
- pip install tox
|
||||
|
||||
python35:
|
||||
image: python:3.5
|
||||
stage: test
|
||||
script: tox -e py35
|
||||
|
||||
python36:
|
||||
image: python:3.6
|
||||
stage: test
|
||||
script: tox -e py36
|
||||
|
||||
python37:
|
||||
image: python:3.7
|
||||
py37-django22:
|
||||
stage: test
|
||||
image: debian:buster-backports
|
||||
before_script:
|
||||
- >
|
||||
apt-get update &&
|
||||
apt-get install --no-install-recommends -t buster-backports -y
|
||||
python3-django python3-django-casclient python3-django-reversion python3-djangorestframework
|
||||
python3-docutils python3-pil python3-tz python3-six python3-sqlparse python3-stdnum python3-yaml python3-coreapi tox
|
||||
script: tox -e py37
|
||||
|
||||
linters:
|
||||
py38-django22:
|
||||
stage: test
|
||||
image: ubuntu:20.04
|
||||
before_script:
|
||||
# Fix tzdata prompt
|
||||
- ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone
|
||||
- >
|
||||
apt-get update &&
|
||||
apt-get install --no-install-recommends -y
|
||||
python3-django python3-django-casclient python3-django-reversion python3-djangorestframework
|
||||
python3-docutils python3-pil python3-tz python3-six python3-sqlparse python3-stdnum python3-yaml python3-coreapi tox
|
||||
script: tox -e py38
|
||||
|
||||
linters:
|
||||
stage: quality-assurance
|
||||
image: debian:buster-backports
|
||||
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
|
||||
|
|
76
README.md
76
README.md
|
@ -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.0 # 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
|
||||
|
@ -55,10 +94,6 @@ bureau
|
|||
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 +118,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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from users.models import User
|
||||
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ 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
|
||||
|
||||
|
||||
|
|
84
med/login.py
84
med/login.py
|
@ -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
|
|
@ -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'),
|
||||
|
@ -45,13 +45,13 @@ INSTALLED_APPS = [
|
|||
'django.contrib.sites',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django_filters',
|
||||
|
||||
# Med apps
|
||||
'users',
|
||||
'med',
|
||||
'media',
|
||||
'logs',
|
||||
'sporz',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
@ -145,7 +145,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 +153,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,14 +163,6 @@ 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
|
||||
|
|
|
@ -33,21 +33,10 @@ 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',
|
||||
# },
|
||||
# }
|
16
med/urls.py
16
med/urls.py
|
@ -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
|
||||
|
@ -16,7 +14,13 @@ 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'media/bd', media.views.BDViewSet)
|
||||
router.register(r'media/manga', media.views.MangaViewSet)
|
||||
router.register(r'media/cd', media.views.CDViewSet)
|
||||
router.register(r'media/vinyle', media.views.VinyleViewSet)
|
||||
router.register(r'media/roman', media.views.RomanViewSet)
|
||||
router.register(r'media/revue', media.views.RevueViewSet)
|
||||
router.register(r'media/future', media.views.FutureMediaViewSet)
|
||||
router.register(r'borrowed_items', media.views.EmpruntViewSet)
|
||||
router.register(r'games', media.views.JeuViewSet)
|
||||
router.register(r'users', users.views.UserViewSet)
|
||||
|
@ -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(),
|
||||
|
|
|
@ -8,8 +8,10 @@ from django.utils.translation import ugettext_lazy as _
|
|||
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 Auteur, BD, CD, Emprunt, FutureMedia, Jeu, Manga,\
|
||||
Revue, Roman, Vinyle
|
||||
|
||||
|
||||
class AuteurAdmin(VersionAdmin):
|
||||
|
@ -60,6 +62,49 @@ class MediaAdmin(VersionAdmin):
|
|||
extra_context=extra_context)
|
||||
|
||||
|
||||
class FutureMediaAdmin(VersionAdmin):
|
||||
list_display = ('isbn',)
|
||||
search_fields = ('isbn',)
|
||||
|
||||
def changeform_view(self, request, object_id=None, form_url='',
|
||||
extra_context=None):
|
||||
"""
|
||||
We use _continue for ISBN fetching, so remove continue button
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
class CDAdmin(VersionAdmin):
|
||||
list_display = ('title', 'authors_list', 'side_identifier',)
|
||||
search_fields = ('title', 'authors__name', 'side_identifier',)
|
||||
autocomplete_fields = ('authors',)
|
||||
|
||||
def authors_list(self, obj):
|
||||
return ", ".join([a.name for a in obj.authors.all()])
|
||||
|
||||
authors_list.short_description = _('authors')
|
||||
|
||||
|
||||
class VinyleAdmin(VersionAdmin):
|
||||
list_display = ('title', 'authors_list', 'side_identifier', 'rpm',)
|
||||
search_fields = ('title', 'authors__name', 'side_identifier', 'rpm',)
|
||||
autocomplete_fields = ('authors',)
|
||||
|
||||
def authors_list(self, obj):
|
||||
return ", ".join([a.name for a in obj.authors.all()])
|
||||
|
||||
authors_list.short_description = _('authors')
|
||||
|
||||
|
||||
class RevueAdmin(VersionAdmin):
|
||||
list_display = ('__str__', 'number', 'year', 'month', 'day', 'double',)
|
||||
search_fields = ('title', 'number', 'year',)
|
||||
|
||||
|
||||
class EmpruntAdmin(VersionAdmin):
|
||||
list_display = ('media', 'user', 'date_emprunt', 'date_rendu',
|
||||
'permanencier_emprunt', 'permanencier_rendu_custom')
|
||||
|
@ -104,6 +149,12 @@ class JeuAdmin(VersionAdmin):
|
|||
|
||||
|
||||
admin_site.register(Auteur, AuteurAdmin)
|
||||
admin_site.register(Media, MediaAdmin)
|
||||
admin_site.register(BD, MediaAdmin)
|
||||
admin_site.register(Manga, MediaAdmin)
|
||||
admin_site.register(Roman, MediaAdmin)
|
||||
admin_site.register(CD, CDAdmin)
|
||||
admin_site.register(Vinyle, VinyleAdmin)
|
||||
admin_site.register(Revue, RevueAdmin)
|
||||
admin_site.register(FutureMedia, FutureMediaAdmin)
|
||||
admin_site.register(Emprunt, EmpruntAdmin)
|
||||
admin_site.register(Jeu, JeuAdmin)
|
||||
|
|
277
media/forms.py
277
media/forms.py
|
@ -1,15 +1,80 @@
|
|||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2017-2020 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.db.models import QuerySet
|
||||
from django.forms import ModelForm
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Auteur, BD
|
||||
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 +82,40 @@ 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(Auteur.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):
|
||||
"""
|
||||
|
@ -32,6 +131,65 @@ class MediaAdminForm(ModelForm):
|
|||
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 = Auteur.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 +199,138 @@ 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.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']
|
||||
return True
|
||||
return False
|
||||
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 = Auteur.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:
|
||||
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
|
||||
self.download_data_openlibrary(isbn)
|
||||
if not self.download_data_openlibrary(isbn):
|
||||
self.add_error('isbn',
|
||||
_("This ISBN is not found."))
|
||||
return self.cleaned_data
|
||||
|
||||
return super().clean()
|
||||
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):
|
||||
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))
|
||||
from django.core.exceptions import ValidationError
|
||||
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 = BD
|
||||
fields = '__all__'
|
||||
|
|
|
@ -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: 2020-10-02 13:02+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"
|
||||
|
@ -13,19 +13,20 @@ 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:34 admin.py:89 admin.py:100 models.py:29 models.py:65 models.py:130
|
||||
#: models.py:192 models.py:243 models.py:274
|
||||
msgid "authors"
|
||||
msgstr "auteurs"
|
||||
|
||||
#: admin.py:42
|
||||
#: admin.py:44
|
||||
msgid "external url"
|
||||
msgstr "URL externe"
|
||||
|
||||
#: admin.py:82
|
||||
#: admin.py:127
|
||||
msgid "Turn back"
|
||||
msgstr "Rendre"
|
||||
|
||||
#: admin.py:85 models.py:112
|
||||
#: admin.py:130 models.py:407
|
||||
msgid "given back to"
|
||||
msgstr "rendu à"
|
||||
|
||||
|
@ -33,134 +34,255 @@ msgstr "rendu à"
|
|||
msgid "ISBN-10 or ISBN-13"
|
||||
msgstr "ISBN-10 ou ISBN-13"
|
||||
|
||||
#: models.py:16 models.py:136
|
||||
#: forms.py:244
|
||||
msgid "This ISBN is not found."
|
||||
msgstr "L'ISBN n'a pas été trouvé."
|
||||
|
||||
#: models.py:16 models.py:431
|
||||
msgid "name"
|
||||
msgstr "nom"
|
||||
|
||||
#: models.py:23
|
||||
#: models.py:21
|
||||
msgid "note"
|
||||
msgstr "note"
|
||||
|
||||
#: models.py:28
|
||||
msgid "author"
|
||||
msgstr "auteur"
|
||||
|
||||
#: models.py:30
|
||||
#: models.py:35 models.py:100 models.py:162 models.py:345
|
||||
msgid "ISBN"
|
||||
msgstr "ISBN"
|
||||
|
||||
#: models.py:31
|
||||
#: models.py:36 models.py:101 models.py:163 models.py:346
|
||||
msgid "You may be able to scan it from a bar code."
|
||||
msgstr "Peut souvent être scanné à partir du code barre."
|
||||
|
||||
#: models.py:36
|
||||
#: models.py:43 models.py:108 models.py:170 models.py:224 models.py:263
|
||||
#: models.py:294
|
||||
msgid "title"
|
||||
msgstr "titre"
|
||||
|
||||
#: models.py:40
|
||||
#: models.py:48 models.py:113 models.py:175
|
||||
msgid "subtitle"
|
||||
msgstr "sous-titre"
|
||||
|
||||
#: models.py:46
|
||||
#: models.py:54 models.py:119 models.py:181
|
||||
msgid "external URL"
|
||||
msgstr "URL externe"
|
||||
|
||||
#: models.py:51
|
||||
#: models.py:59 models.py:124 models.py:186 models.py:229 models.py:268
|
||||
msgid "side identifier"
|
||||
msgstr "côte"
|
||||
|
||||
#: models.py:59
|
||||
#: models.py:69 models.py:134 models.py:196
|
||||
msgid "number of pages"
|
||||
msgstr "nombre de pages"
|
||||
|
||||
#: models.py:64
|
||||
#: models.py:75 models.py:140 models.py:202
|
||||
msgid "publish date"
|
||||
msgstr "date de publication"
|
||||
|
||||
#: models.py:76
|
||||
msgid "medium"
|
||||
msgstr "medium"
|
||||
#: models.py:81 models.py:146 models.py:208 models.py:247 models.py:278
|
||||
#: models.py:329 models.py:363
|
||||
msgid "present"
|
||||
msgstr "présent"
|
||||
|
||||
#: models.py:77
|
||||
msgid "media"
|
||||
msgstr "media"
|
||||
#: models.py:82 models.py:147 models.py:209 models.py:248 models.py:279
|
||||
#: models.py:330 models.py:364
|
||||
msgid "Tell that the medium is present in the Mediatek."
|
||||
msgstr "Indique que le medium est présent à la Mediatek."
|
||||
|
||||
#: models.py:89
|
||||
#: models.py:93 models.py:355
|
||||
msgid "BD"
|
||||
msgstr "BD"
|
||||
|
||||
#: models.py:94
|
||||
msgid "BDs"
|
||||
msgstr "BDs"
|
||||
|
||||
#: models.py:155
|
||||
msgid "manga"
|
||||
msgstr "manga"
|
||||
|
||||
#: models.py:156
|
||||
msgid "mangas"
|
||||
msgstr "mangas"
|
||||
|
||||
#: models.py:217
|
||||
msgid "roman"
|
||||
msgstr "roman"
|
||||
|
||||
#: models.py:218
|
||||
msgid "romans"
|
||||
msgstr "romans"
|
||||
|
||||
#: models.py:234
|
||||
msgid "rounds per minute"
|
||||
msgstr "tours par minute"
|
||||
|
||||
#: models.py:236
|
||||
msgid "33 RPM"
|
||||
msgstr "33 TPM"
|
||||
|
||||
#: models.py:237
|
||||
msgid "45 RPM"
|
||||
msgstr "45 TPM"
|
||||
|
||||
#: models.py:256
|
||||
msgid "vinyle"
|
||||
msgstr "vinyle"
|
||||
|
||||
#: models.py:257
|
||||
msgid "vinyles"
|
||||
msgstr "vinyle"
|
||||
|
||||
#: models.py:287
|
||||
msgid "CD"
|
||||
msgstr "CD"
|
||||
|
||||
#: models.py:288
|
||||
msgid "CDs"
|
||||
msgstr "CDs"
|
||||
|
||||
#: models.py:299
|
||||
msgid "number"
|
||||
msgstr "nombre"
|
||||
|
||||
#: models.py:303
|
||||
msgid "year"
|
||||
msgstr "année"
|
||||
|
||||
#: models.py:310
|
||||
msgid "month"
|
||||
msgstr "mois"
|
||||
|
||||
#: models.py:317
|
||||
msgid "day"
|
||||
msgstr "jour"
|
||||
|
||||
#: models.py:324
|
||||
msgid "double"
|
||||
msgstr "double"
|
||||
|
||||
#: models.py:338
|
||||
msgid "revue"
|
||||
msgstr "revue"
|
||||
|
||||
#: models.py:339
|
||||
msgid "revues"
|
||||
msgstr "revues"
|
||||
|
||||
#: models.py:353
|
||||
msgid "type"
|
||||
msgstr "type"
|
||||
|
||||
#: models.py:356
|
||||
msgid "Manga"
|
||||
msgstr "Manga"
|
||||
|
||||
#: models.py:357
|
||||
msgid "Roman"
|
||||
msgstr "Roman"
|
||||
|
||||
#: models.py:369
|
||||
msgid "future medium"
|
||||
msgstr "medium à importer"
|
||||
|
||||
#: models.py:370
|
||||
msgid "future media"
|
||||
msgstr "medias à importer"
|
||||
|
||||
#: models.py:384
|
||||
msgid "borrower"
|
||||
msgstr "emprunteur"
|
||||
|
||||
#: models.py:92
|
||||
#: models.py:387
|
||||
msgid "borrowed on"
|
||||
msgstr "emprunté le"
|
||||
|
||||
#: models.py:97
|
||||
#: models.py:392
|
||||
msgid "given back on"
|
||||
msgstr "rendu le"
|
||||
|
||||
#: models.py:103
|
||||
#: models.py:398
|
||||
msgid "borrowed with"
|
||||
msgstr "emprunté avec"
|
||||
|
||||
#: models.py:104
|
||||
#: models.py:399
|
||||
msgid "The keyholder that registered this borrowed item."
|
||||
msgstr "Le permanencier qui enregistre cet emprunt."
|
||||
|
||||
#: models.py:113
|
||||
#: models.py:408
|
||||
msgid "The keyholder to whom this item was given back."
|
||||
msgstr "Le permanencier à qui l'emprunt a été rendu."
|
||||
|
||||
#: models.py:120
|
||||
#: models.py:415
|
||||
msgid "borrowed item"
|
||||
msgstr "emprunt"
|
||||
|
||||
#: models.py:121
|
||||
#: models.py:416
|
||||
msgid "borrowed items"
|
||||
msgstr "emprunts"
|
||||
|
||||
#: models.py:141
|
||||
#: models.py:436
|
||||
msgid "owner"
|
||||
msgstr "propriétaire"
|
||||
|
||||
#: models.py:146
|
||||
#: models.py:441
|
||||
msgid "duration"
|
||||
msgstr "durée"
|
||||
|
||||
#: models.py:150
|
||||
#: models.py:445
|
||||
msgid "minimum number of players"
|
||||
msgstr "nombre minimum de joueurs"
|
||||
|
||||
#: models.py:154
|
||||
#: models.py:449
|
||||
msgid "maximum number of players"
|
||||
msgstr "nombre maximum de joueurs"
|
||||
|
||||
#: models.py:160
|
||||
#: models.py:454
|
||||
msgid "comment"
|
||||
msgstr "commentaire"
|
||||
|
||||
#: models.py:167
|
||||
#: models.py:461
|
||||
msgid "game"
|
||||
msgstr "jeu"
|
||||
|
||||
#: models.py:168
|
||||
#: models.py:462
|
||||
msgid "games"
|
||||
msgstr "jeux"
|
||||
|
||||
#: templates/media/isbn_button.html:3
|
||||
msgid "Fetch data"
|
||||
msgstr "Télécharger les données"
|
||||
#: templates/media/generate_side_identifier.html:3
|
||||
msgid "Generate side identifier"
|
||||
msgstr "Générer la cote"
|
||||
|
||||
#: validators.py:20
|
||||
#: 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:50
|
||||
msgid "Welcome to the Mediatek database"
|
||||
msgstr "Bienvenue sur la base de données de la Mediatek"
|
||||
|
||||
#~ msgid "medium"
|
||||
#~ msgstr "medium"
|
||||
|
||||
#~ msgid "media"
|
||||
#~ msgstr "media"
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
from argparse import FileType
|
||||
from sys import stdin
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
from media.models import Auteur, 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 = [Auteur.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)))
|
|
@ -0,0 +1,49 @@
|
|||
from random import random
|
||||
from time import sleep
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.management import BaseCommand
|
||||
from media.forms import MediaAdminForm
|
||||
from media.models import BD, FutureMedia, Manga, Roman
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
for future_medium in FutureMedia.objects.all():
|
||||
isbn = future_medium.isbn
|
||||
type_str = future_medium.type
|
||||
if type_str == 'bd':
|
||||
cl = BD
|
||||
elif type_str == 'manga':
|
||||
cl = Manga
|
||||
elif type_str == 'roman':
|
||||
cl = Roman
|
||||
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, 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) + ")")))
|
|
@ -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 BD, FutureMedia, Manga, Roman
|
||||
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 = [BD, Manga, Roman, FutureMedia]
|
||||
|
||||
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
|
||||
|
||||
FutureMedia.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))))
|
|
@ -0,0 +1,50 @@
|
|||
from argparse import FileType
|
||||
from sys import stdin
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
from media.models import Auteur, BD
|
||||
|
||||
|
||||
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 = [Auteur.objects.get_or_create(name=n)[0]
|
||||
for n in revue[2].split('|')]
|
||||
bd = BD.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)))
|
|
@ -0,0 +1,47 @@
|
|||
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 Roman, Auteur
|
||||
|
||||
|
||||
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 = [Auteur.objects.get_or_create(name=n)[0]
|
||||
for n in book[0].split(';')]
|
||||
side_identifier = generate_side_identifier(title, authors)
|
||||
roman = Roman.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)))
|
|
@ -0,0 +1,58 @@
|
|||
from argparse import FileType
|
||||
from sys import stdin
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
from media.models import Revue
|
||||
|
||||
|
||||
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 = Revue.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)))
|
|
@ -0,0 +1,58 @@
|
|||
from argparse import FileType
|
||||
from sys import stdin
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
from media.models import Auteur, Vinyle
|
||||
|
||||
|
||||
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 = [Auteur.objects.get_or_create(name=author)[0]
|
||||
for author in authors_str]
|
||||
vinyle, created = Vinyle.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)))
|
|
@ -0,0 +1,59 @@
|
|||
from django.core.management import BaseCommand
|
||||
from django.db import transaction
|
||||
|
||||
from media.forms import generate_side_identifier
|
||||
from media.models import BD, Manga, Roman
|
||||
|
||||
|
||||
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 = BD
|
||||
elif t == "manga":
|
||||
medium_class = Manga
|
||||
elif t == "roman":
|
||||
medium_class = Roman
|
||||
|
||||
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: {current_side_identifier}, generated side: "
|
||||
f"{generated_side_identifier}, 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."))
|
|
@ -0,0 +1,61 @@
|
|||
from time import sleep
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
from media.forms import MediaAdminForm
|
||||
from media.models import BD, 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 BD.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)))
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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'],
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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'},
|
||||
),
|
||||
]
|
|
@ -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'],
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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'},
|
||||
),
|
||||
]
|
|
@ -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'],
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
),
|
||||
]
|
|
@ -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'],
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
308
media/models.py
308
media/models.py
|
@ -16,6 +16,11 @@ class Auteur(models.Model):
|
|||
verbose_name=_('name'),
|
||||
)
|
||||
|
||||
note = models.IntegerField(
|
||||
default=0,
|
||||
verbose_name=_("note"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
@ -25,47 +30,59 @@ class Auteur(models.Model):
|
|||
ordering = ['name']
|
||||
|
||||
|
||||
class Media(models.Model):
|
||||
class BD(models.Model):
|
||||
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,
|
||||
)
|
||||
|
||||
subtitle = models.CharField(
|
||||
verbose_name=_('subtitle'),
|
||||
max_length=255,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
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',
|
||||
verbose_name=_('authors'),
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
present = models.BooleanField(
|
||||
verbose_name=_("present"),
|
||||
help_text=_("Tell that the medium is present in the Mediatek."),
|
||||
default=False,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
if self.subtitle:
|
||||
return "{} : {}".format(self.title, self.subtitle)
|
||||
|
@ -73,14 +90,292 @@ class Media(models.Model):
|
|||
return self.title
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("medium")
|
||||
verbose_name_plural = _("media")
|
||||
verbose_name = _("BD")
|
||||
verbose_name_plural = _("BDs")
|
||||
ordering = ['title', 'subtitle']
|
||||
|
||||
|
||||
class Manga(models.Model):
|
||||
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,
|
||||
)
|
||||
|
||||
subtitle = models.CharField(
|
||||
verbose_name=_('subtitle'),
|
||||
max_length=255,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
external_url = models.URLField(
|
||||
verbose_name=_('external URL'),
|
||||
blank=True,
|
||||
)
|
||||
|
||||
side_identifier = models.CharField(
|
||||
verbose_name=_('side identifier'),
|
||||
max_length=255,
|
||||
)
|
||||
|
||||
authors = models.ManyToManyField(
|
||||
'Auteur',
|
||||
verbose_name=_('authors'),
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
present = models.BooleanField(
|
||||
verbose_name=_("present"),
|
||||
help_text=_("Tell that the medium is present in the Mediatek."),
|
||||
default=False,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("manga")
|
||||
verbose_name_plural = _("mangas")
|
||||
ordering = ['title']
|
||||
|
||||
|
||||
class Roman(models.Model):
|
||||
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,
|
||||
)
|
||||
|
||||
subtitle = models.CharField(
|
||||
verbose_name=_('subtitle'),
|
||||
max_length=255,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
external_url = models.URLField(
|
||||
verbose_name=_('external URL'),
|
||||
blank=True,
|
||||
)
|
||||
|
||||
side_identifier = models.CharField(
|
||||
verbose_name=_('side identifier'),
|
||||
max_length=255,
|
||||
)
|
||||
|
||||
authors = models.ManyToManyField(
|
||||
'Auteur',
|
||||
verbose_name=_('authors'),
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
present = models.BooleanField(
|
||||
verbose_name=_("present"),
|
||||
help_text=_("Tell that the medium is present in the Mediatek."),
|
||||
default=False,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("roman")
|
||||
verbose_name_plural = _("romans")
|
||||
ordering = ['title', 'subtitle']
|
||||
|
||||
|
||||
class Vinyle(models.Model):
|
||||
title = models.CharField(
|
||||
verbose_name=_('title'),
|
||||
max_length=255,
|
||||
)
|
||||
|
||||
side_identifier = models.CharField(
|
||||
verbose_name=_('side identifier'),
|
||||
max_length=255,
|
||||
)
|
||||
|
||||
rpm = models.PositiveIntegerField(
|
||||
verbose_name=_('rounds per minute'),
|
||||
choices=[
|
||||
(33, _('33 RPM')),
|
||||
(45, _('45 RPM')),
|
||||
],
|
||||
)
|
||||
|
||||
authors = models.ManyToManyField(
|
||||
'Auteur',
|
||||
verbose_name=_('authors'),
|
||||
)
|
||||
|
||||
present = models.BooleanField(
|
||||
verbose_name=_("present"),
|
||||
help_text=_("Tell that the medium is present in the Mediatek."),
|
||||
default=False,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("vinyle")
|
||||
verbose_name_plural = _("vinyles")
|
||||
ordering = ['title']
|
||||
|
||||
|
||||
class CD(models.Model):
|
||||
title = models.CharField(
|
||||
verbose_name=_('title'),
|
||||
max_length=255,
|
||||
)
|
||||
|
||||
side_identifier = models.CharField(
|
||||
verbose_name=_('side identifier'),
|
||||
max_length=255,
|
||||
)
|
||||
|
||||
authors = models.ManyToManyField(
|
||||
'Auteur',
|
||||
verbose_name=_('authors'),
|
||||
)
|
||||
|
||||
present = models.BooleanField(
|
||||
verbose_name=_("present"),
|
||||
help_text=_("Tell that the medium is present in the Mediatek."),
|
||||
default=False,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("CD")
|
||||
verbose_name_plural = _("CDs")
|
||||
ordering = ['title']
|
||||
|
||||
|
||||
class Revue(models.Model):
|
||||
title = models.CharField(
|
||||
verbose_name=_('title'),
|
||||
max_length=255,
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
present = models.BooleanField(
|
||||
verbose_name=_("present"),
|
||||
help_text=_("Tell that the medium is present in the Mediatek."),
|
||||
default=False,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.title + " n°" + str(self.number)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("revue")
|
||||
verbose_name_plural = _("revues")
|
||||
ordering = ['title', 'number']
|
||||
|
||||
|
||||
class FutureMedia(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', _('BD')),
|
||||
('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 Emprunt(models.Model):
|
||||
media = models.ForeignKey(
|
||||
'Media',
|
||||
'BD',
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
|
@ -156,7 +451,6 @@ class Jeu(models.Model):
|
|||
comment = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('comment'),
|
||||
)
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import re
|
||||
|
||||
import requests
|
||||
from media.models import Auteur
|
||||
|
||||
|
||||
class BedetequeScraper:
|
||||
|
@ -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 = Auteur.objects.get_or_create(
|
||||
name=author.group(1))[0]
|
||||
data['authors'].append(author_obj)
|
||||
illustrator = re.search(regex_illustrator, content)
|
||||
if illustrator:
|
||||
author_obj = Auteur.objects.get_or_create(
|
||||
name=illustrator.group(1))[0]
|
||||
data['authors'].append(author_obj)
|
||||
|
||||
return data
|
||||
|
|
|
@ -1,20 +1,54 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from .models import Auteur, Emprunt, Jeu, Media
|
||||
from .models import Auteur, BD, CD, FutureMedia, Manga, Emprunt, Jeu, Revue, Roman, Vinyle
|
||||
|
||||
|
||||
class AuteurSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class AuteurSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Auteur
|
||||
fields = ['url', 'name']
|
||||
|
||||
|
||||
class MediaSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class BDSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Media
|
||||
fields = ['url', 'isbn', 'title', 'subtitle', 'external_url',
|
||||
'side_identifier', 'authors', 'number_of_pages',
|
||||
'publish_date']
|
||||
model = BD
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class MangaSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Manga
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class CDSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = CD
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class VinyleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Vinyle
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class RomanSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Roman
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class RevueSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Revue
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class FutureMediaSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = FutureMedia
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class EmpruntSerializer(serializers.HyperlinkedModelSerializer):
|
||||
|
|
|
@ -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/bd/?search=' + isbn, true);
|
||||
bd_request.onload = function () {
|
||||
let data = JSON.parse(this.response);
|
||||
data.results.forEach(bd => {
|
||||
let present = bd.present;
|
||||
if (markAsPresent && isbn === bd.isbn) {
|
||||
present = true;
|
||||
let presentRequest = new XMLHttpRequest();
|
||||
presentRequest.open("GET", "/media/mark-as-present/bd/" + bd.id + "/", true);
|
||||
presentRequest.send();
|
||||
}
|
||||
result_div.innerHTML += "<li id='bd_" + bd.id + "'>" +
|
||||
"<a href='/database/media/bd/" + bd.id + "/change/'>BD : "
|
||||
+ bd.title + (bd.subtitle ? " - " + bd.subtitle : "") + "</a>"
|
||||
+ (present ? " (<a class='absent' href='#' onclick=\"markAsPresent('bd', " + bd.id + ", false)\">marquer comme absent</a>)"
|
||||
: " (absent, <a class='present' href='#' onclick=\"markAsPresent('bd', " + bd.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/vinyle/?search=' + isbn, true);
|
||||
vinyle_request.onload = function () {
|
||||
let data = JSON.parse(this.response);
|
||||
data.results.forEach(vinyle => {
|
||||
let present = markAsPresent || vinyle.present;
|
||||
result_div.innerHTML += "<li id='vinyle_" + vinyle.id + "'>" +
|
||||
"<a href='/database/media/vinyle/" + vinyle.id + "/change/'>Vinyle : " + vinyle.title + "</a>"
|
||||
+ (present ? " (<a class='absent' href='#' onclick=\"markAsPresent('vinyle', " + vinyle.id + ", false)\">marquer comme absent</a>)"
|
||||
: " (absent, <a class='present' href='#' onclick=\"markAsPresent('vinyle', " + vinyle.id + ")\">marquer comme présent</a>)") + "</li>";
|
||||
});
|
||||
}
|
||||
vinyle_request.send();
|
||||
|
||||
let roman_request = new XMLHttpRequest();
|
||||
roman_request.open('GET', '/api/media/roman/?search=' + isbn, true);
|
||||
roman_request.onload = function () {
|
||||
let data = JSON.parse(this.response);
|
||||
data.results.forEach(roman => {
|
||||
let present = roman.present;
|
||||
if (markAsPresent && isbn === roman.isbn) {
|
||||
present = true;
|
||||
let presentRequest = new XMLHttpRequest();
|
||||
presentRequest.open("GET", "/media/mark-as-present/roman/" + roman.id + "/", true);
|
||||
presentRequest.send();
|
||||
}
|
||||
result_div.innerHTML += "<li id='roman_" + roman.id + "'>" +
|
||||
"<a href='/database/media/roman/" + roman.id + "/change/'>Roman : " + roman.title + "</a>"
|
||||
+ (present ? " (<a class='absent' href='#' onclick=\"markAsPresent('roman', " + roman.id + ", false)\">marquer comme absent</a>)"
|
||||
: " (absent, <a class='present' href='#' onclick=\"markAsPresent('roman', " + roman.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 %}
|
|
@ -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>
|
|
@ -1,3 +1,4 @@
|
|||
{% load i18n %}
|
||||
{% include "django/forms/widgets/input.html" %}
|
||||
<input type="submit" value="{% trans "Fetch data" %}" name="_continue">
|
||||
<input type="button" value="{% trans "Fetch data and add another" %}" name="_isbn_addanother" onclick="form.submit()">
|
||||
<input type="button" value="{% trans "Fetch only" %}" name="_isbn" onclick="form.submit()">
|
|
@ -3,8 +3,7 @@
|
|||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from media.models import Auteur, Media
|
||||
from media.models import Auteur, BD
|
||||
from users.models import User
|
||||
|
||||
"""
|
||||
|
@ -25,41 +24,41 @@ class TemplateTests(TestCase):
|
|||
self.dummy_author = Auteur.objects.create(name="Test author")
|
||||
|
||||
# Create media
|
||||
self.dummy_media1 = Media.objects.create(
|
||||
self.dummy_bd1 = BD.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 = BD.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_bd_bd_changelist(self):
|
||||
response = self.client.get(reverse('admin:media_bd_changelist'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_media_media_add(self):
|
||||
response = self.client.get(reverse('admin:media_media_add'))
|
||||
def test_bd_bd_add(self):
|
||||
response = self.client.get(reverse('admin:media_bd_add'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_media_isbn_download(self):
|
||||
def test_bd_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_bd_change',
|
||||
args=[self.dummy_bd1.id],
|
||||
), data=data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_media_emprunt_changelist(self):
|
||||
def test_bd_emprunt_changelist(self):
|
||||
response = self.client.get(reverse('admin:media_emprunt_changelist'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_media_emprunt_add(self):
|
||||
def test_bd_emprunt_add(self):
|
||||
response = self.client.get(reverse('admin:media_emprunt_add'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
|
@ -10,4 +11,12 @@ 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/bd/<int:pk>/', views.MarkBDAsPresent.as_view(), name="mark_bd_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/vinyle/<int:pk>/', views.MarkVinyleAsPresent.as_view(), name="mark_vinyle_as_present"),
|
||||
path('mark-as-present/roman/<int:pk>/', views.MarkRomanAsPresent.as_view(), name="mark_roman_as_present"),
|
||||
path('mark-as-present/revue/<int:pk>/', views.MarkRevueAsPresent.as_view(), name="mark_revue_as_present"),
|
||||
path('mark-as-present/future/<int:pk>/', views.MarkFutureAsPresent.as_view(), name="mark_future_as_present"),
|
||||
]
|
||||
|
|
|
@ -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'))
|
||||
|
|
127
media/views.py
127
media/views.py
|
@ -1,19 +1,25 @@
|
|||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from http.client import NO_CONTENT
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpResponse
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django.db import transaction
|
||||
from django.shortcuts import redirect, render
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import TemplateView, DetailView
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.filters import SearchFilter
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from .models import Auteur, Emprunt, Jeu, Media
|
||||
from .serializers import AuteurSerializer, EmpruntSerializer, \
|
||||
JeuSerializer, MediaSerializer
|
||||
from .models import Auteur, BD, CD, Emprunt, FutureMedia, Jeu, Manga, Revue, Roman, Vinyle
|
||||
from .serializers import AuteurSerializer, BDSerializer, CDSerializer, EmpruntSerializer, FutureMediaSerializer, \
|
||||
JeuSerializer, MangaSerializer, RevueSerializer, RomanSerializer, VinyleSerializer
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -45,6 +51,46 @@ def index(request):
|
|||
})
|
||||
|
||||
|
||||
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 MarkBDAsPresent(MarkMediumAsPresent):
|
||||
model = BD
|
||||
|
||||
|
||||
class MarkMangaAsPresent(MarkMediumAsPresent):
|
||||
model = Manga
|
||||
|
||||
|
||||
class MarkCDAsPresent(MarkMediumAsPresent):
|
||||
model = CD
|
||||
|
||||
|
||||
class MarkVinyleAsPresent(MarkMediumAsPresent):
|
||||
model = Vinyle
|
||||
|
||||
|
||||
class MarkRomanAsPresent(MarkMediumAsPresent):
|
||||
model = Roman
|
||||
|
||||
|
||||
class MarkRevueAsPresent(MarkMediumAsPresent):
|
||||
model = Revue
|
||||
|
||||
|
||||
class MarkFutureAsPresent(MarkMediumAsPresent):
|
||||
model = FutureMedia
|
||||
|
||||
|
||||
class AuteurViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows authors to be viewed or edited.
|
||||
|
@ -53,12 +99,81 @@ class AuteurViewSet(viewsets.ModelViewSet):
|
|||
serializer_class = AuteurSerializer
|
||||
|
||||
|
||||
class MediaViewSet(viewsets.ModelViewSet):
|
||||
class BDViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows media to be viewed or edited.
|
||||
"""
|
||||
queryset = Media.objects.all()
|
||||
serializer_class = MediaSerializer
|
||||
queryset = BD.objects.all()
|
||||
serializer_class = BDSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ["isbn", "side_identifier"]
|
||||
search_fields = ["=isbn", "title", "subtitle", "side_identifier", "authors__name"]
|
||||
|
||||
|
||||
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 VinyleViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows media to be viewed or edited.
|
||||
"""
|
||||
queryset = Vinyle.objects.all()
|
||||
serializer_class = VinyleSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ["side_identifier", "rpm"]
|
||||
search_fields = ["title", "side_identifier", "authors__name"]
|
||||
|
||||
|
||||
class RomanViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows media to be viewed or edited.
|
||||
"""
|
||||
queryset = Roman.objects.all()
|
||||
serializer_class = RomanSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ["isbn", "side_identifier", "number_of_pages"]
|
||||
search_fields = ["=isbn", "title", "subtitle", "side_identifier", "authors__name"]
|
||||
|
||||
|
||||
class RevueViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows media to be viewed or edited.
|
||||
"""
|
||||
queryset = Revue.objects.all()
|
||||
serializer_class = RevueSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ["number", "year", "month", "day", "double"]
|
||||
search_fields = ["title"]
|
||||
|
||||
|
||||
class FutureMediaViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows media to be viewed or edited.
|
||||
"""
|
||||
queryset = FutureMedia.objects.all()
|
||||
serializer_class = FutureMediaSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ["isbn"]
|
||||
search_fields = ["=isbn"]
|
||||
|
||||
|
||||
class EmpruntViewSet(viewsets.ModelViewSet):
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
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
|
||||
Django~=2.2.10
|
||||
docutils~=0.14
|
||||
Pillow>=5.4.1
|
||||
pytz~=2019.1
|
||||
six~=1.12.0
|
||||
sqlparse~=0.2.4
|
||||
django-filter~=2.1.0
|
||||
django-reversion~=3.0.3
|
||||
python-stdnum~=1.10
|
||||
djangorestframework~=3.9.0
|
||||
pyyaml~=3.13
|
||||
coreapi~=2.3.3
|
||||
|
|
|
@ -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'
|
|
@ -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)
|
|
@ -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')
|
|
@ -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"
|
|
@ -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')},
|
||||
},
|
||||
),
|
||||
]
|
117
sporz/models.py
117
sporz/models.py
|
@ -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']
|
|
@ -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
|
|
@ -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 %}
|
||||
|
@ -96,8 +97,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||
</form>
|
||||
<p>
|
||||
Mediatek 2017-2020 —
|
||||
<a href="mailto:club-med@crans.org">Nous contactez</a> —
|
||||
<a href="{% url "redoc" %}">Explorer l'API</a>
|
||||
<a href="mailto:club-med@crans.org">Nous contacter</a> —
|
||||
<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 %}
|
||||
|
|
|
@ -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 %}
|
|
@ -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
22
tox.ini
|
@ -1,38 +1,30 @@
|
|||
[tox]
|
||||
envlist = py35,py36,py37,linters
|
||||
envlist = py37,py38,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
|
||||
|
|
|
@ -10,17 +10,10 @@ from django.urls import reverse
|
|||
from django.utils.html import format_html
|
||||
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',)
|
||||
from .models import Adhesion, User
|
||||
|
||||
|
||||
class AdhesionAdmin(VersionAdmin):
|
||||
|
@ -116,4 +109,3 @@ class UserAdmin(VersionAdmin, BaseUserAdmin):
|
|||
|
||||
admin_site.register(User, UserAdmin)
|
||||
admin_site.register(Adhesion, AdhesionAdmin)
|
||||
admin_site.register(Clef, ClefAdmin)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
),
|
||||
]
|
|
@ -6,7 +6,6 @@ from django.contrib.auth.models import AbstractUser
|
|||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from med.settings import MAX_EMPRUNT
|
||||
|
||||
|
||||
|
@ -14,13 +13,11 @@ class User(AbstractUser):
|
|||
telephone = 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(
|
||||
|
@ -50,31 +47,6 @@ class User(AbstractUser):
|
|||
return last_year and self in last_year.members.all()
|
||||
|
||||
|
||||
class Clef(models.Model):
|
||||
name = models.CharField(
|
||||
verbose_name=_('name'),
|
||||
max_length=255,
|
||||
unique=True,
|
||||
)
|
||||
owner = models.ForeignKey(
|
||||
'User',
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name=_('owner'),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
comment = models.CharField(
|
||||
verbose_name=_('comment'),
|
||||
max_length=255,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('key')
|
||||
verbose_name_plural = _('keys')
|
||||
|
||||
|
||||
class Adhesion(models.Model):
|
||||
starting_in = models.IntegerField(
|
||||
verbose_name=_('starting in'),
|
||||
|
@ -95,3 +67,6 @@ class Adhesion(models.Model):
|
|||
class Meta:
|
||||
verbose_name = _('membership year')
|
||||
verbose_name_plural = _('membership years')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.starting_in} - {self.ending_in}"
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
from django.core import mail
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from users.models import User
|
||||
|
||||
"""
|
||||
|
|
|
@ -11,9 +11,9 @@ from django.template.context_processors import csrf
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import viewsets
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from users.forms import BaseInfoForm
|
||||
from users.models import Adhesion, User
|
||||
|
||||
from .serializers import GroupSerializer, UserSerializer
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue