Merge branch 'cartons' into 'master'

Cartons

See merge request mediatek/med!2
This commit is contained in:
erdnaxe 2020-09-23 16:29:02 +02:00
commit a6db8a37e7
59 changed files with 1624 additions and 734 deletions

View File

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

3
.gitignore vendored
View File

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

View File

@ -1,4 +1,4 @@
image: python:3.6 image: python:3.8
stages: stages:
- test - test
@ -21,6 +21,12 @@ python37:
stage: test stage: test
script: tox -e py37 script: tox -e py37
python38:
image: python:3.8
stage: test
script: tox -e py37
linters: linters:
stage: test stage: test
script: tox -e linters script: tox -e linters
allow_failure: true

View File

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

View File

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

View File

@ -7,7 +7,6 @@ from django.contrib.auth.admin import Group, GroupAdmin
from django.contrib.sites.admin import Site, SiteAdmin from django.contrib.sites.admin import Site, SiteAdmin
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
from media.models import Emprunt from media.models import Emprunt

View File

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

View File

@ -16,7 +16,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SECRET_KEY = 'CHANGE_ME_IN_LOCAL_SETTINGS!' SECRET_KEY = 'CHANGE_ME_IN_LOCAL_SETTINGS!'
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False DEBUG = True
ADMINS = ( ADMINS = (
# ('Admin', 'webmaster@example.com'), # ('Admin', 'webmaster@example.com'),
@ -51,7 +51,6 @@ INSTALLED_APPS = [
'med', 'med',
'media', 'media',
'logs', 'logs',
'sporz',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -145,7 +144,7 @@ USE_TZ = True
# Don't put anything in this directory yourself; store your static files # Don't put anything in this directory yourself; store your static files
# in apps' "static/" subdirectories and in STATICFILES_DIRS. # in apps' "static/" subdirectories and in STATICFILES_DIRS.
# Example: "/var/www/example.com/static/" # Example: "/var/www/example.com/static/"
STATIC_ROOT = os.path.join(BASE_DIR, 'static_files') STATIC_ROOT = os.path.join(BASE_DIR, 'static')
# URL prefix for static files. # URL prefix for static files.
# Example: "http://example.com/static/", "http://static.example.com/" # Example: "http://example.com/static/", "http://static.example.com/"
@ -153,6 +152,8 @@ STATIC_URL = '/static/'
# Django REST Framework # Django REST Framework
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
'DEFAULT_PERMISSION_CLASSES': [ 'DEFAULT_PERMISSION_CLASSES': [
'med.permissions.DjangoViewModelPermissions', 'med.permissions.DjangoViewModelPermissions',
] ]
@ -161,14 +162,6 @@ REST_FRAMEWORK = {
# Med configuration # Med configuration
PAGINATION_NUMBER = 25 PAGINATION_NUMBER = 25
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
'med.login.SSHAPasswordHasher',
]
AUTH_USER_MODEL = 'users.User' AUTH_USER_MODEL = 'users.User'
MAX_EMPRUNT = 5 # Max emprunts MAX_EMPRUNT = 5 # Max emprunts

View File

@ -33,21 +33,10 @@ DEBUG = True
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2', 'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'med', 'NAME': 'club-med',
'USER': 'med', 'USER': 'club-med',
'PASSWORD': 'password_to_store_in_env', 'PASSWORD': 'password_to_store_in_env',
'HOST': 'db', 'HOST': 'db',
'PORT': '', 'PORT': '',
} }
} }
# or MySQL database for Zamok
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.mysql',
# 'NAME': 'club-med',
# 'USER': 'club-med',
# 'PASSWORD': 'CHANGE ME !!!',
# 'HOST': 'localhost',
# },
# }

View File

