Compare commits
No commits in common. "9dd2a142b7b0b2b3f9c70a7d15fff366cd2e6b80" and "017aaa45d53b7385db9ad9cab9db1fb275effe9c" have entirely different histories.
9dd2a142b7
...
017aaa45d5
|
@ -3,7 +3,9 @@ source =
|
||||||
logs
|
logs
|
||||||
med
|
med
|
||||||
media
|
media
|
||||||
|
search
|
||||||
static
|
static
|
||||||
|
templates
|
||||||
theme
|
theme
|
||||||
users
|
users
|
||||||
omit =
|
omit =
|
||||||
|
|
|
@ -33,9 +33,8 @@ coverage
|
||||||
|
|
||||||
# Local data
|
# Local data
|
||||||
settings_local.py
|
settings_local.py
|
||||||
static/*
|
static_files/*
|
||||||
*.log
|
*.log
|
||||||
*.pid
|
|
||||||
|
|
||||||
# Virtualenv
|
# Virtualenv
|
||||||
env/
|
env/
|
||||||
|
|
|
@ -1,37 +1,26 @@
|
||||||
|
image: python:3.6
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
- quality-assurance
|
|
||||||
|
|
||||||
py37-django22:
|
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
|
||||||
stage: test
|
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
|
script: tox -e py37
|
||||||
|
|
||||||
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:
|
linters:
|
||||||
stage: quality-assurance
|
stage: test
|
||||||
image: debian:buster-backports
|
|
||||||
before_script:
|
|
||||||
- apt-get update && apt-get install -y tox
|
|
||||||
script: tox -e linters
|
script: tox -e linters
|
||||||
|
|
||||||
# Be nice to new contributors, but please use `tox`
|
|
||||||
allow_failure: true
|
|
||||||
|
|
80
README.md
80
README.md
|
@ -11,75 +11,36 @@ 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.
|
Ce projet est sous la licence GNU public license v3.0.
|
||||||
|
|
||||||
## Installation
|
## Développement
|
||||||
|
|
||||||
### Développement
|
Après avoir installé un environnement Django,
|
||||||
|
|
||||||
On peut soit développer avec Docker, soit utiliser un VirtualEnv.
|
|
||||||
|
|
||||||
Dans le cas du VirtualEnv,
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -m venv venv
|
|
||||||
. venv/bin/activate
|
|
||||||
pip install -r requirements.txt
|
|
||||||
./manage.py compilemessages
|
|
||||||
./manage.py makemigrations
|
|
||||||
./manage.py migrate
|
./manage.py migrate
|
||||||
|
./manage.py collectstatic
|
||||||
./manage.py runserver
|
./manage.py runserver
|
||||||
```
|
```
|
||||||
|
|
||||||
### Production
|
## Configuration d'une base MySQL
|
||||||
|
|
||||||
Vous pouvez soit utiliser Docker, soit configurer manuellement le serveur.
|
Sur le serveur mysql ou postgresl, il est nécessaire de créer une base de donnée med,
|
||||||
|
|
||||||
#### Mise en place du projet sur Zamok
|
|
||||||
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
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é.
|
ainsi qu'un user med et un mot de passe associé.
|
||||||
|
|
||||||
Voici les étapes à executer pour PostgreSQL :
|
Voici les étapes à éxecuter pour mysql :
|
||||||
|
|
||||||
```SQL
|
```SQL
|
||||||
CREATE DATABASE "club-med";
|
CREATE DATABASE med;
|
||||||
CREATE USER "club-med" WITH PASSWORD 'MY-STRONG-PASSWORD';
|
CREATE USER 'med'@'localhost' IDENTIFIED BY 'password';
|
||||||
GRANT ALL PRIVILEGES ON DATABASE "club-med" TO "club-med";
|
GRANT ALL PRIVILEGES ON med.* TO 'med'@'localhost';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
```
|
||||||
|
|
||||||
|
Et pour postgresql :
|
||||||
|
|
||||||
|
```SQL
|
||||||
|
CREATE DATABASE med;
|
||||||
|
CREATE USER med WITH PASSWORD 'password';
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE med TO med;
|
||||||
```
|
```
|
||||||
|
|
||||||
## Exemple de groupes de droits
|
## Exemple de groupes de droits
|
||||||
|
@ -94,6 +55,10 @@ bureau
|
||||||
users | Can add adhesion
|
users | Can add adhesion
|
||||||
users | Can change adhesion
|
users | Can change adhesion
|
||||||
users | Can delete 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 view user
|
||||||
users | Can add user
|
users | Can add user
|
||||||
users | Can change user
|
users | Can change user
|
||||||
|
@ -118,6 +83,7 @@ keyholder
|
||||||
media | Can change borrowed item
|
media | Can change borrowed item
|
||||||
media | Can delete borrowed item
|
media | Can delete borrowed item
|
||||||
users | Can view user
|
users | Can view user
|
||||||
|
users | Can view clef
|
||||||
|
|
||||||
users (default group for everyone)
|
users (default group for everyone)
|
||||||
media | Can view author
|
media | Can view author
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
# 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,21 +1,8 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# This will launch the Django project as a fastcgi socket
|
|
||||||
# then Apache or NGINX will be able to use that socket
|
|
||||||
|
|
||||||
# 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
|
python manage.py compilemessages
|
||||||
|
python manage.py makemigrations
|
||||||
# Wait for database (docker)
|
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
python manage.py migrate
|
python manage.py migrate
|
||||||
python manage.py collectstatic --no-input
|
|
||||||
|
|
||||||
# harakiri parameter respawns processes taking more than 20 seconds
|
# TODO: use uwsgi in production
|
||||||
# max-requests parameter respawns processes after serving 5000 requests
|
python manage.py runserver 0.0.0.0:8000
|
||||||
# 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,6 +3,7 @@
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -9,6 +9,7 @@ from django.db.models import Count
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from reversion.models import Revision
|
from reversion.models import Revision
|
||||||
|
|
||||||
from med.settings import PAGINATION_NUMBER
|
from med.settings import PAGINATION_NUMBER
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ from django.contrib.auth.admin import Group, GroupAdmin
|
||||||
from django.contrib.sites.admin import Site, SiteAdmin
|
from django.contrib.sites.admin import Site, SiteAdmin
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.cache import never_cache
|
from django.views.decorators.cache import never_cache
|
||||||
|
|
||||||
from media.models import Emprunt
|
from media.models import Emprunt
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
# -*- 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!'
|
SECRET_KEY = 'CHANGE_ME_IN_LOCAL_SETTINGS!'
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = False
|
||||||
|
|
||||||
ADMINS = (
|
ADMINS = (
|
||||||
# ('Admin', 'webmaster@example.com'),
|
# ('Admin', 'webmaster@example.com'),
|
||||||
|
@ -45,13 +45,13 @@ INSTALLED_APPS = [
|
||||||
'django.contrib.sites',
|
'django.contrib.sites',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'django_filters',
|
|
||||||
|
|
||||||
# Med apps
|
# Med apps
|
||||||
'users',
|
'users',
|
||||||
'med',
|
'med',
|
||||||
'media',
|
'media',
|
||||||
'logs',
|
'logs',
|
||||||
|
'sporz',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
@ -145,7 +145,7 @@ USE_TZ = True
|
||||||
# Don't put anything in this directory yourself; store your static files
|
# Don't put anything in this directory yourself; store your static files
|
||||||
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
|
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
|
||||||
# Example: "/var/www/example.com/static/"
|
# Example: "/var/www/example.com/static/"
|
||||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
STATIC_ROOT = os.path.join(BASE_DIR, 'static_files')
|
||||||
|
|
||||||
# URL prefix for static files.
|
# URL prefix for static files.
|
||||||
# Example: "http://example.com/static/", "http://static.example.com/"
|
# Example: "http://example.com/static/", "http://static.example.com/"
|
||||||
|
@ -153,8 +153,6 @@ STATIC_URL = '/static/'
|
||||||
|
|
||||||
# Django REST Framework
|
# Django REST Framework
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
|
||||||
'PAGE_SIZE': 10,
|
|
||||||
'DEFAULT_PERMISSION_CLASSES': [
|
'DEFAULT_PERMISSION_CLASSES': [
|
||||||
'med.permissions.DjangoViewModelPermissions',
|
'med.permissions.DjangoViewModelPermissions',
|
||||||
]
|
]
|
||||||
|
@ -163,6 +161,14 @@ REST_FRAMEWORK = {
|
||||||
# Med configuration
|
# Med configuration
|
||||||
PAGINATION_NUMBER = 25
|
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'
|
AUTH_USER_MODEL = 'users.User'
|
||||||
|
|
||||||
MAX_EMPRUNT = 5 # Max emprunts
|
MAX_EMPRUNT = 5 # Max emprunts
|
||||||
|
|
|
@ -33,10 +33,21 @@ DEBUG = True
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||||
'NAME': 'club-med',
|
'NAME': 'med',
|
||||||
'USER': 'club-med',
|
'USER': 'med',
|
||||||
'PASSWORD': 'password_to_store_in_env',
|
'PASSWORD': 'password_to_store_in_env',
|
||||||
'HOST': 'db',
|
'HOST': 'db',
|
||||||
'PORT': '',
|
'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,10 +2,12 @@
|
||||||
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
|
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# 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.contrib.auth.views import PasswordResetView
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView, TemplateView
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
from rest_framework.schemas import get_schema_view
|
||||||
|
|
||||||
import media.views
|
import media.views
|
||||||
import users.views
|
import users.views
|
||||||
|
@ -14,13 +16,7 @@ from .admin import admin_site
|
||||||
# API router
|
# API router
|
||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
router.register(r'authors', media.views.AuteurViewSet)
|
router.register(r'authors', media.views.AuteurViewSet)
|
||||||
router.register(r'media/bd', media.views.BDViewSet)
|
router.register(r'media', media.views.MediaViewSet)
|
||||||
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'borrowed_items', media.views.EmpruntViewSet)
|
||||||
router.register(r'games', media.views.JeuViewSet)
|
router.register(r'games', media.views.JeuViewSet)
|
||||||
router.register(r'users', users.views.UserViewSet)
|
router.register(r'users', users.views.UserViewSet)
|
||||||
|
@ -37,6 +33,10 @@ urlpatterns = [
|
||||||
# REST API
|
# REST API
|
||||||
path('api/', include(router.urls)),
|
path('api/', include(router.urls)),
|
||||||
path('api-auth/', include('rest_framework.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
|
# Include Django Contrib and Core routers
|
||||||
path('accounts/password_reset/', PasswordResetView.as_view(),
|
path('accounts/password_reset/', PasswordResetView.as_view(),
|
||||||
|
|
|
@ -8,10 +8,8 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from reversion.admin import VersionAdmin
|
from reversion.admin import VersionAdmin
|
||||||
|
|
||||||
from med.admin import admin_site
|
from med.admin import admin_site
|
||||||
|
|
||||||
from .forms import MediaAdminForm
|
from .forms import MediaAdminForm
|
||||||
from .models import Auteur, BD, CD, Emprunt, FutureMedia, Jeu, Manga,\
|
from .models import Auteur, Emprunt, Jeu, Media
|
||||||
Revue, Roman, Vinyle
|
|
||||||
|
|
||||||
|
|
||||||
class AuteurAdmin(VersionAdmin):
|
class AuteurAdmin(VersionAdmin):
|
||||||
|
@ -62,49 +60,6 @@ class MediaAdmin(VersionAdmin):
|
||||||
extra_context=extra_context)
|
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):
|
class EmpruntAdmin(VersionAdmin):
|
||||||
list_display = ('media', 'user', 'date_emprunt', 'date_rendu',
|
list_display = ('media', 'user', 'date_emprunt', 'date_rendu',
|
||||||
'permanencier_emprunt', 'permanencier_rendu_custom')
|
'permanencier_emprunt', 'permanencier_rendu_custom')
|
||||||
|
@ -149,12 +104,6 @@ class JeuAdmin(VersionAdmin):
|
||||||
|
|
||||||
|
|
||||||
admin_site.register(Auteur, AuteurAdmin)
|
admin_site.register(Auteur, AuteurAdmin)
|
||||||
admin_site.register(BD, MediaAdmin)
|
admin_site.register(Media, 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(Emprunt, EmpruntAdmin)
|
||||||
admin_site.register(Jeu, JeuAdmin)
|
admin_site.register(Jeu, JeuAdmin)
|
||||||
|
|
295
media/forms.py
295
media/forms.py
|
@ -1,80 +1,15 @@
|
||||||
# -*- mode: python; coding: utf-8 -*-
|
# -*- mode: python; coding: utf-8 -*-
|
||||||
# Copyright (C) 2017-2020 by BDE ENS Paris-Saclay
|
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import unicodedata
|
|
||||||
from urllib.error import HTTPError
|
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
from django.db.models import QuerySet
|
|
||||||
from django.forms import ModelForm
|
from django.forms import ModelForm
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from .models import Auteur, BD
|
|
||||||
from .scraper import BedetequeScraper
|
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):
|
class MediaAdminForm(ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
@ -82,40 +17,6 @@ class MediaAdminForm(ModelForm):
|
||||||
if isbn_field:
|
if isbn_field:
|
||||||
isbn_field.widget.template_name = "media/isbn_button.html"
|
isbn_field.widget.template_name = "media/isbn_button.html"
|
||||||
isbn_field.widget.attrs.update({'autofocus': 'autofocus'})
|
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):
|
def download_data_bedeteque(self, isbn):
|
||||||
"""
|
"""
|
||||||
|
@ -131,65 +32,6 @@ class MediaAdminForm(ModelForm):
|
||||||
self.cleaned_data.update(data)
|
self.cleaned_data.update(data)
|
||||||
return True
|
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):
|
def download_data_openlibrary(self, isbn):
|
||||||
"""
|
"""
|
||||||
Download data from openlibrary
|
Download data from openlibrary
|
||||||
|
@ -199,138 +41,33 @@ class MediaAdminForm(ModelForm):
|
||||||
"&format=json&jscmd=data".format(isbn)
|
"&format=json&jscmd=data".format(isbn)
|
||||||
with urllib.request.urlopen(api_url) as url:
|
with urllib.request.urlopen(api_url) as url:
|
||||||
data = json.loads(url.read().decode())
|
data = json.loads(url.read().decode())
|
||||||
|
|
||||||
if data and data['ISBN:' + isbn]:
|
if data and data['ISBN:' + isbn]:
|
||||||
data = data['ISBN:' + isbn]
|
data = data['ISBN:' + isbn]
|
||||||
if 'url' in data:
|
if 'url' in data:
|
||||||
# Fill the data
|
# Fill the data
|
||||||
self.parse_data_openlibrary(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 True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def parse_data_openlibrary(self, data):
|
|
||||||
self.cleaned_data['external_url'] = data['url']
|
|
||||||
if 'title' in data:
|
|
||||||
self.cleaned_data['title'] = data['title']
|
|
||||||
if 'subtitle' in data:
|
|
||||||
self.cleaned_data['subtitle'] = data['subtitle']
|
|
||||||
|
|
||||||
if 'number_of_pages' in data:
|
|
||||||
self.cleaned_data['number_of_pages'] = \
|
|
||||||
data['number_of_pages']
|
|
||||||
elif not self.cleaned_data['number_of_pages']:
|
|
||||||
self.cleaned_data['number_of_pages'] = 0
|
|
||||||
|
|
||||||
if 'publish_date' in data:
|
|
||||||
months = ['January', 'February', "March", "April", "Mai",
|
|
||||||
"June", "July", "August", "September",
|
|
||||||
"October", "November", "December"]
|
|
||||||
split = data['publish_date'].replace(',', '').split(' ')
|
|
||||||
if len(split) == 1:
|
|
||||||
self.cleaned_data['publish_date'] = split[0] + "-01-01"
|
|
||||||
else:
|
|
||||||
month_to_number = dict(
|
|
||||||
Jan="01",
|
|
||||||
Feb="02",
|
|
||||||
Mar="03",
|
|
||||||
Apr="04",
|
|
||||||
May="05",
|
|
||||||
Jun="06",
|
|
||||||
Jul="07",
|
|
||||||
Aug="08",
|
|
||||||
Sep="09",
|
|
||||||
Oct="10",
|
|
||||||
Nov="11",
|
|
||||||
Dec="12",
|
|
||||||
)
|
|
||||||
if split[0][:3] in month_to_number:
|
|
||||||
self.cleaned_data['publish_date']\
|
|
||||||
= split[2] + "-" \
|
|
||||||
+ month_to_number[split[0][:3]] + "-" + split[1]
|
|
||||||
else:
|
|
||||||
self.cleaned_data['publish_date'] = "{}-{:02d}-{:02d}" \
|
|
||||||
.format(split[2], months.index(split[0])
|
|
||||||
+ 1, int(split[1]), )
|
|
||||||
|
|
||||||
if 'authors' not in self.cleaned_data \
|
|
||||||
or not self.cleaned_data['authors']:
|
|
||||||
self.cleaned_data['authors'] = list()
|
|
||||||
|
|
||||||
if 'authors' in data:
|
|
||||||
for author in data['authors']:
|
|
||||||
author_obj = Auteur.objects.get_or_create(
|
|
||||||
name=author['name'])[0]
|
|
||||||
self.cleaned_data['authors'].append(author_obj)
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""
|
"""
|
||||||
If user fetch ISBN data, then download data before validating the form
|
If user fetch ISBN data, then download data before validating the form
|
||||||
"""
|
"""
|
||||||
super().clean()
|
# TODO implement authors, side_identifier
|
||||||
|
if "_continue" in self.request.POST:
|
||||||
if "_isbn" in self.data\
|
|
||||||
or "_isbn_addanother" in self.data:
|
|
||||||
isbn = self.cleaned_data.get('isbn')
|
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:
|
if isbn:
|
||||||
scrap_result = self.download_data_isbndb(isbn)
|
# ISBN is present, try with bedeteque
|
||||||
|
scrap_result = self.download_data_bedeteque(isbn)
|
||||||
if not scrap_result:
|
if not scrap_result:
|
||||||
# ISBN is present, try with bedeteque
|
# Try with OpenLibrary
|
||||||
scrap_result = self.download_data_bedeteque(isbn)
|
self.download_data_openlibrary(isbn)
|
||||||
if not scrap_result:
|
|
||||||
# Try with Google
|
|
||||||
scrap_result = self.download_data_google(isbn)
|
|
||||||
if not scrap_result:
|
|
||||||
# Try with OpenLibrary
|
|
||||||
if not self.download_data_openlibrary(isbn):
|
|
||||||
self.add_error('isbn',
|
|
||||||
_("This ISBN is not found."))
|
|
||||||
return self.cleaned_data
|
|
||||||
|
|
||||||
if self.cleaned_data['title']:
|
return super().clean()
|
||||||
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 ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2020-10-02 13:02+0200\n"
|
"POT-Creation-Date: 2019-08-16 14:00+0200\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
@ -13,20 +13,19 @@ msgstr ""
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||||
|
|
||||||
#: admin.py:34 admin.py:89 admin.py:100 models.py:29 models.py:65 models.py:130
|
#: admin.py:32 models.py:24 models.py:56
|
||||||
#: models.py:192 models.py:243 models.py:274
|
|
||||||
msgid "authors"
|
msgid "authors"
|
||||||
msgstr "auteurs"
|
msgstr "auteurs"
|
||||||
|
|
||||||
#: admin.py:44
|
#: admin.py:42
|
||||||
msgid "external url"
|
msgid "external url"
|
||||||
msgstr "URL externe"
|
msgstr "URL externe"
|
||||||
|
|
||||||
#: admin.py:127
|
#: admin.py:82
|
||||||
msgid "Turn back"
|
msgid "Turn back"
|
||||||
msgstr "Rendre"
|
msgstr "Rendre"
|
||||||
|
|
||||||
#: admin.py:130 models.py:407
|
#: admin.py:85 models.py:112
|
||||||
msgid "given back to"
|
msgid "given back to"
|
||||||
msgstr "rendu à"
|
msgstr "rendu à"
|
||||||
|
|
||||||
|
@ -34,255 +33,134 @@ msgstr "rendu à"
|
||||||
msgid "ISBN-10 or ISBN-13"
|
msgid "ISBN-10 or ISBN-13"
|
||||||
msgstr "ISBN-10 ou ISBN-13"
|
msgstr "ISBN-10 ou ISBN-13"
|
||||||
|
|
||||||
#: forms.py:244
|
#: models.py:16 models.py:136
|
||||||
msgid "This ISBN is not found."
|
|
||||||
msgstr "L'ISBN n'a pas été trouvé."
|
|
||||||
|
|
||||||
#: models.py:16 models.py:431
|
|
||||||
msgid "name"
|
msgid "name"
|
||||||
msgstr "nom"
|
msgstr "nom"
|
||||||
|
|
||||||
#: models.py:21
|
#: models.py:23
|
||||||
msgid "note"
|
|
||||||
msgstr "note"
|
|
||||||
|
|
||||||
#: models.py:28
|
|
||||||
msgid "author"
|
msgid "author"
|
||||||
msgstr "auteur"
|
msgstr "auteur"
|
||||||
|
|
||||||
#: models.py:35 models.py:100 models.py:162 models.py:345
|
#: models.py:30
|
||||||
msgid "ISBN"
|
msgid "ISBN"
|
||||||
msgstr "ISBN"
|
msgstr "ISBN"
|
||||||
|
|
||||||
#: models.py:36 models.py:101 models.py:163 models.py:346
|
#: models.py:31
|
||||||
msgid "You may be able to scan it from a bar code."
|
msgid "You may be able to scan it from a bar code."
|
||||||
msgstr "Peut souvent être scanné à partir du code barre."
|
msgstr "Peut souvent être scanné à partir du code barre."
|
||||||
|
|
||||||
#: models.py:43 models.py:108 models.py:170 models.py:224 models.py:263
|
#: models.py:36
|
||||||
#: models.py:294
|
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr "titre"
|
msgstr "titre"
|
||||||
|
|
||||||
#: models.py:48 models.py:113 models.py:175
|
#: models.py:40
|
||||||
msgid "subtitle"
|
msgid "subtitle"
|
||||||
msgstr "sous-titre"
|
msgstr "sous-titre"
|
||||||
|
|
||||||
#: models.py:54 models.py:119 models.py:181
|
#: models.py:46
|
||||||
msgid "external URL"
|
msgid "external URL"
|
||||||
msgstr "URL externe"
|
msgstr "URL externe"
|
||||||
|
|
||||||
#: models.py:59 models.py:124 models.py:186 models.py:229 models.py:268
|
#: models.py:51
|
||||||
msgid "side identifier"
|
msgid "side identifier"
|
||||||
msgstr "côte"
|
msgstr "côte"
|
||||||
|
|
||||||
#: models.py:69 models.py:134 models.py:196
|
#: models.py:59
|
||||||
msgid "number of pages"
|
msgid "number of pages"
|
||||||
msgstr "nombre de pages"
|
msgstr "nombre de pages"
|
||||||
|
|
||||||
#: models.py:75 models.py:140 models.py:202
|
#: models.py:64
|
||||||
msgid "publish date"
|
msgid "publish date"
|
||||||
msgstr "date de publication"
|
msgstr "date de publication"
|
||||||
|
|
||||||
#: models.py:81 models.py:146 models.py:208 models.py:247 models.py:278
|
#: models.py:76
|
||||||
#: models.py:329 models.py:363
|
msgid "medium"
|
||||||
msgid "present"
|
msgstr "medium"
|
||||||
msgstr "présent"
|
|
||||||
|
|
||||||
#: models.py:82 models.py:147 models.py:209 models.py:248 models.py:279
|
#: models.py:77
|
||||||
#: models.py:330 models.py:364
|
msgid "media"
|
||||||
msgid "Tell that the medium is present in the Mediatek."
|
msgstr "media"
|
||||||
msgstr "Indique que le medium est présent à la Mediatek."
|
|
||||||
|
|
||||||
#: models.py:93 models.py:355
|
#: models.py:89
|
||||||
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"
|
msgid "borrower"
|
||||||
msgstr "emprunteur"
|
msgstr "emprunteur"
|
||||||
|
|
||||||
#: models.py:387
|
#: models.py:92
|
||||||
msgid "borrowed on"
|
msgid "borrowed on"
|
||||||
msgstr "emprunté le"
|
msgstr "emprunté le"
|
||||||
|
|
||||||
#: models.py:392
|
#: models.py:97
|
||||||
msgid "given back on"
|
msgid "given back on"
|
||||||
msgstr "rendu le"
|
msgstr "rendu le"
|
||||||
|
|
||||||
#: models.py:398
|
#: models.py:103
|
||||||
msgid "borrowed with"
|
msgid "borrowed with"
|
||||||
msgstr "emprunté avec"
|
msgstr "emprunté avec"
|
||||||
|
|
||||||
#: models.py:399
|
#: models.py:104
|
||||||
msgid "The keyholder that registered this borrowed item."
|
msgid "The keyholder that registered this borrowed item."
|
||||||
msgstr "Le permanencier qui enregistre cet emprunt."
|
msgstr "Le permanencier qui enregistre cet emprunt."
|
||||||
|
|
||||||
#: models.py:408
|
#: models.py:113
|
||||||
msgid "The keyholder to whom this item was given back."
|
msgid "The keyholder to whom this item was given back."
|
||||||
msgstr "Le permanencier à qui l'emprunt a été rendu."
|
msgstr "Le permanencier à qui l'emprunt a été rendu."
|
||||||
|
|
||||||
#: models.py:415
|
#: models.py:120
|
||||||
msgid "borrowed item"
|
msgid "borrowed item"
|
||||||
msgstr "emprunt"
|
msgstr "emprunt"
|
||||||
|
|
||||||
#: models.py:416
|
#: models.py:121
|
||||||
msgid "borrowed items"
|
msgid "borrowed items"
|
||||||
msgstr "emprunts"
|
msgstr "emprunts"
|
||||||
|
|
||||||
#: models.py:436
|
#: models.py:141
|
||||||
msgid "owner"
|
msgid "owner"
|
||||||
msgstr "propriétaire"
|
msgstr "propriétaire"
|
||||||
|
|
||||||
#: models.py:441
|
#: models.py:146
|
||||||
msgid "duration"
|
msgid "duration"
|
||||||
msgstr "durée"
|
msgstr "durée"
|
||||||
|
|
||||||
#: models.py:445
|
#: models.py:150
|
||||||
msgid "minimum number of players"
|
msgid "minimum number of players"
|
||||||
msgstr "nombre minimum de joueurs"
|
msgstr "nombre minimum de joueurs"
|
||||||
|
|
||||||
#: models.py:449
|
#: models.py:154
|
||||||
msgid "maximum number of players"
|
msgid "maximum number of players"
|
||||||
msgstr "nombre maximum de joueurs"
|
msgstr "nombre maximum de joueurs"
|
||||||
|
|
||||||
#: models.py:454
|
#: models.py:160
|
||||||
msgid "comment"
|
msgid "comment"
|
||||||
msgstr "commentaire"
|
msgstr "commentaire"
|
||||||
|
|
||||||
#: models.py:461
|
#: models.py:167
|
||||||
msgid "game"
|
msgid "game"
|
||||||
msgstr "jeu"
|
msgstr "jeu"
|
||||||
|
|
||||||
#: models.py:462
|
#: models.py:168
|
||||||
msgid "games"
|
msgid "games"
|
||||||
msgstr "jeux"
|
msgstr "jeux"
|
||||||
|
|
||||||
#: templates/media/generate_side_identifier.html:3
|
|
||||||
msgid "Generate side identifier"
|
|
||||||
msgstr "Générer la cote"
|
|
||||||
|
|
||||||
#: templates/media/isbn_button.html:3
|
#: templates/media/isbn_button.html:3
|
||||||
msgid "Fetch data and add another"
|
msgid "Fetch data"
|
||||||
msgstr "Télécharger les données et ajouter un nouveau medium"
|
msgstr "Télécharger les données"
|
||||||
|
|
||||||
#: templates/media/isbn_button.html:4
|
#: validators.py:20
|
||||||
msgid "Fetch only"
|
|
||||||
msgstr "Télécharger les données seulement"
|
|
||||||
|
|
||||||
#: validators.py:18
|
|
||||||
msgid "Invalid ISBN: Not a string"
|
msgid "Invalid ISBN: Not a string"
|
||||||
msgstr "ISBN invalide : ce n'est pas une chaîne de caractères"
|
msgstr "ISBN invalide : ce n'est pas une chaîne de caractères"
|
||||||
|
|
||||||
#: validators.py:21
|
#: validators.py:23
|
||||||
msgid "Invalid ISBN: Wrong length"
|
msgid "Invalid ISBN: Wrong length"
|
||||||
msgstr "ISBN invalide : mauvaise longueur"
|
msgstr "ISBN invalide : mauvaise longueur"
|
||||||
|
|
||||||
#: validators.py:27
|
#: validators.py:26
|
||||||
|
msgid "Invalid ISBN: Failed checksum"
|
||||||
|
msgstr "ISBN invalide : mauvais checksum"
|
||||||
|
|
||||||
|
#: validators.py:29
|
||||||
msgid "Invalid ISBN: Only upper case allowed"
|
msgid "Invalid ISBN: Only upper case allowed"
|
||||||
msgstr "ISBN invalide : seulement les majuscules sont autorisées"
|
msgstr "ISBN invalide : seulement les majuscules sont autorisées"
|
||||||
|
|
||||||
#: views.py:50
|
#: views.py:41
|
||||||
msgid "Welcome to the Mediatek database"
|
msgid "Welcome to the Mediatek database"
|
||||||
msgstr "Bienvenue sur la base de données de la Mediatek"
|
msgstr "Bienvenue sur la base de données de la Mediatek"
|
||||||
|
|
||||||
#~ msgid "medium"
|
|
||||||
#~ msgstr "medium"
|
|
||||||
|
|
||||||
#~ msgid "media"
|
|
||||||
#~ msgstr "media"
|
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
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)))
|
|
|
@ -1,49 +0,0 @@
|
||||||
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) + ")")))
|
|
|
@ -1,92 +0,0 @@
|
||||||
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))))
|
|
|
@ -1,50 +0,0 @@
|
||||||
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)))
|
|
|
@ -1,47 +0,0 @@
|
||||||
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)))
|
|
|
@ -1,58 +0,0 @@
|
||||||
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)))
|
|
|
@ -1,58 +0,0 @@
|
||||||
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)))
|
|
|
@ -1,59 +0,0 @@
|
||||||
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."))
|
|
|
@ -1,61 +0,0 @@
|
||||||
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)))
|
|
|
@ -1,18 +0,0 @@
|
||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,20 +0,0 @@
|
||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,26 +0,0 @@
|
||||||
# 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',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,34 +0,0 @@
|
||||||
# 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'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,17 +0,0 @@
|
||||||
# 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'},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,49 +0,0 @@
|
||||||
# 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'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,17 +0,0 @@
|
||||||
# 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'},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,38 +0,0 @@
|
||||||
# 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'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,19 +0,0 @@
|
||||||
# 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,
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,19 +0,0 @@
|
||||||
# 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,
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,29 +0,0 @@
|
||||||
# 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'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,18 +0,0 @@
|
||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,18 +0,0 @@
|
||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,55 +0,0 @@
|
||||||
# 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,
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,48 +0,0 @@
|
||||||
# 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,11 +16,6 @@ class Auteur(models.Model):
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
)
|
)
|
||||||
|
|
||||||
note = models.IntegerField(
|
|
||||||
default=0,
|
|
||||||
verbose_name=_("note"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@ -30,59 +25,47 @@ class Auteur(models.Model):
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
|
|
||||||
|
|
||||||
class BD(models.Model):
|
class Media(models.Model):
|
||||||
isbn = ISBNField(
|
isbn = ISBNField(
|
||||||
_('ISBN'),
|
_('ISBN'),
|
||||||
help_text=_('You may be able to scan it from a bar code.'),
|
help_text=_('You may be able to scan it from a bar code.'),
|
||||||
unique=True,
|
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
title = models.CharField(
|
title = models.CharField(
|
||||||
verbose_name=_('title'),
|
verbose_name=_('title'),
|
||||||
max_length=255,
|
max_length=255,
|
||||||
)
|
)
|
||||||
|
|
||||||
subtitle = models.CharField(
|
subtitle = models.CharField(
|
||||||
verbose_name=_('subtitle'),
|
verbose_name=_('subtitle'),
|
||||||
max_length=255,
|
max_length=255,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
null=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
external_url = models.URLField(
|
external_url = models.URLField(
|
||||||
verbose_name=_('external URL'),
|
verbose_name=_('external URL'),
|
||||||
blank=True,
|
blank=True,
|
||||||
|
null=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
side_identifier = models.CharField(
|
side_identifier = models.CharField(
|
||||||
verbose_name=_('side identifier'),
|
verbose_name=_('side identifier'),
|
||||||
max_length=255,
|
max_length=255,
|
||||||
)
|
)
|
||||||
|
|
||||||
authors = models.ManyToManyField(
|
authors = models.ManyToManyField(
|
||||||
'Auteur',
|
'Auteur',
|
||||||
verbose_name=_('authors'),
|
verbose_name=_('authors'),
|
||||||
)
|
)
|
||||||
|
|
||||||
number_of_pages = models.PositiveIntegerField(
|
number_of_pages = models.PositiveIntegerField(
|
||||||
verbose_name=_('number of pages'),
|
verbose_name=_('number of pages'),
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
publish_date = models.DateField(
|
publish_date = models.DateField(
|
||||||
verbose_name=_('publish date'),
|
verbose_name=_('publish date'),
|
||||||
blank=True,
|
blank=True,
|
||||||
null=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):
|
def __str__(self):
|
||||||
if self.subtitle:
|
if self.subtitle:
|
||||||
return "{} : {}".format(self.title, self.subtitle)
|
return "{} : {}".format(self.title, self.subtitle)
|
||||||
|
@ -90,292 +73,14 @@ class BD(models.Model):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("BD")
|
verbose_name = _("medium")
|
||||||
verbose_name_plural = _("BDs")
|
verbose_name_plural = _("media")
|
||||||
ordering = ['title', 'subtitle']
|
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):
|
class Emprunt(models.Model):
|
||||||
media = models.ForeignKey(
|
media = models.ForeignKey(
|
||||||
'BD',
|
'Media',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
)
|
)
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
|
@ -451,6 +156,7 @@ class Jeu(models.Model):
|
||||||
comment = models.CharField(
|
comment = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
null=True,
|
||||||
verbose_name=_('comment'),
|
verbose_name=_('comment'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from media.models import Auteur
|
|
||||||
|
|
||||||
|
|
||||||
class BedetequeScraper:
|
class BedetequeScraper:
|
||||||
|
@ -57,9 +56,6 @@ class BedetequeScraper:
|
||||||
regex_subtitle = r'<h2>\s*(.*)</h2>'
|
regex_subtitle = r'<h2>\s*(.*)</h2>'
|
||||||
regex_publish_date = r'datePublished\" content=\"([\d-]*)\">'
|
regex_publish_date = r'datePublished\" content=\"([\d-]*)\">'
|
||||||
regex_nb_of_pages = r'numberOfPages\">(\d*)</span'
|
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 = {
|
data = {
|
||||||
'external_url': bd_url,
|
'external_url': bd_url,
|
||||||
|
@ -77,6 +73,10 @@ class BedetequeScraper:
|
||||||
subtitle = subtitle.replace('<span class="numa"></span>', '')
|
subtitle = subtitle.replace('<span class="numa"></span>', '')
|
||||||
data['subtitle'] = ' '.join(subtitle.split())
|
data['subtitle'] = ' '.join(subtitle.split())
|
||||||
|
|
||||||
|
# TODO implement author
|
||||||
|
# regex_author = r'author\">([^<]*)</span'
|
||||||
|
# 'author': re.search(regex_author, content).group(1),
|
||||||
|
|
||||||
# Get publish date
|
# Get publish date
|
||||||
search_publish_date = re.search(regex_publish_date, content)
|
search_publish_date = re.search(regex_publish_date, content)
|
||||||
if search_publish_date:
|
if search_publish_date:
|
||||||
|
@ -86,26 +86,5 @@ class BedetequeScraper:
|
||||||
search_nb_pages = re.search(regex_nb_of_pages, content)
|
search_nb_pages = re.search(regex_nb_of_pages, content)
|
||||||
if search_nb_pages and search_nb_pages.group(1).isnumeric():
|
if search_nb_pages and search_nb_pages.group(1).isnumeric():
|
||||||
data['number_of_pages'] = search_nb_pages.group(1)
|
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
|
return data
|
||||||
|
|
|
@ -1,54 +1,20 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from .models import Auteur, BD, CD, FutureMedia, Manga, Emprunt, Jeu, Revue, Roman, Vinyle
|
from .models import Auteur, Emprunt, Jeu, Media
|
||||||
|
|
||||||
|
|
||||||
class AuteurSerializer(serializers.ModelSerializer):
|
class AuteurSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Auteur
|
model = Auteur
|
||||||
fields = ['url', 'name']
|
fields = ['url', 'name']
|
||||||
|
|
||||||
|
|
||||||
class BDSerializer(serializers.ModelSerializer):
|
class MediaSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = BD
|
model = Media
|
||||||
fields = '__all__'
|
fields = ['url', 'isbn', 'title', 'subtitle', 'external_url',
|
||||||
|
'side_identifier', 'authors', 'number_of_pages',
|
||||||
|
'publish_date']
|
||||||
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):
|
class EmpruntSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
|
|
@ -1,149 +0,0 @@
|
||||||
{% 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 %}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{% 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,4 +1,3 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% include "django/forms/widgets/input.html" %}
|
{% include "django/forms/widgets/input.html" %}
|
||||||
<input type="button" value="{% trans "Fetch data and add another" %}" name="_isbn_addanother" onclick="form.submit()">
|
<input type="submit" value="{% trans "Fetch data" %}" name="_continue">
|
||||||
<input type="button" value="{% trans "Fetch only" %}" name="_isbn" onclick="form.submit()">
|
|
|
@ -3,7 +3,8 @@
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from media.models import Auteur, BD
|
|
||||||
|
from media.models import Auteur, Media
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -24,41 +25,41 @@ class TemplateTests(TestCase):
|
||||||
self.dummy_author = Auteur.objects.create(name="Test author")
|
self.dummy_author = Auteur.objects.create(name="Test author")
|
||||||
|
|
||||||
# Create media
|
# Create media
|
||||||
self.dummy_bd1 = BD.objects.create(
|
self.dummy_media1 = Media.objects.create(
|
||||||
title="Test media",
|
title="Test media",
|
||||||
side_identifier="T M",
|
side_identifier="T M",
|
||||||
)
|
)
|
||||||
self.dummy_bd1.authors.add(self.dummy_author)
|
self.dummy_media1.authors.add(self.dummy_author)
|
||||||
self.dummy_bd2 = BD.objects.create(
|
self.dummy_media2 = Media.objects.create(
|
||||||
title="Test media bis",
|
title="Test media bis",
|
||||||
side_identifier="T M 2",
|
side_identifier="T M 2",
|
||||||
external_url="https://example.com/",
|
external_url="https://example.com/",
|
||||||
)
|
)
|
||||||
self.dummy_bd2.authors.add(self.dummy_author)
|
self.dummy_media2.authors.add(self.dummy_author)
|
||||||
|
|
||||||
def test_bd_bd_changelist(self):
|
def test_media_media_changelist(self):
|
||||||
response = self.client.get(reverse('admin:media_bd_changelist'))
|
response = self.client.get(reverse('admin:media_media_changelist'))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_bd_bd_add(self):
|
def test_media_media_add(self):
|
||||||
response = self.client.get(reverse('admin:media_bd_add'))
|
response = self.client.get(reverse('admin:media_media_add'))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_bd_isbn_download(self):
|
def test_media_isbn_download(self):
|
||||||
data = {
|
data = {
|
||||||
'_isbn': True,
|
'_continue': True,
|
||||||
'isbn': "0316358525",
|
'isbn': "0316358525",
|
||||||
}
|
}
|
||||||
response = self.client.post(reverse(
|
response = self.client.post(reverse(
|
||||||
'admin:media_bd_change',
|
'admin:media_media_change',
|
||||||
args=[self.dummy_bd1.id],
|
args=[self.dummy_media1.id],
|
||||||
), data=data)
|
), data=data)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_bd_emprunt_changelist(self):
|
def test_media_emprunt_changelist(self):
|
||||||
response = self.client.get(reverse('admin:media_emprunt_changelist'))
|
response = self.client.get(reverse('admin:media_emprunt_changelist'))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_bd_emprunt_add(self):
|
def test_media_emprunt_add(self):
|
||||||
response = self.client.get(reverse('admin:media_emprunt_add'))
|
response = self.client.get(reverse('admin:media_emprunt_add'))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
|
@ -11,12 +10,4 @@ app_name = 'media'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^retour_emprunt/(?P<empruntid>[0-9]+)$', views.retour_emprunt,
|
url(r'^retour_emprunt/(?P<empruntid>[0-9]+)$', views.retour_emprunt,
|
||||||
name='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,6 +8,7 @@ Based on https://github.com/secnot/django-isbn-field
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from stdnum import isbn
|
||||||
|
|
||||||
|
|
||||||
def isbn_validator(raw_isbn):
|
def isbn_validator(raw_isbn):
|
||||||
|
@ -20,8 +21,8 @@ def isbn_validator(raw_isbn):
|
||||||
if len(isbn_to_check) != 10 and len(isbn_to_check) != 13:
|
if len(isbn_to_check) != 10 and len(isbn_to_check) != 13:
|
||||||
raise ValidationError(_('Invalid ISBN: Wrong length'))
|
raise ValidationError(_('Invalid ISBN: Wrong length'))
|
||||||
|
|
||||||
# if not isbn.is_valid(isbn_to_check):
|
if not isbn.is_valid(isbn_to_check):
|
||||||
# raise ValidationError(_('Invalid ISBN: Failed checksum'))
|
raise ValidationError(_('Invalid ISBN: Failed checksum'))
|
||||||
|
|
||||||
if isbn_to_check != isbn_to_check.upper():
|
if isbn_to_check != isbn_to_check.upper():
|
||||||
raise ValidationError(_('Invalid ISBN: Only upper case allowed'))
|
raise ValidationError(_('Invalid ISBN: Only upper case allowed'))
|
||||||
|
|
127
media/views.py
127
media/views.py
|
@ -1,25 +1,19 @@
|
||||||
# -*- mode: python; coding: utf-8 -*-
|
# -*- mode: python; coding: utf-8 -*-
|
||||||
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
|
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
from http.client import NO_CONTENT
|
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required, permission_required
|
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.db import transaction
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import TemplateView, DetailView
|
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.filters import SearchFilter
|
|
||||||
from reversion import revisions as reversion
|
from reversion import revisions as reversion
|
||||||
|
|
||||||
from .models import Auteur, BD, CD, Emprunt, FutureMedia, Jeu, Manga, Revue, Roman, Vinyle
|
from .models import Auteur, Emprunt, Jeu, Media
|
||||||
from .serializers import AuteurSerializer, BDSerializer, CDSerializer, EmpruntSerializer, FutureMediaSerializer, \
|
from .serializers import AuteurSerializer, EmpruntSerializer, \
|
||||||
JeuSerializer, MangaSerializer, RevueSerializer, RomanSerializer, VinyleSerializer
|
JeuSerializer, MediaSerializer
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -51,46 +45,6 @@ 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):
|
class AuteurViewSet(viewsets.ModelViewSet):
|
||||||
"""
|
"""
|
||||||
API endpoint that allows authors to be viewed or edited.
|
API endpoint that allows authors to be viewed or edited.
|
||||||
|
@ -99,81 +53,12 @@ class AuteurViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = AuteurSerializer
|
serializer_class = AuteurSerializer
|
||||||
|
|
||||||
|
|
||||||
class BDViewSet(viewsets.ModelViewSet):
|
class MediaViewSet(viewsets.ModelViewSet):
|
||||||
"""
|
"""
|
||||||
API endpoint that allows media to be viewed or edited.
|
API endpoint that allows media to be viewed or edited.
|
||||||
"""
|
"""
|
||||||
queryset = BD.objects.all()
|
queryset = Media.objects.all()
|
||||||
serializer_class = BDSerializer
|
serializer_class = MediaSerializer
|
||||||
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):
|
class EmpruntViewSet(viewsets.ModelViewSet):
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
Django~=2.2.10
|
Django==2.2.4
|
||||||
docutils~=0.14
|
docutils==0.14
|
||||||
Pillow>=5.4.1
|
Pillow==5.4.1
|
||||||
pytz~=2019.1
|
pytz==2019.1
|
||||||
six~=1.12.0
|
six==1.12.0
|
||||||
sqlparse~=0.2.4
|
sqlparse==0.2.4
|
||||||
django-filter~=2.1.0
|
django-reversion==3.0.3
|
||||||
django-reversion~=3.0.3
|
python-stdnum==1.10
|
||||||
python-stdnum~=1.10
|
djangorestframework==3.9.2
|
||||||
djangorestframework~=3.9.0
|
pyyaml==3.13
|
||||||
pyyaml~=3.13
|
coreapi==2.3.3
|
||||||
coreapi~=2.3.3
|
psycopg2
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
# -*- 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'
|
|
@ -0,0 +1,76 @@
|
||||||
|
# -*- 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)
|
|
@ -0,0 +1,11 @@
|
||||||
|
# -*- 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')
|
|
@ -0,0 +1,134 @@
|
||||||
|
#, 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"
|
|
@ -0,0 +1,51 @@
|
||||||
|
# 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')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,117 @@
|
||||||
|
# -*- 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']
|
|
@ -0,0 +1,19 @@
|
||||||
|
#!/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,7 +30,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<span class="dropdown">
|
<span class="dropdown">
|
||||||
<a href="{% url 'admin:index' %}">{% trans 'Explore database' %}</a>
|
<a href="{% url 'admin:index' %}">{% trans 'Explore database' %}</a>
|
||||||
<span class="dropdown-content">
|
<span class="dropdown-content">
|
||||||
<a href="{% url "media:find" %}">Recherche ...</a>
|
|
||||||
{% for app in available_apps %}
|
{% for app in available_apps %}
|
||||||
{% for model in app.models %}
|
{% for model in app.models %}
|
||||||
{% if model.admin_url %}
|
{% if model.admin_url %}
|
||||||
|
@ -97,8 +96,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
</form>
|
</form>
|
||||||
<p>
|
<p>
|
||||||
Mediatek 2017-2020 —
|
Mediatek 2017-2020 —
|
||||||
<a href="mailto:club-med@crans.org">Nous contacter</a> —
|
<a href="mailto:club-med@crans.org">Nous contactez</a> —
|
||||||
<a href="{% url "api-root" %}">Explorer l'API</a>
|
<a href="{% url "redoc" %}">Explorer l'API</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -116,6 +115,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% block extrajavascript %}{% endblock %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
{% 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,8 +1,6 @@
|
||||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import socket
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
GetBlue Android parameters
|
GetBlue Android parameters
|
||||||
|
@ -22,25 +20,16 @@ class Server(BaseHTTPRequestHandler):
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
self._set_headers()
|
self._set_headers()
|
||||||
isbn = self.path[7:-24]
|
isbn = self.path[7:-24]
|
||||||
if not isbn.isnumeric():
|
|
||||||
print("Mauvais ISBN.")
|
|
||||||
return
|
|
||||||
print("Hey j'ai un ISBN :", isbn)
|
print("Hey j'ai un ISBN :", isbn)
|
||||||
os.system("xdotool type " + isbn)
|
os.system("xdotool type " + isbn)
|
||||||
os.system("xdotool key KP_Enter")
|
os.system("xdotool key KP_Enter")
|
||||||
sleep(1)
|
|
||||||
os.system("xdotool click 1")
|
|
||||||
|
|
||||||
def do_HEAD(self):
|
def do_HEAD(self):
|
||||||
self._set_headers()
|
self._set_headers()
|
||||||
|
|
||||||
|
|
||||||
class HTTPServerV6(HTTPServer):
|
|
||||||
address_family = socket.AF_INET6
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
server_address = ('::', 8080)
|
server_address = ('', 8080)
|
||||||
httpd = HTTPServerV6(server_address, Server)
|
httpd = HTTPServer(server_address, Server)
|
||||||
print('Starting httpd...')
|
print('Starting httpd...')
|
||||||
httpd.serve_forever()
|
httpd.serve_forever()
|
||||||
|
|
22
tox.ini
22
tox.ini
|
@ -1,30 +1,38 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist = py37,py38,linters
|
envlist = py35,py36,py37,linters
|
||||||
skipsdist = True
|
skipsdist = True
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
sitepackages = True
|
basepython = python3
|
||||||
deps =
|
deps =
|
||||||
-r{toxinidir}/requirements.txt
|
-r{toxinidir}/requirements.txt
|
||||||
coverage
|
coverage
|
||||||
commands =
|
commands =
|
||||||
coverage run --omit='*migrations*' ./manage.py test {posargs}
|
./manage.py makemigrations
|
||||||
|
coverage run ./manage.py test {posargs}
|
||||||
coverage report -m
|
coverage report -m
|
||||||
|
|
||||||
|
[testenv:pre-commit]
|
||||||
|
deps = pre-commit
|
||||||
|
commands =
|
||||||
|
pre-commit run --all-files --show-diff-on-failure
|
||||||
|
|
||||||
[testenv:linters]
|
[testenv:linters]
|
||||||
deps =
|
deps =
|
||||||
|
-r{toxinidir}/requirements.txt
|
||||||
flake8
|
flake8
|
||||||
flake8-colors
|
flake8-colors
|
||||||
flake8-django
|
|
||||||
flake8-import-order
|
flake8-import-order
|
||||||
flake8-typing-imports
|
flake8-typing-imports
|
||||||
pep8-naming
|
pep8-naming
|
||||||
pyflakes
|
pyflakes
|
||||||
|
pylint
|
||||||
commands =
|
commands =
|
||||||
flake8 logs media users
|
flake8 logs media search users
|
||||||
|
pylint .
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
ignore = W503, I100, I101
|
ignore = D203, W503, E203, I100, I201, I202
|
||||||
exclude =
|
exclude =
|
||||||
.tox,
|
.tox,
|
||||||
.git,
|
.git,
|
||||||
|
@ -36,7 +44,7 @@ exclude =
|
||||||
.cache,
|
.cache,
|
||||||
.eggs,
|
.eggs,
|
||||||
*migrations*
|
*migrations*
|
||||||
max-complexity = 15
|
max-complexity = 10
|
||||||
import-order-style = google
|
import-order-style = google
|
||||||
application-import-names = flake8
|
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
|
format = ${cyan}%(path)s${reset}:${yellow_bold}%(row)d${reset}:${green_bold}%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s
|
||||||
|
|
|
@ -10,10 +10,17 @@ from django.urls import reverse
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from reversion.admin import VersionAdmin
|
from reversion.admin import VersionAdmin
|
||||||
from med.admin import admin_site
|
|
||||||
|
|
||||||
|
from med.admin import admin_site
|
||||||
from .forms import UserCreationAdminForm
|
from .forms import UserCreationAdminForm
|
||||||
from .models import Adhesion, User
|
from .models import Adhesion, Clef, User
|
||||||
|
|
||||||
|
|
||||||
|
class ClefAdmin(VersionAdmin):
|
||||||
|
list_display = ('name', 'owner', 'comment')
|
||||||
|
ordering = ('name',)
|
||||||
|
search_fields = ('name', 'owner__username', 'comment')
|
||||||
|
autocomplete_fields = ('owner',)
|
||||||
|
|
||||||
|
|
||||||
class AdhesionAdmin(VersionAdmin):
|
class AdhesionAdmin(VersionAdmin):
|
||||||
|
@ -109,3 +116,4 @@ class UserAdmin(VersionAdmin, BaseUserAdmin):
|
||||||
|
|
||||||
admin_site.register(User, UserAdmin)
|
admin_site.register(User, UserAdmin)
|
||||||
admin_site.register(Adhesion, AdhesionAdmin)
|
admin_site.register(Adhesion, AdhesionAdmin)
|
||||||
|
admin_site.register(Clef, ClefAdmin)
|
||||||
|
|
|
@ -3,7 +3,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2020-02-20 13:51+0100\n"
|
"POT-Creation-Date: 2019-08-10 16:20+0200\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
@ -13,39 +13,39 @@ msgstr ""
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||||
|
|
||||||
#: admin.py:25
|
#: admin.py:32
|
||||||
msgid "membership status"
|
msgid "membership status"
|
||||||
msgstr "statut adhérent"
|
msgstr "statut adhérent"
|
||||||
|
|
||||||
#: admin.py:30
|
#: admin.py:37
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr "Oui"
|
msgstr "Oui"
|
||||||
|
|
||||||
#: admin.py:47
|
#: admin.py:54
|
||||||
msgid "Personal info"
|
msgid "Personal info"
|
||||||
msgstr "Informations personnelles"
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:49
|
#: admin.py:56
|
||||||
msgid "Permissions"
|
msgid "Permissions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:52
|
#: admin.py:59
|
||||||
msgid "Important dates"
|
msgid "Important dates"
|
||||||
msgstr "Dates importantes"
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:82
|
#: admin.py:89
|
||||||
msgid "An email to set the password was sent."
|
msgid "An email to set the password was sent."
|
||||||
msgstr "Un mail pour initialiser le mot de passe a été envoyé."
|
msgstr "Un mail pour initialiser le mot de passe a été envoyé."
|
||||||
|
|
||||||
#: admin.py:85
|
#: admin.py:92
|
||||||
msgid "The email is invalid."
|
msgid "The email is invalid."
|
||||||
msgstr "L'adresse mail est invalide."
|
msgstr "L'adresse mail est invalide."
|
||||||
|
|
||||||
#: admin.py:103
|
#: admin.py:111
|
||||||
msgid "Adhere"
|
msgid "Adhere"
|
||||||
msgstr "Adhérer"
|
msgstr "Adhérer"
|
||||||
|
|
||||||
#: admin.py:106
|
#: admin.py:114
|
||||||
msgid "is member"
|
msgid "is member"
|
||||||
msgstr "statut adhérent"
|
msgstr "statut adhérent"
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ msgstr "emprunts maximal"
|
||||||
msgid "Maximal amount of simultaneous borrowed item authorized."
|
msgid "Maximal amount of simultaneous borrowed item authorized."
|
||||||
msgstr "Nombre maximal d'objets empruntés en même temps."
|
msgstr "Nombre maximal d'objets empruntés en même temps."
|
||||||
|
|
||||||
#: models.py:33
|
#: models.py:33 models.py:67
|
||||||
msgid "comment"
|
msgid "comment"
|
||||||
msgstr "commentaire"
|
msgstr "commentaire"
|
||||||
|
|
||||||
|
@ -82,30 +82,46 @@ msgid "date joined"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:55
|
#: 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"
|
msgid "starting in"
|
||||||
msgstr "commence en"
|
msgstr "commence en"
|
||||||
|
|
||||||
#: models.py:56
|
#: models.py:81
|
||||||
msgid "Year in which the membership year starts."
|
msgid "Year in which the membership year starts."
|
||||||
msgstr "Année dans laquelle la plage d'adhésion commence."
|
msgstr "Année dans laquelle la plage d'adhésion commence."
|
||||||
|
|
||||||
#: models.py:60
|
#: models.py:85
|
||||||
msgid "ending in"
|
msgid "ending in"
|
||||||
msgstr "finie en"
|
msgstr "finie en"
|
||||||
|
|
||||||
#: models.py:61
|
#: models.py:86
|
||||||
msgid "Year in which the membership year ends."
|
msgid "Year in which the membership year ends."
|
||||||
msgstr "Année dans laquelle la plage d'adhésion finie."
|
msgstr "Année dans laquelle la plage d'adhésion finie."
|
||||||
|
|
||||||
#: models.py:66
|
#: models.py:91
|
||||||
msgid "members"
|
msgid "members"
|
||||||
msgstr "adhérents"
|
msgstr "adhérents"
|
||||||
|
|
||||||
#: models.py:71
|
#: models.py:96
|
||||||
msgid "membership year"
|
msgid "membership year"
|
||||||
msgstr "année d'adhésion"
|
msgstr "année d'adhésion"
|
||||||
|
|
||||||
#: models.py:72
|
#: models.py:97
|
||||||
msgid "membership years"
|
msgid "membership years"
|
||||||
msgstr "années d'adhésion"
|
msgstr "années d'adhésion"
|
||||||
|
|
||||||
|
@ -117,18 +133,6 @@ msgstr ""
|
||||||
msgid "Save"
|
msgid "Save"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: views.py:43
|
#: views.py:40
|
||||||
msgid "Edit user profile"
|
msgid "Edit user profile"
|
||||||
msgstr "Editer le profil utilisateur"
|
msgstr "Editer le profil utilisateur"
|
||||||
|
|
||||||
#~ msgid "name"
|
|
||||||
#~ msgstr "nom"
|
|
||||||
|
|
||||||
#~ msgid "owner"
|
|
||||||
#~ msgstr "propriétaire"
|
|
||||||
|
|
||||||
#~ msgid "key"
|
|
||||||
#~ msgstr "clé"
|
|
||||||
|
|
||||||
#~ msgid "keys"
|
|
||||||
#~ msgstr "clés"
|
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
# 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',
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,25 +0,0 @@
|
||||||
# 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,6 +6,7 @@ from django.contrib.auth.models import AbstractUser
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from med.settings import MAX_EMPRUNT
|
from med.settings import MAX_EMPRUNT
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,11 +14,13 @@ class User(AbstractUser):
|
||||||
telephone = models.CharField(
|
telephone = models.CharField(
|
||||||
verbose_name=_('phone number'),
|
verbose_name=_('phone number'),
|
||||||
max_length=15,
|
max_length=15,
|
||||||
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
address = models.CharField(
|
address = models.CharField(
|
||||||
verbose_name=_('address'),
|
verbose_name=_('address'),
|
||||||
max_length=255,
|
max_length=255,
|
||||||
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
maxemprunt = models.IntegerField(
|
maxemprunt = models.IntegerField(
|
||||||
|
@ -47,6 +50,31 @@ class User(AbstractUser):
|
||||||
return last_year and self in last_year.members.all()
|
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):
|
class Adhesion(models.Model):
|
||||||
starting_in = models.IntegerField(
|
starting_in = models.IntegerField(
|
||||||
verbose_name=_('starting in'),
|
verbose_name=_('starting in'),
|
||||||
|
@ -67,6 +95,3 @@ class Adhesion(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _('membership year')
|
verbose_name = _('membership year')
|
||||||
verbose_name_plural = _('membership years')
|
verbose_name_plural = _('membership years')
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.starting_in} - {self.ending_in}"
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from users.models import User
|
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 django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from reversion import revisions as reversion
|
from reversion import revisions as reversion
|
||||||
|
|
||||||
from users.forms import BaseInfoForm
|
from users.forms import BaseInfoForm
|
||||||
from users.models import Adhesion, User
|
from users.models import Adhesion, User
|
||||||
|
|
||||||
from .serializers import GroupSerializer, UserSerializer
|
from .serializers import GroupSerializer, UserSerializer
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue