mirror of
https://gitlab.crans.org/mediatek/med.git
synced 2024-11-26 21:27:11 +00:00
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
|
||||
med
|
||||
media
|
||||
search
|
||||
static
|
||||
templates
|
||||
theme
|
||||
users
|
||||
omit =
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -33,8 +33,9 @@ coverage
|
||||
|
||||
# Local data
|
||||
settings_local.py
|
||||
static_files/*
|
||||
static/*
|
||||
*.log
|
||||
*.pid
|
||||
|
||||
# Virtualenv
|
||||
env/
|
||||
|
@ -1,4 +1,4 @@
|
||||
image: python:3.6
|
||||
image: python:3.8
|
||||
|
||||
stages:
|
||||
- test
|
||||
@ -21,6 +21,12 @@ python37:
|
||||
stage: test
|
||||
script: tox -e py37
|
||||
|
||||
python38:
|
||||
image: python:3.8
|
||||
stage: test
|
||||
script: tox -e py37
|
||||
|
||||
linters:
|
||||
stage: test
|
||||
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.
|
||||
|
||||
## Développement
|
||||
## Installation
|
||||
|
||||
Après avoir installé un environnement Django,
|
||||
### Développement
|
||||
|
||||
On peut soit développer avec Docker, soit utiliser un VirtualEnv.
|
||||
|
||||
Dans le cas du VirtualEnv,
|
||||
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
. venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
./manage.py compilemessages
|
||||
./manage.py makemigrations
|
||||
./manage.py migrate
|
||||
./manage.py collectstatic
|
||||
./manage.py runserver
|
||||
```
|
||||
|
||||
## Configuration d'une base MySQL
|
||||
### Production
|
||||
|
||||
Sur le serveur mysql ou postgresl, il est nécessaire de créer une base de donnée med,
|
||||
ainsi qu'un user med et un mot de passe associé.
|
||||
Vous pouvez soit utiliser Docker, soit configurer manuellement le serveur.
|
||||
|
||||
Voici les étapes à éxecuter pour mysql :
|
||||
#### Mise en place du projet sur Zamok
|
||||
|
||||
```SQL
|
||||
CREATE DATABASE med;
|
||||
CREATE USER 'med'@'localhost' IDENTIFIED BY 'password';
|
||||
GRANT ALL PRIVILEGES ON med.* TO 'med'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
Pour mettre en place le projet sans droits root,
|
||||
on va créer un socket uwsgi dans le répertoire personnel de l'utilisateur `club-med`
|
||||
puis on va dire à Apache2 d'utiliser ce socket avec un `.htaccess`.
|
||||
|
||||
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
|
||||
CREATE DATABASE med;
|
||||
CREATE USER med WITH PASSWORD 'password';
|
||||
GRANT ALL PRIVILEGES ON DATABASE med TO med;
|
||||
CREATE DATABASE "club-med";
|
||||
CREATE USER "club-med" WITH PASSWORD 'MY-STRONG-PASSWORD';
|
||||
GRANT ALL PRIVILEGES ON DATABASE "club-med" TO "club-med";
|
||||
```
|
||||
|
||||
## Exemple de groupes de droits
|
||||
@ -55,10 +112,6 @@ bureau
|
||||
users | Can add adhesion
|
||||
users | Can change adhesion
|
||||
users | Can delete adhesion
|
||||
users | Can view clef
|
||||
users | Can add clef
|
||||
users | Can change clef
|
||||
users | Can delete clef
|
||||
users | Can view user
|
||||
users | Can add user
|
||||
users | Can change user
|
||||
@ -83,7 +136,6 @@ keyholder
|
||||
media | Can change borrowed item
|
||||
media | Can delete borrowed item
|
||||
users | Can view user
|
||||
users | Can view clef
|
||||
|
||||
users (default group for everyone)
|
||||
media | Can view author
|
||||
|
@ -1,8 +1,31 @@
|
||||
#!/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 makemigrations
|
||||
sleep 2
|
||||
python manage.py migrate
|
||||
|
||||
# TODO: use uwsgi in production
|
||||
python manage.py runserver 0.0.0.0:8000
|
||||
# Wait for database
|
||||
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.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.cache import never_cache
|
||||
|
||||
from media.models import Emprunt
|
||||
|
||||
|
||||
|
84
med/login.py
84
med/login.py
@ -1,84 +0,0 @@
|
||||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
import binascii
|
||||
import hashlib
|
||||
import os
|
||||
from base64 import decodestring
|
||||
from base64 import encodestring
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.contrib.auth import hashers
|
||||
|
||||
ALGO_NAME = "{SSHA}"
|
||||
ALGO_LEN = len(ALGO_NAME + "$")
|
||||
DIGEST_LEN = 20
|
||||
|
||||
|
||||
def make_secret(password):
|
||||
salt = os.urandom(4)
|
||||
h = hashlib.sha1(password.encode())
|
||||
h.update(salt)
|
||||
return ALGO_NAME + "$" + encodestring(h.digest() + salt).decode()[:-1]
|
||||
|
||||
|
||||
def check_password(challenge_password, password):
|
||||
challenge_bytes = decodestring(challenge_password[ALGO_LEN:].encode())
|
||||
digest = challenge_bytes[:DIGEST_LEN]
|
||||
salt = challenge_bytes[DIGEST_LEN:]
|
||||
hr = hashlib.sha1(password.encode())
|
||||
hr.update(salt)
|
||||
valid_password = True
|
||||
# La comparaison est volontairement en temps constant
|
||||
# (pour éviter les timing-attacks)
|
||||
for i, j in zip(digest, hr.digest()):
|
||||
valid_password &= i == j
|
||||
return valid_password
|
||||
|
||||
|
||||
class SSHAPasswordHasher(hashers.BasePasswordHasher):
|
||||
"""
|
||||
SSHA password hashing to allow for LDAP auth compatibility
|
||||
"""
|
||||
|
||||
algorithm = ALGO_NAME
|
||||
|
||||
def encode(self, password, salt, iterations=None):
|
||||
"""
|
||||
Hash and salt the given password using SSHA algorithm
|
||||
|
||||
salt is overridden
|
||||
"""
|
||||
assert password is not None
|
||||
return make_secret(password)
|
||||
|
||||
def verify(self, password, encoded):
|
||||
"""
|
||||
Check password against encoded using SSHA algorithm
|
||||
"""
|
||||
assert encoded.startswith(self.algorithm)
|
||||
return check_password(encoded, password)
|
||||
|
||||
def safe_summary(self, encoded):
|
||||
"""
|
||||
Provides a safe summary ofthe password
|
||||
"""
|
||||
assert encoded.startswith(self.algorithm)
|
||||
hash = encoded[ALGO_LEN:]
|
||||
hash = binascii.hexlify(decodestring(hash.encode())).decode()
|
||||
return OrderedDict([
|
||||
('algorithm', self.algorithm),
|
||||
('iterations', 0),
|
||||
('salt', hashers.mask_hash(hash[2 * DIGEST_LEN:], show=2)),
|
||||
('hash', hashers.mask_hash(hash[:2 * DIGEST_LEN])),
|
||||
])
|
||||
|
||||
def harden_runtime(self, password, encoded):
|
||||
"""
|
||||
Method implemented to shut up BasePasswordHasher warning
|
||||
|
||||
As we are not using multiple iterations the method is pretty useless
|
||||
"""
|
||||
pass
|
@ -16,7 +16,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
SECRET_KEY = 'CHANGE_ME_IN_LOCAL_SETTINGS!'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = False
|
||||
DEBUG = True
|
||||
|
||||
ADMINS = (
|
||||
# ('Admin', 'webmaster@example.com'),
|
||||
@ -51,7 +51,6 @@ INSTALLED_APPS = [
|
||||
'med',
|
||||
'media',
|
||||
'logs',
|
||||
'sporz',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@ -145,7 +144,7 @@ USE_TZ = True
|
||||
# Don't put anything in this directory yourself; store your static files
|
||||
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
|
||||
# Example: "/var/www/example.com/static/"
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static_files')
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
||||
|
||||
# URL prefix for static files.
|
||||
# Example: "http://example.com/static/", "http://static.example.com/"
|
||||
@ -153,6 +152,8 @@ STATIC_URL = '/static/'
|
||||
|
||||
# Django REST Framework
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
||||
'PAGE_SIZE': 10,
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
'med.permissions.DjangoViewModelPermissions',
|
||||
]
|
||||
@ -161,14 +162,6 @@ REST_FRAMEWORK = {
|
||||
# Med configuration
|
||||
PAGINATION_NUMBER = 25
|
||||
|
||||
PASSWORD_HASHERS = [
|
||||
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
||||
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||
'med.login.SSHAPasswordHasher',
|
||||
]
|
||||
|
||||
AUTH_USER_MODEL = 'users.User'
|
||||
|
||||
MAX_EMPRUNT = 5 # Max emprunts
|
||||
|
@ -33,21 +33,10 @@ DEBUG = True
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||
'NAME': 'med',
|
||||
'USER': 'med',
|
||||
'NAME': 'club-med',
|
||||
'USER': 'club-med',
|
||||
'PASSWORD': 'password_to_store_in_env',
|
||||
'HOST': 'db',
|
||||
'PORT': '',
|
||||
}
|
||||
}
|
||||
|
||||
# or MySQL database for Zamok
|
||||
# DATABASES = {
|
||||
# 'default': {
|
||||
# 'ENGINE': 'django.db.backends.mysql',
|
||||
# 'NAME': 'club-med',
|
||||
# 'USER': 'club-med',
|
||||
# 'PASSWORD': 'CHANGE ME !!!',
|
||||
# 'HOST': 'localhost',
|
||||
# },
|
||||
# }
|
@ -7,7 +7,6 @@ from django.contrib.auth.views import PasswordResetView
|
||||
from django.urls import include, path
|
||||
from django.views.generic import RedirectView, TemplateView
|
||||
from rest_framework import routers
|
||||
from rest_framework.schemas import get_schema_view
|
||||
|
||||
import media.views
|
||||
import users.views
|
||||
@ -33,10 +32,6 @@ urlpatterns = [
|
||||
# REST API
|
||||
path('api/', include(router.urls)),
|
||||
path('api-auth/', include('rest_framework.urls')),
|
||||
path('openapi', login_required(get_schema_view()), name='openapi-schema'),
|
||||
path('redoc/',
|
||||
login_required(TemplateView.as_view(template_name='redoc.html')),
|
||||
name='redoc'),
|
||||
|
||||
# Include Django Contrib and Core routers
|
||||
path('accounts/password_reset/', PasswordResetView.as_view(),
|
||||
|
@ -9,7 +9,8 @@ from reversion.admin import VersionAdmin
|
||||
|
||||
from med.admin import admin_site
|
||||
from .forms import MediaAdminForm
|
||||
from .models import Auteur, Emprunt, Jeu, Media
|
||||
from .models import Auteur, BD, CD, Emprunt, FutureMedia, Jeu, Manga,\
|
||||
Revue, Roman, Vinyle
|
||||
|
||||
|
||||
class AuteurAdmin(VersionAdmin):
|
||||
@ -60,6 +61,49 @@ class MediaAdmin(VersionAdmin):
|
||||
extra_context=extra_context)
|
||||
|
||||
|
||||
class FutureMediaAdmin(VersionAdmin):
|
||||
list_display = ('isbn',)
|
||||
search_fields = ('isbn',)
|
||||
|
||||
def changeform_view(self, request, object_id=None, form_url='',
|
||||
extra_context=None):
|
||||
"""
|
||||
We use _continue for ISBN fetching, so remove continue button
|
||||
"""
|
||||
extra_context = extra_context or {}
|
||||
extra_context['show_save_and_continue'] = False
|
||||
extra_context['show_save'] = False
|
||||
return super().changeform_view(request, object_id, form_url,
|
||||
extra_context=extra_context)
|
||||
|
||||
|
||||
class CDAdmin(VersionAdmin):
|
||||
list_display = ('title', 'authors_list', 'side_identifier',)
|
||||
search_fields = ('title', 'authors__name', 'side_identifier',)
|
||||
autocomplete_fields = ('authors',)
|
||||
|
||||
def authors_list(self, obj):
|
||||
return ", ".join([a.name for a in obj.authors.all()])
|
||||
|
||||
authors_list.short_description = _('authors')
|
||||
|
||||
|
||||
class VinyleAdmin(VersionAdmin):
|
||||
list_display = ('title', 'authors_list', 'side_identifier', 'rpm',)
|
||||
search_fields = ('title', 'authors__name', 'side_identifier', 'rpm',)
|
||||
autocomplete_fields = ('authors',)
|
||||
|
||||
def authors_list(self, obj):
|
||||
return ", ".join([a.name for a in obj.authors.all()])
|
||||
|
||||
authors_list.short_description = _('authors')
|
||||
|
||||
|
||||
class RevueAdmin(VersionAdmin):
|
||||
list_display = ('__str__', 'number', 'year', 'month', 'day', 'double',)
|
||||
search_fields = ('title', 'number', 'year',)
|
||||
|
||||
|
||||
class EmpruntAdmin(VersionAdmin):
|
||||
list_display = ('media', 'user', 'date_emprunt', 'date_rendu',
|
||||
'permanencier_emprunt', 'permanencier_rendu_custom')
|
||||
@ -104,6 +148,12 @@ class JeuAdmin(VersionAdmin):
|
||||
|
||||
|
||||
admin_site.register(Auteur, AuteurAdmin)
|
||||
admin_site.register(Media, MediaAdmin)
|
||||
admin_site.register(BD, MediaAdmin)
|
||||
admin_site.register(Manga, MediaAdmin)
|
||||
admin_site.register(Roman, MediaAdmin)
|
||||
admin_site.register(CD, CDAdmin)
|
||||
admin_site.register(Vinyle, VinyleAdmin)
|
||||
admin_site.register(Revue, RevueAdmin)
|
||||
admin_site.register(FutureMedia, FutureMediaAdmin)
|
||||
admin_site.register(Emprunt, EmpruntAdmin)
|
||||
admin_site.register(Jeu, JeuAdmin)
|
||||
|
221
media/forms.py
221
media/forms.py
@ -3,10 +3,14 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import json
|
||||
import re
|
||||
import unicodedata
|
||||
import urllib.request
|
||||
|
||||
from django.forms import ModelForm
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Auteur, BD
|
||||
from .scraper import BedetequeScraper
|
||||
|
||||
|
||||
@ -32,6 +36,52 @@ class MediaAdminForm(ModelForm):
|
||||
self.cleaned_data.update(data)
|
||||
return True
|
||||
|
||||
def download_data_google(self, isbn):
|
||||
"""
|
||||
Download data from google books
|
||||
:return True if success
|
||||
"""
|
||||
api_url = "https://www.googleapis.com/books/v1/volumes?q=ISBN:{}"\
|
||||
.format(isbn)
|
||||
with urllib.request.urlopen(api_url) as url:
|
||||
data = json.loads(url.read().decode())
|
||||
|
||||
if data and data['totalItems']:
|
||||
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):
|
||||
"""
|
||||
Download data from openlibrary
|
||||
@ -41,33 +91,178 @@ class MediaAdminForm(ModelForm):
|
||||
"&format=json&jscmd=data".format(isbn)
|
||||
with urllib.request.urlopen(api_url) as url:
|
||||
data = json.loads(url.read().decode())
|
||||
|
||||
if data and data['ISBN:' + isbn]:
|
||||
data = data['ISBN:' + isbn]
|
||||
if 'url' in data:
|
||||
# Fill the data
|
||||
self.cleaned_data['external_url'] = data['url']
|
||||
if 'title' in data:
|
||||
self.cleaned_data['title'] = data['title']
|
||||
if 'subtitle' in data:
|
||||
self.cleaned_data['subtitle'] = data['subtitle']
|
||||
if 'number_of_pages' in data:
|
||||
self.cleaned_data['number_of_pages'] = \
|
||||
data['number_of_pages']
|
||||
self.parse_data_openlibrary(data)
|
||||
return True
|
||||
return False
|
||||
|
||||
def parse_data_openlibrary(self, data):
|
||||
self.cleaned_data['external_url'] = data['url']
|
||||
if 'title' in data:
|
||||
self.cleaned_data['title'] = data['title']
|
||||
if 'subtitle' in data:
|
||||
self.cleaned_data['subtitle'] = data['subtitle']
|
||||
|
||||
if 'number_of_pages' in data:
|
||||
self.cleaned_data['number_of_pages'] = \
|
||||
data['number_of_pages']
|
||||
elif not self.cleaned_data['number_of_pages']:
|
||||
self.cleaned_data['number_of_pages'] = 0
|
||||
|
||||
if 'publish_date' in data:
|
||||
months = ['January', 'February', "March", "April", "Mai",
|
||||
"June", "July", "August", "September",
|
||||
"October", "November", "December"]
|
||||
split = data['publish_date'].replace(',', '').split(' ')
|
||||
if len(split) == 1:
|
||||
self.cleaned_data['publish_date'] = split[0] + "-01-01"
|
||||
else:
|
||||
month_to_number = dict(
|
||||
Jan="01",
|
||||
Feb="02",
|
||||
Mar="03",
|
||||
Apr="04",
|
||||
May="05",
|
||||
Jun="06",
|
||||
Jul="07",
|
||||
Aug="08",
|
||||
Sep="09",
|
||||
Oct="10",
|
||||
Nov="11",
|
||||
Dec="12",
|
||||
)
|
||||
if split[0][:3] in month_to_number:
|
||||
self.cleaned_data['publish_date']\
|
||||
= split[2] + "-" \
|
||||
+ month_to_number[split[0][:3]] + "-" + split[1]
|
||||
else:
|
||||
self.cleaned_data['publish_date'] = "{}-{:02d}-{:02d}" \
|
||||
.format(split[2], months.index(split[0])
|
||||
+ 1, int(split[1]), )
|
||||
|
||||
if 'authors' not in self.cleaned_data \
|
||||
or not self.cleaned_data['authors']:
|
||||
self.cleaned_data['authors'] = list()
|
||||
|
||||
if 'authors' in data:
|
||||
for author in data['authors']:
|
||||
author_obj = Auteur.objects.get_or_create(
|
||||
name=author['name'])[0]
|
||||
self.cleaned_data['authors'].append(author_obj)
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
If user fetch ISBN data, then download data before validating the form
|
||||
"""
|
||||
# TODO implement authors, side_identifier
|
||||
if "_continue" in self.request.POST:
|
||||
super().clean()
|
||||
|
||||
if "_isbn" in self.data\
|
||||
or "_isbn_addanother" in self.data:
|
||||
isbn = self.cleaned_data.get('isbn')
|
||||
if "_isbn_addanother" in self.data:
|
||||
self.data = self.data.copy()
|
||||
self.data['_addanother'] = 42
|
||||
self.request.POST = self.data
|
||||
if isbn:
|
||||
# ISBN is present, try with bedeteque
|
||||
scrap_result = self.download_data_bedeteque(isbn)
|
||||
if not scrap_result:
|
||||
# Try with OpenLibrary
|
||||
self.download_data_openlibrary(isbn)
|
||||
# Try with Google
|
||||
scrap_result = self.download_data_google(isbn)
|
||||
if not scrap_result:
|
||||
# Try with OpenLibrary
|
||||
if not self.download_data_openlibrary(isbn):
|
||||
self.add_error('isbn',
|
||||
_("This ISBN is not found."))
|
||||
return self.cleaned_data
|
||||
|
||||
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 ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-16 14:00+0200\n"
|
||||
"POT-Creation-Date: 2020-05-12 17:42+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -13,7 +13,7 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: admin.py:32 models.py:24 models.py:56
|
||||
#: admin.py:32 models.py:29 models.py:62
|
||||
msgid "authors"
|
||||
msgstr "auteurs"
|
||||
|
||||
@ -21,11 +21,11 @@ msgstr "auteurs"
|
||||
msgid "external url"
|
||||
msgstr "URL externe"
|
||||
|
||||
#: admin.py:82
|
||||
#: admin.py:98
|
||||
msgid "Turn back"
|
||||
msgstr "Rendre"
|
||||
|
||||
#: admin.py:85 models.py:112
|
||||
#: admin.py:101 models.py:135
|
||||
msgid "given back to"
|
||||
msgstr "rendu à"
|
||||
|
||||
@ -33,134 +33,152 @@ msgstr "rendu à"
|
||||
msgid "ISBN-10 or 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"
|
||||
msgstr "nom"
|
||||
|
||||
#: models.py:23
|
||||
#: models.py:21
|
||||
msgid "note"
|
||||
msgstr "note"
|
||||
|
||||
#: models.py:28
|
||||
msgid "author"
|
||||
msgstr "auteur"
|
||||
|
||||
#: models.py:30
|
||||
#: models.py:35 models.py:89
|
||||
msgid "ISBN"
|
||||
msgstr "ISBN"
|
||||
|
||||
#: models.py:31
|
||||
#: models.py:36 models.py:90
|
||||
msgid "You may be able to scan it from a bar code."
|
||||
msgstr "Peut souvent être scanné à partir du code barre."
|
||||
|
||||
#: models.py:36
|
||||
#: models.py:42
|
||||
msgid "title"
|
||||
msgstr "titre"
|
||||
|
||||
#: models.py:40
|
||||
#: models.py:46
|
||||
msgid "subtitle"
|
||||
msgstr "sous-titre"
|
||||
|
||||
#: models.py:46
|
||||
#: models.py:52
|
||||
msgid "external URL"
|
||||
msgstr "URL externe"
|
||||
|
||||
#: models.py:51
|
||||
#: models.py:57
|
||||
msgid "side identifier"
|
||||
msgstr "côte"
|
||||
|
||||
#: models.py:59
|
||||
#: models.py:65
|
||||
msgid "number of pages"
|
||||
msgstr "nombre de pages"
|
||||
|
||||
#: models.py:64
|
||||
#: models.py:70
|
||||
msgid "publish date"
|
||||
msgstr "date de publication"
|
||||
|
||||
#: models.py:76
|
||||
#: models.py:82
|
||||
msgid "medium"
|
||||
msgstr "medium"
|
||||
|
||||
#: models.py:77
|
||||
#: models.py:83
|
||||
msgid "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"
|
||||
msgstr "emprunteur"
|
||||
|
||||
#: models.py:92
|
||||
#: models.py:115
|
||||
msgid "borrowed on"
|
||||
msgstr "emprunté le"
|
||||
|
||||
#: models.py:97
|
||||
#: models.py:120
|
||||
msgid "given back on"
|
||||
msgstr "rendu le"
|
||||
|
||||
#: models.py:103
|
||||
#: models.py:126
|
||||
msgid "borrowed with"
|
||||
msgstr "emprunté avec"
|
||||
|
||||
#: models.py:104
|
||||
#: models.py:127
|
||||
msgid "The keyholder that registered this borrowed item."
|
||||
msgstr "Le permanencier qui enregistre cet emprunt."
|
||||
|
||||
#: models.py:113
|
||||
#: models.py:136
|
||||
msgid "The keyholder to whom this item was given back."
|
||||
msgstr "Le permanencier à qui l'emprunt a été rendu."
|
||||
|
||||
#: models.py:120
|
||||
#: models.py:143
|
||||
msgid "borrowed item"
|
||||
msgstr "emprunt"
|
||||
|
||||
#: models.py:121
|
||||
#: models.py:144
|
||||
msgid "borrowed items"
|
||||
msgstr "emprunts"
|
||||
|
||||
#: models.py:141
|
||||
#: models.py:164
|
||||
msgid "owner"
|
||||
msgstr "propriétaire"
|
||||
|
||||
#: models.py:146
|
||||
#: models.py:169
|
||||
msgid "duration"
|
||||
msgstr "durée"
|
||||
|
||||
#: models.py:150
|
||||
#: models.py:173
|
||||
msgid "minimum number of players"
|
||||
msgstr "nombre minimum de joueurs"
|
||||
|
||||
#: models.py:154
|
||||
#: models.py:177
|
||||
msgid "maximum number of players"
|
||||
msgstr "nombre maximum de joueurs"
|
||||
|
||||
#: models.py:160
|
||||
#: models.py:183
|
||||
msgid "comment"
|
||||
msgstr "commentaire"
|
||||
|
||||
#: models.py:167
|
||||
#: models.py:190
|
||||
msgid "game"
|
||||
msgstr "jeu"
|
||||
|
||||
#: models.py:168
|
||||
#: models.py:191
|
||||
msgid "games"
|
||||
msgstr "jeux"
|
||||
|
||||
#: templates/media/isbn_button.html:3
|
||||
msgid "Fetch data"
|
||||
msgstr "Télécharger les données"
|
||||
msgid "Fetch data and add another"
|
||||
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"
|
||||
msgstr "ISBN invalide : ce n'est pas une chaîne de caractères"
|
||||
|
||||
#: validators.py:23
|
||||
#: validators.py:21
|
||||
msgid "Invalid ISBN: Wrong length"
|
||||
msgstr "ISBN invalide : mauvaise longueur"
|
||||
|
||||
#: validators.py:26
|
||||
msgid "Invalid ISBN: Failed checksum"
|
||||
msgstr "ISBN invalide : mauvais checksum"
|
||||
|
||||
#: validators.py:29
|
||||
#: validators.py:27
|
||||
msgid "Invalid ISBN: Only upper case allowed"
|
||||
msgstr "ISBN invalide : seulement les majuscules sont autorisées"
|
||||
|
||||
#: views.py:41
|
||||
#: views.py:44
|
||||
msgid "Welcome to the Mediatek database"
|
||||
msgstr "Bienvenue sur la base de données de la Mediatek"
|
||||
|
0
media/management/commands/__init__.py
Normal file
0
media/management/commands/__init__.py
Normal file
51
media/management/commands/import_cds.py
Normal file
51
media/management/commands/import_cds.py
Normal file
@ -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)))
|
47
media/management/commands/import_future_media.py
Normal file
47
media/management/commands/import_future_media.py
Normal file
@ -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))))
|
93
media/management/commands/import_isbn.py
Normal file
93
media/management/commands/import_isbn.py
Normal file
@ -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))))
|
50
media/management/commands/import_marvel.py
Normal file
50
media/management/commands/import_marvel.py
Normal file
@ -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)))
|
65
media/management/commands/import_no_isbn_roman.py
Normal file
65
media/management/commands/import_no_isbn_roman.py
Normal file
@ -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)))
|
58
media/management/commands/import_revues.py
Normal file
58
media/management/commands/import_revues.py
Normal file
@ -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)))
|
59
media/management/commands/import_vinyles.py
Normal file
59
media/management/commands/import_vinyles.py
Normal file
@ -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)))
|
62
media/management/commands/split_media_types.py
Normal file
62
media/management/commands/split_media_types.py
Normal file
@ -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)))
|
18
media/migrations/0025_auteur_note.py
Normal file
18
media/migrations/0025_auteur_note.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2.4 on 2020-02-10 16:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('media', '0024_auto_20190816_1356'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='auteur',
|
||||
name='note',
|
||||
field=models.IntegerField(default=0, verbose_name='note'),
|
||||
),
|
||||
]
|
20
media/migrations/0026_auto_20200210_1740.py
Normal file
20
media/migrations/0026_auto_20200210_1740.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 2.2.4 on 2020-02-10 16:40
|
||||
|
||||
from django.db import migrations
|
||||
import media.fields
|
||||
import media.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('media', '0025_auteur_note'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='media',
|
||||
name='isbn',
|
||||
field=media.fields.ISBNField(blank=True, help_text='You may be able to scan it from a bar code.', max_length=28, null=True, unique=True, validators=[media.validators.isbn_validator], verbose_name='ISBN'),
|
||||
),
|
||||
]
|
26
media/migrations/0027_futuremedia.py
Normal file
26
media/migrations/0027_futuremedia.py
Normal file
@ -0,0 +1,26 @@
|
||||
# Generated by Django 2.2.10 on 2020-05-12 15:23
|
||||
|
||||
from django.db import migrations, models
|
||||
import media.fields
|
||||
import media.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('media', '0026_auto_20200210_1740'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='FutureMedia',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('isbn', media.fields.ISBNField(blank=True, help_text='You may be able to scan it from a bar code.', max_length=28, null=True, unique=True, validators=[media.validators.isbn_validator], verbose_name='ISBN')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'future medium',
|
||||
'verbose_name_plural': 'future media',
|
||||
},
|
||||
),
|
||||
]
|
34
media/migrations/0028_manga.py
Normal file
34
media/migrations/0028_manga.py
Normal file
@ -0,0 +1,34 @@
|
||||
# Generated by Django 2.2.10 on 2020-05-21 14:28
|
||||
|
||||
from django.db import migrations, models
|
||||
import media.fields
|
||||
import media.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('media', '0027_futuremedia'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Manga',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('isbn', media.fields.ISBNField(blank=True, help_text='You may be able to scan it from a bar code.', max_length=28, null=True, unique=True, validators=[media.validators.isbn_validator], verbose_name='ISBN')),
|
||||
('title', models.CharField(max_length=255, verbose_name='title')),
|
||||
('subtitle', models.CharField(blank=True, max_length=255, null=True, verbose_name='subtitle')),
|
||||
('external_url', models.URLField(blank=True, null=True, verbose_name='external URL')),
|
||||
('side_identifier', models.CharField(max_length=255, verbose_name='side identifier')),
|
||||
('number_of_pages', models.PositiveIntegerField(blank=True, null=True, verbose_name='number of pages')),
|
||||
('publish_date', models.DateField(blank=True, null=True, verbose_name='publish date')),
|
||||
('authors', models.ManyToManyField(to='media.Auteur', verbose_name='authors')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'medium',
|
||||
'verbose_name_plural': 'media',
|
||||
'ordering': ['title', 'subtitle'],
|
||||
},
|
||||
),
|
||||
]
|
17
media/migrations/0029_auto_20200521_1659.py
Normal file
17
media/migrations/0029_auto_20200521_1659.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 2.2.10 on 2020-05-21 14:59
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('media', '0028_manga'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='manga',
|
||||
options={'ordering': ['title', 'subtitle'], 'verbose_name': 'manga', 'verbose_name_plural': 'mangas'},
|
||||
),
|
||||
]
|
49
media/migrations/0030_auto_20200522_1757.py
Normal file
49
media/migrations/0030_auto_20200522_1757.py
Normal file
@ -0,0 +1,49 @@
|
||||
# Generated by Django 2.2.10 on 2020-05-22 15:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('media', '0029_auto_20200521_1659'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='Media',
|
||||
new_name='BD',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='manga',
|
||||
options={},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Vinyle',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=255, verbose_name='title')),
|
||||
('side_identifier', models.CharField(max_length=255, verbose_name='side identifier')),
|
||||
('authors', models.ManyToManyField(to='media.Auteur', verbose_name='authors')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'vinyle',
|
||||
'verbose_name_plural': 'vinyles',
|
||||
'ordering': ['title'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CD',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=255, verbose_name='title')),
|
||||
('side_identifier', models.CharField(max_length=255, verbose_name='side identifier')),
|
||||
('authors', models.ManyToManyField(to='media.Auteur', verbose_name='authors')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'CD',
|
||||
'verbose_name_plural': 'CDs',
|
||||
'ordering': ['title'],
|
||||
},
|
||||
),
|
||||
]
|
17
media/migrations/0031_auto_20200522_1758.py
Normal file
17
media/migrations/0031_auto_20200522_1758.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 2.2.10 on 2020-05-22 15:58
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('media', '0030_auto_20200522_1757'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='bd',
|
||||
options={'ordering': ['title', 'subtitle'], 'verbose_name': 'BD', 'verbose_name_plural': 'BDs'},
|
||||
),
|
||||
]
|
38
media/migrations/0032_auto_20200522_2107.py
Normal file
38
media/migrations/0032_auto_20200522_2107.py
Normal file
@ -0,0 +1,38 @@
|
||||
# Generated by Django 2.2.10 on 2020-05-22 19:07
|
||||
|
||||
from django.db import migrations, models
|
||||
import media.fields
|
||||
import media.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('media', '0031_auto_20200522_1758'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='manga',
|
||||
options={'ordering': ['title'], 'verbose_name': 'manga', 'verbose_name_plural': 'mangas'},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Roman',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('isbn', media.fields.ISBNField(blank=True, help_text='You may be able to scan it from a bar code.', max_length=28, null=True, unique=True, validators=[media.validators.isbn_validator], verbose_name='ISBN')),
|
||||
('title', models.CharField(max_length=255, verbose_name='title')),
|
||||
('subtitle', models.CharField(blank=True, max_length=255, null=True, verbose_name='subtitle')),
|
||||
('external_url', models.URLField(blank=True, null=True, verbose_name='external URL')),
|
||||
('side_identifier', models.CharField(max_length=255, verbose_name='side identifier')),
|
||||
('number_of_pages', models.PositiveIntegerField(blank=True, null=True, verbose_name='number of pages')),
|
||||
('publish_date', models.DateField(blank=True, null=True, verbose_name='publish date')),
|
||||
('authors', models.ManyToManyField(to='media.Auteur', verbose_name='authors')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'roman',
|
||||
'verbose_name_plural': 'romans',
|
||||
'ordering': ['title', 'subtitle'],
|
||||
},
|
||||
),
|
||||
]
|
19
media/migrations/0033_futuremedia_type.py
Normal file
19
media/migrations/0033_futuremedia_type.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 2.2.10 on 2020-05-22 19:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('media', '0032_auto_20200522_2107'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='futuremedia',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('bd', 'BD'), ('manga', 'Manga'), ('roman', 'Roman')], default='bd', max_length=8, verbose_name='type'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
19
media/migrations/0034_vinyle_rpm.py
Normal file
19
media/migrations/0034_vinyle_rpm.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 2.2.10 on 2020-05-23 12:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('media', '0033_futuremedia_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='vinyle',
|
||||
name='rpm',
|
||||
field=models.PositiveIntegerField(choices=[(33, '33 RPM'), (45, '45 RPM')], default=45, verbose_name='rounds per minute'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
29
media/migrations/0035_revue.py
Normal file
29
media/migrations/0035_revue.py
Normal file
@ -0,0 +1,29 @@
|
||||
# Generated by Django 2.2.10 on 2020-05-24 12:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('media', '0034_vinyle_rpm'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Revue',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=255, verbose_name='title')),
|
||||
('number', models.PositiveIntegerField(verbose_name='number')),
|
||||
('year', models.PositiveIntegerField(verbose_name='year')),
|
||||
('month', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='month')),
|
||||
('day', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='day')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'revue',
|
||||
'verbose_name_plural': 'revues',
|
||||
'ordering': ['title', 'number'],
|
||||
},
|
||||
),
|
||||
]
|
18
media/migrations/0036_auto_20200524_1500.py
Normal file
18
media/migrations/0036_auto_20200524_1500.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2.10 on 2020-05-24 13:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('media', '0035_revue'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='revue',
|
||||
name='year',
|
||||
field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='year'),
|
||||
),
|
||||
]
|
18
media/migrations/0037_revue_double.py
Normal file
18
media/migrations/0037_revue_double.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2.10 on 2020-05-24 13:36
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('media', '0036_auto_20200524_1500'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='revue',
|
||||
name='double',
|
||||
field=models.BooleanField(default=False, verbose_name='double'),
|
||||
),
|
||||
]
|
267
media/models.py
267
media/models.py
@ -16,6 +16,11 @@ class Auteur(models.Model):
|
||||
verbose_name=_('name'),
|
||||
)
|
||||
|
||||
note = models.IntegerField(
|
||||
default=0,
|
||||
verbose_name=_("note"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@ -25,41 +30,49 @@ class Auteur(models.Model):
|
||||
ordering = ['name']
|
||||
|
||||
|
||||
class Media(models.Model):
|
||||
class BD(models.Model):
|
||||
isbn = ISBNField(
|
||||
_('ISBN'),
|
||||
help_text=_('You may be able to scan it from a bar code.'),
|
||||
unique=True,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
title = models.CharField(
|
||||
verbose_name=_('title'),
|
||||
max_length=255,
|
||||
)
|
||||
|
||||
subtitle = models.CharField(
|
||||
verbose_name=_('subtitle'),
|
||||
max_length=255,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
external_url = models.URLField(
|
||||
verbose_name=_('external URL'),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
side_identifier = models.CharField(
|
||||
verbose_name=_('side identifier'),
|
||||
max_length=255,
|
||||
)
|
||||
|
||||
authors = models.ManyToManyField(
|
||||
'Auteur',
|
||||
verbose_name=_('authors'),
|
||||
)
|
||||
|
||||
number_of_pages = models.PositiveIntegerField(
|
||||
verbose_name=_('number of pages'),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
publish_date = models.DateField(
|
||||
verbose_name=_('publish date'),
|
||||
blank=True,
|
||||
@ -73,14 +86,260 @@ class Media(models.Model):
|
||||
return self.title
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("medium")
|
||||
verbose_name_plural = _("media")
|
||||
verbose_name = _("BD")
|
||||
verbose_name_plural = _("BDs")
|
||||
ordering = ['title', 'subtitle']
|
||||
|
||||
|
||||
class Manga(models.Model):
|
||||
isbn = ISBNField(
|
||||
_('ISBN'),
|
||||
help_text=_('You may be able to scan it from a bar code.'),
|
||||
unique=True,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
title = models.CharField(
|
||||
verbose_name=_('title'),
|
||||
max_length=255,
|
||||
)
|
||||
|
||||
subtitle = models.CharField(
|
||||
verbose_name=_('subtitle'),
|
||||
max_length=255,
|
||||
blank=True,
|
||||
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):
|
||||
media = models.ForeignKey(
|
||||
'Media',
|
||||
'BD',
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
|
@ -5,6 +5,8 @@ import re
|
||||
|
||||
import requests
|
||||
|
||||
from media.models import Auteur
|
||||
|
||||
|
||||
class BedetequeScraper:
|
||||
"""
|
||||
@ -56,6 +58,9 @@ class BedetequeScraper:
|
||||
regex_subtitle = r'<h2>\s*(.*)</h2>'
|
||||
regex_publish_date = r'datePublished\" content=\"([\d-]*)\">'
|
||||
regex_nb_of_pages = r'numberOfPages\">(\d*)</span'
|
||||
regex_format = r'<label>Format : </label>Format (\w+)</li>'
|
||||
regex_author = r'<span itemprop=\"author\">(((?!<).)*)</span>'
|
||||
regex_illustrator = r'span itemprop=\"illustrator\">(((?!<).)*)</span'
|
||||
|
||||
data = {
|
||||
'external_url': bd_url,
|
||||
@ -73,10 +78,6 @@ class BedetequeScraper:
|
||||
subtitle = subtitle.replace('<span class="numa"></span>', '')
|
||||
data['subtitle'] = ' '.join(subtitle.split())
|
||||
|
||||
# TODO implement author
|
||||
# regex_author = r'author\">([^<]*)</span'
|
||||
# 'author': re.search(regex_author, content).group(1),
|
||||
|
||||
# Get publish date
|
||||
search_publish_date = re.search(regex_publish_date, content)
|
||||
if search_publish_date:
|
||||
@ -86,5 +87,26 @@ class BedetequeScraper:
|
||||
search_nb_pages = re.search(regex_nb_of_pages, content)
|
||||
if search_nb_pages and search_nb_pages.group(1).isnumeric():
|
||||
data['number_of_pages'] = search_nb_pages.group(1)
|
||||
elif 'number_of_pages' not in data:
|
||||
data['number_of_pages'] = 0
|
||||
|
||||
# Get format of the book
|
||||
search_format = re.search(regex_format, content)
|
||||
if search_format:
|
||||
data['format'] = search_format.group(1).lower()
|
||||
|
||||
# Get author and illustrator
|
||||
author = re.search(regex_author, content)
|
||||
if 'author' not in data:
|
||||
data['authors'] = list()
|
||||
if author:
|
||||
author_obj = Auteur.objects.get_or_create(
|
||||
name=author.group(1))[0]
|
||||
data['authors'].append(author_obj)
|
||||
illustrator = re.search(regex_illustrator, content)
|
||||
if illustrator:
|
||||
author_obj = Auteur.objects.get_or_create(
|
||||
name=illustrator.group(1))[0]
|
||||
data['authors'].append(author_obj)
|
||||
|
||||
return data
|
||||
|
@ -1,6 +1,6 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import Auteur, Emprunt, Jeu, Media
|
||||
from .models import Auteur, BD, Emprunt, Jeu
|
||||
|
||||
|
||||
class AuteurSerializer(serializers.HyperlinkedModelSerializer):
|
||||
@ -11,7 +11,7 @@ class AuteurSerializer(serializers.HyperlinkedModelSerializer):
|
||||
|
||||
class MediaSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Media
|
||||
model = BD
|
||||
fields = ['url', 'isbn', 'title', 'subtitle', 'external_url',
|
||||
'side_identifier', 'authors', 'number_of_pages',
|
||||
'publish_date']
|
||||
|
@ -1,3 +1,4 @@
|
||||
{% load i18n %}
|
||||
{% include "django/forms/widgets/input.html" %}
|
||||
<input type="submit" value="{% trans "Fetch data" %}" name="_continue">
|
||||
<input type="submit" value="{% trans "Fetch data and add another" %}" name="_isbn_addanother">
|
||||
<input type="submit" value="{% trans "Fetch only" %}" name="_isbn">
|
@ -4,7 +4,7 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from media.models import Auteur, Media
|
||||
from media.models import Auteur, BD
|
||||
from users.models import User
|
||||
|
||||
"""
|
||||
@ -25,41 +25,41 @@ class TemplateTests(TestCase):
|
||||
self.dummy_author = Auteur.objects.create(name="Test author")
|
||||
|
||||
# Create media
|
||||
self.dummy_media1 = Media.objects.create(
|
||||
self.dummy_bd1 = BD.objects.create(
|
||||
title="Test media",
|
||||
side_identifier="T M",
|
||||
)
|
||||
self.dummy_media1.authors.add(self.dummy_author)
|
||||
self.dummy_media2 = Media.objects.create(
|
||||
self.dummy_bd1.authors.add(self.dummy_author)
|
||||
self.dummy_bd2 = BD.objects.create(
|
||||
title="Test media bis",
|
||||
side_identifier="T M 2",
|
||||
external_url="https://example.com/",
|
||||
)
|
||||
self.dummy_media2.authors.add(self.dummy_author)
|
||||
self.dummy_bd2.authors.add(self.dummy_author)
|
||||
|
||||
def test_media_media_changelist(self):
|
||||
response = self.client.get(reverse('admin:media_media_changelist'))
|
||||
def test_bd_bd_changelist(self):
|
||||
response = self.client.get(reverse('admin:media_bd_changelist'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_media_media_add(self):
|
||||
response = self.client.get(reverse('admin:media_media_add'))
|
||||
def test_bd_bd_add(self):
|
||||
response = self.client.get(reverse('admin:media_bd_add'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_media_isbn_download(self):
|
||||
def test_bd_isbn_download(self):
|
||||
data = {
|
||||
'_continue': True,
|
||||
'_isbn': True,
|
||||
'isbn': "0316358525",
|
||||
}
|
||||
response = self.client.post(reverse(
|
||||
'admin:media_media_change',
|
||||
args=[self.dummy_media1.id],
|
||||
'admin:media_bd_change',
|
||||
args=[self.dummy_bd1.id],
|
||||
), data=data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_media_emprunt_changelist(self):
|
||||
def test_bd_emprunt_changelist(self):
|
||||
response = self.client.get(reverse('admin:media_emprunt_changelist'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_media_emprunt_add(self):
|
||||
def test_bd_emprunt_add(self):
|
||||
response = self.client.get(reverse('admin:media_emprunt_add'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
@ -8,7 +8,6 @@ Based on https://github.com/secnot/django-isbn-field
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from stdnum import isbn
|
||||
|
||||
|
||||
def isbn_validator(raw_isbn):
|
||||
@ -21,8 +20,8 @@ def isbn_validator(raw_isbn):
|
||||
if len(isbn_to_check) != 10 and len(isbn_to_check) != 13:
|
||||
raise ValidationError(_('Invalid ISBN: Wrong length'))
|
||||
|
||||
if not isbn.is_valid(isbn_to_check):
|
||||
raise ValidationError(_('Invalid ISBN: Failed checksum'))
|
||||
# if not isbn.is_valid(isbn_to_check):
|
||||
# raise ValidationError(_('Invalid ISBN: Failed checksum'))
|
||||
|
||||
if isbn_to_check != isbn_to_check.upper():
|
||||
raise ValidationError(_('Invalid ISBN: Only upper case allowed'))
|
||||
|
@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import viewsets
|
||||
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, \
|
||||
JeuSerializer, MediaSerializer
|
||||
|
||||
@ -57,7 +57,7 @@ class MediaViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows media to be viewed or edited.
|
||||
"""
|
||||
queryset = Media.objects.all()
|
||||
queryset = BD.objects.all()
|
||||
serializer_class = MediaSerializer
|
||||
|
||||
|
||||
|
@ -1,12 +1,14 @@
|
||||
Django==2.2.4
|
||||
Django==2.2.10
|
||||
docutils==0.14
|
||||
Pillow==5.4.1
|
||||
pytz==2019.1
|
||||
six==1.12.0
|
||||
sqlparse==0.2.4
|
||||
django-cas-client==1.5.3
|
||||
django-reversion==3.0.3
|
||||
python-stdnum==1.10
|
||||
djangorestframework==3.9.2
|
||||
pyyaml==3.13
|
||||
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>
|
||||
Mediatek 2017-2020 —
|
||||
<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>
|
||||
</div>
|
||||
{% 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
|
||||
|
||||
import os
|
||||
import socket
|
||||
from time import sleep
|
||||
|
||||
"""
|
||||
GetBlue Android parameters
|
||||
@ -20,16 +22,25 @@ class Server(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
self._set_headers()
|
||||
isbn = self.path[7:-24]
|
||||
if not isbn.isnumeric():
|
||||
print("Mauvais ISBN.")
|
||||
return
|
||||
print("Hey j'ai un ISBN :", isbn)
|
||||
os.system("xdotool type " + isbn)
|
||||
os.system("xdotool key KP_Enter")
|
||||
sleep(1)
|
||||
os.system("xdotool click 1")
|
||||
|
||||
def do_HEAD(self):
|
||||
self._set_headers()
|
||||
|
||||
|
||||
class HTTPServerV6(HTTPServer):
|
||||
address_family = socket.AF_INET6
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
server_address = ('', 8080)
|
||||
httpd = HTTPServer(server_address, Server)
|
||||
server_address = ('::', 8080)
|
||||
httpd = HTTPServerV6(server_address, Server)
|
||||
print('Starting httpd...')
|
||||
httpd.serve_forever()
|
||||
|
6
tox.ini
6
tox.ini
@ -1,5 +1,5 @@
|
||||
[tox]
|
||||
envlist = py35,py36,py37,linters
|
||||
envlist = py35,py36,py37,py38,linters
|
||||
skipsdist = True
|
||||
|
||||
[testenv]
|
||||
@ -28,11 +28,11 @@ deps =
|
||||
pyflakes
|
||||
pylint
|
||||
commands =
|
||||
flake8 logs media search users
|
||||
flake8 logs media users
|
||||
pylint .
|
||||
|
||||
[flake8]
|
||||
ignore = D203, W503, E203, I100, I201, I202
|
||||
ignore = D203, W503, E203, I100, I201, I202, C901
|
||||
exclude =
|
||||
.tox,
|
||||
.git,
|
||||
|
@ -13,14 +13,7 @@ from reversion.admin import VersionAdmin
|
||||
|
||||
from med.admin import admin_site
|
||||
from .forms import UserCreationAdminForm
|
||||
from .models import Adhesion, Clef, User
|
||||
|
||||
|
||||
class ClefAdmin(VersionAdmin):
|
||||
list_display = ('name', 'owner', 'comment')
|
||||
ordering = ('name',)
|
||||
search_fields = ('name', 'owner__username', 'comment')
|
||||
autocomplete_fields = ('owner',)
|
||||
from .models import Adhesion, User
|
||||
|
||||
|
||||
class AdhesionAdmin(VersionAdmin):
|
||||
@ -116,4 +109,3 @@ class UserAdmin(VersionAdmin, BaseUserAdmin):
|
||||
|
||||
admin_site.register(User, UserAdmin)
|
||||
admin_site.register(Adhesion, AdhesionAdmin)
|
||||
admin_site.register(Clef, ClefAdmin)
|
||||
|
@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-10 16:20+0200\n"
|
||||
"POT-Creation-Date: 2020-02-20 13:51+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -13,39 +13,39 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: admin.py:32
|
||||
#: admin.py:25
|
||||
msgid "membership status"
|
||||
msgstr "statut adhérent"
|
||||
|
||||
#: admin.py:37
|
||||
#: admin.py:30
|
||||
msgid "Yes"
|
||||
msgstr "Oui"
|
||||
|
||||
#: admin.py:54
|
||||
#: admin.py:47
|
||||
msgid "Personal info"
|
||||
msgstr ""
|
||||
msgstr "Informations personnelles"
|
||||
|
||||
#: admin.py:56
|
||||
#: admin.py:49
|
||||
msgid "Permissions"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:59
|
||||
#: admin.py:52
|
||||
msgid "Important dates"
|
||||
msgstr ""
|
||||
msgstr "Dates importantes"
|
||||
|
||||
#: admin.py:89
|
||||
#: admin.py:82
|
||||
msgid "An email to set the password was sent."
|
||||
msgstr "Un mail pour initialiser le mot de passe a été envoyé."
|
||||
|
||||
#: admin.py:92
|
||||
#: admin.py:85
|
||||
msgid "The email is invalid."
|
||||
msgstr "L'adresse mail est invalide."
|
||||
|
||||
#: admin.py:111
|
||||
#: admin.py:103
|
||||
msgid "Adhere"
|
||||
msgstr "Adhérer"
|
||||
|
||||
#: admin.py:114
|
||||
#: admin.py:106
|
||||
msgid "is member"
|
||||
msgstr "statut adhérent"
|
||||
|
||||
@ -69,7 +69,7 @@ msgstr "emprunts maximal"
|
||||
msgid "Maximal amount of simultaneous borrowed item authorized."
|
||||
msgstr "Nombre maximal d'objets empruntés en même temps."
|
||||
|
||||
#: models.py:33 models.py:67
|
||||
#: models.py:33
|
||||
msgid "comment"
|
||||
msgstr "commentaire"
|
||||
|
||||
@ -82,46 +82,30 @@ msgid "date joined"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:55
|
||||
msgid "name"
|
||||
msgstr "nom"
|
||||
|
||||
#: models.py:62
|
||||
msgid "owner"
|
||||
msgstr "propriétaire"
|
||||
|
||||
#: models.py:74
|
||||
msgid "key"
|
||||
msgstr "clé"
|
||||
|
||||
#: models.py:75
|
||||
msgid "keys"
|
||||
msgstr "clés"
|
||||
|
||||
#: models.py:80
|
||||
msgid "starting in"
|
||||
msgstr "commence en"
|
||||
|
||||
#: models.py:81
|
||||
#: models.py:56
|
||||
msgid "Year in which the membership year starts."
|
||||
msgstr "Année dans laquelle la plage d'adhésion commence."
|
||||
|
||||
#: models.py:85
|
||||
#: models.py:60
|
||||
msgid "ending in"
|
||||
msgstr "finie en"
|
||||
|
||||
#: models.py:86
|
||||
#: models.py:61
|
||||
msgid "Year in which the membership year ends."
|
||||
msgstr "Année dans laquelle la plage d'adhésion finie."
|
||||
|
||||
#: models.py:91
|
||||
#: models.py:66
|
||||
msgid "members"
|
||||
msgstr "adhérents"
|
||||
|
||||
#: models.py:96
|
||||
#: models.py:71
|
||||
msgid "membership year"
|
||||
msgstr "année d'adhésion"
|
||||
|
||||
#: models.py:97
|
||||
#: models.py:72
|
||||
msgid "membership years"
|
||||
msgstr "années d'adhésion"
|
||||
|
||||
@ -133,6 +117,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
#: views.py:40
|
||||
#: views.py:43
|
||||
msgid "Edit user profile"
|
||||
msgstr "Editer le profil utilisateur"
|
||||
|
||||
#~ msgid "name"
|
||||
#~ msgstr "nom"
|
||||
|
||||
#~ msgid "owner"
|
||||
#~ msgstr "propriétaire"
|
||||
|
||||
#~ msgid "key"
|
||||
#~ msgstr "clé"
|
||||
|
||||
#~ msgid "keys"
|
||||
#~ msgstr "clés"
|
||||
|
16
users/migrations/0040_delete_clef.py
Normal file
16
users/migrations/0040_delete_clef.py
Normal file
@ -0,0 +1,16 @@
|
||||
# Generated by Django 2.2.4 on 2020-02-09 16:54
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0039_auto_20190810_1610'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='Clef',
|
||||
),
|
||||
]
|
@ -50,31 +50,6 @@ class User(AbstractUser):
|
||||
return last_year and self in last_year.members.all()
|
||||
|
||||
|
||||
class Clef(models.Model):
|
||||
name = models.CharField(
|
||||
verbose_name=_('name'),
|
||||
max_length=255,
|
||||
unique=True,
|
||||
)
|
||||
owner = models.ForeignKey(
|
||||
'User',
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name=_('owner'),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
comment = models.CharField(
|
||||
verbose_name=_('comment'),
|
||||
max_length=255,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('key')
|
||||
verbose_name_plural = _('keys')
|
||||
|
||||
|
||||
class Adhesion(models.Model):
|
||||
starting_in = models.IntegerField(
|
||||
verbose_name=_('starting in'),
|
||||
|
Loading…
Reference in New Issue
Block a user