@ -7,7 +7,6 @@ from django.contrib.auth.views import PasswordResetView
from django.urls import include, path from django.urls import include, path
from django.views.generic import RedirectView, TemplateView from django.views.generic import RedirectView, TemplateView
from rest_framework import routers from rest_framework import routers
from rest_framework.schemas import get_schema_view
import media.views import media.views
import users.views import users.views
@ -33,10 +32,6 @@ urlpatterns = [
# REST API # REST API
path('api/', include(router.urls)), path('api/', include(router.urls)),
path('api-auth/', include('rest_framework.urls')), path('api-auth/', include('rest_framework.urls')),
path('openapi', login_required(get_schema_view()), name='openapi-schema'),
path('redoc/',
login_required(TemplateView.as_view(template_name='redoc.html')),
name='redoc'),
# Include Django Contrib and Core routers # Include Django Contrib and Core routers
path('accounts/password_reset/', PasswordResetView.as_view(), path('accounts/password_reset/', PasswordResetView.as_view(),

View File

@ -9,7 +9,8 @@ from reversion.admin import VersionAdmin
from med.admin import admin_site from med.admin import admin_site
from .forms import MediaAdminForm from .forms import MediaAdminForm
from .models import Auteur, Emprunt, Jeu, Media from .models import Auteur, BD, CD, Emprunt, FutureMedia, Jeu, Manga,\
Revue, Roman, Vinyle
class AuteurAdmin(VersionAdmin): class AuteurAdmin(VersionAdmin):
@ -60,6 +61,49 @@ class MediaAdmin(VersionAdmin):
extra_context=extra_context) extra_context=extra_context)
class FutureMediaAdmin(VersionAdmin):
list_display = ('isbn',)
search_fields = ('isbn',)
def changeform_view(self, request, object_id=None, form_url='',
extra_context=None):
"""
We use _continue for ISBN fetching, so remove continue button
"""
extra_context = extra_context or {}
extra_context['show_save_and_continue'] = False
extra_context['show_save'] = False
return super().changeform_view(request, object_id, form_url,
extra_context=extra_context)
class CDAdmin(VersionAdmin):
list_display = ('title', 'authors_list', 'side_identifier',)
search_fields = ('title', 'authors__name', 'side_identifier',)
autocomplete_fields = ('authors',)
def authors_list(self, obj):
return ", ".join([a.name for a in obj.authors.all()])
authors_list.short_description = _('authors')
class VinyleAdmin(VersionAdmin):
list_display = ('title', 'authors_list', 'side_identifier', 'rpm',)
search_fields = ('title', 'authors__name', 'side_identifier', 'rpm',)
autocomplete_fields = ('authors',)
def authors_list(self, obj):
return ", ".join([a.name for a in obj.authors.all()])
authors_list.short_description = _('authors')
class RevueAdmin(VersionAdmin):
list_display = ('__str__', 'number', 'year', 'month', 'day', 'double',)
search_fields = ('title', 'number', 'year',)
class EmpruntAdmin(VersionAdmin): class EmpruntAdmin(VersionAdmin):
list_display = ('media', 'user', 'date_emprunt', 'date_rendu', list_display = ('media', 'user', 'date_emprunt', 'date_rendu',
'permanencier_emprunt', 'permanencier_rendu_custom') 'permanencier_emprunt', 'permanencier_rendu_custom')
@ -104,6 +148,12 @@ class JeuAdmin(VersionAdmin):
admin_site.register(Auteur, AuteurAdmin) admin_site.register(Auteur, AuteurAdmin)
admin_site.register(Media, MediaAdmin) admin_site.register(BD, MediaAdmin)
admin_site.register(Manga, MediaAdmin)
admin_site.register(Roman, MediaAdmin)
admin_site.register(CD, CDAdmin)
admin_site.register(Vinyle, VinyleAdmin)
admin_site.register(Revue, RevueAdmin)
admin_site.register(FutureMedia, FutureMediaAdmin)
admin_site.register(Emprunt, EmpruntAdmin) admin_site.register(Emprunt, EmpruntAdmin)
admin_site.register(Jeu, JeuAdmin) admin_site.register(Jeu, JeuAdmin)

View File

@ -3,10 +3,14 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import json import json
import re
import unicodedata
import urllib.request import urllib.request
from django.forms import ModelForm from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from .models import Auteur, BD
from .scraper import BedetequeScraper from .scraper import BedetequeScraper
@ -32,6 +36,52 @@ class MediaAdminForm(ModelForm):
self.cleaned_data.update(data) self.cleaned_data.update(data)
return True return True
def download_data_google(self, isbn):
"""
Download data from google books
:return True if success
"""
api_url = "https://www.googleapis.com/books/v1/volumes?q=ISBN:{}"\
.format(isbn)
with urllib.request.urlopen(api_url) as url:
data = json.loads(url.read().decode())
if data and data['totalItems']:
data = data['items'][0]
# Fill the data
self.parse_data_google(data)
return True
return False
def parse_data_google(self, data):
info = data['volumeInfo']
self.cleaned_data['external_url'] = info['canonicalVolumeLink']
if 'title' in info:
self.cleaned_data['title'] = info['title']
if 'subtitle' in data:
self.cleaned_data['subtitle'] = info['subtitle']
if 'pageCount' in info:
self.cleaned_data['number_of_pages'] = \
info['pageCount']
elif not self.cleaned_data['number_of_pages']:
self.cleaned_data['number_of_pages'] = 0
if 'publishedDate' in info:
self.cleaned_data['publish_date'] = info['publishedDate']
if 'authors' not in self.cleaned_data \
or not self.cleaned_data['authors']:
self.cleaned_data['authors'] = list()
if 'authors' in info:
for author in info['authors']:
author_obj = Auteur.objects.get_or_create(
name=author)[0]
self.cleaned_data['authors'].append(author_obj)
print(self.cleaned_data)
def download_data_openlibrary(self, isbn): def download_data_openlibrary(self, isbn):
""" """
Download data from openlibrary Download data from openlibrary
@ -41,33 +91,178 @@ class MediaAdminForm(ModelForm):
"&format=json&jscmd=data".format(isbn) "&format=json&jscmd=data".format(isbn)
with urllib.request.urlopen(api_url) as url: with urllib.request.urlopen(api_url) as url:
data = json.loads(url.read().decode()) data = json.loads(url.read().decode())
if data and data['ISBN:' + isbn]: if data and data['ISBN:' + isbn]:
data = data['ISBN:' + isbn] data = data['ISBN:' + isbn]
if 'url' in data: if 'url' in data:
# Fill the data # Fill the data
self.cleaned_data['external_url'] = data['url'] self.parse_data_openlibrary(data)
if 'title' in data:
self.cleaned_data['title'] = data['title']
if 'subtitle' in data:
self.cleaned_data['subtitle'] = data['subtitle']
if 'number_of_pages' in data:
self.cleaned_data['number_of_pages'] = \
data['number_of_pages']
return True return True
return False return False
def parse_data_openlibrary(self, data):
self.cleaned_data['external_url'] = data['url']
if 'title' in data:
self.cleaned_data['title'] = data['title']
if 'subtitle' in data:
self.cleaned_data['subtitle'] = data['subtitle']
if 'number_of_pages' in data:
self.cleaned_data['number_of_pages'] = \
data['number_of_pages']
elif not self.cleaned_data['number_of_pages']:
self.cleaned_data['number_of_pages'] = 0
if 'publish_date' in data:
months = ['January', 'February', "March", "April", "Mai",
"June", "July", "August", "September",
"October", "November", "December"]
split = data['publish_date'].replace(',', '').split(' ')
if len(split) == 1:
self.cleaned_data['publish_date'] = split[0] + "-01-01"
else:
month_to_number = dict(
Jan="01",
Feb="02",
Mar="03",
Apr="04",
May="05",
Jun="06",
Jul="07",
Aug="08",
Sep="09",
Oct="10",
Nov="11",
Dec="12",
)
if split[0][:3] in month_to_number:
self.cleaned_data['publish_date']\
= split[2] + "-" \
+ month_to_number[split[0][:3]] + "-" + split[1]
else:
self.cleaned_data['publish_date'] = "{}-{:02d}-{:02d}" \
.format(split[2], months.index(split[0])
+ 1, int(split[1]), )
if 'authors' not in self.cleaned_data \
or not self.cleaned_data['authors']:
self.cleaned_data['authors'] = list()
if 'authors' in data:
for author in data['authors']:
author_obj = Auteur.objects.get_or_create(
name=author['name'])[0]
self.cleaned_data['authors'].append(author_obj)
def clean(self): def clean(self):
""" """
If user fetch ISBN data, then download data before validating the form If user fetch ISBN data, then download data before validating the form
""" """
# TODO implement authors, side_identifier super().clean()
if "_continue" in self.request.POST:
if "_isbn" in self.data\
or "_isbn_addanother" in self.data:
isbn = self.cleaned_data.get('isbn') isbn = self.cleaned_data.get('isbn')
if "_isbn_addanother" in self.data:
self.data = self.data.copy()
self.data['_addanother'] = 42
self.request.POST = self.data
if isbn: if isbn:
# ISBN is present, try with bedeteque # ISBN is present, try with bedeteque
scrap_result = self.download_data_bedeteque(isbn) scrap_result = self.download_data_bedeteque(isbn)
if not scrap_result: if not scrap_result:
# Try with OpenLibrary # Try with Google
self.download_data_openlibrary(isbn) 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__'

View File

@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-16 14:00+0200\n" "POT-Creation-Date: 2020-05-12 17:42+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -13,7 +13,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: admin.py:32 models.py:24 models.py:56 #: admin.py:32 models.py:29 models.py:62
msgid "authors" msgid "authors"
msgstr "auteurs" msgstr "auteurs"
@ -21,11 +21,11 @@ msgstr "auteurs"
msgid "external url" msgid "external url"
msgstr "URL externe" msgstr "URL externe"
#: admin.py:82 #: admin.py:98
msgid "Turn back" msgid "Turn back"
msgstr "Rendre" msgstr "Rendre"
#: admin.py:85 models.py:112 #: admin.py:101 models.py:135
msgid "given back to" msgid "given back to"
msgstr "rendu à" msgstr "rendu à"
@ -33,134 +33,152 @@ msgstr "rendu à"
msgid "ISBN-10 or ISBN-13" msgid "ISBN-10 or ISBN-13"
msgstr "ISBN-10 ou ISBN-13" msgstr "ISBN-10 ou ISBN-13"
#: models.py:16 models.py:136 #: forms.py:178
msgid "This ISBN is not found."
msgstr "L'ISBN n'a pas été trouvé."
#: models.py:16 models.py:159
msgid "name" msgid "name"
msgstr "nom" msgstr "nom"
#: models.py:23 #: models.py:21
msgid "note"
msgstr "note"
#: models.py:28
msgid "author" msgid "author"
msgstr "auteur" msgstr "auteur"
#: models.py:30 #: models.py:35 models.py:89
msgid "ISBN" msgid "ISBN"
msgstr "ISBN" msgstr "ISBN"
#: models.py:31 #: models.py:36 models.py:90
msgid "You may be able to scan it from a bar code." msgid "You may be able to scan it from a bar code."
msgstr "Peut souvent être scanné à partir du code barre." msgstr "Peut souvent être scanné à partir du code barre."
#: models.py:36 #: models.py:42
msgid "title" msgid "title"
msgstr "titre" msgstr "titre"
#: models.py:40 #: models.py:46
msgid "subtitle" msgid "subtitle"
msgstr "sous-titre" msgstr "sous-titre"
#: models.py:46 #: models.py:52
msgid "external URL" msgid "external URL"
msgstr "URL externe" msgstr "URL externe"
#: models.py:51 #: models.py:57
msgid "side identifier" msgid "side identifier"
msgstr "côte" msgstr "côte"
#: models.py:59 #: models.py:65
msgid "number of pages" msgid "number of pages"
msgstr "nombre de pages" msgstr "nombre de pages"
#: models.py:64 #: models.py:70
msgid "publish date" msgid "publish date"
msgstr "date de publication" msgstr "date de publication"
#: models.py:76 #: models.py:82
msgid "medium" msgid "medium"
msgstr "medium" msgstr "medium"
#: models.py:77 #: models.py:83
msgid "media" msgid "media"
msgstr "media" msgstr "media"
#: models.py:89 #: models.py:97
msgid "future medium"
msgstr "medium à importer"
#: models.py:98
msgid "future media"
msgstr "medias à importer"
#: models.py:112
msgid "borrower" msgid "borrower"
msgstr "emprunteur" msgstr "emprunteur"
#: models.py:92 #: models.py:115
msgid "borrowed on" msgid "borrowed on"
msgstr "emprunté le" msgstr "emprunté le"
#: models.py:97 #: models.py:120
msgid "given back on" msgid "given back on"
msgstr "rendu le" msgstr "rendu le"
#: models.py:103 #: models.py:126
msgid "borrowed with" msgid "borrowed with"
msgstr "emprunté avec" msgstr "emprunté avec"
#: models.py:104 #: models.py:127
msgid "The keyholder that registered this borrowed item." msgid "The keyholder that registered this borrowed item."
msgstr "Le permanencier qui enregistre cet emprunt." msgstr "Le permanencier qui enregistre cet emprunt."
#: models.py:113 #: models.py:136
msgid "The keyholder to whom this item was given back." msgid "The keyholder to whom this item was given back."
msgstr "Le permanencier à qui l'emprunt a été rendu." msgstr "Le permanencier à qui l'emprunt a été rendu."
#: models.py:120 #: models.py:143
msgid "borrowed item" msgid "borrowed item"
msgstr "emprunt" msgstr "emprunt"
#: models.py:121 #: models.py:144
msgid "borrowed items" msgid "borrowed items"
msgstr "emprunts" msgstr "emprunts"
#: models.py:141 #: models.py:164
msgid "owner" msgid "owner"
msgstr "propriétaire" msgstr "propriétaire"
#: models.py:146 #: models.py:169
msgid "duration" msgid "duration"
msgstr "durée" msgstr "durée"
#: models.py:150 #: models.py:173
msgid "minimum number of players" msgid "minimum number of players"
msgstr "nombre minimum de joueurs" msgstr "nombre minimum de joueurs"
#: models.py:154 #: models.py:177
msgid "maximum number of players" msgid "maximum number of players"
msgstr "nombre maximum de joueurs" msgstr "nombre maximum de joueurs"
#: models.py:160 #: models.py:183
msgid "comment" msgid "comment"
msgstr "commentaire" msgstr "commentaire"
#: models.py:167 #: models.py:190
msgid "game" msgid "game"
msgstr "jeu" msgstr "jeu"
#: models.py:168 #: models.py:191
msgid "games" msgid "games"
msgstr "jeux" msgstr "jeux"
#: templates/media/isbn_button.html:3 #: templates/media/isbn_button.html:3
msgid "Fetch data" msgid "Fetch data and add another"
msgstr "Télécharger les données" msgstr "Télécharger les données et ajouter un nouveau medium"
#: validators.py:20 #: templates/media/isbn_button.html:4
#, fuzzy
#| msgid "Fetch data"
msgid "Fetch only"
msgstr "Télécharger uniquement les données"
#: validators.py:18
msgid "Invalid ISBN: Not a string" msgid "Invalid ISBN: Not a string"
msgstr "ISBN invalide : ce n'est pas une chaîne de caractères" msgstr "ISBN invalide : ce n'est pas une chaîne de caractères"
#: validators.py:23 #: validators.py:21
msgid "Invalid ISBN: Wrong length" msgid "Invalid ISBN: Wrong length"
msgstr "ISBN invalide : mauvaise longueur" msgstr "ISBN invalide : mauvaise longueur"
#: validators.py:26 #: validators.py:27
msgid "Invalid ISBN: Failed checksum"
msgstr "ISBN invalide : mauvais checksum"
#: validators.py:29
msgid "Invalid ISBN: Only upper case allowed" msgid "Invalid ISBN: Only upper case allowed"
msgstr "ISBN invalide : seulement les majuscules sont autorisées" msgstr "ISBN invalide : seulement les majuscules sont autorisées"
#: views.py:41 #: views.py:44
msgid "Welcome to the Mediatek database" msgid "Welcome to the Mediatek database"
msgstr "Bienvenue sur la base de données de la Mediatek" msgstr "Bienvenue sur la base de données de la Mediatek"

View File

View 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)))

