mirror of
https://gitlab.crans.org/mediatek/med.git
synced 2025-07-04 08:52:15 +02:00
Compare commits
105 Commits
44abcaf202
...
harden
Author | SHA1 | Date | |
---|---|---|---|
48c056b210 | |||
cf544dc596
|
|||
c0521005ef
|
|||
09c61091d5
|
|||
145806b5ce | |||
49898143ce
|
|||
fe55a2a5ea
|
|||
cdcb743b55
|
|||
e75f04b530
|
|||
34766257b3
|
|||
d0877f5cdc
|
|||
aceb0d893c
|
|||
41435a6838
|
|||
d3c2441111
|
|||
d036ea6f27
|
|||
e2aa645bbf
|
|||
faf697d3cf
|
|||
b0a1602ea2 | |||
d2ad52c15a
|
|||
8c6828564c | |||
d75250f436
|
|||
c424c7c040
|
|||
a01d480dd2
|
|||
079ade9bbb
|
|||
4928b555b7
|
|||
ae0d1a080e
|
|||
1e6e033cdd
|
|||
d0805ebe8a
|
|||
6789b9e3ac
|
|||
54f8198b86
|
|||
e3bab2389c
|
|||
39e345ee67
|
|||
0b3701f01f
|
|||
1ab06af5c7 | |||
2c4aacfc08
|
|||
70018f0043
|
|||
b638add396
|
|||
9f5807e3b5
|
|||
7396cfc017
|
|||
52ecd59bf6
|
|||
2a17a32d4c
|
|||
7f24e5c1bf
|
|||
69a8080050
|
|||
a78d0b4904
|
|||
15fc5cd73f
|
|||
94c6c73615 | |||
33a7e5adc6
|
|||
1a96b1a2aa
|
|||
e7dfaf8b8b | |||
74f453637a
|
|||
b8ccb40ded
|
|||
574233acd0
|
|||
746d5cc816
|
|||
4de83344a7 | |||
a64d600645 | |||
22408b16c7 | |||
6abbaaf75d | |||
9afb750be6 | |||
997c2eac50 | |||
be2094b263 | |||
cb9cd8f9b6 | |||
9dd2a142b7 | |||
dcba832549 | |||
aa51a40cf6 | |||
263b4cff77 | |||
91aeb28c3a | |||
3af19c0f27 | |||
80c520d76c | |||
665f7a2875 | |||
c1098577e1 | |||
e09b503ee1 | |||
9c53d89ad3 | |||
8c8692b8d2 | |||
3edc3ffa02 | |||
8fa724e848 | |||
838fcecb56 | |||
8b097dc4e0 | |||
6985e39130 | |||
6f60de1838 | |||
9ecd876923 | |||
be76bf4857 | |||
4198ea8a72 | |||
7ed6b9712b | |||
57659acc93 | |||
a06ae5c9b9 | |||
796c985ffb | |||
dc4cb56dd0 | |||
b2f0ee0b44 | |||
35ecc2800f | |||
73615afa77 | |||
8d76bd255a | |||
5e3003720f | |||
952a3ddddf | |||
91b361d7a6 | |||
9a7304f573 | |||
8d20b14cbb | |||
ad33d33e6c | |||
d1e9693647 | |||
df1a1cb5de | |||
47292feab2 | |||
52af84b146 | |||
a6db8a37e7 | |||
4409911659 | |||
0147c5b42c | |||
e63d8630cc |
@ -1,32 +1,24 @@
|
||||
image: python:3.8
|
||||
|
||||
stages:
|
||||
- test
|
||||
- quality-assurance
|
||||
|
||||
before_script:
|
||||
- pip install tox
|
||||
|
||||
python35:
|
||||
image: python:3.5
|
||||
py39-django22:
|
||||
stage: test
|
||||
script: tox -e py35
|
||||
|
||||
python36:
|
||||
image: python:3.6
|
||||
stage: test
|
||||
script: tox -e py36
|
||||
|
||||
python37:
|
||||
image: python:3.7
|
||||
stage: test
|
||||
script: tox -e py37
|
||||
|
||||
python38:
|
||||
image: python:3.8
|
||||
stage: test
|
||||
script: tox -e py37
|
||||
image: debian:bullseye
|
||||
before_script:
|
||||
- >
|
||||
apt-get update &&
|
||||
apt-get install --no-install-recommends -y
|
||||
python3-django python3-django-polymorphic python3-django-reversion
|
||||
python3-djangorestframework python3-docutils python3-requests tox
|
||||
script: tox -e py39
|
||||
|
||||
linters:
|
||||
stage: test
|
||||
stage: quality-assurance
|
||||
image: debian:bullseye
|
||||
before_script:
|
||||
- apt-get update && apt-get install -y tox
|
||||
script: tox -e linters
|
||||
|
||||
# Be nice to new contributors, but please use `tox`
|
||||
allow_failure: true
|
||||
|
32
README.md
32
README.md
@ -39,30 +39,18 @@ 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
|
||||
python3 -m venv venv --system-site-packages
|
||||
. venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
./entrypoint.sh
|
||||
pip install mysqlclient~=1.4.4 # si base MySQL
|
||||
pip install uwsgi~=2.0.18 # si production
|
||||
./entrypoint.sh # lance en shell
|
||||
```
|
||||
|
||||
Pour lancer le serveur au démarrage de Zamok,
|
||||
on 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 lancer le serveur au démarrage de Zamok, on suit les instructions dans `django-med.service`.
|
||||
|
||||
Pour reverse-proxyfier le serveur derrière Apache, on place dans `~/www/.htaccess` :
|
||||
|
||||
@ -78,12 +66,6 @@ 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.
|
||||
|
||||
@ -108,10 +90,6 @@ bureau
|
||||
media | Can add borrowed item
|
||||
media | Can change borrowed item
|
||||
media | Can delete borrowed item
|
||||
users | Can view adhesion
|
||||
users | Can add adhesion
|
||||
users | Can change adhesion
|
||||
users | Can delete adhesion
|
||||
users | Can view user
|
||||
users | Can add user
|
||||
users | Can change user
|
||||
|
22
django-med.service
Normal file
22
django-med.service
Normal file
@ -0,0 +1,22 @@
|
||||
# Copy to ~/.config/systemd/user/django-med.service then
|
||||
# systemctl --user daemon-reload
|
||||
# systemctl --user start django-med.service
|
||||
|
||||
[Unit]
|
||||
Description=Mediatek Django project
|
||||
After=syslog.target
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/home/c/club-med/django-med
|
||||
Environment="PATH=/home/c/club-med/django-med/venv/bin"
|
||||
ExecStart=/home/c/club-med/django-med/entrypoint.sh
|
||||
Restart=on-failure
|
||||
KillSignal=SIGQUIT
|
||||
Type=notify
|
||||
StandardError=syslog
|
||||
NotifyAccess=all
|
||||
StandardOutput=append:/home/c/club-med/django-med/service.log
|
||||
StandardError=append:/home/c/club-med/django-med/service_error.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@ -5,9 +5,8 @@
|
||||
# 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
|
||||
|
||||
# Wait for database
|
||||
# Wait for database (docker)
|
||||
sleep 2
|
||||
|
||||
python manage.py migrate
|
||||
@ -16,16 +15,7 @@ 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
|
||||
uwsgi --socket "$HOME/www/uwsgi.sock" --chmod-socket=666 --master --plugins python3 \
|
||||
--module med.wsgi:application --env DJANGO_SETTINGS_MODULE=med.settings \
|
||||
--processes 4 --harakiri=20 --max-requests=5000 --vacuum \
|
||||
--static-map /static="$(pwd)/static" --protocol=fastcgi
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from users.models import User
|
||||
|
||||
"""
|
||||
|
@ -9,7 +9,6 @@ from django.db.models import Count
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from reversion.models import Revision
|
||||
|
||||
from med.settings import PAGINATION_NUMBER
|
||||
from users.models import User
|
||||
|
||||
|
@ -7,7 +7,7 @@ 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
|
||||
from media.models import Borrow
|
||||
|
||||
|
||||
class DatabaseAdmin(AdminSite):
|
||||
@ -22,8 +22,8 @@ class DatabaseAdmin(AdminSite):
|
||||
|
||||
# User is always authenticated
|
||||
# Get currently borrowed items
|
||||
user_borrowed = Emprunt.objects.filter(user=request.user,
|
||||
date_rendu=None)
|
||||
user_borrowed = Borrow.objects.filter(user=request.user,
|
||||
given_back=None)
|
||||
response.context_data["borrowed_items"] = user_borrowed
|
||||
|
||||
return response
|
||||
|
@ -26,6 +26,16 @@ SITE_ID = 1
|
||||
|
||||
ALLOWED_HOSTS = ['127.0.0.1']
|
||||
|
||||
# Use secure cookies in production
|
||||
SESSION_COOKIE_SECURE = not DEBUG
|
||||
CSRF_COOKIE_SECURE = not DEBUG
|
||||
|
||||
# Remember HTTPS for 1 year
|
||||
SECURE_HSTS_SECONDS = 31536000
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||
SECURE_HSTS_PRELOAD = True
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
@ -35,7 +45,8 @@ INSTALLED_APPS = [
|
||||
# External apps
|
||||
'reversion',
|
||||
'rest_framework',
|
||||
'cas',
|
||||
'django_extensions',
|
||||
'polymorphic',
|
||||
|
||||
# Django contrib
|
||||
'django.contrib.admin',
|
||||
@ -46,6 +57,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.sites',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django_filters',
|
||||
|
||||
# Med apps
|
||||
'users',
|
||||
@ -65,14 +77,8 @@ MIDDLEWARE = [
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.contrib.sites.middleware.CurrentSiteMiddleware',
|
||||
# 'cas.middleware.CASMiddleware',
|
||||
]
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
'cas.backends.CASBackend',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'med.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
@ -171,9 +177,26 @@ PAGINATION_NUMBER = 25
|
||||
|
||||
AUTH_USER_MODEL = 'users.User'
|
||||
|
||||
MAX_EMPRUNT = 5 # Max emprunts
|
||||
NOTE_KFET_URL = 'https://note.crans.org'
|
||||
NOTE_KFET_CLIENT_ID = 'CHANGE_ME'
|
||||
NOTE_KFET_CLIENT_SECRET = 'CHANGE_ME'
|
||||
NOTE_KFET_SCOPES = '1_1 2_1 48_1'
|
||||
|
||||
try:
|
||||
from .settings_local import *
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
AUTHLIB_OAUTH_CLIENTS = {
|
||||
'notekfet': {
|
||||
'client_id': f'{NOTE_KFET_CLIENT_ID}',
|
||||
'client_secret': f'{NOTE_KFET_CLIENT_SECRET}',
|
||||
'access_token_url': f'{NOTE_KFET_URL}/o/token/',
|
||||
'refresh_token_url': f'{NOTE_KFET_URL}/o/token/',
|
||||
'authorize_url': f'{NOTE_KFET_URL}/o/authorize/',
|
||||
'userinfo_endpoint': f'{NOTE_KFET_URL}/api/me/',
|
||||
'client_kwargs': {
|
||||
'scope': NOTE_KFET_SCOPES,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,10 +16,6 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
|
||||
SERVER_EMAIL = 'club-med@crans.org'
|
||||
|
||||
CAS_SERVER_URL = "https://note.crans.org/cas/"
|
||||
CAS_LOGOUT_COMPLETELY = True
|
||||
CAS_PROVIDE_URL_TO_LOGOUT = True
|
||||
|
||||
# Security settings
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = False
|
||||
SECURE_BROWSER_XSS_FILTER = False
|
||||
@ -44,3 +40,8 @@ DATABASES = {
|
||||
'PORT': '',
|
||||
}
|
||||
}
|
||||
|
||||
NOTE_KFET_URL = 'https://note.crans.org'
|
||||
NOTE_KFET_CLIENT_ID = 'CHANGE_ME'
|
||||
NOTE_KFET_CLIENT_SECRET = 'CHANGE_ME'
|
||||
NOTE_KFET_SCOPES = '1_1 2_1 48_1'
|
||||
|
25
med/urls.py
25
med/urls.py
@ -2,11 +2,9 @@
|
||||
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from cas import views as cas_views
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.views import PasswordResetView
|
||||
from django.urls import include, path
|
||||
from django.views.generic import RedirectView, TemplateView
|
||||
from django.views.generic import RedirectView
|
||||
from rest_framework import routers
|
||||
|
||||
import media.views
|
||||
@ -15,15 +13,21 @@ from .admin import admin_site
|
||||
|
||||
# API router
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r'authors', media.views.AuteurViewSet)
|
||||
router.register(r'media', media.views.MediaViewSet)
|
||||
router.register(r'borrowed_items', media.views.EmpruntViewSet)
|
||||
router.register(r'games', media.views.JeuViewSet)
|
||||
router.register(r'authors', media.views.AuthorViewSet)
|
||||
router.register(r'media/comic', media.views.ComicViewSet)
|
||||
router.register(r'media/manga', media.views.MangaViewSet)
|
||||
router.register(r'media/cd', media.views.CDViewSet)
|
||||
router.register(r'media/vinyl', media.views.VinylViewSet)
|
||||
router.register(r'media/novel', media.views.NovelViewSet)
|
||||
router.register(r'media/review', media.views.ReviewViewSet)
|
||||
router.register(r'media/future', media.views.FutureMediumViewSet)
|
||||
router.register(r'borrowed_items', media.views.BorrowViewSet)
|
||||
router.register(r'games', media.views.GameViewSet)
|
||||
router.register(r'users', users.views.UserViewSet)
|
||||
router.register(r'groups', users.views.GroupViewSet)
|
||||
|
||||
urlpatterns = [
|
||||
path('', media.views.index, name='index'),
|
||||
path('', media.views.IndexView.as_view(), name='index'),
|
||||
|
||||
# Include project routers
|
||||
path('users/', include('users.urls')),
|
||||
@ -42,9 +46,4 @@ urlpatterns = [
|
||||
path('accounts/profile/', RedirectView.as_view(pattern_name='index')),
|
||||
path('database/doc/', include('django.contrib.admindocs.urls')),
|
||||
path('database/', admin_site.urls),
|
||||
|
||||
# Include CAS authentication
|
||||
# TODO Uncomment when NK20 will be ready
|
||||
# path('admin/login/', cas_views.login, name='login'),
|
||||
# path('admin/logout/', cas_views.logout, name='logout'),
|
||||
]
|
||||
|
109
media/admin.py
109
media/admin.py
@ -2,23 +2,34 @@
|
||||
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from polymorphic.admin import PolymorphicChildModelAdmin, \
|
||||
PolymorphicParentModelAdmin
|
||||
from med.admin import admin_site
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
from med.admin import admin_site
|
||||
from .forms import MediaAdminForm
|
||||
from .models import Auteur, BD, CD, Emprunt, FutureMedia, Jeu, Manga,\
|
||||
Revue, Roman, Vinyle
|
||||
from .models import Author, Borrow, Borrowable, CD, Comic, FutureMedium, \
|
||||
Game, Manga, Novel, Review, Vinyl
|
||||
|
||||
|
||||
class AuteurAdmin(VersionAdmin):
|
||||
class AuthorAdmin(VersionAdmin):
|
||||
list_display = ('name',)
|
||||
search_fields = ('name',)
|
||||
|
||||
|
||||
class MediaAdmin(VersionAdmin):
|
||||
class BorrowableAdmin(PolymorphicParentModelAdmin):
|
||||
search_fields = ('title',)
|
||||
child_models = (CD, Comic, Manga, Novel, Review, Vinyl,)
|
||||
|
||||
def get_model_perms(self, request):
|
||||
# We don't want that the borrowable items appear directly in
|
||||
# main menu, but we still want search borrowable items.
|
||||
return {}
|
||||
|
||||
|
||||
class MediumAdmin(VersionAdmin, PolymorphicChildModelAdmin):
|
||||
list_display = ('__str__', 'authors_list', 'side_identifier', 'isbn',
|
||||
'external_link')
|
||||
search_fields = ('title', 'authors__name', 'side_identifier', 'subtitle',
|
||||
@ -26,6 +37,7 @@ class MediaAdmin(VersionAdmin):
|
||||
autocomplete_fields = ('authors',)
|
||||
date_hierarchy = 'publish_date'
|
||||
form = MediaAdminForm
|
||||
show_in_index = True
|
||||
|
||||
def authors_list(self, obj):
|
||||
return ", ".join([a.name for a in obj.authors.all()])
|
||||
@ -61,7 +73,7 @@ class MediaAdmin(VersionAdmin):
|
||||
extra_context=extra_context)
|
||||
|
||||
|
||||
class FutureMediaAdmin(VersionAdmin):
|
||||
class FutureMediumAdmin(VersionAdmin):
|
||||
list_display = ('isbn',)
|
||||
search_fields = ('isbn',)
|
||||
|
||||
@ -77,10 +89,11 @@ class FutureMediaAdmin(VersionAdmin):
|
||||
extra_context=extra_context)
|
||||
|
||||
|
||||
class CDAdmin(VersionAdmin):
|
||||
class CDAdmin(VersionAdmin, PolymorphicChildModelAdmin):
|
||||
list_display = ('title', 'authors_list', 'side_identifier',)
|
||||
search_fields = ('title', 'authors__name', 'side_identifier',)
|
||||
autocomplete_fields = ('authors',)
|
||||
show_in_index = True
|
||||
|
||||
def authors_list(self, obj):
|
||||
return ", ".join([a.name for a in obj.authors.all()])
|
||||
@ -88,35 +101,33 @@ class CDAdmin(VersionAdmin):
|
||||
authors_list.short_description = _('authors')
|
||||
|
||||
|
||||
class RevueAdmin(VersionAdmin):
|
||||
class VinylAdmin(VersionAdmin, PolymorphicChildModelAdmin):
|
||||
list_display = ('title', 'authors_list', 'side_identifier', 'rpm',)
|
||||
search_fields = ('title', 'authors__name', 'side_identifier', 'rpm',)
|
||||
autocomplete_fields = ('authors',)
|
||||
show_in_index = True
|
||||
|
||||
def authors_list(self, obj):
|
||||
return ", ".join([a.name for a in obj.authors.all()])
|
||||
|
||||
authors_list.short_description = _('authors')
|
||||
|
||||
|
||||
class ReviewAdmin(VersionAdmin, PolymorphicChildModelAdmin):
|
||||
list_display = ('__str__', 'number', 'year', 'month', 'day', 'double',)
|
||||
search_fields = ('title', 'number', 'year',)
|
||||
show_in_index = True
|
||||
|
||||
|
||||
class EmpruntAdmin(VersionAdmin):
|
||||
list_display = ('media', 'user', 'date_emprunt', 'date_rendu',
|
||||
'permanencier_emprunt', 'permanencier_rendu_custom')
|
||||
search_fields = ('media__title', 'media__side_identifier',
|
||||
'user__username', 'date_emprunt', 'date_rendu')
|
||||
date_hierarchy = 'date_emprunt'
|
||||
autocomplete_fields = ('media', 'user', 'permanencier_emprunt',
|
||||
'permanencier_rendu')
|
||||
|
||||
def permanencier_rendu_custom(self, obj):
|
||||
"""
|
||||
Show a button if item has not been returned yet
|
||||
"""
|
||||
if obj.permanencier_rendu:
|
||||
return obj.permanencier_rendu
|
||||
else:
|
||||
return format_html(
|
||||
'<a class="button" href="{}">{}</a>',
|
||||
reverse('media:retour-emprunt', args=[obj.pk]),
|
||||
_('Turn back')
|
||||
)
|
||||
|
||||
permanencier_rendu_custom.short_description = _('given back to')
|
||||
permanencier_rendu_custom.allow_tags = True
|
||||
class BorrowAdmin(VersionAdmin):
|
||||
list_display = ('borrowable', 'user', 'borrow_date', 'borrowed_with',
|
||||
'given_back_to')
|
||||
search_fields = ('borrowable__isbn', 'borrowable__title',
|
||||
'borrowable__medium__side_identifier',
|
||||
'user__username', 'borrow_date', 'given_back')
|
||||
date_hierarchy = 'borrow_date'
|
||||
autocomplete_fields = ('borrowable', 'user', 'borrowed_with',
|
||||
'given_back_to')
|
||||
|
||||
def add_view(self, request, form_url='', extra_context=None):
|
||||
"""
|
||||
@ -124,25 +135,27 @@ class EmpruntAdmin(VersionAdmin):
|
||||
"""
|
||||
# Make GET data mutable
|
||||
data = request.GET.copy()
|
||||
data['permanencier_emprunt'] = request.user
|
||||
data['borrowed_with'] = request.user
|
||||
request.GET = data
|
||||
return super().add_view(request, form_url, extra_context)
|
||||
|
||||
|
||||
class JeuAdmin(VersionAdmin):
|
||||
list_display = ('name', 'proprietaire', 'duree', 'nombre_joueurs_min',
|
||||
'nombre_joueurs_max', 'comment')
|
||||
search_fields = ('name', 'proprietaire__username', 'duree', 'comment')
|
||||
autocomplete_fields = ('proprietaire',)
|
||||
class GameAdmin(VersionAdmin, PolymorphicChildModelAdmin):
|
||||
list_display = ('title', 'owner', 'duration', 'players_min',
|
||||
'players_max', 'comment', 'isbn')
|
||||
search_fields = ('isbn', 'title', 'owner__username', 'duration', 'comment')
|
||||
autocomplete_fields = ('owner',)
|
||||
show_in_index = True
|
||||
|
||||
|
||||
admin_site.register(Auteur, AuteurAdmin)
|
||||
admin_site.register(BD, MediaAdmin)
|
||||
admin_site.register(Manga, MediaAdmin)
|
||||
admin_site.register(Roman, MediaAdmin)
|
||||
admin_site.register(Author, AuthorAdmin)
|
||||
admin_site.register(Borrowable, BorrowableAdmin)
|
||||
admin_site.register(Comic, MediumAdmin)
|
||||
admin_site.register(Manga, MediumAdmin)
|
||||
admin_site.register(Novel, MediumAdmin)
|
||||
admin_site.register(CD, CDAdmin)
|
||||
admin_site.register(Vinyle, CDAdmin)
|
||||
admin_site.register(Revue, RevueAdmin)
|
||||
admin_site.register(FutureMedia, FutureMediaAdmin)
|
||||
admin_site.register(Emprunt, EmpruntAdmin)
|
||||
admin_site.register(Jeu, JeuAdmin)
|
||||
admin_site.register(Vinyl, VinylAdmin)
|
||||
admin_site.register(Review, ReviewAdmin)
|
||||
admin_site.register(FutureMedium, FutureMediumAdmin)
|
||||
admin_site.register(Borrow, BorrowAdmin)
|
||||
admin_site.register(Game, GameAdmin)
|
||||
|
222
media/forms.py
222
media/forms.py
@ -1,19 +1,86 @@
|
||||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2017-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import unicodedata
|
||||
from urllib.error import HTTPError
|
||||
import urllib.request
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import QuerySet
|
||||
from django.forms import ModelForm
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Auteur, BD
|
||||
from .models import Author, Comic
|
||||
from .scraper import BedetequeScraper
|
||||
|
||||
|
||||
def generate_side_identifier(title, authors, subtitle=None):
|
||||
if isinstance(authors, QuerySet):
|
||||
authors = list(authors)
|
||||
|
||||
title_normalized = title.upper()
|
||||
title_normalized = title_normalized.replace('’', '\'')
|
||||
title_normalized = re.sub(r'^DE ', '', title_normalized)
|
||||
title_normalized = re.sub(r'^DES ', '', title_normalized)
|
||||
title_normalized = re.sub(r'^LE ', '', title_normalized)
|
||||
title_normalized = re.sub(r'^LA ', '', title_normalized)
|
||||
title_normalized = re.sub(r'^LES ', '', title_normalized)
|
||||
title_normalized = re.sub(r'^L\'', '', title_normalized)
|
||||
title_normalized = re.sub(r'^UN ', '', title_normalized)
|
||||
title_normalized = re.sub(r'^UNE ', '', title_normalized)
|
||||
title_normalized = re.sub(r'^THE ', '', title_normalized)
|
||||
title_normalized = re.sub(r'Œ', 'OE', title_normalized)
|
||||
title_normalized = title_normalized.replace(' ', '')
|
||||
title_normalized = ''.join(
|
||||
char
|
||||
for char in unicodedata.normalize(
|
||||
'NFKD', title_normalized.casefold())
|
||||
if all(not unicodedata.category(char).startswith(cat)
|
||||
for cat in {'M', 'P', 'Z', 'C'}) or char == ' '
|
||||
).casefold().upper()
|
||||
title_normalized = re.sub("[^A-Z0-9$]", "", title_normalized)
|
||||
authors = authors.copy()
|
||||
|
||||
def sort(author):
|
||||
return "{:042d}".format(-author.note) + author.name.split(" ")[-1]\
|
||||
+ ".{:042d}".format(author.pk)
|
||||
|
||||
authors.sort(key=sort)
|
||||
primary_author = authors[0]
|
||||
author_name = primary_author.name.upper()
|
||||
if ',' not in author_name and ' ' in author_name:
|
||||
author_name = author_name.split(' ')[-1]
|
||||
author_name = ''.join(
|
||||
char for char in unicodedata.normalize('NFKD', author_name.casefold())
|
||||
if all(not unicodedata.category(char).startswith(cat)
|
||||
for cat in {'M', 'P', 'Z', 'C'}) or char == ' '
|
||||
).casefold().upper()
|
||||
author_name = re.sub("[^A-Z]", "", author_name)
|
||||
side_identifier = "{:.3} {:.3}".format(author_name, title_normalized, )
|
||||
if subtitle:
|
||||
subtitle = re.sub(r'</span>', '', subtitle)
|
||||
subtitle = re.sub(r'<span.*>', '', subtitle)
|
||||
start = subtitle.split(' ')[0].replace('.', '')
|
||||
start = re.sub("^R?", "", start)
|
||||
|
||||
if start.isnumeric():
|
||||
side_identifier += " {:0>2}".format(start, )
|
||||
|
||||
# Normalize side identifier, in order to remove accents
|
||||
side_identifier = ''.join(
|
||||
char for char in unicodedata.normalize('NFKD',
|
||||
side_identifier.casefold())
|
||||
if all(not unicodedata.category(char).startswith(cat)
|
||||
for cat in {'M', 'P', 'Z', 'C'})
|
||||
or char == ' ').casefold().upper()
|
||||
|
||||
return side_identifier
|
||||
|
||||
|
||||
class MediaAdminForm(ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -21,6 +88,46 @@ class MediaAdminForm(ModelForm):
|
||||
if isbn_field:
|
||||
isbn_field.widget.template_name = "media/isbn_button.html"
|
||||
isbn_field.widget.attrs.update({'autofocus': 'autofocus'})
|
||||
side_identifier_field = self.fields.get('side_identifier')
|
||||
if side_identifier_field and self.instance and self.instance.pk:
|
||||
instance = self.instance
|
||||
title, authors, subtitle = instance.title,\
|
||||
instance.authors.all(), None
|
||||
if hasattr(instance, "subtitle"):
|
||||
subtitle = instance.subtitle
|
||||
side_identifier_field.widget.attrs.update(
|
||||
{'data-generated-side-identifier':
|
||||
generate_side_identifier(title, authors, subtitle)})
|
||||
side_identifier_field.widget.template_name =\
|
||||
"media/generate_side_identifier.html"
|
||||
|
||||
def download_data_isbndb(self, isbn):
|
||||
api_url = "https://api2.isbndb.com/book/" + str(isbn)\
|
||||
+ "?Authorization=" + os.getenv("ISBNDB_KEY", "")
|
||||
req = urllib.request.Request(api_url)
|
||||
req.add_header("Authorization", os.getenv("ISBNDB_KEY", ""))
|
||||
try:
|
||||
with urllib.request.urlopen(req) as url:
|
||||
data: dict = json.loads(url.read().decode())["book"]
|
||||
except HTTPError:
|
||||
return False
|
||||
print(data)
|
||||
data.setdefault("title", "")
|
||||
data.setdefault("date_published", "1970-01-01")
|
||||
data.setdefault("pages", 0)
|
||||
data.setdefault("authors", [])
|
||||
data.setdefault("image", "")
|
||||
self.cleaned_data["title"] = data["title"]
|
||||
self.cleaned_data["publish_date"] = data["date_published"][:10]
|
||||
while len(self.cleaned_data["publish_date"]) == 4 \
|
||||
or len(self.cleaned_data["publish_date"]) == 7:
|
||||
self.cleaned_data["publish_date"] += "-01"
|
||||
self.cleaned_data["number_of_pages"] = data["pages"]
|
||||
self.cleaned_data["authors"] = \
|
||||
list(Author.objects.get_or_create(name=author_name)[0]
|
||||
for author_name in data["authors"])
|
||||
self.cleaned_data["external_url"] = data["image"]
|
||||
return True
|
||||
|
||||
def download_data_bedeteque(self, isbn):
|
||||
"""
|
||||
@ -32,7 +139,7 @@ class MediaAdminForm(ModelForm):
|
||||
if not r:
|
||||
return False
|
||||
# If results, then take the most accurate
|
||||
data = scraper.scrap_bd_info(r[0])
|
||||
data = scraper.scrap_comic_info(r[0])
|
||||
self.cleaned_data.update(data)
|
||||
return True
|
||||
|
||||
@ -47,9 +154,18 @@ class MediaAdminForm(ModelForm):
|
||||
data = json.loads(url.read().decode())
|
||||
|
||||
if data and data['totalItems']:
|
||||
data = data['items'][0]
|
||||
fetched_item = None
|
||||
for item in data['items']:
|
||||
for identifiers in item["volumeInfo"]["industryIdentifiers"]:
|
||||
if identifiers["identifier"] == isbn:
|
||||
fetched_item = item
|
||||
break
|
||||
if fetched_item:
|
||||
break
|
||||
if not fetched_item:
|
||||
return False
|
||||
# Fill the data
|
||||
self.parse_data_google(data)
|
||||
self.parse_data_google(fetched_item)
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -68,7 +184,11 @@ class MediaAdminForm(ModelForm):
|
||||
self.cleaned_data['number_of_pages'] = 0
|
||||
|
||||
if 'publishedDate' in info:
|
||||
self.cleaned_data['publish_date'] = info['publishedDate']
|
||||
if "-" not in info["publishedDate"]:
|
||||
info["publishedDate"] += "-01-01"
|
||||
elif len(info["publishedDate"]) == 7:
|
||||
info["publishedDate"] += "-01"
|
||||
self.cleaned_data['publish_date'] = info['publishedDate'][:10]
|
||||
|
||||
if 'authors' not in self.cleaned_data \
|
||||
or not self.cleaned_data['authors']:
|
||||
@ -76,7 +196,7 @@ class MediaAdminForm(ModelForm):
|
||||
|
||||
if 'authors' in info:
|
||||
for author in info['authors']:
|
||||
author_obj = Auteur.objects.get_or_create(
|
||||
author_obj = Author.objects.get_or_create(
|
||||
name=author)[0]
|
||||
self.cleaned_data['authors'].append(author_obj)
|
||||
|
||||
@ -150,7 +270,7 @@ class MediaAdminForm(ModelForm):
|
||||
|
||||
if 'authors' in data:
|
||||
for author in data['authors']:
|
||||
author_obj = Auteur.objects.get_or_create(
|
||||
author_obj = Author.objects.get_or_create(
|
||||
name=author['name'])[0]
|
||||
self.cleaned_data['authors'].append(author_obj)
|
||||
|
||||
@ -168,17 +288,19 @@ class MediaAdminForm(ModelForm):
|
||||
self.data['_addanother'] = 42
|
||||
self.request.POST = self.data
|
||||
if isbn:
|
||||
# ISBN is present, try with bedeteque
|
||||
scrap_result = self.download_data_bedeteque(isbn)
|
||||
scrap_result = self.download_data_isbndb(isbn)
|
||||
if not scrap_result:
|
||||
# Try with Google
|
||||
scrap_result = self.download_data_google(isbn)
|
||||
# ISBN is present, try with bedeteque
|
||||
scrap_result = self.download_data_bedeteque(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
|
||||
# Try with Google
|
||||
scrap_result = self.download_data_google(isbn)
|
||||
if not scrap_result:
|
||||
# Try with OpenLibrary
|
||||
if not self.download_data_openlibrary(isbn):
|
||||
self.add_error('isbn',
|
||||
_("This ISBN is not found."))
|
||||
return self.cleaned_data
|
||||
|
||||
if self.cleaned_data['title']:
|
||||
self.cleaned_data['title'] = re.sub(
|
||||
@ -188,59 +310,24 @@ class MediaAdminForm(ModelForm):
|
||||
)
|
||||
|
||||
if self.cleaned_data['authors']:
|
||||
authors = self.cleaned_data['authors']
|
||||
old_authors = authors.copy()
|
||||
side_identifier = generate_side_identifier(
|
||||
self.cleaned_data["title"],
|
||||
self.cleaned_data["authors"],
|
||||
self.cleaned_data["subtitle"],
|
||||
)
|
||||
|
||||
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):
|
||||
# First clean ISBN field
|
||||
isbn_field = self.fields['isbn']
|
||||
isbn = isbn_field.widget.value_from_datadict(
|
||||
self.data, self.files, self.add_prefix('isbn'))
|
||||
isbn = isbn_field.clean(isbn)
|
||||
self.cleaned_data['isbn'] = isbn
|
||||
|
||||
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
|
||||
@ -250,7 +337,6 @@ class MediaAdminForm(ModelForm):
|
||||
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 \
|
||||
@ -264,5 +350,7 @@ class MediaAdminForm(ModelForm):
|
||||
self.add_error(name, e)
|
||||
|
||||
class Meta:
|
||||
model = BD
|
||||
fields = '__all__'
|
||||
model = Comic
|
||||
fields = ('isbn', 'title', 'subtitle', 'external_url',
|
||||
'side_identifier', 'authors', 'number_of_pages',
|
||||
'publish_date', 'present', )
|
||||
|
@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-05-12 17:42+0200\n"
|
||||
"POT-Creation-Date: 2021-11-14 14:25+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,159 +13,281 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: admin.py:32 models.py:29 models.py:62
|
||||
#: admin.py:46 admin.py:102 admin.py:114 models.py:30 models.py:85
|
||||
msgid "authors"
|
||||
msgstr "auteurs"
|
||||
|
||||
#: admin.py:42
|
||||
#: admin.py:56
|
||||
msgid "external url"
|
||||
msgstr "URL externe"
|
||||
|
||||
#: admin.py:98
|
||||
msgid "Turn back"
|
||||
msgstr "Rendre"
|
||||
|
||||
#: admin.py:101 models.py:135
|
||||
msgid "given back to"
|
||||
msgstr "rendu à"
|
||||
|
||||
#: fields.py:17
|
||||
msgid "ISBN-10 or ISBN-13"
|
||||
msgstr "ISBN-10 ou ISBN-13"
|
||||
|
||||
#: forms.py:178
|
||||
#: forms.py:302
|
||||
msgid "This ISBN is not found."
|
||||
msgstr "L'ISBN n'a pas été trouvé."
|
||||
|
||||
#: models.py:16 models.py:159
|
||||
msgid "name"
|
||||
msgstr "nom"
|
||||
#: management/commands/migrate_to_new_format.py:57 models.py:156
|
||||
msgid "CDs"
|
||||
msgstr "CDs"
|
||||
|
||||
#: models.py:21
|
||||
msgid "note"
|
||||
msgstr "note"
|
||||
#: management/commands/migrate_to_new_format.py:57 models.py:155
|
||||
msgid "CD"
|
||||
msgstr "CD"
|
||||
|
||||
#: models.py:28
|
||||
msgid "author"
|
||||
msgstr "auteur"
|
||||
#: management/commands/migrate_to_new_format.py:73 models.py:149
|
||||
msgid "vinyls"
|
||||
msgstr "vinyles"
|
||||
|
||||
#: models.py:35 models.py:89
|
||||
msgid "ISBN"
|
||||
msgstr "ISBN"
|
||||
#: management/commands/migrate_to_new_format.py:73 models.py:148
|
||||
msgid "vinyl"
|
||||
msgstr "vinyle"
|
||||
|
||||
#: 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."
|
||||
#: management/commands/migrate_to_new_format.py:91 models.py:196
|
||||
msgid "reviews"
|
||||
msgstr "revues"
|
||||
|
||||
#: models.py:42
|
||||
msgid "title"
|
||||
msgstr "titre"
|
||||
#: management/commands/migrate_to_new_format.py:91 models.py:195
|
||||
msgid "review"
|
||||
msgstr "revue"
|
||||
|
||||
#: models.py:46
|
||||
msgid "subtitle"
|
||||
msgstr "sous-titre"
|
||||
#: management/commands/migrate_to_new_format.py:111 models.py:315
|
||||
msgid "games"
|
||||
msgstr "jeux"
|
||||
|
||||
#: models.py:52
|
||||
msgid "external URL"
|
||||
msgstr "URL externe"
|
||||
|
||||
#: models.py:57
|
||||
msgid "side identifier"
|
||||
msgstr "côte"
|
||||
|
||||
#: models.py:65
|
||||
msgid "number of pages"
|
||||
msgstr "nombre de pages"
|
||||
|
||||
#: models.py:70
|
||||
msgid "publish date"
|
||||
msgstr "date de publication"
|
||||
|
||||
#: models.py:82
|
||||
msgid "medium"
|
||||
msgstr "medium"
|
||||
|
||||
#: models.py:83
|
||||
msgid "media"
|
||||
msgstr "media"
|
||||
|
||||
#: 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:115
|
||||
msgid "borrowed on"
|
||||
msgstr "emprunté le"
|
||||
|
||||
#: models.py:120
|
||||
msgid "given back on"
|
||||
msgstr "rendu le"
|
||||
|
||||
#: models.py:126
|
||||
msgid "borrowed with"
|
||||
msgstr "emprunté avec"
|
||||
|
||||
#: models.py:127
|
||||
msgid "The keyholder that registered this borrowed item."
|
||||
msgstr "Le permanencier qui enregistre cet emprunt."
|
||||
|
||||
#: models.py:136
|
||||
msgid "The keyholder to whom this item was given back."
|
||||
msgstr "Le permanencier à qui l'emprunt a été rendu."
|
||||
|
||||
#: models.py:143
|
||||
msgid "borrowed item"
|
||||
msgstr "emprunt"
|
||||
|
||||
#: models.py:144
|
||||
msgid "borrowed items"
|
||||
msgstr "emprunts"
|
||||
|
||||
#: models.py:164
|
||||
msgid "owner"
|
||||
msgstr "propriétaire"
|
||||
|
||||
#: models.py:169
|
||||
msgid "duration"
|
||||
msgstr "durée"
|
||||
|
||||
#: models.py:173
|
||||
msgid "minimum number of players"
|
||||
msgstr "nombre minimum de joueurs"
|
||||
|
||||
#: models.py:177
|
||||
msgid "maximum number of players"
|
||||
msgstr "nombre maximum de joueurs"
|
||||
|
||||
#: models.py:183
|
||||
msgid "comment"
|
||||
msgstr "commentaire"
|
||||
|
||||
#: models.py:190
|
||||
#: management/commands/migrate_to_new_format.py:111 models.py:314
|
||||
msgid "game"
|
||||
msgstr "jeu"
|
||||
|
||||
#: models.py:191
|
||||
msgid "games"
|
||||
msgstr "jeux"
|
||||
#: models.py:17
|
||||
msgid "name"
|
||||
msgstr "nom"
|
||||
|
||||
#: models.py:22
|
||||
msgid "note"
|
||||
msgstr "note"
|
||||
|
||||
#: models.py:29
|
||||
msgid "author"
|
||||
msgstr "auteur"
|
||||
|
||||
#: models.py:36 models.py:202
|
||||
msgid "ISBN"
|
||||
msgstr "ISBN"
|
||||
|
||||
#: models.py:37 models.py:203
|
||||
msgid "You may be able to scan it from a bar code."
|
||||
msgstr "Peut souvent être scanné à partir du code barre."
|
||||
|
||||
#: models.py:45
|
||||
msgid "title"
|
||||
msgstr "titre"
|
||||
|
||||
#: models.py:49 models.py:220
|
||||
msgid "present"
|
||||
msgstr "présent"
|
||||
|
||||
#: models.py:50 models.py:221
|
||||
msgid "Tell that the medium is present in the Mediatek."
|
||||
msgstr "Indique que le medium est présent à la Mediatek."
|
||||
|
||||
#: models.py:68
|
||||
msgid "borrowable"
|
||||
msgstr "empruntable"
|
||||
|
||||
#: models.py:69
|
||||
msgid "borrowables"
|
||||
msgstr "empruntables"
|
||||
|
||||
#: models.py:74
|
||||
msgid "external URL"
|
||||
msgstr "URL externe"
|
||||
|
||||
#: models.py:79
|
||||
msgid "side identifier"
|
||||
msgstr "côte"
|
||||
|
||||
#: models.py:89
|
||||
msgid "medium"
|
||||
msgstr "medium"
|
||||
|
||||
#: models.py:90
|
||||
msgid "media"
|
||||
msgstr "media"
|
||||
|
||||
#: models.py:95
|
||||
msgid "subtitle"
|
||||
msgstr "sous-titre"
|
||||
|
||||
#: models.py:101
|
||||
msgid "number of pages"
|
||||
msgstr "nombre de pages"
|
||||
|
||||
#: models.py:107
|
||||
msgid "publish date"
|
||||
msgstr "date de publication"
|
||||
|
||||
#: models.py:113
|
||||
msgid "book"
|
||||
msgstr "livre"
|
||||
|
||||
#: models.py:114
|
||||
msgid "books"
|
||||
msgstr "livres"
|
||||
|
||||
#: models.py:119
|
||||
msgid "comic"
|
||||
msgstr "BD"
|
||||
|
||||
#: models.py:120
|
||||
msgid "comics"
|
||||
msgstr "BDs"
|
||||
|
||||
#: models.py:126
|
||||
msgid "manga"
|
||||
msgstr "manga"
|
||||
|
||||
#: models.py:127
|
||||
msgid "mangas"
|
||||
msgstr "mangas"
|
||||
|
||||
#: models.py:133
|
||||
msgid "novel"
|
||||
msgstr "roman"
|
||||
|
||||
#: models.py:134
|
||||
msgid "novels"
|
||||
msgstr "romans"
|
||||
|
||||
#: models.py:140
|
||||
msgid "rounds per minute"
|
||||
msgstr "tours par minute"
|
||||
|
||||
#: models.py:142
|
||||
msgid "33 RPM"
|
||||
msgstr "33 TPM"
|
||||
|
||||
#: models.py:143
|
||||
msgid "45 RPM"
|
||||
msgstr "45 TPM"
|
||||
|
||||
#: models.py:162
|
||||
msgid "number"
|
||||
msgstr "nombre"
|
||||
|
||||
#: models.py:166
|
||||
msgid "year"
|
||||
msgstr "année"
|
||||
|
||||
#: models.py:173
|
||||
msgid "month"
|
||||
msgstr "mois"
|
||||
|
||||
#: models.py:180
|
||||
msgid "day"
|
||||
msgstr "jour"
|
||||
|
||||
#: models.py:187
|
||||
msgid "double"
|
||||
msgstr "double"
|
||||
|
||||
#: models.py:210
|
||||
msgid "type"
|
||||
msgstr "type"
|
||||
|
||||
#: models.py:212
|
||||
msgid "Comic"
|
||||
msgstr "BD"
|
||||
|
||||
#: models.py:213
|
||||
msgid "Manga"
|
||||
msgstr "Manga"
|
||||
|
||||
#: models.py:214
|
||||
msgid "Roman"
|
||||
msgstr "Roman"
|
||||
|
||||
#: models.py:226
|
||||
msgid "future medium"
|
||||
msgstr "medium à importer"
|
||||
|
||||
#: models.py:227
|
||||
msgid "future media"
|
||||
msgstr "medias à importer"
|
||||
|
||||
#: models.py:237
|
||||
msgid "object"
|
||||
msgstr "objet"
|
||||
|
||||
#: models.py:242
|
||||
msgid "borrower"
|
||||
msgstr "emprunteur"
|
||||
|
||||
#: models.py:245
|
||||
msgid "borrowed on"
|
||||
msgstr "emprunté le"
|
||||
|
||||
#: models.py:250
|
||||
msgid "given back on"
|
||||
msgstr "rendu le"
|
||||
|
||||
#: models.py:256
|
||||
msgid "borrowed with"
|
||||
msgstr "emprunté avec"
|
||||
|
||||
#: models.py:257
|
||||
msgid "The keyholder that registered this borrowed item."
|
||||
msgstr "Le permanencier qui enregistre cet emprunt."
|
||||
|
||||
#: models.py:265
|
||||
msgid "given back to"
|
||||
msgstr "rendu à"
|
||||
|
||||
#: models.py:266
|
||||
msgid "The keyholder to whom this item was given back."
|
||||
msgstr "Le permanencier à qui l'emprunt a été rendu."
|
||||
|
||||
#: models.py:273
|
||||
msgid "borrowed item"
|
||||
msgstr "emprunt"
|
||||
|
||||
#: models.py:274
|
||||
msgid "borrowed items"
|
||||
msgstr "emprunts"
|
||||
|
||||
#: models.py:289
|
||||
msgid "owner"
|
||||
msgstr "propriétaire"
|
||||
|
||||
#: models.py:294
|
||||
msgid "duration"
|
||||
msgstr "durée"
|
||||
|
||||
#: models.py:298
|
||||
msgid "minimum number of players"
|
||||
msgstr "nombre minimum de joueurs"
|
||||
|
||||
#: models.py:302
|
||||
msgid "maximum number of players"
|
||||
msgstr "nombre maximum de joueurs"
|
||||
|
||||
#: models.py:307
|
||||
msgid "comment"
|
||||
msgstr "commentaire"
|
||||
|
||||
#: templates/media/generate_side_identifier.html:3
|
||||
msgid "Generate side identifier"
|
||||
msgstr "Générer la cote"
|
||||
|
||||
#: templates/media/isbn_button.html:3
|
||||
msgid "Fetch data and add another"
|
||||
msgstr "Télécharger les données et ajouter un nouveau medium"
|
||||
|
||||
#: templates/media/isbn_button.html:4
|
||||
#, fuzzy
|
||||
#| msgid "Fetch data"
|
||||
msgid "Fetch only"
|
||||
msgstr "Télécharger uniquement les données"
|
||||
msgstr "Télécharger les données seulement"
|
||||
|
||||
#: validators.py:18
|
||||
msgid "Invalid ISBN: Not a string"
|
||||
@ -179,6 +301,6 @@ msgstr "ISBN invalide : mauvaise longueur"
|
||||
msgid "Invalid ISBN: Only upper case allowed"
|
||||
msgstr "ISBN invalide : seulement les majuscules sont autorisées"
|
||||
|
||||
#: views.py:44
|
||||
#: views.py:25
|
||||
msgid "Welcome to the Mediatek database"
|
||||
msgstr "Bienvenue sur la base de données de la Mediatek"
|
||||
|
99
media/management/commands/export_markdown_site.py
Normal file
99
media/management/commands/export_markdown_site.py
Normal file
@ -0,0 +1,99 @@
|
||||
from django.core.management import BaseCommand
|
||||
from media.models import Comic, CD, Manga, Review, Novel, Vinyl, Game
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Extract the database into a user-friendly website written in Markdown.
|
||||
"""
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--directory', '-d', type=str, default='.',
|
||||
help="Directory where mkdocs is running.")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
directory = options["directory"]
|
||||
|
||||
with open(directory + "/docs/index.md", "w") as f:
|
||||
f.write("# Media de la Mediatek\n\n\n")
|
||||
f.write("Ce site répertorie l'intégralité des media présents "
|
||||
"à la Mediatek de l'ENS Paris-Saclay.\n")
|
||||
|
||||
for model_class, file_name in [(Comic, "bd.md"), (Manga, "mangas.md"),
|
||||
(Novel, "romans.md"),
|
||||
(CD, "cd.md"), (Vinyl, "vinyles.md")]:
|
||||
self.process_model_class(model_class, file_name, f, directory)
|
||||
|
||||
# Traitement différent pour les revues
|
||||
with open(directory + "/docs/revues.md", "w") as f:
|
||||
f.write("# Revues\n\n\n")
|
||||
|
||||
titles = list(set(obj["title"] for obj in
|
||||
Review.objects.values("title").distinct().all()))
|
||||
titles.sort()
|
||||
|
||||
for title in titles:
|
||||
f.write(f"## {title}\n\n\n")
|
||||
|
||||
for medium in Review.objects.filter(title=title)\
|
||||
.order_by("number").all():
|
||||
f.write(f"### Numéro {medium.number}\n\n\n")
|
||||
if medium.double:
|
||||
f.write("Double revue\n\n")
|
||||
if medium.year:
|
||||
f.write(f"Année : {medium.year}\n\n")
|
||||
if medium.month:
|
||||
f.write(f"Mois : {medium.month}\n\n")
|
||||
if medium.day:
|
||||
f.write(f"Jour : {medium.day}\n\n")
|
||||
f.write("\n\n\n")
|
||||
|
||||
# Traitement différent pour les jeux
|
||||
with open(directory + "/docs/jeux.md", "w") as f:
|
||||
f.write("# Jeux\n\n\n")
|
||||
|
||||
for game in Game.objects.order_by("name").all():
|
||||
f.write(f"## {game.name}\n\n\n")
|
||||
f.write(f"Durée : {game.duration}\n\n")
|
||||
f.write(f"Nombre de joueurs : {game.players_min} "
|
||||
f"- {game.players_max}\n\n")
|
||||
if game.owner.username != "Med":
|
||||
f.write(f"Propriétaire : {game.owner.username}\n\n")
|
||||
if game.comment:
|
||||
f.write(f"Commentaire : {game.comment}\n\n")
|
||||
f.write("\n\n\n")
|
||||
|
||||
def process_model_class(self, model_class, file_name, f, directory):
|
||||
with open(directory + "/docs/" + file_name, "w") as f:
|
||||
f.write("# " + str(model_class._meta.verbose_name_plural)
|
||||
.capitalize() + "\n\n\n")
|
||||
|
||||
titles = list(set(obj["title"] for obj in model_class.objects
|
||||
.values("title").distinct().all()))
|
||||
titles.sort()
|
||||
|
||||
for title in titles:
|
||||
f.write(f"## {title}\n\n\n")
|
||||
|
||||
for medium in model_class.objects.filter(title=title) \
|
||||
.order_by("side_identifier").all():
|
||||
if hasattr(medium, "subtitle"):
|
||||
f.write(f"### {medium.subtitle}\n\n\n")
|
||||
if hasattr(medium, "isbn"):
|
||||
f.write(f"ISBN : {medium.isbn}\n\n")
|
||||
f.write(f"Cote : {medium.side_identifier}\n\n")
|
||||
f.write("Auteurs : " + ", ".join(
|
||||
author.name for author in medium.authors.all())
|
||||
+ "\n\n")
|
||||
if hasattr(medium, "number_of_pages"):
|
||||
f.write(f"Nombre de pages : "
|
||||
f"{medium.number_of_pages}\n\n")
|
||||
if hasattr(medium, "rpm"):
|
||||
f.write(f"Tours par minute : "
|
||||
f"{medium.rpm}\n\n")
|
||||
if hasattr(medium, "publish_date"):
|
||||
f.write(f"Publié le : "
|
||||
f"{medium.publish_date}\n\n")
|
||||
if hasattr(medium, "external_url"):
|
||||
f.write(f"Lien : [{medium.external_url}]"
|
||||
f"({medium.external_url})\n\n")
|
||||
f.write("\n\n\n")
|
@ -2,8 +2,7 @@ from argparse import FileType
|
||||
from sys import stdin
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
|
||||
from media.models import Auteur, CD
|
||||
from media.models import Author, CD
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@ -30,7 +29,7 @@ class Command(BaseCommand):
|
||||
title = cd[0]
|
||||
side = cd[1]
|
||||
authors_str = cd[2].split('|')
|
||||
authors = [Auteur.objects.get_or_create(name=author)[0]
|
||||
authors = [Author.objects.get_or_create(name=author)[0]
|
||||
for author in authors_str]
|
||||
cd, created = CD.objects.get_or_create(
|
||||
title=title,
|
||||
|
@ -1,23 +1,22 @@
|
||||
from random import random
|
||||
from time import sleep
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.management import BaseCommand
|
||||
|
||||
from media.forms import MediaAdminForm
|
||||
from media.models import BD, FutureMedia, Manga, Roman
|
||||
from media.models import Comic, FutureMedium, Manga, Novel
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
for future_medium in FutureMedia.objects.all():
|
||||
for future_medium in FutureMedium.objects.all():
|
||||
isbn = future_medium.isbn
|
||||
type_str = future_medium.type
|
||||
if type_str == 'bd':
|
||||
cl = BD
|
||||
cl = Comic
|
||||
elif type_str == 'manga':
|
||||
cl = Manga
|
||||
elif type_str == 'roman':
|
||||
cl = Roman
|
||||
cl = Novel
|
||||
else:
|
||||
self.stderr.write(self.style.WARNING(
|
||||
"Unknown medium type: {type}. Ignoring..."
|
||||
@ -26,22 +25,28 @@ class Command(BaseCommand):
|
||||
|
||||
if cl.objects.filter(isbn=isbn).exists():
|
||||
self.stderr.write(self.style.WARNING(
|
||||
"ISBN {isbn} already exists".format(isbn=isbn)
|
||||
))
|
||||
f"ISBN {isbn} for type {type_str} already exists, "
|
||||
f"remove it"))
|
||||
future_medium.delete()
|
||||
continue
|
||||
|
||||
form = MediaAdminForm(instance=cl(),
|
||||
data={"isbn": isbn, "_isbn": True, })
|
||||
# Don't DDOS any website
|
||||
sleep(5)
|
||||
sleep(5 + (4 * random() - 1))
|
||||
|
||||
try:
|
||||
form.full_clean()
|
||||
if hasattr(form.instance, "subtitle") and \
|
||||
not form.instance.subtitle:
|
||||
form.instance.subtitle = ""
|
||||
form.save()
|
||||
future_medium.delete()
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
"Medium with ISBN {isbn} successfully imported"
|
||||
.format(isbn=isbn)))
|
||||
except (ValidationError, ValueError) as e:
|
||||
except Exception as e:
|
||||
self.stderr.write(self.style.WARNING(
|
||||
"An error occured while importing ISBN {isbn}: {error}"
|
||||
.format(isbn=isbn, error=str(e))))
|
||||
.format(isbn=isbn,
|
||||
error=str(e.__class__) + "(" + str(e) + ")")))
|
||||
|
@ -3,8 +3,7 @@ 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.models import Comic, FutureMedium, Manga, Novel
|
||||
from media.validators import isbn_validator
|
||||
|
||||
|
||||
@ -28,7 +27,7 @@ class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
type_str = options["media_type"]
|
||||
|
||||
media_classes = [BD, Manga, Roman, FutureMedia]
|
||||
media_classes = [Comic, Manga, Novel, FutureMedium]
|
||||
|
||||
file = options["input"]
|
||||
isbns = []
|
||||
@ -71,7 +70,7 @@ class Command(BaseCommand):
|
||||
if isbn_exists:
|
||||
continue
|
||||
|
||||
FutureMedia.objects.create(isbn=isbn, type=type_str)
|
||||
FutureMedium.objects.create(isbn=isbn, type=type_str)
|
||||
self.stdout.write(self.style.SUCCESS("ISBN {isbn} imported"
|
||||
.format(isbn=isbn)))
|
||||
imported += 1
|
||||
|
@ -2,7 +2,7 @@ from argparse import FileType
|
||||
from sys import stdin
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
from media.models import Auteur, BD
|
||||
from media.models import Author, Comic
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@ -28,9 +28,9 @@ class Command(BaseCommand):
|
||||
|
||||
title = revue[0]
|
||||
number = revue[1]
|
||||
authors = [Auteur.objects.get_or_create(name=n)[0]
|
||||
authors = [Author.objects.get_or_create(name=n)[0]
|
||||
for n in revue[2].split('|')]
|
||||
bd = BD.objects.create(
|
||||
bd = Comic.objects.create(
|
||||
title=title,
|
||||
subtitle=number,
|
||||
side_identifier="{:.3} {:.3} {:0>2}"
|
||||
|
@ -1,11 +1,9 @@
|
||||
import re
|
||||
import unicodedata
|
||||
from argparse import FileType
|
||||
from sys import stdin
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
|
||||
from media.models import Auteur, Roman
|
||||
from media.forms import generate_side_identifier
|
||||
from media.models import Novel, Author
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@ -30,27 +28,10 @@ class Command(BaseCommand):
|
||||
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]
|
||||
authors = [Author.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(
|
||||
side_identifier = generate_side_identifier(title, authors)
|
||||
roman = Novel.objects.create(
|
||||
title=title,
|
||||
side_identifier=side_identifier,
|
||||
)
|
||||
|
@ -2,7 +2,7 @@ from argparse import FileType
|
||||
from sys import stdin
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
from media.models import Revue
|
||||
from media.models import Review
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@ -37,7 +37,7 @@ class Command(BaseCommand):
|
||||
year = revue[4]
|
||||
if not year:
|
||||
year = None
|
||||
revue, created = Revue.objects.get_or_create(
|
||||
revue, created = Review.objects.get_or_create(
|
||||
title=title,
|
||||
number=number.replace('*', ''),
|
||||
year=year,
|
||||
|
@ -2,8 +2,7 @@ from argparse import FileType
|
||||
from sys import stdin
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
|
||||
from media.models import Auteur, Vinyle
|
||||
from media.models import Author, Vinyl
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@ -34,14 +33,15 @@ class Command(BaseCommand):
|
||||
continue
|
||||
|
||||
side = vinyle[0]
|
||||
title = vinyle[1]
|
||||
authors_str = vinyle[2].split('|')
|
||||
authors = [Auteur.objects.get_or_create(name=author)[0]
|
||||
title = vinyle[1 if rpm == 33 else 2]
|
||||
authors_str = vinyle[2 if rpm == 33 else 1]\
|
||||
.split('|' if rpm == 33 else ';')
|
||||
authors = [Author.objects.get_or_create(name=author)[0]
|
||||
for author in authors_str]
|
||||
vinyle, created = Vinyle.objects.get_or_create(
|
||||
vinyle, created = Vinyl.objects.get_or_create(
|
||||
title=title,
|
||||
side_identifier=side,
|
||||
rp=rpm,
|
||||
rpm=rpm,
|
||||
)
|
||||
vinyle.authors.set(authors)
|
||||
vinyle.save()
|
||||
|
133
media/management/commands/migrate_to_new_format.py
Normal file
133
media/management/commands/migrate_to_new_format.py
Normal file
@ -0,0 +1,133 @@
|
||||
from django.core.management import BaseCommand
|
||||
from django.db import transaction
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from media.models import CD, Comic, Game, Manga, Novel, Review, Vinyl
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Convert old format into new format
|
||||
"""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--doit', action='store_true',
|
||||
help="Actually do the mogration.")
|
||||
|
||||
@transaction.atomic
|
||||
def handle(self, *args, **options): # noqa: C901
|
||||
self.stderr.write(self.style.WARNING(
|
||||
"Old data structure has been deleted. This script won't work "
|
||||
"anymore (and is now useless)"))
|
||||
|
||||
from media.models import OldCD, OldComic, OldGame, OldManga, OldNovel,\
|
||||
OldReview, OldVinyl
|
||||
|
||||
# Migrate books
|
||||
for old_book_class, book_class in [(OldComic, Comic),
|
||||
(OldManga, Manga),
|
||||
(OldNovel, Novel)]:
|
||||
name = book_class._meta.verbose_name
|
||||
name_plural = book_class._meta.verbose_name_plural
|
||||
for book in tqdm(old_book_class.objects.all(),
|
||||
desc=name_plural, unit=str(name)):
|
||||
try:
|
||||
new_book = book_class.objects.create(
|
||||
isbn=book.isbn,
|
||||
title=book.title,
|
||||
subtitle=book.subtitle,
|
||||
external_url=book.external_url,
|
||||
side_identifier=book.side_identifier,
|
||||
number_of_pages=book.number_of_pages,
|
||||
publish_date=book.publish_date,
|
||||
present=book.present,
|
||||
)
|
||||
new_book.authors.set(book.authors.all())
|
||||
new_book.save()
|
||||
except Exception:
|
||||
self.stderr.write(f"There was an error with {name} "
|
||||
f"{book} ({book.pk})")
|
||||
raise
|
||||
|
||||
self.stdout.write(f"{book_class.objects.count()} {name_plural} "
|
||||
"migrated")
|
||||
|
||||
# Migrate CDs
|
||||
for cd in tqdm(OldCD.objects.all(),
|
||||
desc=_("CDs"), unit=str(_("CD"))):
|
||||
try:
|
||||
new_cd = CD.objects.create(
|
||||
title=cd.title,
|
||||
present=cd.present,
|
||||
)
|
||||
new_cd.authors.set(cd.authors.all())
|
||||
new_cd.save()
|
||||
except Exception:
|
||||
self.stderr.write(f"There was an error with {cd} ({cd.pk})")
|
||||
raise
|
||||
|
||||
self.stdout.write(f"{CD.objects.count()} {_('CDs')} migrated")
|
||||
|
||||
# Migrate vinyls
|
||||
for vinyl in tqdm(OldVinyl.objects.all(),
|
||||
desc=_("vinyls"), unit=str(_("vinyl"))):
|
||||
try:
|
||||
new_vinyl = Vinyl.objects.create(
|
||||
title=vinyl.title,
|
||||
present=vinyl.present,
|
||||
rpm=vinyl.rpm,
|
||||
)
|
||||
new_vinyl.authors.set(vinyl.authors.all())
|
||||
new_vinyl.save()
|
||||
except Exception:
|
||||
self.stderr.write(f"There was an error with {vinyl} "
|
||||
f"({vinyl.pk})")
|
||||
raise
|
||||
|
||||
self.stdout.write(f"{Vinyl.objects.count()} {_('vinyls')} migrated")
|
||||
|
||||
# Migrate reviews
|
||||
for review in tqdm(OldReview.objects.all(),
|
||||
desc=_("reviews"), unit=str(_("review"))):
|
||||
try:
|
||||
Review.objects.create(
|
||||
title=review.title,
|
||||
number=review.number,
|
||||
year=review.year,
|
||||
month=review.month,
|
||||
day=review.day,
|
||||
double=review.double,
|
||||
present=review.present,
|
||||
)
|
||||
except Exception:
|
||||
self.stderr.write(f"There was an error with {review} "
|
||||
f"({review.pk})")
|
||||
raise
|
||||
|
||||
self.stdout.write(f"{Review.objects.count()} {_('reviews')} migrated")
|
||||
|
||||
# Migrate games
|
||||
for game in tqdm(OldGame.objects.all(),
|
||||
desc=_("games"), unit=str(_("game"))):
|
||||
try:
|
||||
Game.objects.create(
|
||||
title=game.title,
|
||||
owner=game.owner,
|
||||
duration=game.duration,
|
||||
players_min=game.players_min,
|
||||
players_max=game.players_max,
|
||||
comment=game.comment,
|
||||
)
|
||||
except Exception:
|
||||
self.stderr.write(f"There was an error with {game} "
|
||||
f"({game.pk})")
|
||||
raise
|
||||
|
||||
self.stdout.write(f"{Game.objects.count()} {_('games')} migrated")
|
||||
|
||||
if not options['doit']:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
"Warning: Data were't saved. Please use --doit option "
|
||||
"to really perform the migration."
|
||||
))
|
||||
exit(1)
|
68
media/management/commands/regenerate_side_identifiers.py
Normal file
68
media/management/commands/regenerate_side_identifiers.py
Normal file
@ -0,0 +1,68 @@
|
||||
from django.core.management import BaseCommand
|
||||
from django.db import transaction
|
||||
from media.forms import generate_side_identifier
|
||||
from media.models import Comic, Manga, Novel
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--type', '-t',
|
||||
type=str,
|
||||
default='bd',
|
||||
choices=['bd', 'manga', 'roman'],
|
||||
help="Type of medium where the "
|
||||
"sides need to be regenerated.")
|
||||
parser.add_argument('--noninteractivemode', '-ni', action="store_true",
|
||||
help="Disable the interaction mode and replace "
|
||||
"existing side identifiers.")
|
||||
parser.add_argument('--no-commit', '-nc', action="store_true",
|
||||
help="Only show modifications, don't commit "
|
||||
"them to database.")
|
||||
|
||||
@transaction.atomic
|
||||
def handle(self, *args, **options):
|
||||
t = options["type"]
|
||||
medium_class = None
|
||||
if t == "bd":
|
||||
medium_class = Comic
|
||||
elif t == "manga":
|
||||
medium_class = Manga
|
||||
elif t == "roman":
|
||||
medium_class = Novel
|
||||
|
||||
interactive_mode = not options["noninteractivemode"]
|
||||
|
||||
replaced = 0
|
||||
|
||||
for obj in medium_class.objects.all():
|
||||
current_side_identifier = obj.side_identifier
|
||||
if not obj.authors.all():
|
||||
self.stdout.write(str(obj))
|
||||
subtitle = obj.subtitle if hasattr(obj, "subtitle") else None
|
||||
generated_side_identifier = generate_side_identifier(
|
||||
obj.title, obj.authors.all(), subtitle)
|
||||
if current_side_identifier != generated_side_identifier:
|
||||
answer = 'y'
|
||||
if interactive_mode:
|
||||
answer = ''
|
||||
while answer != 'y' and answer != 'n':
|
||||
answer = input(f"For medium {obj}, current side: "
|
||||
f"{current_side_identifier}, "
|
||||
f"generated side: "
|
||||
f"{generated_side_identifier}, "
|
||||
f"would you like to replace ? [y/n]")\
|
||||
.lower()[0]
|
||||
if answer == 'y':
|
||||
self.stdout.write(self.style.WARNING(
|
||||
f"Replace side of {obj} from {current_side_identifier}"
|
||||
f" to {generated_side_identifier}..."))
|
||||
obj.side_identifier = generated_side_identifier
|
||||
if not options["no_commit"]:
|
||||
obj.save()
|
||||
replaced += 1
|
||||
|
||||
if replaced:
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f"{replaced} side identifiers were replaced."))
|
||||
else:
|
||||
self.stdout.write(self.style.WARNING("Nothing changed."))
|
@ -1,9 +1,8 @@
|
||||
from time import sleep
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
|
||||
from media.forms import MediaAdminForm
|
||||
from media.models import BD, Manga
|
||||
from media.models import Comic, Manga
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@ -15,7 +14,7 @@ class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
converted = 0
|
||||
|
||||
for media in BD.objects.all():
|
||||
for media in Comic.objects.all():
|
||||
if media.pk < 3400:
|
||||
continue
|
||||
# We sleep 5 seconds to avoid a ban from Bedetheque
|
||||
|
55
media/migrations/0038_auto_20200923_2030.py
Normal file
55
media/migrations/0038_auto_20200923_2030.py
Normal file
@ -0,0 +1,55 @@
|
||||
# Generated by Django 2.2.12 on 2020-09-23 18:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('media', '0037_revue_double'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='bd',
|
||||
name='external_url',
|
||||
field=models.URLField(blank=True, default='', verbose_name='external URL'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='bd',
|
||||
name='subtitle',
|
||||
field=models.CharField(blank=True, default='', max_length=255, verbose_name='subtitle'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='jeu',
|
||||
name='comment',
|
||||
field=models.CharField(blank=True, default='', max_length=255, verbose_name='comment'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='manga',
|
||||
name='external_url',
|
||||
field=models.URLField(blank=True, default='', verbose_name='external URL'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='manga',
|
||||
name='subtitle',
|
||||
field=models.CharField(blank=True, default='', max_length=255, verbose_name='subtitle'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='roman',
|
||||
name='external_url',
|
||||
field=models.URLField(blank=True, default='', verbose_name='external URL'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='roman',
|
||||
name='subtitle',
|
||||
field=models.CharField(blank=True, default='', max_length=255, verbose_name='subtitle'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
48
media/migrations/0039_mark_media_present.py
Normal file
48
media/migrations/0039_mark_media_present.py
Normal file
@ -0,0 +1,48 @@
|
||||
# Generated by Django 2.2.16 on 2020-09-25 12:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('media', '0038_auto_20200923_2030'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='bd',
|
||||
name='present',
|
||||
field=models.BooleanField(default=False, help_text='Tell that the medium is present in the Mediatek.', verbose_name='present'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cd',
|
||||
name='present',
|
||||
field=models.BooleanField(default=False, help_text='Tell that the medium is present in the Mediatek.', verbose_name='present'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='futuremedia',
|
||||
name='present',
|
||||
field=models.BooleanField(default=False, help_text='Tell that the medium is present in the Mediatek.', verbose_name='present'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='manga',
|
||||
name='present',
|
||||
field=models.BooleanField(default=False, help_text='Tell that the medium is present in the Mediatek.', verbose_name='present'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='revue',
|
||||
name='present',
|
||||
field=models.BooleanField(default=False, help_text='Tell that the medium is present in the Mediatek.', verbose_name='present'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='roman',
|
||||
name='present',
|
||||
field=models.BooleanField(default=False, help_text='Tell that the medium is present in the Mediatek.', verbose_name='present'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='vinyle',
|
||||
name='present',
|
||||
field=models.BooleanField(default=False, help_text='Tell that the medium is present in the Mediatek.', verbose_name='present'),
|
||||
),
|
||||
]
|
61
media/migrations/0040_auto_20211023_1830.py
Normal file
61
media/migrations/0040_auto_20211023_1830.py
Normal file
@ -0,0 +1,61 @@
|
||||
# Generated by Django 2.2.17 on 2021-10-23 16:30
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import media.fields
|
||||
import media.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('media', '0039_mark_media_present'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='Auteur',
|
||||
new_name='Author',
|
||||
),
|
||||
migrations.RenameModel(
|
||||
old_name='Roman',
|
||||
new_name='Comic',
|
||||
),
|
||||
migrations.RenameModel(
|
||||
old_name='Jeu',
|
||||
new_name='Game',
|
||||
),
|
||||
migrations.RenameModel(
|
||||
old_name='BD',
|
||||
new_name='Novel',
|
||||
),
|
||||
migrations.RenameModel(
|
||||
old_name='Revue',
|
||||
new_name='Review',
|
||||
),
|
||||
migrations.RenameModel(
|
||||
old_name='Vinyle',
|
||||
new_name='Vinyl',
|
||||
),
|
||||
migrations.RenameModel(
|
||||
old_name='FutureMedia',
|
||||
new_name='FutureMedium',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='comic',
|
||||
options={'ordering': ['title', 'subtitle'], 'verbose_name': 'comic', 'verbose_name_plural': 'comics'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='novel',
|
||||
options={'ordering': ['title', 'subtitle'], 'verbose_name': 'novel', 'verbose_name_plural': 'novels'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='review',
|
||||
options={'ordering': ['title', 'number'], 'verbose_name': 'review', 'verbose_name_plural': 'reviews'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='vinyl',
|
||||
options={'ordering': ['title'], 'verbose_name': 'vinyl', 'verbose_name_plural': 'vinyls'},
|
||||
),
|
||||
]
|
44
media/migrations/0041_auto_20211023_1838.py
Normal file
44
media/migrations/0041_auto_20211023_1838.py
Normal file
@ -0,0 +1,44 @@
|
||||
# Generated by Django 2.2.17 on 2021-10-23 16:38
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('media', '0040_auto_20211023_1830'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='game',
|
||||
old_name='duree',
|
||||
new_name='duration',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='game',
|
||||
old_name='proprietaire',
|
||||
new_name='owner',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='game',
|
||||
old_name='nombre_joueurs_max',
|
||||
new_name='players_max',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='game',
|
||||
old_name='nombre_joueurs_min',
|
||||
new_name='players_min',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='emprunt',
|
||||
name='media',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='media.Comic'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='futuremedium',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('bd', 'Comic'), ('manga', 'Manga'), ('roman', 'Roman')], max_length=8, verbose_name='type'),
|
||||
),
|
||||
]
|
70
media/migrations/0042_auto_20211023_1929.py
Normal file
70
media/migrations/0042_auto_20211023_1929.py
Normal file
@ -0,0 +1,70 @@
|
||||
# Generated by Django 2.2.17 on 2021-10-23 17:29
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('media', '0041_auto_20211023_1838'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='CD',
|
||||
new_name='OldCD',
|
||||
),
|
||||
migrations.RenameModel(
|
||||
old_name='Manga',
|
||||
new_name='OldManga',
|
||||
),
|
||||
# Remove index before renaming the model
|
||||
migrations.AlterField(
|
||||
model_name='game',
|
||||
name='owner',
|
||||
field=models.ForeignKey(db_index=False, on_delete=models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='owner'),
|
||||
),
|
||||
migrations.RenameModel(
|
||||
old_name='Game',
|
||||
new_name='OldGame',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='oldgame',
|
||||
name='owner',
|
||||
field=models.ForeignKey(db_index=True, on_delete=models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='owner'),
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='oldgame',
|
||||
old_name='name',
|
||||
new_name='title',
|
||||
),
|
||||
migrations.RenameModel(
|
||||
old_name='Novel',
|
||||
new_name='OldNovel',
|
||||
),
|
||||
migrations.RenameModel(
|
||||
old_name='Comic',
|
||||
new_name='OldComic',
|
||||
),
|
||||
migrations.RenameModel(
|
||||
old_name='Review',
|
||||
new_name='OldReview',
|
||||
),
|
||||
migrations.RenameModel(
|
||||
old_name='Vinyl',
|
||||
new_name='OldVinyl',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='oldcomic',
|
||||
options={'ordering': ['title', 'subtitle'], 'verbose_name': 'comic', 'verbose_name_plural': 'comics'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='oldmanga',
|
||||
options={'ordering': ['title'], 'verbose_name': 'manga', 'verbose_name_plural': 'mangas'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='oldnovel',
|
||||
options={'ordering': ['title', 'subtitle'], 'verbose_name': 'novel', 'verbose_name_plural': 'novels'},
|
||||
),
|
||||
]
|
166
media/migrations/0043_auto_20211023_2012.py
Normal file
166
media/migrations/0043_auto_20211023_2012.py
Normal file
@ -0,0 +1,166 @@
|
||||
# Generated by Django 2.2.17 on 2021-10-23 18:12
|
||||
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import media.fields
|
||||
import media.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('media', '0042_auto_20211023_1929'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Borrowable',
|
||||
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')),
|
||||
('present', models.BooleanField(default=False, help_text='Tell that the medium is present in the Mediatek.', verbose_name='present')),
|
||||
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_media.borrowable_set+', to='contenttypes.ContentType')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'borrowable',
|
||||
'verbose_name_plural': 'borrowables',
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='oldgame',
|
||||
options={'ordering': ['title'], 'verbose_name': 'game', 'verbose_name_plural': 'games'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='emprunt',
|
||||
name='media',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='media.Borrowable'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Medium',
|
||||
fields=[
|
||||
('borrowable_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Borrowable')),
|
||||
('external_url', models.URLField(blank=True, verbose_name='external URL')),
|
||||
('side_identifier', models.CharField(max_length=255, verbose_name='side identifier')),
|
||||
('authors', models.ManyToManyField(to='media.Author', verbose_name='authors')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'medium',
|
||||
'verbose_name_plural': 'media',
|
||||
},
|
||||
bases=('media.borrowable',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Review',
|
||||
fields=[
|
||||
('borrowable_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Borrowable')),
|
||||
('number', models.PositiveIntegerField(verbose_name='number')),
|
||||
('year', models.PositiveIntegerField(blank=True, default=None, null=True, 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')),
|
||||
('double', models.BooleanField(default=False, verbose_name='double')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'review',
|
||||
'verbose_name_plural': 'reviews',
|
||||
'ordering': ['title', 'number'],
|
||||
},
|
||||
bases=('media.borrowable',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Book',
|
||||
fields=[
|
||||
('medium_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Medium')),
|
||||
('subtitle', models.CharField(blank=True, max_length=255, verbose_name='subtitle')),
|
||||
('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')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'book',
|
||||
'verbose_name_plural': 'books',
|
||||
},
|
||||
bases=('media.medium',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CD',
|
||||
fields=[
|
||||
('medium_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Medium')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'CD',
|
||||
'verbose_name_plural': 'CDs',
|
||||
'ordering': ['title'],
|
||||
},
|
||||
bases=('media.medium',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Vinyl',
|
||||
fields=[
|
||||
('medium_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Medium')),
|
||||
('rpm', models.PositiveIntegerField(choices=[(33, '33 RPM'), (45, '45 RPM')], verbose_name='rounds per minute')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'vinyl',
|
||||
'verbose_name_plural': 'vinyls',
|
||||
'ordering': ['title'],
|
||||
},
|
||||
bases=('media.medium',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Game',
|
||||
fields=[
|
||||
('borrowable_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Borrowable')),
|
||||
('duration', models.CharField(choices=[('-1h', '-1h'), ('1-2h', '1-2h'), ('2-3h', '2-3h'), ('3-4h', '3-4h'), ('4h+', '4h+')], max_length=255, verbose_name='duration')),
|
||||
('players_min', models.IntegerField(validators=[django.core.validators.MinValueValidator(1)], verbose_name='minimum number of players')),
|
||||
('players_max', models.IntegerField(validators=[django.core.validators.MinValueValidator(1)], verbose_name='maximum number of players')),
|
||||
('comment', models.CharField(blank=True, max_length=255, verbose_name='comment')),
|
||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='owner')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'game',
|
||||
'verbose_name_plural': 'games',
|
||||
'ordering': ['title'],
|
||||
},
|
||||
bases=('media.borrowable',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Comic',
|
||||
fields=[
|
||||
('book_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Book')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'comic',
|
||||
'verbose_name_plural': 'comics',
|
||||
'ordering': ['title', 'subtitle'],
|
||||
},
|
||||
bases=('media.book',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Manga',
|
||||
fields=[
|
||||
('book_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Book')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'manga',
|
||||
'verbose_name_plural': 'mangas',
|
||||
'ordering': ['title', 'subtitle'],
|
||||
},
|
||||
bases=('media.book',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Novel',
|
||||
fields=[
|
||||
('book_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Book')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'novel',
|
||||
'verbose_name_plural': 'novels',
|
||||
'ordering': ['title', 'subtitle'],
|
||||
},
|
||||
bases=('media.book',),
|
||||
),
|
||||
]
|
58
media/migrations/0044_auto_20211102_1254.py
Normal file
58
media/migrations/0044_auto_20211102_1254.py
Normal file
@ -0,0 +1,58 @@
|
||||
# Generated by Django 2.2.24 on 2021-11-02 11:54
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('media', '0043_auto_20211023_2012'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='oldcd',
|
||||
name='authors',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='oldcomic',
|
||||
name='authors',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='oldgame',
|
||||
name='owner',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='oldmanga',
|
||||
name='authors',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='oldnovel',
|
||||
name='authors',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='OldReview',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='oldvinyl',
|
||||
name='authors',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='OldCD',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='OldComic',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='OldGame',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='OldManga',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='OldNovel',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='OldVinyl',
|
||||
),
|
||||
]
|
36
media/migrations/0045_auto_20211114_1423.py
Normal file
36
media/migrations/0045_auto_20211114_1423.py
Normal file
@ -0,0 +1,36 @@
|
||||
# Generated by Django 2.2.24 on 2021-11-14 13:23
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('media', '0044_auto_20211102_1254'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Borrow',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('borrow_date', models.DateTimeField(verbose_name='borrowed on')),
|
||||
('given_back', models.DateTimeField(blank=True, null=True, verbose_name='given back on')),
|
||||
('borrowable', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='media.Borrowable', verbose_name='object')),
|
||||
('borrowed_with', models.ForeignKey(help_text='The keyholder that registered this borrowed item.', on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='borrowed with')),
|
||||
('given_back_to', models.ForeignKey(blank=True, help_text='The keyholder to whom this item was given back.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='given back to')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='borrower')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'borrowed item',
|
||||
'verbose_name_plural': 'borrowed items',
|
||||
'ordering': ['-borrow_date'],
|
||||
},
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Emprunt',
|
||||
),
|
||||
]
|
302
media/models.py
302
media/models.py
@ -1,15 +1,16 @@
|
||||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2017-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from polymorphic.models import PolymorphicModel
|
||||
|
||||
from .fields import ISBNField
|
||||
|
||||
|
||||
class Auteur(models.Model):
|
||||
class Author(models.Model):
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
unique=True,
|
||||
@ -30,7 +31,7 @@ class Auteur(models.Model):
|
||||
ordering = ['name']
|
||||
|
||||
|
||||
class BD(models.Model):
|
||||
class Borrowable(PolymorphicModel):
|
||||
isbn = ISBNField(
|
||||
_('ISBN'),
|
||||
help_text=_('You may be able to scan it from a bar code.'),
|
||||
@ -40,21 +41,38 @@ class BD(models.Model):
|
||||
)
|
||||
|
||||
title = models.CharField(
|
||||
verbose_name=_('title'),
|
||||
max_length=255,
|
||||
verbose_name=_("title"),
|
||||
)
|
||||
|
||||
subtitle = models.CharField(
|
||||
verbose_name=_('subtitle'),
|
||||
max_length=255,
|
||||
blank=True,
|
||||
null=True,
|
||||
present = models.BooleanField(
|
||||
verbose_name=_("present"),
|
||||
help_text=_("Tell that the medium is present in the Mediatek."),
|
||||
default=False,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
obj = self
|
||||
if obj.__class__ == Borrowable:
|
||||
# Get true object instance, useful for autocompletion
|
||||
obj = Borrowable.objects.get(pk=obj.pk)
|
||||
|
||||
title = obj.title
|
||||
if hasattr(obj, 'subtitle'):
|
||||
subtitle = obj.subtitle
|
||||
if subtitle:
|
||||
title = f"{title} : {subtitle}"
|
||||
return title
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('borrowable')
|
||||
verbose_name_plural = _('borrowables')
|
||||
|
||||
|
||||
class Medium(Borrowable):
|
||||
external_url = models.URLField(
|
||||
verbose_name=_('external URL'),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
side_identifier = models.CharField(
|
||||
@ -63,10 +81,22 @@ class BD(models.Model):
|
||||
)
|
||||
|
||||
authors = models.ManyToManyField(
|
||||
'Auteur',
|
||||
'Author',
|
||||
verbose_name=_('authors'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("medium")
|
||||
verbose_name_plural = _("media")
|
||||
|
||||
|
||||
class Book(Medium):
|
||||
subtitle = models.CharField(
|
||||
verbose_name=_('subtitle'),
|
||||
max_length=255,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
number_of_pages = models.PositiveIntegerField(
|
||||
verbose_name=_('number of pages'),
|
||||
blank=True,
|
||||
@ -79,145 +109,33 @@ class BD(models.Model):
|
||||
null=True,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
if self.subtitle:
|
||||
return "{} : {}".format(self.title, self.subtitle)
|
||||
else:
|
||||
return self.title
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("BD")
|
||||
verbose_name_plural = _("BDs")
|
||||
verbose_name = _("book")
|
||||
verbose_name_plural = _("books")
|
||||
|
||||
|
||||
class Comic(Book):
|
||||
class Meta:
|
||||
verbose_name = _("comic")
|
||||
verbose_name_plural = _("comics")
|
||||
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 Manga(Book):
|
||||
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,
|
||||
)
|
||||
class Novel(Book):
|
||||
class Meta:
|
||||
verbose_name = _("novel")
|
||||
verbose_name_plural = _("novels")
|
||||
ordering = ['title', 'subtitle']
|
||||
|
||||
side_identifier = models.CharField(
|
||||
verbose_name=_('side identifier'),
|
||||
max_length=255,
|
||||
)
|
||||
|
||||
class Vinyl(Medium):
|
||||
rpm = models.PositiveIntegerField(
|
||||
verbose_name=_('rounds per minute'),
|
||||
choices=[
|
||||
@ -226,51 +144,20 @@ class Vinyle(models.Model):
|
||||
],
|
||||
)
|
||||
|
||||
authors = models.ManyToManyField(
|
||||
'Auteur',
|
||||
verbose_name=_('authors'),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("vinyle")
|
||||
verbose_name_plural = _("vinyles")
|
||||
verbose_name = _("vinyl")
|
||||
verbose_name_plural = _("vinyls")
|
||||
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 CD(Medium):
|
||||
class Meta:
|
||||
verbose_name = _("CD")
|
||||
verbose_name_plural = _("CDs")
|
||||
ordering = ['title']
|
||||
|
||||
|
||||
class Revue(models.Model):
|
||||
title = models.CharField(
|
||||
verbose_name=_('title'),
|
||||
max_length=255,
|
||||
)
|
||||
|
||||
class Review(Borrowable):
|
||||
number = models.PositiveIntegerField(
|
||||
verbose_name=_('number'),
|
||||
)
|
||||
@ -305,12 +192,12 @@ class Revue(models.Model):
|
||||
return self.title + " n°" + str(self.number)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("revue")
|
||||
verbose_name_plural = _("revues")
|
||||
verbose_name = _("review")
|
||||
verbose_name_plural = _("reviews")
|
||||
ordering = ['title', 'number']
|
||||
|
||||
|
||||
class FutureMedia(models.Model):
|
||||
class FutureMedium(models.Model):
|
||||
isbn = ISBNField(
|
||||
_('ISBN'),
|
||||
help_text=_('You may be able to scan it from a bar code.'),
|
||||
@ -322,13 +209,19 @@ class FutureMedia(models.Model):
|
||||
type = models.CharField(
|
||||
_('type'),
|
||||
choices=[
|
||||
('bd', _('BD')),
|
||||
('bd', _('Comic')),
|
||||
('manga', _('Manga')),
|
||||
('roman', _('Roman')),
|
||||
],
|
||||
max_length=8,
|
||||
)
|
||||
|
||||
present = models.BooleanField(
|
||||
verbose_name=_("present"),
|
||||
help_text=_("Tell that the medium is present in the Mediatek."),
|
||||
default=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("future medium")
|
||||
verbose_name_plural = _("future media")
|
||||
@ -337,35 +230,36 @@ class FutureMedia(models.Model):
|
||||
return "Future medium (ISBN: {isbn})".format(isbn=self.isbn, )
|
||||
|
||||
|
||||
class Emprunt(models.Model):
|
||||
media = models.ForeignKey(
|
||||
'BD',
|
||||
class Borrow(models.Model):
|
||||
borrowable = models.ForeignKey(
|
||||
'media.Borrowable',
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name=_('object'),
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
'users.User',
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name=_("borrower"),
|
||||
)
|
||||
date_emprunt = models.DateTimeField(
|
||||
borrow_date = models.DateTimeField(
|
||||
verbose_name=_('borrowed on'),
|
||||
)
|
||||
date_rendu = models.DateTimeField(
|
||||
given_back = models.DateTimeField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('given back on'),
|
||||
)
|
||||
permanencier_emprunt = models.ForeignKey(
|
||||
'users.User',
|
||||
borrowed_with = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='user_permanencier_emprunt',
|
||||
related_name='+',
|
||||
verbose_name=_('borrowed with'),
|
||||
help_text=_('The keyholder that registered this borrowed item.')
|
||||
)
|
||||
permanencier_rendu = models.ForeignKey(
|
||||
'users.User',
|
||||
given_back_to = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='user_permanencier_rendu',
|
||||
related_name='+',
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('given back to'),
|
||||
@ -373,56 +267,50 @@ class Emprunt(models.Model):
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.media) + str(self.user)
|
||||
return str(self.borrowable) + str(self.user)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("borrowed item")
|
||||
verbose_name_plural = _("borrowed items")
|
||||
ordering = ['-date_emprunt']
|
||||
ordering = ['-borrow_date']
|
||||
|
||||
|
||||
class Jeu(models.Model):
|
||||
DUREE = (
|
||||
class Game(Borrowable):
|
||||
DURATIONS = (
|
||||
('-1h', '-1h'),
|
||||
('1-2h', '1-2h'),
|
||||
('2-3h', '2-3h'),
|
||||
('3-4h', '3-4h'),
|
||||
('4h+', '4h+'),
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("name"),
|
||||
)
|
||||
proprietaire = models.ForeignKey(
|
||||
owner = models.ForeignKey(
|
||||
'users.User',
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name=_("owner"),
|
||||
)
|
||||
duree = models.CharField(
|
||||
choices=DUREE,
|
||||
duration = models.CharField(
|
||||
choices=DURATIONS,
|
||||
max_length=255,
|
||||
verbose_name=_("duration"),
|
||||
)
|
||||
nombre_joueurs_min = models.IntegerField(
|
||||
players_min = models.IntegerField(
|
||||
validators=[MinValueValidator(1)],
|
||||
verbose_name=_("minimum number of players"),
|
||||
)
|
||||
nombre_joueurs_max = models.IntegerField(
|
||||
players_max = models.IntegerField(
|
||||
validators=[MinValueValidator(1)],
|
||||
verbose_name=_('maximum number of players'),
|
||||
)
|
||||
comment = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('comment'),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
return str(self.title)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("game")
|
||||
verbose_name_plural = _("games")
|
||||
ordering = ['name']
|
||||
ordering = ['title']
|
||||
|
@ -4,8 +4,7 @@
|
||||
import re
|
||||
|
||||
import requests
|
||||
|
||||
from media.models import Auteur
|
||||
from media.models import Author
|
||||
|
||||
|
||||
class BedetequeScraper:
|
||||
@ -24,7 +23,7 @@ class BedetequeScraper:
|
||||
:return: CSRF token
|
||||
"""
|
||||
response = self.session.get(self.referer).content.decode()
|
||||
regex = r'csrf_token_bedetheque\"\s*value=\"(\w*)\"'
|
||||
regex = r'csrf_token_bel\"\s*value=\"(\w*)\"'
|
||||
return re.search(regex, response).group(1)
|
||||
|
||||
def search_by_isbn(self, isbn: str) -> [str]:
|
||||
@ -45,7 +44,7 @@ class BedetequeScraper:
|
||||
regex = r'href=\"(https://www\.bedetheque\.com/BD.*.html)\"'
|
||||
return re.findall(regex, content)
|
||||
|
||||
def scrap_bd_info(self, bd_url: str) -> dict:
|
||||
def scrap_comic_info(self, bd_url: str) -> dict:
|
||||
"""
|
||||
Load BD web page and scrap data
|
||||
:param bd_url: URL where to find BD data
|
||||
@ -100,12 +99,12 @@ class BedetequeScraper:
|
||||
if 'author' not in data:
|
||||
data['authors'] = list()
|
||||
if author:
|
||||
author_obj = Auteur.objects.get_or_create(
|
||||
author_obj = Author.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(
|
||||
author_obj = Author.objects.get_or_create(
|
||||
name=illustrator.group(1))[0]
|
||||
data['authors'].append(author_obj)
|
||||
|
||||
|
@ -1,31 +1,64 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import Auteur, BD, Emprunt, Jeu
|
||||
from .models import Author, Borrow, CD, Comic, FutureMedium, Manga, Game, \
|
||||
Novel, Review, Vinyl
|
||||
|
||||
|
||||
class AuteurSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class AuthorSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Auteur
|
||||
model = Author
|
||||
fields = ['url', 'name']
|
||||
|
||||
|
||||
class MediaSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class ComicSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = BD
|
||||
fields = ['url', 'isbn', 'title', 'subtitle', 'external_url',
|
||||
'side_identifier', 'authors', 'number_of_pages',
|
||||
'publish_date']
|
||||
model = Comic
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class EmpruntSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class MangaSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Emprunt
|
||||
fields = ['url', 'media', 'user', 'date_emprunt', 'date_rendu',
|
||||
'permanencier_emprunt', 'permanencier_rendu']
|
||||
model = Manga
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class JeuSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class CDSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Jeu
|
||||
fields = ['url', 'name', 'proprietaire', 'duree', 'nombre_joueurs_min',
|
||||
'nombre_joueurs_max', 'comment']
|
||||
model = CD
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class VinylSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Vinyl
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class NovelSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Novel
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class ReviewSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Review
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class FutureMediumSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = FutureMedium
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class BorrowSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Borrow
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class GameSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Game
|
||||
fields = '__all__'
|
||||
|
149
media/templates/media/find_medium.html
Normal file
149
media/templates/media/find_medium.html
Normal file
@ -0,0 +1,149 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<form id="form" action="#" onsubmit="searchISBN()">
|
||||
<label for="isbn" id="id_isbn_label">ISBN :</label>
|
||||
<input type="text" id="isbn" autofocus>
|
||||
<input type="hidden" id="old-isbn">
|
||||
<input type="submit" id="isbn_search">
|
||||
<input type="checkbox" id="mark_as_present" checked onchange="document.getElementById('isbn').focus()" />
|
||||
<label for="mark_as_present">Marquer automatiquement comme présent si trouvé et que je cherche par ISBN</label>
|
||||
</form>
|
||||
<ul id="result"></ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script>
|
||||
function markAsPresent(type, id, present=true) {
|
||||
let request = new XMLHttpRequest();
|
||||
request.open("GET", "/media/mark-as-present/" + type + "/" + id + "/" + (present ? "" : "?absent=1"), true);
|
||||
request.onload = function() {
|
||||
document.getElementById("isbn").value = document.getElementById("old-isbn").value;
|
||||
searchISBN();
|
||||
};
|
||||
request.send();
|
||||
}
|
||||
|
||||
function searchISBN() {
|
||||
let isbn = document.getElementById("isbn").value;
|
||||
let result_div = document.getElementById("result");
|
||||
let markAsPresent = document.getElementById("mark_as_present").checked;
|
||||
|
||||
result_div.innerHTML = "<li id='recap-isbn'>Recherche : " + isbn + "</li>";
|
||||
|
||||
document.getElementById("isbn").value = "";
|
||||
document.getElementById("old-isbn").value = isbn;
|
||||
document.getElementById("isbn").focus();
|
||||
|
||||
let bd_request = new XMLHttpRequest();
|
||||
bd_request.open('GET', '/api/media/comic/?search=' + isbn, true);
|
||||
bd_request.onload = function () {
|
||||
let data = JSON.parse(this.response);
|
||||
data.results.forEach(comic => {
|
||||
let present = comic.present;
|
||||
if (markAsPresent && isbn === comic.isbn) {
|
||||
present = true;
|
||||
let presentRequest = new XMLHttpRequest();
|
||||
presentRequest.open("GET", "/media/mark-as-present/bd/" + comic.id + "/", true);
|
||||
presentRequest.send();
|
||||
}
|
||||
result_div.innerHTML += "<li id='comic_" + comic.id + "'>" +
|
||||
"<a href='/database/media/comic/" + comic.id + "/change/'>BD : "
|
||||
+ comic.title + (comic.subtitle ? " - " + comic.subtitle : "") + "</a>"
|
||||
+ (present ? " (<a class='absent' href='#' onclick=\"markAsPresent('bd', " + comic.id + ", false)\">marquer comme absent</a>)"
|
||||
: " (absent, <a class='present' href='#' onclick=\"markAsPresent('bd', " + comic.id + ")\">marquer comme présent</a>)") + "</li>";
|
||||
});
|
||||
}
|
||||
bd_request.send();
|
||||
|
||||
let manga_request = new XMLHttpRequest();
|
||||
manga_request.open('GET', '/api/media/manga/?search=' + isbn, true);
|
||||
manga_request.onload = function () {
|
||||
let data = JSON.parse(this.response);
|
||||
data.results.forEach(manga => {
|
||||
let present = manga.present;
|
||||
if (markAsPresent && isbn === manga.isbn) {
|
||||
present = true;
|
||||
let presentRequest = new XMLHttpRequest();
|
||||
presentRequest.open("GET", "/media/mark-as-present/manga/" + manga.id + "/", true);
|
||||
presentRequest.send();
|
||||
}
|
||||
result_div.innerHTML += "<li id='manga_" + manga.id + "'>" +
|
||||
"<a href='/database/media/manga/" + manga.id + "/change/'>Manga : "
|
||||
+ manga.title + (manga.subtitle ? " - " + manga.subtitle : "") + "</a>"
|
||||
+ (present ? " (<a class='absent' href='#' onclick=\"markAsPresent('manga', " + manga.id + ", false)\">marquer comme absent</a>)"
|
||||
: " (absent, <a class='present' href='#' onclick=\"markAsPresent('manga', " + manga.id + ")\">marquer comme présent</a>)") + "</li>";
|
||||
});
|
||||
}
|
||||
manga_request.send();
|
||||
|
||||
let cd_request = new XMLHttpRequest();
|
||||
cd_request.open('GET', '/api/media/cd/?search=' + isbn, true);
|
||||
cd_request.onload = function () {
|
||||
let data = JSON.parse(this.response);
|
||||
data.results.forEach(cd => {
|
||||
let present = cd.present;
|
||||
result_div.innerHTML += "<li id='cd_" + cd.id + "'>" +
|
||||
"<a href='/database/media/cd/" + cd.id + "/change/'>CD : " + cd.title + "</a>"
|
||||
+ (present ? " (<a class='absent' href='#' onclick=\"markAsPresent('cd', " + cd.id + ", false)\">marquer comme absent</a>)"
|
||||
: " (absent, <a class='present' href='#' onclick=\"markAsPresent('cd', " + cd.id + ")\">marquer comme présent</a>)") + "</li>";
|
||||
});
|
||||
}
|
||||
cd_request.send();
|
||||
|
||||
let vinyle_request = new XMLHttpRequest();
|
||||
vinyle_request.open('GET', '/api/media/vinyl/?search=' + isbn, true);
|
||||
vinyle_request.onload = function () {
|
||||
let data = JSON.parse(this.response);
|
||||
data.results.forEach(vinyl => {
|
||||
let present = markAsPresent || vinyl.present;
|
||||
result_div.innerHTML += "<li id='vinyl_" + vinyl.id + "'>" +
|
||||
"<a href='/database/media/vinyl/" + vinyl.id + "/change/'>Vinyle : " + vinyl.title + "</a>"
|
||||
+ (present ? " (<a class='absent' href='#' onclick=\"markAsPresent('vinyl', " + vinyl.id + ", false)\">marquer comme absent</a>)"
|
||||
: " (absent, <a class='present' href='#' onclick=\"markAsPresent('vinyl', " + vinyl.id + ")\">marquer comme présent</a>)") + "</li>";
|
||||
});
|
||||
}
|
||||
vinyle_request.send();
|
||||
|
||||
let roman_request = new XMLHttpRequest();
|
||||
roman_request.open('GET', '/api/media/novel/?search=' + isbn, true);
|
||||
roman_request.onload = function () {
|
||||
let data = JSON.parse(this.response);
|
||||
data.results.forEach(novel => {
|
||||
let present = novel.present;
|
||||
if (markAsPresent && isbn === novel.isbn) {
|
||||
present = true;
|
||||
let presentRequest = new XMLHttpRequest();
|
||||
presentRequest.open("GET", "/media/mark-as-present/novel/" + novel.id + "/", true);
|
||||
presentRequest.send();
|
||||
}
|
||||
result_div.innerHTML += "<li id='roman_" + novel.id + "'>" +
|
||||
"<a href='/database/media/roman/" + novel.id + "/change/'>Roman : " + novel.title + "</a>"
|
||||
+ (present ? " (<a class='absent' href='#' onclick=\"markAsPresent('novel', " + novel.id + ", false)\">marquer comme absent</a>)"
|
||||
: " (absent, <a class='present' href='#' onclick=\"markAsPresent('novel', " + novel.id + ")\">marquer comme présent</a>)") + "</li>";
|
||||
});
|
||||
}
|
||||
roman_request.send();
|
||||
|
||||
let future_request = new XMLHttpRequest();
|
||||
future_request.open('GET', '/api/media/future/?search=' + isbn, true);
|
||||
future_request.onload = function () {
|
||||
let data = JSON.parse(this.response);
|
||||
data.results.forEach(future => {
|
||||
let present = future.present;
|
||||
if (markAsPresent && isbn === future.isbn) {
|
||||
present = true;
|
||||
let presentRequest = new XMLHttpRequest();
|
||||
presentRequest.open("GET", "/media/mark-as-present/future/" + bd.id + "/", true);
|
||||
presentRequest.send();
|
||||
}
|
||||
result_div.innerHTML += "<li id='future_" + future.id + "'>" +
|
||||
"<a href='/database/media/future/" + future.id + "/change/'>Medium non traité : " + future.title + "</a>"
|
||||
+ (present ? " (<a class='absent' href='#' onclick=\"markAsPresent('future', " + future.id + ", false)\">marquer comme absent</a>)"
|
||||
: " (absent, <a class='present' href='#' onclick=\"markAsPresent('future', " + future.id + ")\">marquer comme présent</a>)") + "</li>";
|
||||
});
|
||||
}
|
||||
future_request.send();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
3
media/templates/media/generate_side_identifier.html
Normal file
3
media/templates/media/generate_side_identifier.html
Normal file
@ -0,0 +1,3 @@
|
||||
{% load i18n %}
|
||||
{% include "django/forms/widgets/input.html" %}
|
||||
<a href="#" class="button" onclick="document.getElementById('{{ widget.attrs.id }}').value = document.getElementById('{{ widget.attrs.id }}').getAttribute('data-generated-side-identifier')">{% trans "Generate side identifier" %}</a>
|
@ -1,4 +1,4 @@
|
||||
{% load i18n %}
|
||||
{% include "django/forms/widgets/input.html" %}
|
||||
<input type="submit" value="{% trans "Fetch data and add another" %}" name="_isbn_addanother">
|
||||
<input type="submit" value="{% trans "Fetch only" %}" name="_isbn">
|
||||
<input type="submit" value="{% trans "Fetch data and add another" %}" name="_isbn_addanother" onclick="form.submit()">
|
||||
<input type="submit" value="{% trans "Fetch only" %}" name="_isbn" onclick="form.submit()">
|
||||
|
@ -3,8 +3,7 @@
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from media.models import Auteur, BD
|
||||
from media.models import Author, Comic
|
||||
from users.models import User
|
||||
|
||||
"""
|
||||
@ -22,44 +21,44 @@ class TemplateTests(TestCase):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
# Create an author
|
||||
self.dummy_author = Auteur.objects.create(name="Test author")
|
||||
self.dummy_author = Author.objects.create(name="Test author")
|
||||
|
||||
# Create media
|
||||
self.dummy_bd1 = BD.objects.create(
|
||||
self.dummy_bd1 = Comic.objects.create(
|
||||
title="Test media",
|
||||
side_identifier="T M",
|
||||
)
|
||||
self.dummy_bd1.authors.add(self.dummy_author)
|
||||
self.dummy_bd2 = BD.objects.create(
|
||||
self.dummy_bd2 = Comic.objects.create(
|
||||
title="Test media bis",
|
||||
side_identifier="T M 2",
|
||||
external_url="https://example.com/",
|
||||
)
|
||||
self.dummy_bd2.authors.add(self.dummy_author)
|
||||
|
||||
def test_bd_bd_changelist(self):
|
||||
response = self.client.get(reverse('admin:media_bd_changelist'))
|
||||
def test_comic_bd_changelist(self):
|
||||
response = self.client.get(reverse('admin:media_comic_changelist'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_bd_bd_add(self):
|
||||
response = self.client.get(reverse('admin:media_bd_add'))
|
||||
def test_comic_bd_add(self):
|
||||
response = self.client.get(reverse('admin:media_comic_add'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_bd_isbn_download(self):
|
||||
def test_comic_isbn_download(self):
|
||||
data = {
|
||||
'_isbn': True,
|
||||
'isbn': "0316358525",
|
||||
}
|
||||
response = self.client.post(reverse(
|
||||
'admin:media_bd_change',
|
||||
'admin:media_comic_change',
|
||||
args=[self.dummy_bd1.id],
|
||||
), data=data)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_bd_emprunt_changelist(self):
|
||||
response = self.client.get(reverse('admin:media_emprunt_changelist'))
|
||||
def test_comic_borrow_changelist(self):
|
||||
response = self.client.get(reverse('admin:media_borrow_changelist'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_bd_emprunt_add(self):
|
||||
response = self.client.get(reverse('admin:media_emprunt_add'))
|
||||
def test_comic_borrow_add(self):
|
||||
response = self.client.get(reverse('admin:media_borrow_add'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
@ -2,12 +2,32 @@
|
||||
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'media'
|
||||
urlpatterns = [
|
||||
url(r'^retour_emprunt/(?P<empruntid>[0-9]+)$', views.retour_emprunt,
|
||||
name='retour-emprunt'),
|
||||
path('find/', views.FindMediumView.as_view(), name="find"),
|
||||
path('mark-as-present/comic/<int:pk>/',
|
||||
views.MarkComicAsPresent.as_view(),
|
||||
name="mark_comic_as_present"),
|
||||
path('mark-as-present/manga/<int:pk>/',
|
||||
views.MarkMangaAsPresent.as_view(),
|
||||
name="mark_manga_as_present"),
|
||||
path('mark-as-present/cd/<int:pk>/',
|
||||
views.MarkCDAsPresent.as_view(),
|
||||
name="mark_cd_as_present"),
|
||||
path('mark-as-present/vinyl/<int:pk>/',
|
||||
views.MarkVinylAsPresent.as_view(),
|
||||
name="mark_vinyle_as_present"),
|
||||
path('mark-as-present/novel/<int:pk>/',
|
||||
views.MarkNovelAsPresent.as_view(),
|
||||
name="mark_novel_as_present"),
|
||||
path('mark-as-present/review/<int:pk>/',
|
||||
views.MarkReviewAsPresent.as_view(),
|
||||
name="mark_review_as_present"),
|
||||
path('mark-as-present/future/<int:pk>/',
|
||||
views.MarkFutureAsPresent.as_view(),
|
||||
name="mark_future_as_present"),
|
||||
]
|
||||
|
188
media/views.py
188
media/views.py
@ -2,76 +2,174 @@
|
||||
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.db import transaction
|
||||
from django.shortcuts import redirect, render
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpResponse
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import TemplateView, DetailView
|
||||
from rest_framework import viewsets
|
||||
from reversion import revisions as reversion
|
||||
from rest_framework.filters import SearchFilter
|
||||
|
||||
from .models import Auteur, BD, Emprunt, Jeu
|
||||
from .serializers import AuteurSerializer, EmpruntSerializer, \
|
||||
JeuSerializer, MediaSerializer
|
||||
from .models import Author, Borrow, CD, Comic, FutureMedium, Game, Manga,\
|
||||
Novel, Review, Vinyl
|
||||
from .serializers import AuthorSerializer, BorrowSerializer, ComicSerializer, \
|
||||
CDSerializer, FutureMediumSerializer, GameSerializer, MangaSerializer, \
|
||||
NovelSerializer, ReviewSerializer, VinylSerializer
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('media.change_emprunt')
|
||||
def retour_emprunt(request, empruntid):
|
||||
try:
|
||||
emprunt_instance = Emprunt.objects.get(pk=empruntid)
|
||||
except Emprunt.DoesNotExist:
|
||||
messages.error(request, u"Entrée inexistante")
|
||||
return redirect("admin:media_emprunt_changelist")
|
||||
with transaction.atomic(), reversion.create_revision():
|
||||
emprunt_instance.permanencier_rendu = request.user
|
||||
emprunt_instance.date_rendu = timezone.now()
|
||||
emprunt_instance.save()
|
||||
reversion.set_user(request.user)
|
||||
messages.success(request, "Retour enregistré")
|
||||
return redirect("admin:media_emprunt_changelist")
|
||||
|
||||
|
||||
def index(request):
|
||||
class IndexView(TemplateView):
|
||||
"""
|
||||
Home page which redirect to admin when logged in
|
||||
"""
|
||||
if request.user.is_authenticated:
|
||||
return redirect('admin:index')
|
||||
else:
|
||||
return render(request, 'admin/index.html', {
|
||||
'title': _('Welcome to the Mediatek database'),
|
||||
})
|
||||
extra_context = {'title': _('Welcome to the Mediatek database')}
|
||||
template_name = 'admin/index.html'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if request.user.is_authenticated:
|
||||
return redirect('admin:index')
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class AuteurViewSet(viewsets.ModelViewSet):
|
||||
class FindMediumView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "media/find_medium.html"
|
||||
|
||||
|
||||
class MarkMediumAsPresent(LoginRequiredMixin, DetailView):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
object = self.get_object()
|
||||
object.present = not request.GET.get("absent", False)
|
||||
object.save()
|
||||
return HttpResponse("", content_type=204)
|
||||
|
||||
|
||||
class MarkComicAsPresent(MarkMediumAsPresent):
|
||||
model = Comic
|
||||
|
||||
|
||||
class MarkMangaAsPresent(MarkMediumAsPresent):
|
||||
model = Manga
|
||||
|
||||
|
||||
class MarkCDAsPresent(MarkMediumAsPresent):
|
||||
model = CD
|
||||
|
||||
|
||||
class MarkVinylAsPresent(MarkMediumAsPresent):
|
||||
model = Vinyl
|
||||
|
||||
|
||||
class MarkNovelAsPresent(MarkMediumAsPresent):
|
||||
model = Novel
|
||||
|
||||
|
||||
class MarkReviewAsPresent(MarkMediumAsPresent):
|
||||
model = Review
|
||||
|
||||
|
||||
class MarkFutureAsPresent(MarkMediumAsPresent):
|
||||
model = FutureMedium
|
||||
|
||||
|
||||
class AuthorViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows authors to be viewed or edited.
|
||||
"""
|
||||
queryset = Auteur.objects.all()
|
||||
serializer_class = AuteurSerializer
|
||||
queryset = Author.objects.all()
|
||||
serializer_class = AuthorSerializer
|
||||
|
||||
|
||||
class MediaViewSet(viewsets.ModelViewSet):
|
||||
class ComicViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows media to be viewed or edited.
|
||||
"""
|
||||
queryset = BD.objects.all()
|
||||
serializer_class = MediaSerializer
|
||||
queryset = Comic.objects.all()
|
||||
serializer_class = ComicSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ["isbn", "side_identifier"]
|
||||
search_fields = ["=isbn", "title", "subtitle", "side_identifier",
|
||||
"authors__name"]
|
||||
|
||||
|
||||
class EmpruntViewSet(viewsets.ModelViewSet):
|
||||
class MangaViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows media to be viewed or edited.
|
||||
"""
|
||||
queryset = Manga.objects.all()
|
||||
serializer_class = MangaSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ["isbn", "side_identifier"]
|
||||
search_fields = ["=isbn", "title", "subtitle", "side_identifier",
|
||||
"authors__name"]
|
||||
|
||||
|
||||
class CDViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows media to be viewed or edited.
|
||||
"""
|
||||
queryset = CD.objects.all()
|
||||
serializer_class = CDSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ["side_identifier"]
|
||||
search_fields = ["title", "side_identifier", "authors__name"]
|
||||
|
||||
|
||||
class VinylViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows media to be viewed or edited.
|
||||
"""
|
||||
queryset = Vinyl.objects.all()
|
||||
serializer_class = VinylSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ["side_identifier", "rpm"]
|
||||
search_fields = ["title", "side_identifier", "authors__name"]
|
||||
|
||||
|
||||
class NovelViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows media to be viewed or edited.
|
||||
"""
|
||||
queryset = Novel.objects.all()
|
||||
serializer_class = NovelSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ["isbn", "side_identifier", "number_of_pages"]
|
||||
search_fields = ["=isbn", "title", "subtitle", "side_identifier",
|
||||
"authors__name"]
|
||||
|
||||
|
||||
class ReviewViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows media to be viewed or edited.
|
||||
"""
|
||||
queryset = Review.objects.all()
|
||||
serializer_class = ReviewSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ["number", "year", "month", "day", "double"]
|
||||
search_fields = ["title"]
|
||||
|
||||
|
||||
class FutureMediumViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows media to be viewed or edited.
|
||||
"""
|
||||
queryset = FutureMedium.objects.all()
|
||||
serializer_class = FutureMediumSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ["isbn"]
|
||||
search_fields = ["=isbn"]
|
||||
|
||||
|
||||
class BorrowViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows borrowed items to be viewed or edited.
|
||||
"""
|
||||
queryset = Emprunt.objects.all()
|
||||
serializer_class = EmpruntSerializer
|
||||
queryset = Borrow.objects.all()
|
||||
serializer_class = BorrowSerializer
|
||||
|
||||
|
||||
class JeuViewSet(viewsets.ModelViewSet):
|
||||
class GameViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows games to be viewed or edited.
|
||||
"""
|
||||
queryset = Jeu.objects.all()
|
||||
serializer_class = JeuSerializer
|
||||
queryset = Game.objects.all()
|
||||
serializer_class = GameSerializer
|
||||
|
@ -1,14 +1,9 @@
|
||||
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-binary
|
||||
uwsgi==2.0.18
|
||||
authlib~=0.15
|
||||
docutils~=0.16 # for Django-admin docs
|
||||
Django~=2.2
|
||||
django-filter~=2.4
|
||||
django-polymorphic~=3.0
|
||||
django-reversion~=3.0
|
||||
djangorestframework~=3.12
|
||||
django_extensions~=3.0
|
||||
requests~=2.25 # for scrapping
|
||||
|
@ -30,6 +30,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<span class="dropdown">
|
||||
<a href="{% url 'admin:index' %}">{% trans 'Explore database' %}</a>
|
||||
<span class="dropdown-content">
|
||||
<a href="{% url "media:find" %}">Recherche ...</a>
|
||||
{% for app in available_apps %}
|
||||
{% for model in app.models %}
|
||||
{% if model.admin_url %}
|
||||
@ -53,7 +54,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% if user.is_authenticated %}
|
||||
<a href="{% url 'logout' %}">{% trans 'Log out' %}</a>
|
||||
{% else %}
|
||||
<a href="{% url 'login' %}">{% trans 'Log in' %}</a>
|
||||
<a href="{% url 'users:login' %}">{% trans 'Log in' %}</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
@ -95,8 +96,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
</noscript>
|
||||
</form>
|
||||
<p>
|
||||
Mediatek 2017-2020 —
|
||||
<a href="mailto:club-med@crans.org">Nous contactez</a> —
|
||||
Mediatek 2017-2021 —
|
||||
<a href="mailto:club-med@crans.org">Nous contacter</a> —
|
||||
<a href="{% url "api-root" %}">Explorer l'API</a>
|
||||
</p>
|
||||
</div>
|
||||
@ -115,4 +116,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{% block extrajavascript %}{% endblock %}
|
||||
{% endblock %}
|
||||
|
@ -56,9 +56,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
<h3>
|
||||
{% trans 'My profile' %}
|
||||
<small><a class="changelink" href="{% url 'users:edit-info' %}">
|
||||
{% trans 'Edit' %}
|
||||
</a></small>
|
||||
</h3>
|
||||
<ul>
|
||||
<li><strong>{% trans 'username' %}</strong> : {{ user.username }}</li>
|
||||
@ -67,10 +64,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<li><strong>{% trans 'date joined' %}</strong> : {{ user.date_joined }}</li>
|
||||
<li><strong>{% trans 'last login' %}</strong> : {{ user.last_login }}</li>
|
||||
<li><strong>{% trans 'address' %}</strong> : {{ user.address }}</li>
|
||||
<li><strong>{% trans 'phone number' %}</strong> : {{ user.telephone }}</li>
|
||||
<li><strong>{% trans 'phone number' %}</strong> : {{ user.phone_number }}</li>
|
||||
<li><strong>{% trans 'groups' %}</strong> : {% for g in user.groups.all %}{{ g.name }} {% endfor %}
|
||||
</li>
|
||||
<li><strong>{% trans 'maximum borrowed' %}</strong> : {{ user.maxemprunt }}</li>
|
||||
<li>
|
||||
<strong>{% trans 'membership for current year' %}</strong> :
|
||||
{% if user.is_member %}
|
||||
@ -84,8 +80,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<h3>{% trans 'Current borrowed items' %}</h3>
|
||||
{% if borrowed_items %}
|
||||
<ul>
|
||||
{% for emprunt in borrowed_items %}
|
||||
<li>{{ emprunt.media }} ({% trans 'since' %} {{ emprunt.date_emprunt }})</li>
|
||||
{% for borrow in borrowed_items %}
|
||||
<li>{{ borrow.object }} ({% trans 'since' %} {{ borrow.borrow_date }})</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
|
20
tox.ini
20
tox.ini
@ -1,38 +1,30 @@
|
||||
[tox]
|
||||
envlist = py35,py36,py37,py38,linters
|
||||
envlist = py37,py38,py39,linters
|
||||
skipsdist = True
|
||||
|
||||
[testenv]
|
||||
basepython = python3
|
||||
sitepackages = True
|
||||
deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
coverage
|
||||
commands =
|
||||
./manage.py makemigrations
|
||||
coverage run ./manage.py test {posargs}
|
||||
coverage run --omit='*migrations*' ./manage.py test {posargs}
|
||||
coverage report -m
|
||||
|
||||
[testenv:pre-commit]
|
||||
deps = pre-commit
|
||||
commands =
|
||||
pre-commit run --all-files --show-diff-on-failure
|
||||
|
||||
[testenv:linters]
|
||||
deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
flake8
|
||||
flake8-colors
|
||||
flake8-django
|
||||
flake8-import-order
|
||||
flake8-typing-imports
|
||||
pep8-naming
|
||||
pyflakes
|
||||
pylint
|
||||
commands =
|
||||
flake8 logs media users
|
||||
pylint .
|
||||
|
||||
[flake8]
|
||||
ignore = D203, W503, E203, I100, I201, I202, C901
|
||||
ignore = W503, I100, I101
|
||||
exclude =
|
||||
.tox,
|
||||
.git,
|
||||
@ -44,7 +36,7 @@ exclude =
|
||||
.cache,
|
||||
.eggs,
|
||||
*migrations*
|
||||
max-complexity = 10
|
||||
max-complexity = 15
|
||||
import-order-style = google
|
||||
application-import-names = flake8
|
||||
format = ${cyan}%(path)s${reset}:${yellow_bold}%(row)d${reset}:${green_bold}%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s
|
||||
|
@ -3,22 +3,14 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from django.contrib.auth.forms import PasswordResetForm
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils import timezone
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
from med.admin import admin_site
|
||||
from .forms import UserCreationAdminForm
|
||||
from .models import Adhesion, User
|
||||
|
||||
|
||||
class AdhesionAdmin(VersionAdmin):
|
||||
list_display = ('starting_in', 'ending_in')
|
||||
autocomplete_fields = ('members',)
|
||||
from .models import User
|
||||
|
||||
|
||||
class IsMemberFilter(admin.SimpleListFilter):
|
||||
@ -31,12 +23,12 @@ class IsMemberFilter(admin.SimpleListFilter):
|
||||
)
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
value = self.value()
|
||||
if value == 'Yes':
|
||||
# Get current membership year and list all members
|
||||
last_adh_year = Adhesion.objects.all().order_by('starting_in') \
|
||||
.reverse().first()
|
||||
return last_adh_year.members
|
||||
if self.parameter_name in request.GET:
|
||||
queryset = queryset.filter(
|
||||
membership__date_start__lte=timezone.now(),
|
||||
membership__date_end__gte=timezone.now(),
|
||||
).distinct()
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
@ -45,62 +37,32 @@ class UserAdmin(VersionAdmin, BaseUserAdmin):
|
||||
fieldsets = (
|
||||
(None, {'fields': ('username', 'password')}),
|
||||
(_('Personal info'), {'fields': ('first_name', 'last_name', 'email',
|
||||
'telephone', 'address', 'comment')}),
|
||||
'phone_number', 'address',
|
||||
'comment')}),
|
||||
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
|
||||
'groups', 'user_permissions',
|
||||
'maxemprunt')}),
|
||||
'groups', 'user_permissions')}),
|
||||
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
|
||||
)
|
||||
list_display = ('username', 'email', 'first_name', 'last_name',
|
||||
'maxemprunt', 'is_member', 'is_staff')
|
||||
'is_member', 'is_staff')
|
||||
list_filter = (IsMemberFilter, 'is_staff', 'is_superuser', 'is_active',
|
||||
'groups')
|
||||
|
||||
# Customize required initial fields
|
||||
add_form_template = 'admin/change_form.html'
|
||||
add_form = UserCreationAdminForm
|
||||
add_fieldsets = (
|
||||
(None, {
|
||||
'classes': ('wide',),
|
||||
'fields': ("username", "email", "first_name", "last_name",
|
||||
"address", "telephone"),
|
||||
}),
|
||||
)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""
|
||||
On creation, send a password init mail
|
||||
"""
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
if not change:
|
||||
# Virtually fill the password reset form
|
||||
password_reset = PasswordResetForm(data={'email': obj.email})
|
||||
if password_reset.is_valid():
|
||||
password_reset.save(request=request,
|
||||
use_https=request.is_secure())
|
||||
messages.success(request, _("An email to set the password"
|
||||
" was sent."))
|
||||
else:
|
||||
messages.error(request, _("The email is invalid."))
|
||||
def has_add_permission(self, request):
|
||||
# Only add users through Note Kfet login
|
||||
return False
|
||||
|
||||
def is_member(self, obj):
|
||||
"""
|
||||
Get current membership year and check if user is there
|
||||
"""
|
||||
last_adh_year = Adhesion.objects.all().order_by('starting_in') \
|
||||
.reverse().first()
|
||||
is_member = last_adh_year and obj in last_adh_year.members.all()
|
||||
if is_member:
|
||||
return format_html(
|
||||
if obj.is_member:
|
||||
return mark_safe(
|
||||
'<img src="/static/admin/img/icon-yes.svg" alt="True">'
|
||||
)
|
||||
else:
|
||||
return format_html(
|
||||
'<img src="/static/admin/img/icon-no.svg" alt="False"> '
|
||||
'<a class="button" href="{}">{}</a>',
|
||||
reverse('users:adherer', args=[obj.pk]),
|
||||
_('Adhere')
|
||||
return mark_safe(
|
||||
'<img src="/static/admin/img/icon-no.svg" alt="False">'
|
||||
)
|
||||
|
||||
is_member.short_description = _('is member')
|
||||
@ -108,4 +70,3 @@ class UserAdmin(VersionAdmin, BaseUserAdmin):
|
||||
|
||||
|
||||
admin_site.register(User, UserAdmin)
|
||||
admin_site.register(Adhesion, AdhesionAdmin)
|
||||
|
@ -1,57 +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 import forms
|
||||
from django.contrib.auth.forms import UsernameField
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.forms import ModelForm
|
||||
|
||||
from .models import User
|
||||
|
||||
|
||||
class PassForm(forms.Form):
|
||||
passwd1 = forms.CharField(
|
||||
label=u'Nouveau mot de passe',
|
||||
max_length=255,
|
||||
validators=[MinLengthValidator(8)],
|
||||
widget=forms.PasswordInput,
|
||||
)
|
||||
passwd2 = forms.CharField(
|
||||
label=u'Saisir à nouveau le mot de passe',
|
||||
max_length=255,
|
||||
validators=[MinLengthValidator(8)],
|
||||
widget=forms.PasswordInput
|
||||
)
|
||||
|
||||
|
||||
class BaseInfoForm(ModelForm):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = [
|
||||
'username',
|
||||
'email',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'address',
|
||||
'telephone',
|
||||
]
|
||||
|
||||
|
||||
class UserCreationAdminForm(ModelForm):
|
||||
"""
|
||||
A form that creates a user, with no privileges,
|
||||
from the given information.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['email'].required = True
|
||||
self.fields['first_name'].required = True
|
||||
self.fields['last_name'].required = True
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ("username", "email", "first_name", "last_name", "address",
|
||||
"telephone")
|
||||
field_classes = {'username': UsernameField}
|
25
users/migrations/0041_auto_20200923_2030.py
Normal file
25
users/migrations/0041_auto_20200923_2030.py
Normal file
@ -0,0 +1,25 @@
|
||||
# Generated by Django 2.2.12 on 2020-09-23 18:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0040_delete_clef'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='address',
|
||||
field=models.CharField(blank=True, default='', max_length=255, verbose_name='address'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='telephone',
|
||||
field=models.CharField(blank=True, default='', max_length=15, verbose_name='phone number'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
16
users/migrations/0042_delete_adhesion.py
Normal file
16
users/migrations/0042_delete_adhesion.py
Normal file
@ -0,0 +1,16 @@
|
||||
# Generated by Django 2.2.17 on 2021-10-23 12:48
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0041_auto_20200923_2030'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='Adhesion',
|
||||
),
|
||||
]
|
31
users/migrations/0043_accesstoken.py
Normal file
31
users/migrations/0043_accesstoken.py
Normal file
@ -0,0 +1,31 @@
|
||||
# Generated by Django 2.2.24 on 2021-11-02 15:11
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0042_delete_adhesion'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AccessToken',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('access_token', models.CharField(max_length=32, verbose_name='access token')),
|
||||
('expires_in', models.PositiveIntegerField(verbose_name='expires in')),
|
||||
('scopes', models.CharField(max_length=255, verbose_name='scopes')),
|
||||
('refresh_token', models.CharField(max_length=32, verbose_name='refresh token')),
|
||||
('expires_at', models.DateTimeField(verbose_name='expires at')),
|
||||
('owner', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='owner')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'access token',
|
||||
'verbose_name_plural': 'access tokens',
|
||||
},
|
||||
),
|
||||
]
|
28
users/migrations/0044_membership.py
Normal file
28
users/migrations/0044_membership.py
Normal file
@ -0,0 +1,28 @@
|
||||
# Generated by Django 2.2.24 on 2021-11-04 13:20
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0043_accesstoken'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Membership',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date_start', models.DateField(auto_now_add=True, verbose_name='start date')),
|
||||
('date_end', models.DateField(auto_now_add=True, verbose_name='start date')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'membership',
|
||||
'verbose_name_plural': 'memberships',
|
||||
},
|
||||
),
|
||||
]
|
22
users/migrations/0045_auto_20211114_1423.py
Normal file
22
users/migrations/0045_auto_20211114_1423.py
Normal file
@ -0,0 +1,22 @@
|
||||
# Generated by Django 2.2.24 on 2021-11-14 13:23
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0044_membership'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='user',
|
||||
old_name='telephone',
|
||||
new_name='phone_number',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='maxemprunt',
|
||||
),
|
||||
]
|
23
users/migrations/0046_auto_20211114_1624.py
Normal file
23
users/migrations/0046_auto_20211114_1624.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 2.2.24 on 2021-11-14 15:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0045_auto_20211114_1423'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='membership',
|
||||
name='date_end',
|
||||
field=models.DateField(verbose_name='start date'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='membership',
|
||||
name='date_start',
|
||||
field=models.DateField(verbose_name='start date'),
|
||||
),
|
||||
]
|
201
users/models.py
201
users/models.py
@ -1,34 +1,30 @@
|
||||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2017-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
from authlib.integrations.django_client import OAuth
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from med.settings import MAX_EMPRUNT
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
telephone = models.CharField(
|
||||
phone_number = models.CharField(
|
||||
verbose_name=_('phone number'),
|
||||
max_length=15,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
address = models.CharField(
|
||||
verbose_name=_('address'),
|
||||
max_length=255,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
maxemprunt = models.IntegerField(
|
||||
verbose_name=_('maximum borrowed'),
|
||||
help_text=_('Maximal amount of simultaneous borrowed item '
|
||||
'authorized.'),
|
||||
default=MAX_EMPRUNT,
|
||||
)
|
||||
comment = models.CharField(
|
||||
verbose_name=_('comment'),
|
||||
help_text=_('Promotion...'),
|
||||
@ -36,7 +32,7 @@ class User(AbstractUser):
|
||||
blank=True,
|
||||
)
|
||||
date_joined = models.DateTimeField(
|
||||
_('date joined'),
|
||||
verbose_name=_('date joined'),
|
||||
default=timezone.now,
|
||||
null=True,
|
||||
)
|
||||
@ -45,28 +41,171 @@ class User(AbstractUser):
|
||||
|
||||
@property
|
||||
def is_member(self):
|
||||
last_year = Adhesion.objects.all().order_by(
|
||||
'starting_in').reverse().first()
|
||||
return last_year and self in last_year.members.all()
|
||||
"""
|
||||
Return True if user is member of the club.
|
||||
"""
|
||||
return Membership.objects.filter(
|
||||
user=self,
|
||||
date_start__lte=timezone.now(),
|
||||
date_end__gte=timezone.now()).exists()
|
||||
|
||||
def update_data(self, data: dict):
|
||||
"""
|
||||
Update user data from given dictionary.
|
||||
Useful when we want to update user data from Note Kfet.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : dict
|
||||
Dictionary with user data to update.
|
||||
"""
|
||||
self.email = data['email'] or ''
|
||||
self.first_name = data['first_name'] or ''
|
||||
self.last_name = data['last_name'] or ''
|
||||
self.phone_number = data['profile']['phone_number'] or ''
|
||||
self.address = data['profile']['address'] or ''
|
||||
self.comment = data['profile']['section'] or ''
|
||||
|
||||
for membership_dict in data['memberships']:
|
||||
if membership_dict['club'] != 22: # Med
|
||||
continue
|
||||
# Add membership if not exists
|
||||
Membership.objects.get_or_create(
|
||||
user=self,
|
||||
date_start=membership_dict['date_start'],
|
||||
date_end=membership_dict['date_end'],
|
||||
)
|
||||
|
||||
# Only members or old members are allow to connect to the website
|
||||
self.is_active = Membership.objects.filter(user=self).exists()
|
||||
|
||||
|
||||
class Adhesion(models.Model):
|
||||
starting_in = models.IntegerField(
|
||||
verbose_name=_('starting in'),
|
||||
help_text=_('Year in which the membership year starts.'),
|
||||
unique=True,
|
||||
class Membership(models.Model):
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_('user'),
|
||||
)
|
||||
ending_in = models.IntegerField(
|
||||
verbose_name=_('ending in'),
|
||||
help_text=_('Year in which the membership year ends.'),
|
||||
unique=True,
|
||||
|
||||
date_start = models.DateField(
|
||||
verbose_name=_('start date'),
|
||||
)
|
||||
members = models.ManyToManyField(
|
||||
'User',
|
||||
verbose_name=_('members'),
|
||||
blank=True,
|
||||
|
||||
date_end = models.DateField(
|
||||
verbose_name=_('start date'),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.user}: {self.date_start} to {self.date_end}'
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('membership year')
|
||||
verbose_name_plural = _('membership years')
|
||||
verbose_name = _('membership')
|
||||
verbose_name_plural = _('memberships')
|
||||
|
||||
|
||||
class AccessToken(models.Model):
|
||||
owner = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_('owner'),
|
||||
)
|
||||
|
||||
access_token = models.CharField(
|
||||
max_length=32,
|
||||
verbose_name=_('access token'),
|
||||
)
|
||||
|
||||
expires_in = models.PositiveIntegerField(
|
||||
verbose_name=_('expires in'),
|
||||
)
|
||||
|
||||
scopes = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_('scopes'),
|
||||
)
|
||||
|
||||
refresh_token = models.CharField(
|
||||
max_length=32,
|
||||
verbose_name=_('refresh token'),
|
||||
)
|
||||
|
||||
expires_at = models.DateTimeField(
|
||||
verbose_name=_('expires at'),
|
||||
)
|
||||
|
||||
def refresh(self):
|
||||
"""
|
||||
Refresh the access token.
|
||||
"""
|
||||
oauth = OAuth()
|
||||
oauth.register('notekfet')
|
||||
# Get the OAuth client
|
||||
oauth_client = oauth.notekfet._get_oauth_client()
|
||||
# Actually refresh the token
|
||||
token = oauth_client.refresh_token(oauth.notekfet.access_token_url,
|
||||
refresh_token=self.refresh_token)
|
||||
self.access_token = token['access_token']
|
||||
self.expires_in = token['expires_in']
|
||||
self.scopes = token['scope']
|
||||
self.refresh_token = token['refresh_token']
|
||||
self.expires_at = timezone.utc.fromutc(
|
||||
datetime.fromtimestamp(token['expires_at'])
|
||||
)
|
||||
|
||||
self.save()
|
||||
|
||||
def refresh_if_expired(self):
|
||||
"""
|
||||
Refresh the current token if it is invalid.
|
||||
"""
|
||||
if self.expires_at < timezone.now():
|
||||
self.refresh()
|
||||
|
||||
def auth_header(self):
|
||||
"""
|
||||
Return HTTP header that contains the bearer access token.
|
||||
Refresh the token if needed.
|
||||
"""
|
||||
self.refresh_if_expired()
|
||||
return {'Authorization': f'Bearer {self.access_token}'}
|
||||
|
||||
def fetch_user(self, create_if_not_exist: bool = False):
|
||||
"""
|
||||
Extract information about the Note Kfet API by using the current
|
||||
access token.
|
||||
"""
|
||||
data = requests.get(f'{settings.NOTE_KFET_URL}/api/me/',
|
||||
headers=self.auth_header()).json()
|
||||
username = data['username']
|
||||
email = data['email']
|
||||
qs = User.objects.filter(Q(username=username) | Q(email=email))
|
||||
if not qs.exists():
|
||||
if create_if_not_exist:
|
||||
user = User.objects.create(username=username, email=email)
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
user = qs.get()
|
||||
|
||||
# Update user data from Note Kfet
|
||||
user.update_data(data)
|
||||
user.save()
|
||||
|
||||
# Store token owner
|
||||
self.owner = user
|
||||
self.save()
|
||||
|
||||
return user
|
||||
|
||||
@classmethod
|
||||
def get_token(cls, request):
|
||||
return AccessToken.objects.get(pk=request.session['access_token_id'])
|
||||
|
||||
def __str__(self):
|
||||
return self.access_token
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('access token')
|
||||
verbose_name_plural = _('access tokens')
|
||||
|
@ -8,7 +8,7 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['url', 'username', 'first_name', 'last_name', 'email',
|
||||
'groups', 'telephone', 'address', 'maxemprunt', 'comment',
|
||||
'groups', 'phone_number', 'address', 'comment',
|
||||
'date_joined']
|
||||
|
||||
|
||||
|
@ -1,10 +1,8 @@
|
||||
# -*- mode: python; coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.core import mail
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from users.models import User
|
||||
|
||||
"""
|
||||
@ -21,30 +19,6 @@ class TemplateTests(TestCase):
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_users_edit_info(self):
|
||||
response = self.client.get(reverse('users:edit-info'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_users_user_changelist(self):
|
||||
response = self.client.get(reverse('admin:users_user_changelist'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_users_user_creation_form(self):
|
||||
response = self.client.get(reverse('admin:users_user_add'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_users_user_add_init_mail(self):
|
||||
"""
|
||||
Test that an initialization mail is send when a new user is added
|
||||
"""
|
||||
data = {
|
||||
'username': "test_user",
|
||||
'email': "test@example.com",
|
||||
'first_name': "Test",
|
||||
'last_name': "User",
|
||||
}
|
||||
response = self.client.post(reverse(
|
||||
'admin:users_user_add',
|
||||
), data=data)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
@ -8,6 +8,6 @@ from . import views
|
||||
|
||||
app_name = 'users'
|
||||
urlpatterns = [
|
||||
url(r'^edit_info/$', views.edit_info, name='edit-info'),
|
||||
url(r'^adherer/(?P<userid>[0-9]+)$', views.adherer, name='adherer'),
|
||||
url('login/', views.LoginView.as_view(), name='login'),
|
||||
url('authorize/', views.AuthorizeView.as_view(), name='auth'),
|
||||
]
|
||||
|
@ -1,68 +1,47 @@
|
||||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from datetime import datetime
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from authlib.integrations.django_client import OAuth
|
||||
from django.contrib.auth import login
|
||||
from django.contrib.auth.models import Group
|
||||
from django.db import transaction
|
||||
from django.shortcuts import redirect, render
|
||||
from django.template.context_processors import csrf
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views.generic import RedirectView
|
||||
from rest_framework import viewsets
|
||||
from reversion import revisions as reversion
|
||||
from users.models import User, AccessToken
|
||||
|
||||
from users.forms import BaseInfoForm
|
||||
from users.models import Adhesion, User
|
||||
from .serializers import GroupSerializer, UserSerializer
|
||||
|
||||
|
||||
def form(ctx, template, request):
|
||||
c = ctx
|
||||
c.update(csrf(request))
|
||||
return render(request, template, c)
|
||||
class LoginView(RedirectView):
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
oauth = OAuth()
|
||||
oauth.register('notekfet')
|
||||
redirect_url = self.request.build_absolute_uri(reverse('users:auth'))
|
||||
return oauth.notekfet.authorize_redirect(self.request,
|
||||
redirect_url).url
|
||||
|
||||
|
||||
@login_required
|
||||
def edit_info(request):
|
||||
"""
|
||||
Edite son utilisateur
|
||||
"""
|
||||
user = BaseInfoForm(request.POST or None, instance=request.user)
|
||||
if user.is_valid():
|
||||
with transaction.atomic(), reversion.create_revision():
|
||||
user.save()
|
||||
reversion.set_user(request.user)
|
||||
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(
|
||||
field for field in user.changed_data))
|
||||
messages.success(request, "L'user a bien été modifié")
|
||||
return redirect("index")
|
||||
return form({
|
||||
'form': user,
|
||||
'password_change': True,
|
||||
'title': _('Edit user profile'),
|
||||
}, 'users/user.html', request)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('users.add_adhesion')
|
||||
def adherer(request, userid):
|
||||
try:
|
||||
users = User.objects.get(pk=userid)
|
||||
except User.DoesNotExist:
|
||||
messages.error(request, "Utilisateur inexistant")
|
||||
return redirect("admin:users_user_changelist")
|
||||
adh_year = Adhesion.objects.all().order_by('starting_in').reverse().first()
|
||||
if not adh_year:
|
||||
messages.error(request, "Année d'adhésion non définie")
|
||||
return redirect("admin:users_user_changelist")
|
||||
with transaction.atomic(), reversion.create_revision():
|
||||
reversion.set_user(request.user)
|
||||
adh_year.members.add(users)
|
||||
adh_year.save()
|
||||
reversion.set_comment("Adhesion de %s" % users)
|
||||
messages.success(request, "Adhesion effectuee")
|
||||
return redirect("admin:users_user_changelist")
|
||||
class AuthorizeView(RedirectView):
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
oauth = OAuth()
|
||||
oauth.register('notekfet')
|
||||
token = oauth.notekfet.authorize_access_token(self.request)
|
||||
token_obj = AccessToken.objects.create(
|
||||
access_token=token['access_token'],
|
||||
expires_in=token['expires_in'],
|
||||
scopes=token['scope'],
|
||||
refresh_token=token['refresh_token'],
|
||||
expires_at=timezone.utc.fromutc(
|
||||
datetime.fromtimestamp(token['expires_at'])),
|
||||
)
|
||||
user = token_obj.fetch_user(True)
|
||||
self.request.session['access_token_id'] = token_obj.id
|
||||
self.request.session.save()
|
||||
login(self.request, user)
|
||||
return reverse('index')
|
||||
|
||||
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
|
Reference in New Issue
Block a user