Merge branch 'cartons' into 'master'
Cartons See merge request mediatek/med!2
This commit is contained in:
commit
a6db8a37e7
|
@ -3,9 +3,7 @@ source =
|
||||||
logs
|
logs
|
||||||
med
|
med
|
||||||
media
|
media
|
||||||
search
|
|
||||||
static
|
static
|
||||||
templates
|
|
||||||
theme
|
theme
|
||||||
users
|
users
|
||||||
omit =
|
omit =
|
||||||
|
|
|
@ -33,8 +33,9 @@ coverage
|
||||||
|
|
||||||
# Local data
|
# Local data
|
||||||
settings_local.py
|
settings_local.py
|
||||||
static_files/*
|
static/*
|
||||||
*.log
|
*.log
|
||||||
|
*.pid
|
||||||
|
|
||||||
# Virtualenv
|
# Virtualenv
|
||||||
env/
|
env/
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
image: python:3.6
|
image: python:3.8
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
|
@ -21,6 +21,12 @@ python37:
|
||||||
stage: test
|
stage: test
|
||||||
script: tox -e py37
|
script: tox -e py37
|
||||||
|
|
||||||
|
python38:
|
||||||
|
image: python:3.8
|
||||||
|
stage: test
|
||||||
|
script: tox -e py37
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
stage: test
|
stage: test
|
||||||
script: tox -e linters
|
script: tox -e linters
|
||||||
|
allow_failure: true
|
||||||
|
|
94
README.md
94
README.md
|
@ -11,36 +11,93 @@ 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.
|
||||||
|
|
||||||
## Développement
|
## Installation
|
||||||
|
|
||||||
Après avoir installé un environnement Django,
|
### Développement
|
||||||
|
|
||||||
|
On peut soit développer avec Docker, soit utiliser un VirtualEnv.
|
||||||
|
|
||||||
|
Dans le cas du VirtualEnv,
|
||||||
|
|
||||||
```bash
|
```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
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration d'une base MySQL
|
### Production
|
||||||
|
|
||||||
Sur le serveur mysql ou postgresl, il est nécessaire de créer une base de donnée med,
|
Vous pouvez soit utiliser Docker, soit configurer manuellement le serveur.
|
||||||
ainsi qu'un user med et un mot de passe associé.
|
|
||||||
|
|
||||||
Voici les étapes à éxecuter pour mysql :
|
#### Mise en place du projet sur Zamok
|
||||||
|
|
||||||
```SQL
|
Pour mettre en place le projet sans droits root,
|
||||||
CREATE DATABASE med;
|
on va créer un socket uwsgi dans le répertoire personnel de l'utilisateur `club-med`
|
||||||
CREATE USER 'med'@'localhost' IDENTIFIED BY 'password';
|
puis on va dire à Apache2 d'utiliser ce socket avec un `.htaccess`.
|
||||||
GRANT ALL PRIVILEGES ON med.* TO 'med'@'localhost';
|
|
||||||
FLUSH PRIVILEGES;
|
Pour cela on va imiter ce que fait l'image Docker,
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitlab.crans.org/mediatek/med.git django-med
|
||||||
|
chmod go-rwx -R django-med
|
||||||
|
python3 -m venv venv
|
||||||
|
. venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
./entrypoint.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Et pour postgresql :
|
Pour lancer le serveur au démarrage de Zamok,
|
||||||
|
on ajoute dans la crontab de l'utilisateur club-med (`crontab -e`)
|
||||||
|
la ligne suivante :
|
||||||
|
|
||||||
|
```crontab
|
||||||
|
@reboot /home/club-med/django-med/entrypoint.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour couper le serveur, on tue le maître UWSGI,
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kill -INT `cat ~/django-med/uwsgi.pid`
|
||||||
|
```
|
||||||
|
|
||||||
|
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]
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour servir les fichiers statiques, on crée un lien symbolique :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ln -s ~/django-med/static ~/www/static
|
||||||
|
```
|
||||||
|
|
||||||
|
Il est néanmoins une mauvaise idée de faire de la production sur SQLite,
|
||||||
|
on configure donc ensuite Django et une base de données.
|
||||||
|
|
||||||
|
#### Configuration d'une base de données
|
||||||
|
|
||||||
|
Sur le serveur MySQL ou PostgreSQL, il est nécessaire de créer une base de donnée med,
|
||||||
|
ainsi qu'un user med et un mot de passe associé.
|
||||||
|
|
||||||
|
Voici les étapes à executer pour PostgreSQL :
|
||||||
|
|
||||||
```SQL
|
```SQL
|
||||||
CREATE DATABASE med;
|
CREATE DATABASE "club-med";
|
||||||
CREATE USER med WITH PASSWORD 'password';
|
CREATE USER "club-med" WITH PASSWORD 'MY-STRONG-PASSWORD';
|
||||||
GRANT ALL PRIVILEGES ON DATABASE med TO med;
|
GRANT ALL PRIVILEGES ON DATABASE "club-med" TO "club-med";
|
||||||
```
|
```
|
||||||
|
|
||||||
## Exemple de groupes de droits
|
## Exemple de groupes de droits
|
||||||
|
@ -55,10 +112,6 @@ 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
|
||||||
|
@ -83,7 +136,6 @@ 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,8 +1,31 @@
|
||||||
#!/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
|
python manage.py makemigrations
|
||||||
sleep 2
|
|
||||||
python manage.py migrate
|
|
||||||
|
|
||||||
# TODO: use uwsgi in production
|
# Wait for database
|
||||||
python manage.py runserver 0.0.0.0:8000
|
sleep 2
|
||||||
|
|
||||||
|
python manage.py migrate
|
||||||
|
python manage.py collectstatic --no-input
|
||||||
|
|
||||||
|
# harakiri parameter respawns processes taking more than 20 seconds
|
||||||
|
# max-requests parameter respawns processes after serving 5000 requests
|
||||||
|
# vacuum parameter cleans up when stopped
|
||||||
|
uwsgi --chdir="$(pwd)" \
|
||||||
|
--module=med.wsgi:application \
|
||||||
|
--env DJANGO_SETTINGS_MODULE=med.settings \
|
||||||
|
--master \
|
||||||
|
--pidfile="$(pwd)/uwsgi.pid" \
|
||||||
|
--socket="$(pwd)/uwsgi.sock" \
|
||||||
|
--processes=5 \
|
||||||
|
--chmod-socket=600 \
|
||||||
|
--harakiri=20 \
|
||||||
|
--max-requests=5000 \
|
||||||
|
--vacuum \
|
||||||
|
--daemonize="$(pwd)/uwsgi.log" \
|
||||||
|
--protocol=fastcgi
|
||||||
|
|
|
@ -7,7 +7,6 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
|
84
med/login.py
84
med/login.py
|
@ -1,84 +0,0 @@
|
||||||
# -*- mode: python; coding: utf-8 -*-
|
|
||||||
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
|
|
||||||
import binascii
|
|
||||||
import hashlib
|
|
||||||
import os
|
|
||||||
from base64 import decodestring
|
|
||||||
from base64 import encodestring
|
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from django.contrib.auth import hashers
|
|
||||||
|
|
||||||
ALGO_NAME = "{SSHA}"
|
|
||||||
ALGO_LEN = len(ALGO_NAME + "$")
|
|
||||||
DIGEST_LEN = 20
|
|
||||||
|
|
||||||
|
|
||||||
def make_secret(password):
|
|
||||||
salt = os.urandom(4)
|
|
||||||
h = hashlib.sha1(password.encode())
|
|
||||||
h.update(salt)
|
|
||||||
return ALGO_NAME + "$" + encodestring(h.digest() + salt).decode()[:-1]
|
|
||||||
|
|
||||||
|
|
||||||
def check_password(challenge_password, password):
|
|
||||||
challenge_bytes = decodestring(challenge_password[ALGO_LEN:].encode())
|
|
||||||
digest = challenge_bytes[:DIGEST_LEN]
|
|
||||||
salt = challenge_bytes[DIGEST_LEN:]
|
|
||||||
hr = hashlib.sha1(password.encode())
|
|
||||||
hr.update(salt)
|
|
||||||
valid_password = True
|
|
||||||
# La comparaison est volontairement en temps constant
|
|
||||||
# (pour éviter les timing-attacks)
|
|
||||||
for i, j in zip(digest, hr.digest()):
|
|
||||||
valid_password &= i == j
|
|
||||||
return valid_password
|
|
||||||
|
|
||||||
|
|
||||||
class SSHAPasswordHasher(hashers.BasePasswordHasher):
|
|
||||||
"""
|
|
||||||
SSHA password hashing to allow for LDAP auth compatibility
|
|
||||||
"""
|
|
||||||
|
|
||||||
algorithm = ALGO_NAME
|
|
||||||
|
|
||||||
def encode(self, password, salt, iterations=None):
|
|
||||||
"""
|
|
||||||
Hash and salt the given password using SSHA algorithm
|
|
||||||
|
|
||||||
salt is overridden
|
|
||||||
"""
|
|
||||||
assert password is not None
|
|
||||||
return make_secret(password)
|
|
||||||
|
|
||||||
def verify(self, password, encoded):
|
|
||||||
"""
|
|
||||||
Check password against encoded using SSHA algorithm
|
|
||||||
"""
|
|
||||||
assert encoded.startswith(self.algorithm)
|
|
||||||
return check_password(encoded, password)
|
|
||||||
|
|
||||||
def safe_summary(self, encoded):
|
|
||||||
"""
|
|
||||||
Provides a safe summary ofthe password
|
|
||||||
"""
|
|
||||||
assert encoded.startswith(self.algorithm)
|
|
||||||
hash = encoded[ALGO_LEN:]
|
|
||||||
hash = binascii.hexlify(decodestring(hash.encode())).decode()
|
|
||||||
return OrderedDict([
|
|
||||||
('algorithm', self.algorithm),
|
|
||||||
('iterations', 0),
|
|
||||||
('salt', hashers.mask_hash(hash[2 * DIGEST_LEN:], show=2)),
|
|
||||||
('hash', hashers.mask_hash(hash[:2 * DIGEST_LEN])),
|
|
||||||
])
|
|
||||||
|
|
||||||
def harden_runtime(self, password, encoded):
|
|
||||||
"""
|
|
||||||
Method implemented to shut up BasePasswordHasher warning
|
|
||||||
|
|
||||||
As we are not using multiple iterations the method is pretty useless
|
|
||||||
"""
|
|
||||||
pass
|
|
|
@ -16,7 +16,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
SECRET_KEY = 'CHANGE_ME_IN_LOCAL_SETTINGS!'
|
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 = False
|
DEBUG = True
|
||||||
|
|
||||||
ADMINS = (
|
ADMINS = (
|
||||||
# ('Admin', 'webmaster@example.com'),
|
# ('Admin', 'webmaster@example.com'),
|
||||||
|
@ -51,7 +51,6 @@ INSTALLED_APPS = [
|
||||||
'med',
|
'med',
|
||||||
'media',
|
'media',
|
||||||
'logs',
|
'logs',
|
||||||
'sporz',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
@ -145,7 +144,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_files')
|
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
||||||
|
|
||||||
# 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,6 +152,8 @@ 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',
|
||||||
]
|
]
|
||||||
|
@ -161,14 +162,6 @@ 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,21 +33,10 @@ DEBUG = True
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||||
'NAME': 'med',
|
'NAME': 'club-med',
|
||||||
'USER': 'med',
|
'USER': 'club-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',
|
|
||||||
# },
|
|
||||||
# }
|
|
|
@ -7,7 +7,6 @@ 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, TemplateView
|
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
|
||||||
|
@ -33,10 +32,6 @@ 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(),
|
||||||
|
|
|
@ -9,7 +9,8 @@ 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, Emprunt, Jeu, Media
|
from .models import Auteur, BD, CD, Emprunt, FutureMedia, Jeu, Manga,\
|
||||||
|
Revue, Roman, Vinyle
|
||||||
|
|
||||||
|
|
||||||
class AuteurAdmin(VersionAdmin):
|
class AuteurAdmin(VersionAdmin):
|
||||||
|
@ -60,6 +61,49 @@ 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')
|
||||||
|
@ -104,6 +148,12 @@ class JeuAdmin(VersionAdmin):
|
||||||
|
|
||||||
|
|
||||||
admin_site.register(Auteur, AuteurAdmin)
|
admin_site.register(Auteur, AuteurAdmin)
|
||||||
admin_site.register(Media, MediaAdmin)
|
admin_site.register(BD, MediaAdmin)
|
||||||
|
admin_site.register(Manga, MediaAdmin)
|
||||||
|
admin_site.register(Roman, MediaAdmin)
|
||||||
|
admin_site.register(CD, CDAdmin)
|
||||||
|
admin_site.register(Vinyle, VinyleAdmin)
|
||||||
|
admin_site.register(Revue, RevueAdmin)
|
||||||
|
admin_site.register(FutureMedia, FutureMediaAdmin)
|
||||||
admin_site.register(Emprunt, EmpruntAdmin)
|
admin_site.register(Emprunt, EmpruntAdmin)
|
||||||
admin_site.register(Jeu, JeuAdmin)
|
admin_site.register(Jeu, JeuAdmin)
|
||||||
|
|
207
media/forms.py
207
media/forms.py
|
@ -3,10 +3,14 @@
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
|
import unicodedata
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,6 +36,52 @@ 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']:
|
||||||
|
data = data['items'][0]
|
||||||
|
# Fill the data
|
||||||
|
self.parse_data_google(data)
|
||||||
|
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:
|
||||||
|
self.cleaned_data['publish_date'] = info['publishedDate']
|
||||||
|
|
||||||
|
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
|
||||||
|
@ -41,33 +91,178 @@ 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)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def parse_data_openlibrary(self, data):
|
||||||
self.cleaned_data['external_url'] = data['url']
|
self.cleaned_data['external_url'] = data['url']
|
||||||
if 'title' in data:
|
if 'title' in data:
|
||||||
self.cleaned_data['title'] = data['title']
|
self.cleaned_data['title'] = data['title']
|
||||||
if 'subtitle' in data:
|
if 'subtitle' in data:
|
||||||
self.cleaned_data['subtitle'] = data['subtitle']
|
self.cleaned_data['subtitle'] = data['subtitle']
|
||||||
|
|
||||||
if 'number_of_pages' in data:
|
if 'number_of_pages' in data:
|
||||||
self.cleaned_data['number_of_pages'] = \
|
self.cleaned_data['number_of_pages'] = \
|
||||||
data['number_of_pages']
|
data['number_of_pages']
|
||||||
return True
|
elif not self.cleaned_data['number_of_pages']:
|
||||||
return False
|
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
|
||||||
"""
|
"""
|
||||||
# TODO implement authors, side_identifier
|
super().clean()
|
||||||
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:
|
||||||
# ISBN is present, try with bedeteque
|
# ISBN is present, try with bedeteque
|
||||||
scrap_result = self.download_data_bedeteque(isbn)
|
scrap_result = self.download_data_bedeteque(isbn)
|
||||||
|
if not scrap_result:
|
||||||
|
# Try with Google
|
||||||
|
scrap_result = self.download_data_google(isbn)
|
||||||
if not scrap_result:
|
if not scrap_result:
|
||||||
# Try with OpenLibrary
|
# Try with OpenLibrary
|
||||||
self.download_data_openlibrary(isbn)
|
if not self.download_data_openlibrary(isbn):
|
||||||
|
self.add_error('isbn',
|
||||||
|
_("This ISBN is not found."))
|
||||||
|
return self.cleaned_data
|
||||||
|
|
||||||
return super().clean()
|
if self.cleaned_data['title']:
|
||||||
|
self.cleaned_data['title'] = re.sub(
|
||||||
|
r'\(AUT\) ',
|
||||||
|
'',
|
||||||
|
self.cleaned_data['title']
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.cleaned_data['authors']:
|
||||||
|
authors = self.cleaned_data['authors']
|
||||||
|
old_authors = authors.copy()
|
||||||
|
|
||||||
|
def sort(author):
|
||||||
|
return str(-author.note) + "." \
|
||||||
|
+ str(old_authors.index(author)) \
|
||||||
|
+ "." + author.name
|
||||||
|
|
||||||
|
authors.sort(key=sort)
|
||||||
|
author_name = self.cleaned_data['authors'][0].name
|
||||||
|
if ',' not in author_name and ' ' in author_name:
|
||||||
|
author_name = author_name.split(' ')[-1]
|
||||||
|
title_normalized = self.cleaned_data['title'].upper()
|
||||||
|
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'^THE ', '', title_normalized)
|
||||||
|
title_normalized = re.sub(r'Œ', 'OE', title_normalized)
|
||||||
|
side_identifier = "{:.3} {:.3}".format(
|
||||||
|
author_name,
|
||||||
|
title_normalized.replace(' ', ''), )
|
||||||
|
|
||||||
|
if self.cleaned_data['subtitle']:
|
||||||
|
self.cleaned_data['subtitle'] = re.sub(
|
||||||
|
r'</span>',
|
||||||
|
'',
|
||||||
|
self.cleaned_data['subtitle']
|
||||||
|
)
|
||||||
|
self.cleaned_data['subtitle'] = re.sub(
|
||||||
|
r'<span.*>',
|
||||||
|
'',
|
||||||
|
self.cleaned_data['subtitle']
|
||||||
|
)
|
||||||
|
start = self.cleaned_data['subtitle'].split(' ')[0] \
|
||||||
|
.replace('.', '')
|
||||||
|
|
||||||
|
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()
|
||||||
|
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: 2019-08-16 14:00+0200\n"
|
"POT-Creation-Date: 2020-05-12 17:42+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,7 +13,7 @@ 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:32 models.py:24 models.py:56
|
#: admin.py:32 models.py:29 models.py:62
|
||||||
msgid "authors"
|
msgid "authors"
|
||||||
msgstr "auteurs"
|
msgstr "auteurs"
|
||||||
|
|
||||||
|
@ -21,11 +21,11 @@ msgstr "auteurs"
|
||||||
msgid "external url"
|
msgid "external url"
|
||||||
msgstr "URL externe"
|
msgstr "URL externe"
|
||||||
|
|
||||||
#: admin.py:82
|
#: admin.py:98
|
||||||
msgid "Turn back"
|
msgid "Turn back"
|
||||||
msgstr "Rendre"
|
msgstr "Rendre"
|
||||||
|
|
||||||
#: admin.py:85 models.py:112
|
#: admin.py:101 models.py:135
|
||||||
msgid "given back to"
|
msgid "given back to"
|
||||||
msgstr "rendu à"
|
msgstr "rendu à"
|
||||||
|
|
||||||
|
@ -33,134 +33,152 @@ 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"
|
||||||
|
|
||||||
#: models.py:16 models.py:136
|
#: forms.py:178
|
||||||
|
msgid "This ISBN is not found."
|
||||||
|
msgstr "L'ISBN n'a pas été trouvé."
|
||||||
|
|
||||||
|
#: models.py:16 models.py:159
|
||||||
msgid "name"
|
msgid "name"
|
||||||
msgstr "nom"
|
msgstr "nom"
|
||||||
|
|
||||||
#: models.py:23
|
#: models.py:21
|
||||||
|
msgid "note"
|
||||||
|
msgstr "note"
|
||||||
|
|
||||||
|
#: models.py:28
|
||||||
msgid "author"
|
msgid "author"
|
||||||
msgstr "auteur"
|
msgstr "auteur"
|
||||||
|
|
||||||
#: models.py:30
|
#: models.py:35 models.py:89
|
||||||
msgid "ISBN"
|
msgid "ISBN"
|
||||||
msgstr "ISBN"
|
msgstr "ISBN"
|
||||||
|
|
||||||
#: models.py:31
|
#: models.py:36 models.py:90
|
||||||
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:36
|
#: models.py:42
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr "titre"
|
msgstr "titre"
|
||||||
|
|
||||||
#: models.py:40
|
#: models.py:46
|
||||||
msgid "subtitle"
|
msgid "subtitle"
|
||||||
msgstr "sous-titre"
|
msgstr "sous-titre"
|
||||||
|
|
||||||
#: models.py:46
|
#: models.py:52
|
||||||
msgid "external URL"
|
msgid "external URL"
|
||||||
msgstr "URL externe"
|
msgstr "URL externe"
|
||||||
|
|
||||||
#: models.py:51
|
#: models.py:57
|
||||||
msgid "side identifier"
|
msgid "side identifier"
|
||||||
msgstr "côte"
|
msgstr "côte"
|
||||||
|
|
||||||
#: models.py:59
|
#: models.py:65
|
||||||
msgid "number of pages"
|
msgid "number of pages"
|
||||||
msgstr "nombre de pages"
|
msgstr "nombre de pages"
|
||||||
|
|
||||||
#: models.py:64
|
#: models.py:70
|
||||||
msgid "publish date"
|
msgid "publish date"
|
||||||
msgstr "date de publication"
|
msgstr "date de publication"
|
||||||
|
|
||||||
#: models.py:76
|
#: models.py:82
|
||||||
msgid "medium"
|
msgid "medium"
|
||||||
msgstr "medium"
|
msgstr "medium"
|
||||||
|
|
||||||
#: models.py:77
|
#: models.py:83
|
||||||
msgid "media"
|
msgid "media"
|
||||||
msgstr "media"
|
msgstr "media"
|
||||||
|
|
||||||
#: models.py:89
|
#: models.py:97
|
||||||
|
msgid "future medium"
|
||||||
|
msgstr "medium à importer"
|
||||||
|
|
||||||
|
#: models.py:98
|
||||||
|
msgid "future media"
|
||||||
|
msgstr "medias à importer"
|
||||||
|
|
||||||
|
#: models.py:112
|
||||||
msgid "borrower"
|
msgid "borrower"
|
||||||
msgstr "emprunteur"
|
msgstr "emprunteur"
|
||||||
|
|
||||||
#: models.py:92
|
#: models.py:115
|
||||||
msgid "borrowed on"
|
msgid "borrowed on"
|
||||||
msgstr "emprunté le"
|
msgstr "emprunté le"
|
||||||
|
|
||||||
#: models.py:97
|
#: models.py:120
|
||||||
msgid "given back on"
|
msgid "given back on"
|
||||||
msgstr "rendu le"
|
msgstr "rendu le"
|
||||||
|
|
||||||
#: models.py:103
|
#: models.py:126
|
||||||
msgid "borrowed with"
|
msgid "borrowed with"
|
||||||
msgstr "emprunté avec"
|
msgstr "emprunté avec"
|
||||||
|
|
||||||
#: models.py:104
|
#: models.py:127
|
||||||
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:113
|
#: models.py:136
|
||||||
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:120
|
#: models.py:143
|
||||||
msgid "borrowed item"
|
msgid "borrowed item"
|
||||||
msgstr "emprunt"
|
msgstr "emprunt"
|
||||||
|
|
||||||
#: models.py:121
|
#: models.py:144
|
||||||
msgid "borrowed items"
|
msgid "borrowed items"
|
||||||
msgstr "emprunts"
|
msgstr "emprunts"
|
||||||
|
|
||||||
#: models.py:141
|
#: models.py:164
|
||||||
msgid "owner"
|
msgid "owner"
|
||||||
msgstr "propriétaire"
|
msgstr "propriétaire"
|
||||||
|
|
||||||
#: models.py:146
|
#: models.py:169
|
||||||
msgid "duration"
|
msgid "duration"
|
||||||
msgstr "durée"
|
msgstr "durée"
|
||||||
|
|
||||||
#: models.py:150
|
#: models.py:173
|
||||||
msgid "minimum number of players"
|
msgid "minimum number of players"
|
||||||
msgstr "nombre minimum de joueurs"
|
msgstr "nombre minimum de joueurs"
|
||||||
|
|
||||||
#: models.py:154
|
#: models.py:177
|
||||||
msgid "maximum number of players"
|
msgid "maximum number of players"
|
||||||
msgstr "nombre maximum de joueurs"
|
msgstr "nombre maximum de joueurs"
|
||||||
|
|
||||||
#: models.py:160
|
#: models.py:183
|
||||||
msgid "comment"
|
msgid "comment"
|
||||||
msgstr "commentaire"
|
msgstr "commentaire"
|
||||||
|
|
||||||
#: models.py:167
|
#: models.py:190
|
||||||
msgid "game"
|
msgid "game"
|
||||||
msgstr "jeu"
|
msgstr "jeu"
|
||||||
|
|
||||||
#: models.py:168
|
#: models.py:191
|
||||||
msgid "games"
|
msgid "games"
|
||||||
msgstr "jeux"
|
msgstr "jeux"
|
||||||
|
|
||||||
#: templates/media/isbn_button.html:3
|
#: templates/media/isbn_button.html:3
|
||||||
msgid "Fetch data"
|
msgid "Fetch data and add another"
|
||||||
msgstr "Télécharger les données"
|
msgstr "Télécharger les données et ajouter un nouveau medium"
|
||||||
|
|
||||||
#: validators.py:20
|
#: templates/media/isbn_button.html:4
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "Fetch data"
|
||||||
|
msgid "Fetch only"
|
||||||
|
msgstr "Télécharger uniquement les données"
|
||||||
|
|
||||||
|
#: 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:23
|
#: validators.py:21
|
||||||
msgid "Invalid ISBN: Wrong length"
|
msgid "Invalid ISBN: Wrong length"
|
||||||
msgstr "ISBN invalide : mauvaise longueur"
|
msgstr "ISBN invalide : mauvaise longueur"
|
||||||
|
|
||||||
#: validators.py:26
|
#: validators.py:27
|
||||||
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:41
|
#: views.py:44
|
||||||
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"
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
from argparse import FileType
|
||||||
|
from sys import stdin
|
||||||
|
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
|
||||||
|
from media.models import Auteur, CD
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('input', nargs='?',
|
||||||
|
type=FileType('r'),
|
||||||
|
default=stdin,
|
||||||
|
help="CD to be imported.")
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
file = options["input"]
|
||||||
|
cds = []
|
||||||
|
for line in file:
|
||||||
|
cds.append(line[:-1].split('|', 2))
|
||||||
|
|
||||||
|
print("Registering", len(cds), "CDs")
|
||||||
|
|
||||||
|
imported = 0
|
||||||
|
|
||||||
|
for cd in cds:
|
||||||
|
if len(cd) != 3:
|
||||||
|
continue
|
||||||
|
|
||||||
|
title = cd[0]
|
||||||
|
side = cd[1]
|
||||||
|
authors_str = cd[2].split('|')
|
||||||
|
authors = [Auteur.objects.get_or_create(name=author)[0]
|
||||||
|
for author in authors_str]
|
||||||
|
cd, created = CD.objects.get_or_create(
|
||||||
|
title=title,
|
||||||
|
side_identifier=side,
|
||||||
|
)
|
||||||
|
cd.authors.set(authors)
|
||||||
|
cd.save()
|
||||||
|
|
||||||
|
if not created:
|
||||||
|
self.stderr.write(self.style.WARNING(
|
||||||
|
"One CD was already imported. Skipping..."))
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
"CD imported"))
|
||||||
|
imported += 1
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
"{count} CDs imported".format(count=imported)))
|
|
@ -0,0 +1,47 @@
|
||||||
|
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(
|
||||||
|
"ISBN {isbn} already exists".format(isbn=isbn)
|
||||||
|
))
|
||||||
|
|
||||||
|
form = MediaAdminForm(instance=cl(),
|
||||||
|
data={"isbn": isbn, "_isbn": True, })
|
||||||
|
# Don't DDOS any website
|
||||||
|
sleep(5)
|
||||||
|
|
||||||
|
try:
|
||||||
|
form.full_clean()
|
||||||
|
form.save()
|
||||||
|
future_medium.delete()
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
"Medium with ISBN {isbn} successfully imported"
|
||||||
|
.format(isbn=isbn)))
|
||||||
|
except (ValidationError, ValueError) as e:
|
||||||
|
self.stderr.write(self.style.WARNING(
|
||||||
|
"An error occured while importing ISBN {isbn}: {error}"
|
||||||
|
.format(isbn=isbn, error=str(e))))
|
|
@ -0,0 +1,93 @@
|
||||||
|
from argparse import FileType
|
||||||
|
from sys import stdin
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
|
||||||
|
from media.models import BD, FutureMedia, Manga, Roman
|
||||||
|
from media.validators import isbn_validator
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('--media-type',
|
||||||
|
type=str,
|
||||||
|
default='bd',
|
||||||
|
choices=[
|
||||||
|
'bd',
|
||||||
|
'manga',
|
||||||
|
'roman',
|
||||||
|
],
|
||||||
|
help="Type of media to be "
|
||||||
|
"imported.")
|
||||||
|
parser.add_argument('input', nargs='?',
|
||||||
|
type=FileType('r'),
|
||||||
|
default=stdin,
|
||||||
|
help="ISBN to be imported.")
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
type_str = options["media_type"]
|
||||||
|
|
||||||
|
media_classes = [BD, Manga, Roman, FutureMedia]
|
||||||
|
|
||||||
|
file = options["input"]
|
||||||
|
isbns = []
|
||||||
|
for line in file:
|
||||||
|
isbns.append(line[:-1])
|
||||||
|
|
||||||
|
print("Registering", len(isbns), "ISBN")
|
||||||
|
|
||||||
|
imported = 0
|
||||||
|
not_imported = []
|
||||||
|
|
||||||
|
for isbn in isbns:
|
||||||
|
if not isbn:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not isbn_validator(isbn):
|
||||||
|
raise ValidationError(
|
||||||
|
"This ISBN is invalid for an unknown reason")
|
||||||
|
except ValidationError as e:
|
||||||
|
self.stderr.write(self.style.ERROR(
|
||||||
|
"The following ISBN is invalid:"
|
||||||
|
" {isbn}, reason: {reason}. Ignoring...".format(
|
||||||
|
isbn=isbn, reason=e.message)))
|
||||||
|
|
||||||
|
isbn_exists = False
|
||||||
|
for cl in media_classes:
|
||||||
|
if cl.objects.filter(isbn=isbn).exists():
|
||||||
|
isbn_exists = True
|
||||||
|
medium = cl.objects.get(isbn=isbn)
|
||||||
|
self.stderr.write(self.style.WARNING(
|
||||||
|
("Warning: ISBN {isbn} already exists, and is "
|
||||||
|
+ "registered as type {type}: {name}. Ignoring...")
|
||||||
|
.format(isbn=isbn,
|
||||||
|
name=str(medium),
|
||||||
|
type=str(cl._meta.verbose_name))))
|
||||||
|
not_imported.append(medium)
|
||||||
|
break
|
||||||
|
|
||||||
|
if isbn_exists:
|
||||||
|
continue
|
||||||
|
|
||||||
|
FutureMedia.objects.create(isbn=isbn, type=type_str)
|
||||||
|
self.stdout.write(self.style.SUCCESS("ISBN {isbn} imported"
|
||||||
|
.format(isbn=isbn)))
|
||||||
|
imported += 1
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS("{count} media imported"
|
||||||
|
.format(count=imported)))
|
||||||
|
|
||||||
|
with open('not_imported_media.csv', 'w') as f:
|
||||||
|
f.write("isbn|type|title\n")
|
||||||
|
for medium in not_imported:
|
||||||
|
if not hasattr(medium, 'title') or not medium.title:
|
||||||
|
medium.title = ''
|
||||||
|
f.write(medium.isbn + "|"
|
||||||
|
+ str(medium._meta.verbose_name)
|
||||||
|
+ "|" + medium.title + "\n")
|
||||||
|
|
||||||
|
self.stderr.write(self.style.WARNING(("{count} media already "
|
||||||
|
+ "imported").format(
|
||||||
|
count=len(not_imported))))
|
|
@ -0,0 +1,50 @@
|
||||||
|
from argparse import FileType
|
||||||
|
from sys import stdin
|
||||||
|
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
from media.models import Auteur, BD
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('input', nargs='?',
|
||||||
|
type=FileType('r'),
|
||||||
|
default=stdin,
|
||||||
|
help="Marvel comic to be imported.")
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
file = options["input"]
|
||||||
|
revues = []
|
||||||
|
for line in file:
|
||||||
|
revues.append(line[:-1].split('|', 2))
|
||||||
|
|
||||||
|
print("Registering", len(revues), "Marvel comics")
|
||||||
|
|
||||||
|
imported = 0
|
||||||
|
|
||||||
|
for revue in revues:
|
||||||
|
if len(revue) != 3:
|
||||||
|
continue
|
||||||
|
|
||||||
|
title = revue[0]
|
||||||
|
number = revue[1]
|
||||||
|
authors = [Auteur.objects.get_or_create(name=n)[0]
|
||||||
|
for n in revue[2].split('|')]
|
||||||
|
bd = BD.objects.create(
|
||||||
|
title=title,
|
||||||
|
subtitle=number,
|
||||||
|
side_identifier="{:.3} {:.3} {:0>2}"
|
||||||
|
.format(authors[0].name.upper(),
|
||||||
|
title.upper(),
|
||||||
|
number),
|
||||||
|
)
|
||||||
|
|
||||||
|
bd.authors.set(authors)
|
||||||
|
bd.save()
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
"Comic imported"))
|
||||||
|
imported += 1
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
"{count} comics imported".format(count=imported)))
|
|
@ -0,0 +1,65 @@
|
||||||
|
import re
|
||||||
|
import unicodedata
|
||||||
|
from argparse import FileType
|
||||||
|
from sys import stdin
|
||||||
|
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
|
||||||
|
from media.models import Auteur, Roman
|
||||||
|
|
||||||
|
|
||||||
|
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]
|
||||||
|
title_normalized = title.upper()
|
||||||
|
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(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'^THE ', '', title_normalized)
|
||||||
|
title_normalized = re.sub(r'Œ', 'OE', title_normalized)
|
||||||
|
title_normalized = title_normalized.replace(' ', '')
|
||||||
|
authors = [Auteur.objects.get_or_create(name=n)[0]
|
||||||
|
for n in book[0].split(';')]
|
||||||
|
side_identifier = "{:.3} {:.3}" \
|
||||||
|
.format(authors[0].name.upper(), title_normalized, )
|
||||||
|
roman = Roman.objects.create(
|
||||||
|
title=title,
|
||||||
|
side_identifier=side_identifier,
|
||||||
|
)
|
||||||
|
roman.authors.set(authors)
|
||||||
|
roman.save()
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
"Roman imported"))
|
||||||
|
imported += 1
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
"{count} romans imported".format(count=imported)))
|
|
@ -0,0 +1,58 @@
|
||||||
|
from argparse import FileType
|
||||||
|
from sys import stdin
|
||||||
|
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
from media.models import Revue
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('input', nargs='?',
|
||||||
|
type=FileType('r'),
|
||||||
|
default=stdin,
|
||||||
|
help="Revues to be imported.")
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
file = options["input"]
|
||||||
|
revues = []
|
||||||
|
for line in file:
|
||||||
|
revues.append(line[:-1].split('|'))
|
||||||
|
|
||||||
|
print("Registering", len(revues), "revues")
|
||||||
|
|
||||||
|
imported = 0
|
||||||
|
|
||||||
|
for revue in revues:
|
||||||
|
if len(revue) != 5:
|
||||||
|
continue
|
||||||
|
|
||||||
|
title = revue[0]
|
||||||
|
number = revue[1]
|
||||||
|
day = revue[2]
|
||||||
|
if not day:
|
||||||
|
day = None
|
||||||
|
month = revue[3]
|
||||||
|
if not month:
|
||||||
|
month = None
|
||||||
|
year = revue[4]
|
||||||
|
if not year:
|
||||||
|
year = None
|
||||||
|
revue, created = Revue.objects.get_or_create(
|
||||||
|
title=title,
|
||||||
|
number=number.replace('*', ''),
|
||||||
|
year=year,
|
||||||
|
month=month,
|
||||||
|
day=day,
|
||||||
|
double=number.endswith('*'),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not created:
|
||||||
|
self.stderr.write(self.style.WARNING(
|
||||||
|
"One revue was already imported. Skipping..."))
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
"Revue imported"))
|
||||||
|
imported += 1
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
"{count} revues imported".format(count=imported)))
|
|
@ -0,0 +1,59 @@
|
||||||
|
from argparse import FileType
|
||||||
|
from sys import stdin
|
||||||
|
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
|
||||||
|
from media.models import Auteur, Vinyle
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('input', nargs='?',
|
||||||
|
type=FileType('r'),
|
||||||
|
default=stdin,
|
||||||
|
help="Vinyle to be imported.")
|
||||||
|
|
||||||
|
parser.add_argument('--rpm',
|
||||||
|
type=int,
|
||||||
|
default=45,
|
||||||
|
help="RPM of the imported vinyles.")
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
rpm = options["rpm"]
|
||||||
|
file = options["input"]
|
||||||
|
vinyles = []
|
||||||
|
for line in file:
|
||||||
|
vinyles.append(line[:-1].split('|', 2))
|
||||||
|
|
||||||
|
print("Registering", len(vinyles), "vinyles")
|
||||||
|
|
||||||
|
imported = 0
|
||||||
|
|
||||||
|
for vinyle in vinyles:
|
||||||
|
if len(vinyle) != 3:
|
||||||
|
continue
|
||||||
|
|
||||||
|
side = vinyle[0]
|
||||||
|
title = vinyle[1 if rpm == 33 else 2]
|
||||||
|
authors_str = vinyle[2 if rpm == 33 else 1]\
|
||||||
|
.split('|' if rpm == 33 else ';')
|
||||||
|
authors = [Auteur.objects.get_or_create(name=author)[0]
|
||||||
|
for author in authors_str]
|
||||||
|
vinyle, created = Vinyle.objects.get_or_create(
|
||||||
|
title=title,
|
||||||
|
side_identifier=side,
|
||||||
|
rpm=rpm,
|
||||||
|
)
|
||||||
|
vinyle.authors.set(authors)
|
||||||
|
vinyle.save()
|
||||||
|
|
||||||
|
if not created:
|
||||||
|
self.stderr.write(self.style.WARNING(
|
||||||
|
"One vinyle was already imported. Skipping..."))
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
"Vinyle imported"))
|
||||||
|
imported += 1
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
"{count} vinyles imported".format(count=imported)))
|
|
@ -0,0 +1,62 @@
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
|
||||||
|
from media.forms import MediaAdminForm
|
||||||
|
from media.models import BD, Manga
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('--view-only', action="store_true",
|
||||||
|
help="Display only modifications. "
|
||||||
|
+ "Only useful for debug.")
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
converted = 0
|
||||||
|
|
||||||
|
for media in BD.objects.all():
|
||||||
|
if media.pk < 3400:
|
||||||
|
continue
|
||||||
|
# We sleep 5 seconds to avoid a ban from Bedetheque
|
||||||
|
sleep(5)
|
||||||
|
self.stdout.write(str(media))
|
||||||
|
form = MediaAdminForm(instance=media,
|
||||||
|
data={"isbn": media.isbn, "_isbn": True, })
|
||||||
|
form.full_clean()
|
||||||
|
|
||||||
|
if "format" not in form.cleaned_data:
|
||||||
|
self.stdout.write("Format not specified."
|
||||||
|
" Assume it is a comic strip.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
format = form.cleaned_data["format"]
|
||||||
|
self.stdout.write("Format: {}".format(format))
|
||||||
|
|
||||||
|
if not options["view_only"]:
|
||||||
|
if format == "manga":
|
||||||
|
self.stdout.write(self.style.WARNING(
|
||||||
|
"This media is a manga. "
|
||||||
|
"Transfer it into a new object..."))
|
||||||
|
manga = Manga.objects.create(
|
||||||
|
isbn=media.isbn,
|
||||||
|
title=media.title,
|
||||||
|
subtitle=media.subtitle,
|
||||||
|
external_url=media.external_url,
|
||||||
|
side_identifier=media.side_identifier,
|
||||||
|
number_of_pages=media.number_of_pages,
|
||||||
|
publish_date=media.publish_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
manga.authors.set(media.authors.all())
|
||||||
|
manga.save()
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
"Manga successfully saved. Deleting old medium..."))
|
||||||
|
|
||||||
|
media.delete()
|
||||||
|
self.stdout.write(self.style.SUCCESS("Medium deleted"))
|
||||||
|
|
||||||
|
converted += 1
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
"Successfully saved {:d} mangas".format(converted)))
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.4 on 2020-02-10 16:09
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('media', '0024_auto_20190816_1356'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='auteur',
|
||||||
|
name='note',
|
||||||
|
field=models.IntegerField(default=0, verbose_name='note'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 2.2.4 on 2020-02-10 16:40
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import media.fields
|
||||||
|
import media.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('media', '0025_auteur_note'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='media',
|
||||||
|
name='isbn',
|
||||||
|
field=media.fields.ISBNField(blank=True, help_text='You may be able to scan it from a bar code.', max_length=28, null=True, unique=True, validators=[media.validators.isbn_validator], verbose_name='ISBN'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Generated by Django 2.2.10 on 2020-05-12 15:23
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import media.fields
|
||||||
|
import media.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('media', '0026_auto_20200210_1740'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='FutureMedia',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('isbn', media.fields.ISBNField(blank=True, help_text='You may be able to scan it from a bar code.', max_length=28, null=True, unique=True, validators=[media.validators.isbn_validator], verbose_name='ISBN')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'future medium',
|
||||||
|
'verbose_name_plural': 'future media',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Generated by Django 2.2.10 on 2020-05-21 14:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import media.fields
|
||||||
|
import media.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('media', '0027_futuremedia'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Manga',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('isbn', media.fields.ISBNField(blank=True, help_text='You may be able to scan it from a bar code.', max_length=28, null=True, unique=True, validators=[media.validators.isbn_validator], verbose_name='ISBN')),
|
||||||
|
('title', models.CharField(max_length=255, verbose_name='title')),
|
||||||
|
('subtitle', models.CharField(blank=True, max_length=255, null=True, verbose_name='subtitle')),
|
||||||
|
('external_url', models.URLField(blank=True, null=True, verbose_name='external URL')),
|
||||||
|
('side_identifier', models.CharField(max_length=255, verbose_name='side identifier')),
|
||||||
|
('number_of_pages', models.PositiveIntegerField(blank=True, null=True, verbose_name='number of pages')),
|
||||||
|
('publish_date', models.DateField(blank=True, null=True, verbose_name='publish date')),
|
||||||
|
('authors', models.ManyToManyField(to='media.Auteur', verbose_name='authors')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'medium',
|
||||||
|
'verbose_name_plural': 'media',
|
||||||
|
'ordering': ['title', 'subtitle'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 2.2.10 on 2020-05-21 14:59
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('media', '0028_manga'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='manga',
|
||||||
|
options={'ordering': ['title', 'subtitle'], 'verbose_name': 'manga', 'verbose_name_plural': 'mangas'},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Generated by Django 2.2.10 on 2020-05-22 15:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('media', '0029_auto_20200521_1659'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameModel(
|
||||||
|
old_name='Media',
|
||||||
|
new_name='BD',
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='manga',
|
||||||
|
options={},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Vinyle',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=255, verbose_name='title')),
|
||||||
|
('side_identifier', models.CharField(max_length=255, verbose_name='side identifier')),
|
||||||
|
('authors', models.ManyToManyField(to='media.Auteur', verbose_name='authors')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'vinyle',
|
||||||
|
'verbose_name_plural': 'vinyles',
|
||||||
|
'ordering': ['title'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CD',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=255, verbose_name='title')),
|
||||||
|
('side_identifier', models.CharField(max_length=255, verbose_name='side identifier')),
|
||||||
|
('authors', models.ManyToManyField(to='media.Auteur', verbose_name='authors')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'CD',
|
||||||
|
'verbose_name_plural': 'CDs',
|
||||||
|
'ordering': ['title'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 2.2.10 on 2020-05-22 15:58
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('media', '0030_auto_20200522_1757'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='bd',
|
||||||
|
options={'ordering': ['title', 'subtitle'], 'verbose_name': 'BD', 'verbose_name_plural': 'BDs'},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Generated by Django 2.2.10 on 2020-05-22 19:07
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import media.fields
|
||||||
|
import media.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('media', '0031_auto_20200522_1758'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='manga',
|
||||||
|
options={'ordering': ['title'], 'verbose_name': 'manga', 'verbose_name_plural': 'mangas'},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Roman',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('isbn', media.fields.ISBNField(blank=True, help_text='You may be able to scan it from a bar code.', max_length=28, null=True, unique=True, validators=[media.validators.isbn_validator], verbose_name='ISBN')),
|
||||||
|
('title', models.CharField(max_length=255, verbose_name='title')),
|
||||||
|
('subtitle', models.CharField(blank=True, max_length=255, null=True, verbose_name='subtitle')),
|
||||||
|
('external_url', models.URLField(blank=True, null=True, verbose_name='external URL')),
|
||||||
|
('side_identifier', models.CharField(max_length=255, verbose_name='side identifier')),
|
||||||
|
('number_of_pages', models.PositiveIntegerField(blank=True, null=True, verbose_name='number of pages')),
|
||||||
|
('publish_date', models.DateField(blank=True, null=True, verbose_name='publish date')),
|
||||||
|
('authors', models.ManyToManyField(to='media.Auteur', verbose_name='authors')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'roman',
|
||||||
|
'verbose_name_plural': 'romans',
|
||||||
|
'ordering': ['title', 'subtitle'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Generated by Django 2.2.10 on 2020-05-22 19:31
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('media', '0032_auto_20200522_2107'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='futuremedia',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('bd', 'BD'), ('manga', 'Manga'), ('roman', 'Roman')], default='bd', max_length=8, verbose_name='type'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Generated by Django 2.2.10 on 2020-05-23 12:20
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('media', '0033_futuremedia_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='vinyle',
|
||||||
|
name='rpm',
|
||||||
|
field=models.PositiveIntegerField(choices=[(33, '33 RPM'), (45, '45 RPM')], default=45, verbose_name='rounds per minute'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Generated by Django 2.2.10 on 2020-05-24 12:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('media', '0034_vinyle_rpm'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Revue',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=255, verbose_name='title')),
|
||||||
|
('number', models.PositiveIntegerField(verbose_name='number')),
|
||||||
|
('year', models.PositiveIntegerField(verbose_name='year')),
|
||||||
|
('month', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='month')),
|
||||||
|
('day', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='day')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'revue',
|
||||||
|
'verbose_name_plural': 'revues',
|
||||||
|
'ordering': ['title', 'number'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.10 on 2020-05-24 13:00
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('media', '0035_revue'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='revue',
|
||||||
|
name='year',
|
||||||
|
field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='year'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.10 on 2020-05-24 13:36
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('media', '0036_auto_20200524_1500'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='revue',
|
||||||
|
name='double',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='double'),
|
||||||
|
),
|
||||||
|
]
|
267
media/models.py
267
media/models.py
|
@ -16,6 +16,11 @@ 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
|
||||||
|
|
||||||
|
@ -25,41 +30,49 @@ class Auteur(models.Model):
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
|
|
||||||
|
|
||||||
class Media(models.Model):
|
class BD(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,
|
null=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
external_url = models.URLField(
|
external_url = models.URLField(
|
||||||
verbose_name=_('external URL'),
|
verbose_name=_('external URL'),
|
||||||
blank=True,
|
blank=True,
|
||||||
null=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,
|
||||||
|
@ -73,14 +86,260 @@ class Media(models.Model):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("medium")
|
verbose_name = _("BD")
|
||||||
verbose_name_plural = _("media")
|
verbose_name_plural = _("BDs")
|
||||||
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,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
external_url = models.URLField(
|
||||||
|
verbose_name=_('external URL'),
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
side_identifier = models.CharField(
|
||||||
|
verbose_name=_('side identifier'),
|
||||||
|
max_length=255,
|
||||||
|
)
|
||||||
|
|
||||||
|
authors = models.ManyToManyField(
|
||||||
|
'Auteur',
|
||||||
|
verbose_name=_('authors'),
|
||||||
|
)
|
||||||
|
|
||||||
|
number_of_pages = models.PositiveIntegerField(
|
||||||
|
verbose_name=_('number of pages'),
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
publish_date = models.DateField(
|
||||||
|
verbose_name=_('publish date'),
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
external_url = models.URLField(
|
||||||
|
verbose_name=_('external URL'),
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
side_identifier = models.CharField(
|
||||||
|
verbose_name=_('side identifier'),
|
||||||
|
max_length=255,
|
||||||
|
)
|
||||||
|
|
||||||
|
authors = models.ManyToManyField(
|
||||||
|
'Auteur',
|
||||||
|
verbose_name=_('authors'),
|
||||||
|
)
|
||||||
|
|
||||||
|
number_of_pages = models.PositiveIntegerField(
|
||||||
|
verbose_name=_('number of pages'),
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
publish_date = models.DateField(
|
||||||
|
verbose_name=_('publish date'),
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
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'),
|
||||||
|
)
|
||||||
|
|
||||||
|
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'),
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
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(
|
||||||
'Media',
|
'BD',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
)
|
)
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
|
|
|
@ -5,6 +5,8 @@ import re
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from media.models import Auteur
|
||||||
|
|
||||||
|
|
||||||
class BedetequeScraper:
|
class BedetequeScraper:
|
||||||
"""
|
"""
|
||||||
|
@ -56,6 +58,9 @@ 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,
|
||||||
|
@ -73,10 +78,6 @@ 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,5 +87,26 @@ 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,6 +1,6 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from .models import Auteur, Emprunt, Jeu, Media
|
from .models import Auteur, BD, Emprunt, Jeu
|
||||||
|
|
||||||
|
|
||||||
class AuteurSerializer(serializers.HyperlinkedModelSerializer):
|
class AuteurSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
@ -11,7 +11,7 @@ class AuteurSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
|
||||||
class MediaSerializer(serializers.HyperlinkedModelSerializer):
|
class MediaSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Media
|
model = BD
|
||||||
fields = ['url', 'isbn', 'title', 'subtitle', 'external_url',
|
fields = ['url', 'isbn', 'title', 'subtitle', 'external_url',
|
||||||
'side_identifier', 'authors', 'number_of_pages',
|
'side_identifier', 'authors', 'number_of_pages',
|
||||||
'publish_date']
|
'publish_date']
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% include "django/forms/widgets/input.html" %}
|
{% include "django/forms/widgets/input.html" %}
|
||||||
<input type="submit" value="{% trans "Fetch data" %}" name="_continue">
|
<input type="submit" value="{% trans "Fetch data and add another" %}" name="_isbn_addanother">
|
||||||
|
<input type="submit" value="{% trans "Fetch only" %}" name="_isbn">
|
|
@ -4,7 +4,7 @@
|
||||||
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, Media
|
from media.models import Auteur, BD
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -25,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_media1 = Media.objects.create(
|
self.dummy_bd1 = BD.objects.create(
|
||||||
title="Test media",
|
title="Test media",
|
||||||
side_identifier="T M",
|
side_identifier="T M",
|
||||||
)
|
)
|
||||||
self.dummy_media1.authors.add(self.dummy_author)
|
self.dummy_bd1.authors.add(self.dummy_author)
|
||||||
self.dummy_media2 = Media.objects.create(
|
self.dummy_bd2 = BD.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_media2.authors.add(self.dummy_author)
|
self.dummy_bd2.authors.add(self.dummy_author)
|
||||||
|
|
||||||
def test_media_media_changelist(self):
|
def test_bd_bd_changelist(self):
|
||||||
response = self.client.get(reverse('admin:media_media_changelist'))
|
response = self.client.get(reverse('admin:media_bd_changelist'))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_media_media_add(self):
|
def test_bd_bd_add(self):
|
||||||
response = self.client.get(reverse('admin:media_media_add'))
|
response = self.client.get(reverse('admin:media_bd_add'))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_media_isbn_download(self):
|
def test_bd_isbn_download(self):
|
||||||
data = {
|
data = {
|
||||||
'_continue': True,
|
'_isbn': True,
|
||||||
'isbn': "0316358525",
|
'isbn': "0316358525",
|
||||||
}
|
}
|
||||||
response = self.client.post(reverse(
|
response = self.client.post(reverse(
|
||||||
'admin:media_media_change',
|
'admin:media_bd_change',
|
||||||
args=[self.dummy_media1.id],
|
args=[self.dummy_bd1.id],
|
||||||
), data=data)
|
), data=data)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
def test_media_emprunt_changelist(self):
|
def test_bd_emprunt_changelist(self):
|
||||||
response = self.client.get(reverse('admin:media_emprunt_changelist'))
|
response = self.client.get(reverse('admin:media_emprunt_changelist'))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_media_emprunt_add(self):
|
def test_bd_emprunt_add(self):
|
||||||
response = self.client.get(reverse('admin:media_emprunt_add'))
|
response = self.client.get(reverse('admin:media_emprunt_add'))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
|
@ -8,7 +8,6 @@ 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):
|
||||||
|
@ -21,8 +20,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'))
|
||||||
|
|
|
@ -11,7 +11,7 @@ from django.utils.translation import gettext_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 .models import Auteur, Emprunt, Jeu, Media
|
from .models import Auteur, BD, Emprunt, Jeu
|
||||||
from .serializers import AuteurSerializer, EmpruntSerializer, \
|
from .serializers import AuteurSerializer, EmpruntSerializer, \
|
||||||
JeuSerializer, MediaSerializer
|
JeuSerializer, MediaSerializer
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ 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 = Media.objects.all()
|
queryset = BD.objects.all()
|
||||||
serializer_class = MediaSerializer
|
serializer_class = MediaSerializer
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
Django==2.2.4
|
Django==2.2.10
|
||||||
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-cas-client==1.5.3
|
||||||
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.2
|
||||||
pyyaml==3.13
|
pyyaml==3.13
|
||||||
coreapi==2.3.3
|
coreapi==2.3.3
|
||||||
psycopg2
|
psycopg2-binary
|
||||||
|
uwsgi==2.0.18
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
# -*- mode: python; coding: utf-8 -*-
|
|
||||||
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
default_app_config = 'sporz.apps.SporzConfig'
|
|
|
@ -1,76 +0,0 @@
|
||||||
# -*- mode: python; coding: utf-8 -*-
|
|
||||||
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
from django.contrib import admin
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.db.models import Q
|
|
||||||
|
|
||||||
from med.admin import admin_site
|
|
||||||
from .models import GameSave, Player
|
|
||||||
|
|
||||||
|
|
||||||
class PlayerInline(admin.TabularInline):
|
|
||||||
model = Player
|
|
||||||
|
|
||||||
# Do not always show extra players
|
|
||||||
extra = 0
|
|
||||||
min_num = 5
|
|
||||||
|
|
||||||
|
|
||||||
class GameSaveAdmin(admin.ModelAdmin):
|
|
||||||
inlines = [PlayerInline, ]
|
|
||||||
list_display = ('__str__', 'game_master', 'game_has_ended')
|
|
||||||
date_hierarchy = 'created_at'
|
|
||||||
autocomplete_fields = ('game_master',)
|
|
||||||
|
|
||||||
def has_change_permission(self, request, obj=None):
|
|
||||||
"""
|
|
||||||
If user is game master then authorize edit
|
|
||||||
"""
|
|
||||||
if obj and obj.game_master == request.user:
|
|
||||||
return True
|
|
||||||
return super().has_change_permission(request, obj)
|
|
||||||
|
|
||||||
def has_delete_permission(self, request, obj=None):
|
|
||||||
"""
|
|
||||||
If user is game master then authorize deletion
|
|
||||||
"""
|
|
||||||
if obj and obj.game_master == request.user:
|
|
||||||
return True
|
|
||||||
return super().has_delete_permission(request, obj)
|
|
||||||
|
|
||||||
def add_view(self, request, form_url='', extra_context=None):
|
|
||||||
"""
|
|
||||||
Autoselect game master when creating a new game
|
|
||||||
"""
|
|
||||||
# Make GET data mutable
|
|
||||||
data = request.GET.copy()
|
|
||||||
data['game_master'] = request.user
|
|
||||||
request.GET = data
|
|
||||||
return super().add_view(request, form_url, extra_context)
|
|
||||||
|
|
||||||
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
|
||||||
"""
|
|
||||||
Authorize game master change only if user can see all users
|
|
||||||
"""
|
|
||||||
if db_field.name == 'game_master':
|
|
||||||
if not request.user.has_perm('users.view_user'):
|
|
||||||
kwargs['queryset'] = get_user_model().objects.filter(
|
|
||||||
username=request.user.username)
|
|
||||||
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
|
||||||
|
|
||||||
def get_queryset(self, request):
|
|
||||||
"""
|
|
||||||
List all game save only if user has view permission
|
|
||||||
else, list only own games and ended games
|
|
||||||
"""
|
|
||||||
queryset = super().get_queryset(request)
|
|
||||||
if request.user.has_perm('sporz.view_gamesave'):
|
|
||||||
return queryset
|
|
||||||
return queryset.filter(
|
|
||||||
Q(game_master=request.user) | Q(game_has_ended=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
admin_site.register(GameSave, GameSaveAdmin)
|
|
|
@ -1,11 +0,0 @@
|
||||||
# -*- mode: python; coding: utf-8 -*-
|
|
||||||
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
from django.apps import AppConfig
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class SporzConfig(AppConfig):
|
|
||||||
name = 'sporz'
|
|
||||||
verbose_name = _('Sporz game assitant')
|
|
|
@ -1,134 +0,0 @@
|
||||||
#, fuzzy
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
|
||||||
"Report-Msgid-Bugs-To: \n"
|
|
||||||
"POT-Creation-Date: 2019-08-15 11:29+0200\n"
|
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
||||||
"Language: \n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
|
||||||
|
|
||||||
#: apps.py:11
|
|
||||||
msgid "Sporz game assitant"
|
|
||||||
msgstr "Assitant au jeu Sporz"
|
|
||||||
|
|
||||||
#: models.py:13
|
|
||||||
msgid "created at"
|
|
||||||
msgstr "créé le"
|
|
||||||
|
|
||||||
#: models.py:20
|
|
||||||
msgid "game master"
|
|
||||||
msgstr "maître du jeu"
|
|
||||||
|
|
||||||
#: models.py:21
|
|
||||||
msgid "Game master can edit and delete this game save."
|
|
||||||
msgstr "Le maître du jeu peut éditer et supprimer cette sauvegarde."
|
|
||||||
|
|
||||||
#: models.py:24
|
|
||||||
msgid "current round"
|
|
||||||
msgstr "tour actuel"
|
|
||||||
|
|
||||||
#: models.py:28
|
|
||||||
msgid "game has ended"
|
|
||||||
msgstr "la partie est finie"
|
|
||||||
|
|
||||||
#: models.py:29
|
|
||||||
msgid "If true, then everyone will be able to see the game."
|
|
||||||
msgstr "Quand cette case est cochée, tout le monde pourra voir le récapitulatif."
|
|
||||||
|
|
||||||
#: models.py:36 models.py:115
|
|
||||||
msgid "players"
|
|
||||||
msgstr "joueurs"
|
|
||||||
|
|
||||||
#: models.py:40
|
|
||||||
msgid "game save"
|
|
||||||
msgstr "sauvegarde de jeu"
|
|
||||||
|
|
||||||
#: models.py:41
|
|
||||||
msgid "game saves"
|
|
||||||
msgstr "sauvegardes de jeu"
|
|
||||||
|
|
||||||
#: models.py:58
|
|
||||||
msgid "Base astronaut"
|
|
||||||
msgstr "Astronaute de base"
|
|
||||||
|
|
||||||
#: models.py:59
|
|
||||||
msgid "Base mutant"
|
|
||||||
msgstr "Mutant de base"
|
|
||||||
|
|
||||||
#: models.py:60
|
|
||||||
msgid "Healer"
|
|
||||||
msgstr "Médecin"
|
|
||||||
|
|
||||||
#: models.py:61
|
|
||||||
msgid "Psychologist"
|
|
||||||
msgstr "Psychologue"
|
|
||||||
|
|
||||||
#: models.py:62
|
|
||||||
msgid "Geno-technician"
|
|
||||||
msgstr "Geno-technicien"
|
|
||||||
|
|
||||||
#: models.py:63
|
|
||||||
msgid "Computer scientist"
|
|
||||||
msgstr "Informaticien"
|
|
||||||
|
|
||||||
#: models.py:64
|
|
||||||
msgid "Hacker"
|
|
||||||
msgstr "Hackeur"
|
|
||||||
|
|
||||||
#: models.py:65
|
|
||||||
msgid "Spy"
|
|
||||||
msgstr "Espion"
|
|
||||||
|
|
||||||
#: models.py:66
|
|
||||||
msgid "Detective"
|
|
||||||
msgstr "Enquêteur"
|
|
||||||
|
|
||||||
#: models.py:67
|
|
||||||
msgid "Traitor"
|
|
||||||
msgstr "Traître"
|
|
||||||
|
|
||||||
#: models.py:75
|
|
||||||
msgid "Neutral"
|
|
||||||
msgstr "Neutre"
|
|
||||||
|
|
||||||
#: models.py:76
|
|
||||||
msgid "Host"
|
|
||||||
msgstr "Hôte"
|
|
||||||
|
|
||||||
#: models.py:77
|
|
||||||
msgid "Immunized"
|
|
||||||
msgstr "Immunisé"
|
|
||||||
|
|
||||||
#: models.py:83
|
|
||||||
msgid "game"
|
|
||||||
msgstr "jeu"
|
|
||||||
|
|
||||||
#: models.py:87
|
|
||||||
msgid "name"
|
|
||||||
msgstr "nom"
|
|
||||||
|
|
||||||
#: models.py:94
|
|
||||||
msgid "user"
|
|
||||||
msgstr "utilisateur"
|
|
||||||
|
|
||||||
#: models.py:95
|
|
||||||
msgid "Optionnal mapping to an user."
|
|
||||||
msgstr "Lien optionnel à un utilisateur."
|
|
||||||
|
|
||||||
#: models.py:103
|
|
||||||
msgid "genotype"
|
|
||||||
msgstr "génotype"
|
|
||||||
|
|
||||||
#: models.py:107
|
|
||||||
msgid "infected"
|
|
||||||
msgstr "infecté"
|
|
||||||
|
|
||||||
#: models.py:114
|
|
||||||
msgid "player"
|
|
||||||
msgstr "joueur"
|
|
|
@ -1,51 +0,0 @@
|
||||||
# Generated by Django 2.2.4 on 2019-08-15 09:31
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
import django.utils.timezone
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='GameSave',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created_at', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='created at')),
|
|
||||||
('current_round', models.PositiveSmallIntegerField(default=1, verbose_name='current round')),
|
|
||||||
('game_has_ended', models.BooleanField(help_text='If true, then everyone will be able to see the game.', verbose_name='game has ended')),
|
|
||||||
('game_master', models.ForeignKey(help_text='Game master can edit and delete this game save.', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='game master')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'game save',
|
|
||||||
'verbose_name_plural': 'game saves',
|
|
||||||
'ordering': ['-created_at'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Player',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(max_length=150, verbose_name='name')),
|
|
||||||
('role', models.CharField(choices=[('BA', 'Base astronaut'), ('BM', 'Base mutant'), ('HE', 'Healer'), ('PS', 'Psychologist'), ('GE', 'Geno-technician'), ('CO', 'Computer scientist'), ('HA', 'Hacker'), ('SP', 'Spy'), ('DE', 'Detective'), ('TR', 'Traitor')], default='BA', max_length=2)),
|
|
||||||
('genotype', models.NullBooleanField(choices=[(None, 'Neutral'), (False, 'Host'), (True, 'Immunized')], verbose_name='genotype')),
|
|
||||||
('infected', models.BooleanField(verbose_name='infected')),
|
|
||||||
('game', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sporz.GameSave', verbose_name='game')),
|
|
||||||
('user', models.ForeignKey(blank=True, help_text='Optionnal mapping to an user.', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'player',
|
|
||||||
'verbose_name_plural': 'players',
|
|
||||||
'ordering': ['user__username'],
|
|
||||||
'unique_together': {('game', 'name')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
117
sporz/models.py
117
sporz/models.py
|
@ -1,117 +0,0 @@
|
||||||
# -*- mode: python; coding: utf-8 -*-
|
|
||||||
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import models
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class GameSave(models.Model):
|
|
||||||
created_at = models.DateTimeField(
|
|
||||||
verbose_name=_('created at'),
|
|
||||||
default=timezone.now,
|
|
||||||
editable=False,
|
|
||||||
)
|
|
||||||
game_master = models.ForeignKey(
|
|
||||||
settings.AUTH_USER_MODEL,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
verbose_name=_('game master'),
|
|
||||||
help_text=_('Game master can edit and delete this game save.'),
|
|
||||||
)
|
|
||||||
current_round = models.PositiveSmallIntegerField(
|
|
||||||
verbose_name=_('current round'),
|
|
||||||
default=1,
|
|
||||||
)
|
|
||||||
game_has_ended = models.BooleanField(
|
|
||||||
verbose_name=_('game has ended'),
|
|
||||||
help_text=_('If true, then everyone will be able to see the game.'),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "{} ({} {})".format(
|
|
||||||
self.created_at.strftime("%b %d %Y %H:%M:%S"),
|
|
||||||
len(self.player_set.all()),
|
|
||||||
_("players"),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("game save")
|
|
||||||
verbose_name_plural = _("game saves")
|
|
||||||
ordering = ['-created_at']
|
|
||||||
|
|
||||||
|
|
||||||
class Player(models.Model):
|
|
||||||
# Player roles
|
|
||||||
BASE_ASTRONAUT = 'BA'
|
|
||||||
BASE_MUTANT = 'BM'
|
|
||||||
HEALER = 'HE'
|
|
||||||
PSYCHOLOGIST = 'PS'
|
|
||||||
GENO_TECHNICIAN = 'GE'
|
|
||||||
COMPUTER_SCIENTIST = 'CO'
|
|
||||||
HACKER = 'HA'
|
|
||||||
SPY = 'SP'
|
|
||||||
DETECTIVE = 'DE'
|
|
||||||
TRAITOR = 'TR'
|
|
||||||
ROLES = [
|
|
||||||
(BASE_ASTRONAUT, _('Base astronaut')),
|
|
||||||
(BASE_MUTANT, _("Base mutant")),
|
|
||||||
(HEALER, _("Healer")),
|
|
||||||
(PSYCHOLOGIST, _("Psychologist")),
|
|
||||||
(GENO_TECHNICIAN, _("Geno-technician")),
|
|
||||||
(COMPUTER_SCIENTIST, _("Computer scientist")),
|
|
||||||
(HACKER, _("Hacker")),
|
|
||||||
(SPY, _("Spy")),
|
|
||||||
(DETECTIVE, _("Detective")),
|
|
||||||
(TRAITOR, _("Traitor")),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Genotypes
|
|
||||||
NEUTRAL = None
|
|
||||||
HOST = False
|
|
||||||
IMMUNIZED = True
|
|
||||||
GENOTYPES = [
|
|
||||||
(NEUTRAL, _("Neutral")),
|
|
||||||
(HOST, _("Host")),
|
|
||||||
(IMMUNIZED, _("Immunized"))
|
|
||||||
]
|
|
||||||
|
|
||||||
game = models.ForeignKey(
|
|
||||||
GameSave,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
verbose_name=_('game'),
|
|
||||||
)
|
|
||||||
name = models.CharField(
|
|
||||||
max_length=150,
|
|
||||||
verbose_name=_('name'),
|
|
||||||
)
|
|
||||||
user = models.ForeignKey(
|
|
||||||
settings.AUTH_USER_MODEL,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
verbose_name=_('user'),
|
|
||||||
help_text=_('Optionnal mapping to an user.'),
|
|
||||||
)
|
|
||||||
role = models.CharField(
|
|
||||||
max_length=2,
|
|
||||||
choices=ROLES,
|
|
||||||
default=BASE_ASTRONAUT,
|
|
||||||
)
|
|
||||||
genotype = models.NullBooleanField(
|
|
||||||
verbose_name=_('genotype'),
|
|
||||||
choices=GENOTYPES,
|
|
||||||
)
|
|
||||||
infected = models.BooleanField(
|
|
||||||
verbose_name=_('infected'),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return str(self.name)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("player")
|
|
||||||
verbose_name_plural = _("players")
|
|
||||||
ordering = ['user__username']
|
|
||||||
unique_together = ['game', 'name']
|
|
|
@ -1,19 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# This will launch the Django project as a UWSGI socket
|
|
||||||
# then Apache or NGINX will be able to use that socket
|
|
||||||
|
|
||||||
PROJECT_PATH="$(pwd)"
|
|
||||||
|
|
||||||
# Official Django configuration
|
|
||||||
uwsgi_python3 --chdir=$PROJECT_PATH \
|
|
||||||
--module=med.wsgi:application \
|
|
||||||
--env DJANGO_SETTINGS_MODULE=med.settings \
|
|
||||||
--master --pidfile=$PROJECT_PATH/uwsgi.pid \
|
|
||||||
--socket=$PROJECT_PATH/uwsgi.sock \
|
|
||||||
--processes=5 \
|
|
||||||
--chmod-socket=600 \
|
|
||||||
--harakiri=20 \
|
|
||||||
--max-requests=5000 \
|
|
||||||
--vacuum \
|
|
||||||
--daemonize=$PROJECT_PATH/uwsgi.log \
|
|
||||||
--protocol=fastcgi
|
|
|
@ -97,7 +97,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<p>
|
<p>
|
||||||
Mediatek 2017-2020 —
|
Mediatek 2017-2020 —
|
||||||
<a href="mailto:club-med@crans.org">Nous contactez</a> —
|
<a href="mailto:club-med@crans.org">Nous contactez</a> —
|
||||||
<a href="{% url "redoc" %}">Explorer l'API</a>
|
<a href="{% url "api-root" %}">Explorer l'API</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
{% comment %}
|
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
{% endcomment %}
|
|
||||||
{% load i18n static %}
|
|
||||||
|
|
||||||
{% block coltype %}nopadding{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<redoc spec-url='{% url "openapi-schema" %}'></redoc>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"></script>
|
|
||||||
{% endblock %}
|
|
|
@ -1,6 +1,8 @@
|
||||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import socket
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
"""
|
"""
|
||||||
GetBlue Android parameters
|
GetBlue Android parameters
|
||||||
|
@ -20,16 +22,25 @@ 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 = HTTPServer(server_address, Server)
|
httpd = HTTPServerV6(server_address, Server)
|
||||||
print('Starting httpd...')
|
print('Starting httpd...')
|
||||||
httpd.serve_forever()
|
httpd.serve_forever()
|
||||||
|
|
6
tox.ini
6
tox.ini
|
@ -1,5 +1,5 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist = py35,py36,py37,linters
|
envlist = py35,py36,py37,py38,linters
|
||||||
skipsdist = True
|
skipsdist = True
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
|
@ -28,11 +28,11 @@ deps =
|
||||||
pyflakes
|
pyflakes
|
||||||
pylint
|
pylint
|
||||||
commands =
|
commands =
|
||||||
flake8 logs media search users
|
flake8 logs media users
|
||||||
pylint .
|
pylint .
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
ignore = D203, W503, E203, I100, I201, I202
|
ignore = D203, W503, E203, I100, I201, I202, C901
|
||||||
exclude =
|
exclude =
|
||||||
.tox,
|
.tox,
|
||||||
.git,
|
.git,
|
||||||
|
|
|
@ -13,14 +13,7 @@ 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, Clef, User
|
from .models import Adhesion, 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):
|
||||||
|
@ -116,4 +109,3 @@ 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: 2019-08-10 16:20+0200\n"
|
"POT-Creation-Date: 2020-02-20 13:51+0100\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"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:32
|
#: admin.py:25
|
||||||
msgid "membership status"
|
msgid "membership status"
|
||||||
msgstr "statut adhérent"
|
msgstr "statut adhérent"
|
||||||
|
|
||||||
#: admin.py:37
|
#: admin.py:30
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr "Oui"
|
msgstr "Oui"
|
||||||
|
|
||||||
#: admin.py:54
|
#: admin.py:47
|
||||||
msgid "Personal info"
|
msgid "Personal info"
|
||||||
msgstr ""
|
msgstr "Informations personnelles"
|
||||||
|
|
||||||
#: admin.py:56
|
#: admin.py:49
|
||||||
msgid "Permissions"
|
msgid "Permissions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:59
|
#: admin.py:52
|
||||||
msgid "Important dates"
|
msgid "Important dates"
|
||||||
msgstr ""
|
msgstr "Dates importantes"
|
||||||
|
|
||||||
#: admin.py:89
|
#: admin.py:82
|
||||||
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:92
|
#: admin.py:85
|
||||||
msgid "The email is invalid."
|
msgid "The email is invalid."
|
||||||
msgstr "L'adresse mail est invalide."
|
msgstr "L'adresse mail est invalide."
|
||||||
|
|
||||||
#: admin.py:111
|
#: admin.py:103
|
||||||
msgid "Adhere"
|
msgid "Adhere"
|
||||||
msgstr "Adhérer"
|
msgstr "Adhérer"
|
||||||
|
|
||||||
#: admin.py:114
|
#: admin.py:106
|
||||||
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:67
|
#: models.py:33
|
||||||
msgid "comment"
|
msgid "comment"
|
||||||
msgstr "commentaire"
|
msgstr "commentaire"
|
||||||
|
|
||||||
|
@ -82,46 +82,30 @@ 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:81
|
#: models.py:56
|
||||||
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:85
|
#: models.py:60
|
||||||
msgid "ending in"
|
msgid "ending in"
|
||||||
msgstr "finie en"
|
msgstr "finie en"
|
||||||
|
|
||||||
#: models.py:86
|
#: models.py:61
|
||||||
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:91
|
#: models.py:66
|
||||||
msgid "members"
|
msgid "members"
|
||||||
msgstr "adhérents"
|
msgstr "adhérents"
|
||||||
|
|
||||||
#: models.py:96
|
#: models.py:71
|
||||||
msgid "membership year"
|
msgid "membership year"
|
||||||
msgstr "année d'adhésion"
|
msgstr "année d'adhésion"
|
||||||
|
|
||||||
#: models.py:97
|
#: models.py:72
|
||||||
msgid "membership years"
|
msgid "membership years"
|
||||||
msgstr "années d'adhésion"
|
msgstr "années d'adhésion"
|
||||||
|
|
||||||
|
@ -133,6 +117,18 @@ msgstr ""
|
||||||
msgid "Save"
|
msgid "Save"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: views.py:40
|
#: views.py:43
|
||||||
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"
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Generated by Django 2.2.4 on 2020-02-09 16:54
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0039_auto_20190810_1610'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='Clef',
|
||||||
|
),
|
||||||
|
]
|
|
@ -50,31 +50,6 @@ 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'),
|
||||||
|
|
Loading…
Reference in New Issue