View 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))))

View 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))))

View 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)))

View 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)))

View 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)))

View 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)))

View 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)))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,11 @@ class Auteur(models.Model):
verbose_name=_('name'), verbose_name=_('name'),
) )
note = models.IntegerField(
default=0,
verbose_name=_("note"),
)
def __str__(self): def __str__(self):
return self.name return self.name
@ -25,41 +30,49 @@ class Auteur(models.Model):
ordering = ['name'] ordering = ['name']
class Media(models.Model): class BD(models.Model):
isbn = ISBNField( isbn = ISBNField(
_('ISBN'), _('ISBN'),
help_text=_('You may be able to scan it from a bar code.'), help_text=_('You may be able to scan it from a bar code.'),
unique=True,
blank=True, blank=True,
null=True, null=True,
) )
title = models.CharField( title = models.CharField(
verbose_name=_('title'), verbose_name=_('title'),
max_length=255, max_length=255,
) )
subtitle = models.CharField( subtitle = models.CharField(
verbose_name=_('subtitle'), verbose_name=_('subtitle'),
max_length=255, max_length=255,
blank=True, blank=True,
null=True, null=True,
) )
external_url = models.URLField( external_url = models.URLField(
verbose_name=_('external URL'), verbose_name=_('external URL'),
blank=True, blank=True,
null=True, null=True,
) )
side_identifier = models.CharField( side_identifier = models.CharField(
verbose_name=_('side identifier'), verbose_name=_('side identifier'),
max_length=255, max_length=255,
) )
authors = models.ManyToManyField( authors = models.ManyToManyField(
'Auteur', 'Auteur',
verbose_name=_('authors'), verbose_name=_('authors'),
) )
number_of_pages = models.PositiveIntegerField( number_of_pages = models.PositiveIntegerField(
verbose_name=_('number of pages'), verbose_name=_('number of pages'),
blank=True, blank=True,
null=True, null=True,
) )
publish_date = models.DateField( publish_date = models.DateField(
verbose_name=_('publish date'), verbose_name=_('publish date'),
blank=True, blank=True,
@ -73,14 +86,260 @@ class Media(models.Model):
return self.title return self.title
class Meta: class Meta:
verbose_name = _("medium") verbose_name = _("BD")
verbose_name_plural = _("media") verbose_name_plural = _("BDs")
ordering = ['title', 'subtitle'] ordering = ['title', 'subtitle']
class Manga(models.Model):
isbn = ISBNField(
_('ISBN'),
help_text=_('You may be able to scan it from a bar code.'),
unique=True,
blank=True,
null=True,
)
title = models.CharField(
verbose_name=_('title'),
max_length=255,
)
subtitle = models.CharField(
verbose_name=_('subtitle'),
max_length=255,
blank=True,
null=True,
)
external_url = models.URLField(
verbose_name=_('external URL'),
blank=True,
null=True,
)
side_identifier = models.CharField(
verbose_name=_('side identifier'),
max_length=255,
)
authors = models.ManyToManyField(
'Auteur',
verbose_name=_('authors'),
)
number_of_pages = models.PositiveIntegerField(
verbose_name=_('number of pages'),
blank=True,
null=True,
)
publish_date = models.DateField(
verbose_name=_('publish date'),
blank=True,
null=True,
)
def __str__(self):
return self.title
class Meta:
verbose_name = _("manga")
verbose_name_plural = _("mangas")
ordering = ['title']
class Roman(models.Model):
isbn = ISBNField(
_('ISBN'),
help_text=_('You may be able to scan it from a bar code.'),
unique=True,
blank=True,
null=True,
)
title = models.CharField(
verbose_name=_('title'),
max_length=255,
)
subtitle = models.CharField(
verbose_name=_('subtitle'),
max_length=255,
blank=True,
null=True,
)
external_url = models.URLField(
verbose_name=_('external URL'),
blank=True,
null=True,
)
side_identifier = models.CharField(
verbose_name=_('side identifier'),
max_length=255,
)
authors = models.ManyToManyField(
'Auteur',
verbose_name=_('authors'),
)
number_of_pages = models.PositiveIntegerField(
verbose_name=_('number of pages'),
blank=True,
null=True,
)
publish_date = models.DateField(
verbose_name=_('publish date'),
blank=True,
null=True,
)
def __str__(self):
return self.title
class Meta:
verbose_name = _("roman")
verbose_name_plural = _("romans")
ordering = ['title', 'subtitle']
class Vinyle(models.Model):
title = models.CharField(
verbose_name=_('title'),
max_length=255,
)
side_identifier = models.CharField(
verbose_name=_('side identifier'),
max_length=255,
)
rpm = models.PositiveIntegerField(
verbose_name=_('rounds per minute'),
choices=[
(33, _('33 RPM')),
(45, _('45 RPM')),
],
)
authors = models.ManyToManyField(
'Auteur',
verbose_name=_('authors'),
)
def __str__(self):
return self.title
class Meta:
verbose_name = _("vinyle")
verbose_name_plural = _("vinyles")
ordering = ['title']
class CD(models.Model):
title = models.CharField(
verbose_name=_('title'),
max_length=255,
)
side_identifier = models.CharField(
verbose_name=_('side identifier'),
max_length=255,
)
authors = models.ManyToManyField(
'Auteur',
verbose_name=_('authors'),
)
def __str__(self):
return self.title
class Meta:
verbose_name = _("CD")
verbose_name_plural = _("CDs")
ordering = ['title']
class Revue(models.Model):
title = models.CharField(
verbose_name=_('title'),
max_length=255,
)
number = models.PositiveIntegerField(
verbose_name=_('number'),
)
year = models.PositiveIntegerField(
verbose_name=_('year'),
null=True,
blank=True,
default=None,
)
month = models.PositiveIntegerField(
verbose_name=_('month'),
null=True,
blank=True,
default=None,
)
day = models.PositiveIntegerField(
verbose_name=_('day'),
null=True,
blank=True,
default=None,
)
double = models.BooleanField(
verbose_name=_('double'),
default=False,
)
def __str__(self):
return self.title + "" + str(self.number)
class Meta:
verbose_name = _("revue")
verbose_name_plural = _("revues")
ordering = ['title', 'number']
class FutureMedia(models.Model):
isbn = ISBNField(
_('ISBN'),
help_text=_('You may be able to scan it from a bar code.'),
unique=True,
blank=True,
null=True,
)
type = models.CharField(
_('type'),
choices=[
('bd', _('BD')),
('manga', _('Manga')),
('roman', _('Roman')),
],
max_length=8,
)
class Meta:
verbose_name = _("future medium")
verbose_name_plural = _("future media")
def __str__(self):
return "Future medium (ISBN: {isbn})".format(isbn=self.isbn, )
class Emprunt(models.Model): class Emprunt(models.Model):
media = models.ForeignKey( media = models.ForeignKey(
'Media', 'BD',
on_delete=models.PROTECT, on_delete=models.PROTECT,
) )
user = models.ForeignKey( user = models.ForeignKey(

View File

@ -5,6 +5,8 @@ import re
import requests import requests
from media.models import Auteur
class BedetequeScraper: class BedetequeScraper:
""" """
@ -56,6 +58,9 @@ class BedetequeScraper:
regex_subtitle = r'<h2>\s*(.*)</h2>' regex_subtitle = r'<h2>\s*(.*)</h2>'
regex_publish_date = r'datePublished\" content=\"([\d-]*)\">' regex_publish_date = r'datePublished\" content=\"([\d-]*)\">'
regex_nb_of_pages = r'numberOfPages\">(\d*)</span' regex_nb_of_pages = r'numberOfPages\">(\d*)</span'
regex_format = r'<label>Format : </label>Format (\w+)</li>'
regex_author = r'<span itemprop=\"author\">(((?!<).)*)</span>'
regex_illustrator = r'span itemprop=\"illustrator\">(((?!<).)*)</span'
data = { data = {
'external_url': bd_url, 'external_url': bd_url,
@ -73,10 +78,6 @@ class BedetequeScraper:
subtitle = subtitle.replace('<span class="numa"></span>', '') subtitle = subtitle.replace('<span class="numa"></span>', '')
data['subtitle'] = ' '.join(subtitle.split()) data['subtitle'] = ' '.join(subtitle.split())
# TODO implement author
# regex_author = r'author\">([^<]*)</span'
# 'author': re.search(regex_author, content).group(1),
# Get publish date # Get publish date
search_publish_date = re.search(regex_publish_date, content) search_publish_date = re.search(regex_publish_date, content)
if search_publish_date: if search_publish_date:
@ -86,5 +87,26 @@ class BedetequeScraper:
search_nb_pages = re.search(regex_nb_of_pages, content) search_nb_pages = re.search(regex_nb_of_pages, content)
if search_nb_pages and search_nb_pages.group(1).isnumeric(): if search_nb_pages and search_nb_pages.group(1).isnumeric():
data['number_of_pages'] = search_nb_pages.group(1) data['number_of_pages'] = search_nb_pages.group(1)
elif 'number_of_pages' not in data:
data['number_of_pages'] = 0
# Get format of the book
search_format = re.search(regex_format, content)
if search_format:
data['format'] = search_format.group(1).lower()
# Get author and illustrator
author = re.search(regex_author, content)
if 'author' not in data:
data['authors'] = list()
if author:
author_obj = Auteur.objects.get_or_create(
name=author.group(1))[0]
data['authors'].append(author_obj)
illustrator = re.search(regex_illustrator, content)
if illustrator:
author_obj = Auteur.objects.get_or_create(
name=illustrator.group(1))[0]
data['authors'].append(author_obj)
return data return data

View File

@ -1,6 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
from .models import Auteur, Emprunt, Jeu, Media from .models import Auteur, BD, Emprunt, Jeu
class AuteurSerializer(serializers.HyperlinkedModelSerializer): class AuteurSerializer(serializers.HyperlinkedModelSerializer):
@ -11,7 +11,7 @@ class AuteurSerializer(serializers.HyperlinkedModelSerializer):
class MediaSerializer(serializers.HyperlinkedModelSerializer): class MediaSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = Media model = BD
fields = ['url', 'isbn', 'title', 'subtitle', 'external_url', fields = ['url', 'isbn', 'title', 'subtitle', 'external_url',
'side_identifier', 'authors', 'number_of_pages', 'side_identifier', 'authors', 'number_of_pages',
'publish_date'] 'publish_date']

View File

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

View File

@ -4,7 +4,7 @@
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from media.models import Auteur, Media from media.models import Auteur, BD
from users.models import User from users.models import User
""" """
@ -25,41 +25,41 @@ class TemplateTests(TestCase):
self.dummy_author = Auteur.objects.create(name="Test author") self.dummy_author = Auteur.objects.create(name="Test author")
# Create media # Create media
self.dummy_media1 = Media.objects.create( self.dummy_bd1 = BD.objects.create(
title="Test media", title="Test media",
side_identifier="T M", side_identifier="T M",
) )
self.dummy_media1.authors.add(self.dummy_author) self.dummy_bd1.authors.add(self.dummy_author)
self.dummy_media2 = Media.objects.create( self.dummy_bd2 = BD.objects.create(
title="Test media bis", title="Test media bis",
side_identifier="T M 2", side_identifier="T M 2",
external_url="https://example.com/", external_url="https://example.com/",
) )
self.dummy_media2.authors.add(self.dummy_author) self.dummy_bd2.authors.add(self.dummy_author)
def test_media_media_changelist(self): def test_bd_bd_changelist(self):
response = self.client.get(reverse('admin:media_media_changelist')) response = self.client.get(reverse('admin:media_bd_changelist'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_media_media_add(self): def test_bd_bd_add(self):
response = self.client.get(reverse('admin:media_media_add')) response = self.client.get(reverse('admin:media_bd_add'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_media_isbn_download(self): def test_bd_isbn_download(self):
data = { data = {
'_continue': True, '_isbn': True,
'isbn': "0316358525", 'isbn': "0316358525",
} }
response = self.client.post(reverse( response = self.client.post(reverse(
'admin:media_media_change', 'admin:media_bd_change',
args=[self.dummy_media1.id], args=[self.dummy_bd1.id],
), data=data) ), data=data)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 302)
def test_media_emprunt_changelist(self): def test_bd_emprunt_changelist(self):
response = self.client.get(reverse('admin:media_emprunt_changelist')) response = self.client.get(reverse('admin:media_emprunt_changelist'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_media_emprunt_add(self): def test_bd_emprunt_add(self):
response = self.client.get(reverse('admin:media_emprunt_add')) response = self.client.get(reverse('admin:media_emprunt_add'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)

View File

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

View File

@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import viewsets from rest_framework import viewsets
from reversion import revisions as reversion from reversion import revisions as reversion
from .models import Auteur, Emprunt, Jeu, Media from .models import Auteur, BD, Emprunt, Jeu
from .serializers import AuteurSerializer, EmpruntSerializer, \ from .serializers import AuteurSerializer, EmpruntSerializer, \
JeuSerializer, MediaSerializer JeuSerializer, MediaSerializer
@ -57,7 +57,7 @@ class MediaViewSet(viewsets.ModelViewSet):
""" """
API endpoint that allows media to be viewed or edited. API endpoint that allows media to be viewed or edited.
""" """
queryset = Media.objects.all() queryset = BD.objects.all()
serializer_class = MediaSerializer serializer_class = MediaSerializer

View File

@ -1,12 +1,14 @@
Django==2.2.4 Django==2.2.10
docutils==0.14 docutils==0.14
Pillow==5.4.1 Pillow==5.4.1
pytz==2019.1 pytz==2019.1
six==1.12.0 six==1.12.0
sqlparse==0.2.4 sqlparse==0.2.4
django-cas-client==1.5.3
django-reversion==3.0.3 django-reversion==3.0.3
python-stdnum==1.10 python-stdnum==1.10
djangorestframework==3.9.2 djangorestframework==3.9.2
pyyaml==3.13 pyyaml==3.13
coreapi==2.3.3 coreapi==2.3.3
psycopg2 psycopg2-binary
uwsgi==2.0.18

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -97,7 +97,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<p> <p>
Mediatek 2017-2020 &mdash; Mediatek 2017-2020 &mdash;
<a href="mailto:club-med@crans.org">Nous contactez</a> &mdash; <a href="mailto:club-med@crans.org">Nous contactez</a> &mdash;
<a href="{% url "redoc" %}">Explorer l'API</a> <a href="{% url "api-root" %}">Explorer l'API</a>
</p> </p>
</div> </div>
{% endif %} {% endif %}

View File

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

View File

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

View File

@ -1,5 +1,5 @@
[tox] [tox]
envlist = py35,py36,py37,linters envlist = py35,py36,py37,py38,linters
skipsdist = True skipsdist = True
[testenv] [testenv]
@ -28,11 +28,11 @@ deps =
pyflakes pyflakes
pylint pylint
commands = commands =
flake8 logs media search users flake8 logs media users
pylint . pylint .
[flake8] [flake8]
ignore = D203, W503, E203, I100, I201, I202 ignore = D203, W503, E203, I100, I201, I202, C901
exclude = exclude =
.tox, .tox,
.git, .git,

View File

@ -13,14 +13,7 @@ from reversion.admin import VersionAdmin
from med.admin import admin_site from med.admin import admin_site
from .forms import UserCreationAdminForm from .forms import UserCreationAdminForm
from .models import Adhesion, Clef, User from .models import Adhesion, User
class ClefAdmin(VersionAdmin):
list_display = ('name', 'owner', 'comment')
ordering = ('name',)
search_fields = ('name', 'owner__username', 'comment')
autocomplete_fields = ('owner',)
class AdhesionAdmin(VersionAdmin): class AdhesionAdmin(VersionAdmin):
@ -116,4 +109,3 @@ class UserAdmin(VersionAdmin, BaseUserAdmin):
admin_site.register(User, UserAdmin) admin_site.register(User, UserAdmin)
admin_site.register(Adhesion, AdhesionAdmin) admin_site.register(Adhesion, AdhesionAdmin)
admin_site.register(Clef, ClefAdmin)

View File

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

View File

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

View File

@ -50,31 +50,6 @@ class User(AbstractUser):
return last_year and self in last_year.members.all() return last_year and self in last_year.members.all()
class Clef(models.Model):
name = models.CharField(
verbose_name=_('name'),
max_length=255,
unique=True,
)
owner = models.ForeignKey(
'User',
on_delete=models.PROTECT,
verbose_name=_('owner'),
blank=True,
null=True,
)
comment = models.CharField(
verbose_name=_('comment'),
max_length=255,
null=True,
blank=True,
)
class Meta:
verbose_name = _('key')
verbose_name_plural = _('keys')
class Adhesion(models.Model): class Adhesion(models.Model):
starting_in = models.IntegerField( starting_in = models.IntegerField(
verbose_name=_('starting in'), verbose_name=_('starting in'),