1
0
mirror of https://gitlab.crans.org/mediatek/med.git synced 2025-07-04 04:52:13 +02:00

Compare commits

..

61 Commits

Author SHA1 Message Date
48c056b210 Harden Django project configuration
Set session and CSRF cookies as secure for production.
Set HSTS header to let browser remember HTTPS for 1 year.
2022-03-09 12:30:18 +01:00
cf544dc596 Fix game research 2021-11-14 16:41:38 +01:00
c0521005ef Don't put current date as default value for memberships 2021-11-14 16:24:38 +01:00
09c61091d5 Put empty strings instead of None as default values 2021-11-14 16:16:17 +01:00
145806b5ce Merge branch 'note' into 'main'
Note Kfet integration

See merge request mediatek/med!7
2021-11-14 15:57:40 +01:00
49898143ce expires in may be not small 2021-11-14 15:51:45 +01:00
fe55a2a5ea Don't hardcode OAuth2 parameters 2021-11-14 15:22:11 +01:00
cdcb743b55 Fix membership filters 2021-11-14 15:09:04 +01:00
e75f04b530 Don't test to add users since registration is disabled 2021-11-14 14:32:28 +01:00
34766257b3 Linting 2021-11-14 14:28:13 +01:00
d0877f5cdc Translate borrows 2021-11-14 14:26:41 +01:00
aceb0d893c Users can log in iff they are or were a member, don't display the database to everyone 2021-11-04 15:04:57 +01:00
41435a6838 Store access token id in session data 2021-11-04 14:25:35 +01:00
d3c2441111 Get memberships from Note Kfet 2021-11-04 14:23:03 +01:00
d036ea6f27 Get or create a user and query Note Kfet to get data 2021-11-04 14:13:17 +01:00
e2aa645bbf Start implementation of OAuth client 2021-11-04 11:29:03 +01:00
faf697d3cf Drop unusued models. Keep migration script, but this is now unusable 2021-11-02 12:55:03 +01:00
b0a1602ea2 Merge branch 'drop_old_structure' into 'main'
Suppression de l'ancienne structure

See merge request mediatek/med!6
2021-11-02 12:54:17 +01:00
d2ad52c15a Drop unusued models. Keep migration script, but this is now unusable 2021-11-02 12:51:25 +01:00
8c6828564c Merge branch 'medium' into 'main'
Migration des données

See merge request mediatek/med!5
2021-11-02 12:42:46 +01:00
d75250f436 Linting 2021-11-02 12:39:53 +01:00
c424c7c040 Give ISBN to each borrowable object, even if it does not have one 2021-11-02 12:32:31 +01:00
a01d480dd2 Show games in Admin menu 2021-11-02 12:30:44 +01:00
079ade9bbb Fix ISBN scraper 2021-10-26 15:35:47 +02:00
4928b555b7 Fix missing translations 2021-10-26 15:16:00 +02:00
ae0d1a080e Fix borrowable autocompletion 2021-10-26 15:07:37 +02:00
1e6e033cdd Hide borrowable section 2021-10-26 15:07:29 +02:00
d0805ebe8a Migrate old-format into new format 2021-10-26 15:00:50 +02:00
6789b9e3ac Don't mix names 2021-10-26 14:25:21 +02:00
54f8198b86 Keep displaying models in Django-Admin main menu 2021-10-26 11:42:32 +02:00
e3bab2389c Replace old models by new models to update DB structure 2021-10-26 11:37:32 +02:00
39e345ee67 Install Django Polymorphic to use polymorphic models 2021-10-23 19:20:32 +02:00
0b3701f01f Fix index page 2021-10-23 19:20:20 +02:00
1ab06af5c7 Merge branch 'small-cleanup' into 'main'
Small cleanup

See merge request mediatek/med!4
2021-10-23 19:09:04 +02:00
2c4aacfc08 Destroy Django 1.11 functions 2021-10-23 19:03:59 +02:00
70018f0043 Destroy Django 1.11 functions 2021-10-23 19:00:45 +02:00
b638add396 Fix translations 2021-10-23 18:46:41 +02:00
9f5807e3b5 Translate game fields into english 2021-10-23 18:39:51 +02:00
7396cfc017 Fix tests 2021-10-23 18:33:51 +02:00
52ecd59bf6 Translate models in english 2021-10-23 18:31:03 +02:00
2a17a32d4c No more manage memberships, we will use NK20 2021-10-23 14:49:44 +02:00
7f24e5c1bf Install python3-requests 2021-10-23 14:03:58 +02:00
69a8080050 Drop Python 3.7 and 3.8 support 2021-10-23 14:01:08 +02:00
a78d0b4904 Django Extensions is fun 2021-10-23 13:59:24 +02:00
15fc5cd73f Cleanup dependencies 2021-10-23 13:54:08 +02:00
94c6c73615 Fix Bedetheque
Signed-off-by: club-med <club-med@zamokv5.crans.org>
2021-10-01 12:58:45 +02:00
33a7e5adc6 Update Bedetheque scraper 2021-04-29 13:51:52 +02:00
1a96b1a2aa For an unknown reason, button inputs are not sent as POST parameters 2021-04-29 13:51:24 +02:00
e7dfaf8b8b Merge branch '2021' into 'master'
Prepare the website for 2021

See merge request mediatek/med!3
2021-01-01 16:45:14 +01:00
74f453637a Linting 2020-12-28 23:12:27 +01:00
b8ccb40ded Màj Buster 2020-12-28 23:00:01 +01:00
574233acd0 Fix CI, add django22-py39 CI step 2020-12-28 22:58:04 +01:00
746d5cc816 Prepare the website for 2021 2020-12-27 18:22:52 +01:00
4de83344a7 Export games 2020-10-26 15:13:19 +01:00
a64d600645 Index page 2020-10-26 14:44:25 +01:00
22408b16c7 Capitalize title 2020-10-26 14:38:03 +01:00
6abbaaf75d Buggy markdown 2020-10-26 14:33:52 +01:00
9afb750be6 Use _meta field instead of class 2020-10-26 14:29:55 +01:00
997c2eac50 Use _meta field instead of class 2020-10-26 14:29:11 +01:00
be2094b263 Revues are treated separately 2020-10-26 14:27:43 +01:00
cb9cd8f9b6 Add script to export the database into a user-friendly website 2020-10-26 14:21:27 +01:00
50 changed files with 1693 additions and 1003 deletions

View File

@ -2,33 +2,20 @@ stages:
- test - test
- quality-assurance - quality-assurance
py37-django22: py39-django22:
stage: test stage: test
image: debian:buster-backports image: debian:bullseye
before_script: before_script:
- >
apt-get update &&
apt-get install --no-install-recommends -t buster-backports -y
python3-django python3-django-casclient python3-django-reversion python3-djangorestframework
python3-docutils python3-pil python3-tz python3-six python3-sqlparse python3-stdnum python3-yaml python3-coreapi tox
script: tox -e py37
py38-django22:
stage: test
image: ubuntu:20.04
before_script:
# Fix tzdata prompt
- ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone
- > - >
apt-get update && apt-get update &&
apt-get install --no-install-recommends -y apt-get install --no-install-recommends -y
python3-django python3-django-casclient python3-django-reversion python3-djangorestframework python3-django python3-django-polymorphic python3-django-reversion
python3-docutils python3-pil python3-tz python3-six python3-sqlparse python3-stdnum python3-yaml python3-coreapi tox python3-djangorestframework python3-docutils python3-requests tox
script: tox -e py38 script: tox -e py39
linters: linters:
stage: quality-assurance stage: quality-assurance
image: debian:buster-backports image: debian:bullseye
before_script: before_script:
- apt-get update && apt-get install -y tox - apt-get update && apt-get install -y tox
script: tox -e linters script: tox -e linters

View File

@ -45,7 +45,7 @@ chmod go-rwx -R django-med
python3 -m venv venv --system-site-packages python3 -m venv venv --system-site-packages
. venv/bin/activate . venv/bin/activate
pip install -r requirements.txt pip install -r requirements.txt
pip install mysqlclient~=1.4.0 # si base MySQL pip install mysqlclient~=1.4.4 # si base MySQL
pip install uwsgi~=2.0.18 # si production pip install uwsgi~=2.0.18 # si production
./entrypoint.sh # lance en shell ./entrypoint.sh # lance en shell
``` ```
@ -90,10 +90,6 @@ bureau
media | Can add borrowed item media | Can add borrowed item
media | Can change borrowed item media | Can change borrowed item
media | Can delete 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 view user
users | Can add user users | Can add user
users | Can change user users | Can change user

View File

@ -7,7 +7,7 @@ from django.contrib.auth.admin import Group, GroupAdmin
from django.contrib.sites.admin import Site, SiteAdmin from django.contrib.sites.admin import Site, SiteAdmin
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
from media.models import Emprunt from media.models import Borrow
class DatabaseAdmin(AdminSite): class DatabaseAdmin(AdminSite):
@ -22,8 +22,8 @@ class DatabaseAdmin(AdminSite):
# User is always authenticated # User is always authenticated
# Get currently borrowed items # Get currently borrowed items
user_borrowed = Emprunt.objects.filter(user=request.user, user_borrowed = Borrow.objects.filter(user=request.user,
date_rendu=None) given_back=None)
response.context_data["borrowed_items"] = user_borrowed response.context_data["borrowed_items"] = user_borrowed
return response return response

View File

@ -26,6 +26,16 @@ SITE_ID = 1
ALLOWED_HOSTS = ['127.0.0.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 # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
@ -35,6 +45,8 @@ INSTALLED_APPS = [
# External apps # External apps
'reversion', 'reversion',
'rest_framework', 'rest_framework',
'django_extensions',
'polymorphic',
# Django contrib # Django contrib
'django.contrib.admin', 'django.contrib.admin',
@ -165,9 +177,26 @@ PAGINATION_NUMBER = 25
AUTH_USER_MODEL = 'users.User' 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: try:
from .settings_local import * from .settings_local import *
except ImportError: except ImportError:
pass 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,
}
}
}

View File

@ -40,3 +40,8 @@ DATABASES = {
'PORT': '', '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'

View File

@ -13,21 +13,21 @@ from .admin import admin_site
# API router # API router
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register(r'authors', media.views.AuteurViewSet) router.register(r'authors', media.views.AuthorViewSet)
router.register(r'media/bd', media.views.BDViewSet) router.register(r'media/comic', media.views.ComicViewSet)
router.register(r'media/manga', media.views.MangaViewSet) router.register(r'media/manga', media.views.MangaViewSet)
router.register(r'media/cd', media.views.CDViewSet) router.register(r'media/cd', media.views.CDViewSet)
router.register(r'media/vinyle', media.views.VinyleViewSet) router.register(r'media/vinyl', media.views.VinylViewSet)
router.register(r'media/roman', media.views.RomanViewSet) router.register(r'media/novel', media.views.NovelViewSet)
router.register(r'media/revue', media.views.RevueViewSet) router.register(r'media/review', media.views.ReviewViewSet)
router.register(r'media/future', media.views.FutureMediaViewSet) router.register(r'media/future', media.views.FutureMediumViewSet)
router.register(r'borrowed_items', media.views.EmpruntViewSet) router.register(r'borrowed_items', media.views.BorrowViewSet)
router.register(r'games', media.views.JeuViewSet) router.register(r'games', media.views.GameViewSet)
router.register(r'users', users.views.UserViewSet) router.register(r'users', users.views.UserViewSet)
router.register(r'groups', users.views.GroupViewSet) router.register(r'groups', users.views.GroupViewSet)
urlpatterns = [ urlpatterns = [
path('', media.views.index, name='index'), path('', media.views.IndexView.as_view(), name='index'),
# Include project routers # Include project routers
path('users/', include('users.urls')), path('users/', include('users.urls')),

View File

@ -2,24 +2,34 @@
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay # Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import reverse
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from polymorphic.admin import PolymorphicChildModelAdmin, \
PolymorphicParentModelAdmin
from med.admin import admin_site
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from med.admin import admin_site
from .forms import MediaAdminForm from .forms import MediaAdminForm
from .models import Auteur, BD, CD, Emprunt, FutureMedia, Jeu, Manga,\ from .models import Author, Borrow, Borrowable, CD, Comic, FutureMedium, \
Revue, Roman, Vinyle Game, Manga, Novel, Review, Vinyl
class AuteurAdmin(VersionAdmin): class AuthorAdmin(VersionAdmin):
list_display = ('name',) list_display = ('name',)
search_fields = ('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', list_display = ('__str__', 'authors_list', 'side_identifier', 'isbn',
'external_link') 'external_link')
search_fields = ('title', 'authors__name', 'side_identifier', 'subtitle', search_fields = ('title', 'authors__name', 'side_identifier', 'subtitle',
@ -27,6 +37,7 @@ class MediaAdmin(VersionAdmin):
autocomplete_fields = ('authors',) autocomplete_fields = ('authors',)
date_hierarchy = 'publish_date' date_hierarchy = 'publish_date'
form = MediaAdminForm form = MediaAdminForm
show_in_index = True
def authors_list(self, obj): def authors_list(self, obj):
return ", ".join([a.name for a in obj.authors.all()]) return ", ".join([a.name for a in obj.authors.all()])
@ -62,7 +73,7 @@ class MediaAdmin(VersionAdmin):
extra_context=extra_context) extra_context=extra_context)
class FutureMediaAdmin(VersionAdmin): class FutureMediumAdmin(VersionAdmin):
list_display = ('isbn',) list_display = ('isbn',)
search_fields = ('isbn',) search_fields = ('isbn',)
@ -78,10 +89,11 @@ class FutureMediaAdmin(VersionAdmin):
extra_context=extra_context) extra_context=extra_context)
class CDAdmin(VersionAdmin): class CDAdmin(VersionAdmin, PolymorphicChildModelAdmin):
list_display = ('title', 'authors_list', 'side_identifier',) list_display = ('title', 'authors_list', 'side_identifier',)
search_fields = ('title', 'authors__name', 'side_identifier',) search_fields = ('title', 'authors__name', 'side_identifier',)
autocomplete_fields = ('authors',) autocomplete_fields = ('authors',)
show_in_index = True
def authors_list(self, obj): def authors_list(self, obj):
return ", ".join([a.name for a in obj.authors.all()]) return ", ".join([a.name for a in obj.authors.all()])
@ -89,10 +101,11 @@ class CDAdmin(VersionAdmin):
authors_list.short_description = _('authors') authors_list.short_description = _('authors')
class VinyleAdmin(VersionAdmin): class VinylAdmin(VersionAdmin, PolymorphicChildModelAdmin):
list_display = ('title', 'authors_list', 'side_identifier', 'rpm',) list_display = ('title', 'authors_list', 'side_identifier', 'rpm',)
search_fields = ('title', 'authors__name', 'side_identifier', 'rpm',) search_fields = ('title', 'authors__name', 'side_identifier', 'rpm',)
autocomplete_fields = ('authors',) autocomplete_fields = ('authors',)
show_in_index = True
def authors_list(self, obj): def authors_list(self, obj):
return ", ".join([a.name for a in obj.authors.all()]) return ", ".join([a.name for a in obj.authors.all()])
@ -100,35 +113,21 @@ class VinyleAdmin(VersionAdmin):
authors_list.short_description = _('authors') authors_list.short_description = _('authors')
class RevueAdmin(VersionAdmin): class ReviewAdmin(VersionAdmin, PolymorphicChildModelAdmin):
list_display = ('__str__', 'number', 'year', 'month', 'day', 'double',) list_display = ('__str__', 'number', 'year', 'month', 'day', 'double',)
search_fields = ('title', 'number', 'year',) search_fields = ('title', 'number', 'year',)
show_in_index = True
class EmpruntAdmin(VersionAdmin): class BorrowAdmin(VersionAdmin):
list_display = ('media', 'user', 'date_emprunt', 'date_rendu', list_display = ('borrowable', 'user', 'borrow_date', 'borrowed_with',
'permanencier_emprunt', 'permanencier_rendu_custom') 'given_back_to')
search_fields = ('media__title', 'media__side_identifier', search_fields = ('borrowable__isbn', 'borrowable__title',
'user__username', 'date_emprunt', 'date_rendu') 'borrowable__medium__side_identifier',
date_hierarchy = 'date_emprunt' 'user__username', 'borrow_date', 'given_back')
autocomplete_fields = ('media', 'user', 'permanencier_emprunt', date_hierarchy = 'borrow_date'
'permanencier_rendu') autocomplete_fields = ('borrowable', 'user', 'borrowed_with',
'given_back_to')
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
def add_view(self, request, form_url='', extra_context=None): def add_view(self, request, form_url='', extra_context=None):
""" """
@ -136,25 +135,27 @@ class EmpruntAdmin(VersionAdmin):
""" """
# Make GET data mutable # Make GET data mutable
data = request.GET.copy() data = request.GET.copy()
data['permanencier_emprunt'] = request.user data['borrowed_with'] = request.user
request.GET = data request.GET = data
return super().add_view(request, form_url, extra_context) return super().add_view(request, form_url, extra_context)
class JeuAdmin(VersionAdmin): class GameAdmin(VersionAdmin, PolymorphicChildModelAdmin):
list_display = ('name', 'proprietaire', 'duree', 'nombre_joueurs_min', list_display = ('title', 'owner', 'duration', 'players_min',
'nombre_joueurs_max', 'comment') 'players_max', 'comment', 'isbn')
search_fields = ('name', 'proprietaire__username', 'duree', 'comment') search_fields = ('isbn', 'title', 'owner__username', 'duration', 'comment')
autocomplete_fields = ('proprietaire',) autocomplete_fields = ('owner',)
show_in_index = True
admin_site.register(Auteur, AuteurAdmin) admin_site.register(Author, AuthorAdmin)
admin_site.register(BD, MediaAdmin) admin_site.register(Borrowable, BorrowableAdmin)
admin_site.register(Manga, MediaAdmin) admin_site.register(Comic, MediumAdmin)
admin_site.register(Roman, MediaAdmin) admin_site.register(Manga, MediumAdmin)
admin_site.register(Novel, MediumAdmin)
admin_site.register(CD, CDAdmin) admin_site.register(CD, CDAdmin)
admin_site.register(Vinyle, VinyleAdmin) admin_site.register(Vinyl, VinylAdmin)
admin_site.register(Revue, RevueAdmin) admin_site.register(Review, ReviewAdmin)
admin_site.register(FutureMedia, FutureMediaAdmin) admin_site.register(FutureMedium, FutureMediumAdmin)
admin_site.register(Emprunt, EmpruntAdmin) admin_site.register(Borrow, BorrowAdmin)
admin_site.register(Jeu, JeuAdmin) admin_site.register(Game, GameAdmin)

View File

@ -1,5 +1,5 @@
# -*- mode: python; coding: utf-8 -*- # -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2017-2020 by BDE ENS Paris-Saclay # Copyright (C) 2017-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import json import json
@ -9,11 +9,12 @@ import unicodedata
from urllib.error import HTTPError from urllib.error import HTTPError
import urllib.request import urllib.request
from django.core.exceptions import ValidationError
from django.db.models import QuerySet from django.db.models import QuerySet
from django.forms import ModelForm from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .models import Auteur, BD from .models import Author, Comic
from .scraper import BedetequeScraper from .scraper import BedetequeScraper
@ -45,7 +46,8 @@ def generate_side_identifier(title, authors, subtitle=None):
authors = authors.copy() authors = authors.copy()
def sort(author): def sort(author):
return "{:042d}".format(-author.note) + author.name.split(" ")[-1] + ".{:042d}".format(author.pk) return "{:042d}".format(-author.note) + author.name.split(" ")[-1]\
+ ".{:042d}".format(author.pk)
authors.sort(key=sort) authors.sort(key=sort)
primary_author = authors[0] primary_author = authors[0]
@ -54,7 +56,8 @@ def generate_side_identifier(title, authors, subtitle=None):
author_name = author_name.split(' ')[-1] author_name = author_name.split(' ')[-1]
author_name = ''.join( author_name = ''.join(
char for char in unicodedata.normalize('NFKD', author_name.casefold()) 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 == ' ' if all(not unicodedata.category(char).startswith(cat)
for cat in {'M', 'P', 'Z', 'C'}) or char == ' '
).casefold().upper() ).casefold().upper()
author_name = re.sub("[^A-Z]", "", author_name) author_name = re.sub("[^A-Z]", "", author_name)
side_identifier = "{:.3} {:.3}".format(author_name, title_normalized, ) side_identifier = "{:.3} {:.3}".format(author_name, title_normalized, )
@ -68,9 +71,12 @@ def generate_side_identifier(title, authors, subtitle=None):
side_identifier += " {:0>2}".format(start, ) side_identifier += " {:0>2}".format(start, )
# Normalize side identifier, in order to remove accents # Normalize side identifier, in order to remove accents
side_identifier = ''.join(char for char in unicodedata.normalize('NFKD', side_identifier.casefold()) side_identifier = ''.join(
if all(not unicodedata.category(char).startswith(cat) for cat in {'M', 'P', 'Z', 'C'}) char for char in unicodedata.normalize('NFKD',
or char == ' ').casefold().upper() 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 return side_identifier
@ -85,17 +91,21 @@ class MediaAdminForm(ModelForm):
side_identifier_field = self.fields.get('side_identifier') side_identifier_field = self.fields.get('side_identifier')
if side_identifier_field and self.instance and self.instance.pk: if side_identifier_field and self.instance and self.instance.pk:
instance = self.instance instance = self.instance
title, authors, subtitle = instance.title, instance.authors.all(), None title, authors, subtitle = instance.title,\
instance.authors.all(), None
if hasattr(instance, "subtitle"): if hasattr(instance, "subtitle"):
subtitle = instance.subtitle subtitle = instance.subtitle
side_identifier_field.widget.attrs.update( side_identifier_field.widget.attrs.update(
{'data-generated-side-identifier': generate_side_identifier(title, authors, subtitle)}) {'data-generated-side-identifier':
side_identifier_field.widget.template_name = "media/generate_side_identifier.html" generate_side_identifier(title, authors, subtitle)})
side_identifier_field.widget.template_name =\
"media/generate_side_identifier.html"
def download_data_isbndb(self, isbn): def download_data_isbndb(self, isbn):
api_url = "https://api2.isbndb.com/book/" + str(isbn) + "?Authorization=" + os.getenv("ISBNDB_KEY") api_url = "https://api2.isbndb.com/book/" + str(isbn)\
+ "?Authorization=" + os.getenv("ISBNDB_KEY", "")
req = urllib.request.Request(api_url) req = urllib.request.Request(api_url)
req.add_header("Authorization", os.getenv("ISBNDB_KEY")) req.add_header("Authorization", os.getenv("ISBNDB_KEY", ""))
try: try:
with urllib.request.urlopen(req) as url: with urllib.request.urlopen(req) as url:
data: dict = json.loads(url.read().decode())["book"] data: dict = json.loads(url.read().decode())["book"]
@ -109,11 +119,13 @@ class MediaAdminForm(ModelForm):
data.setdefault("image", "") data.setdefault("image", "")
self.cleaned_data["title"] = data["title"] self.cleaned_data["title"] = data["title"]
self.cleaned_data["publish_date"] = data["date_published"][:10] 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: 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["publish_date"] += "-01"
self.cleaned_data["number_of_pages"] = data["pages"] self.cleaned_data["number_of_pages"] = data["pages"]
self.cleaned_data["authors"] = \ self.cleaned_data["authors"] = \
list(Auteur.objects.get_or_create(name=author_name)[0] for author_name in 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"] self.cleaned_data["external_url"] = data["image"]
return True return True
@ -127,7 +139,7 @@ class MediaAdminForm(ModelForm):
if not r: if not r:
return False return False
# If results, then take the most accurate # 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) self.cleaned_data.update(data)
return True return True
@ -184,7 +196,7 @@ class MediaAdminForm(ModelForm):
if 'authors' in info: if 'authors' in info:
for author in info['authors']: for author in info['authors']:
author_obj = Auteur.objects.get_or_create( author_obj = Author.objects.get_or_create(
name=author)[0] name=author)[0]
self.cleaned_data['authors'].append(author_obj) self.cleaned_data['authors'].append(author_obj)
@ -258,7 +270,7 @@ class MediaAdminForm(ModelForm):
if 'authors' in data: if 'authors' in data:
for author in data['authors']: for author in data['authors']:
author_obj = Auteur.objects.get_or_create( author_obj = Author.objects.get_or_create(
name=author['name'])[0] name=author['name'])[0]
self.cleaned_data['authors'].append(author_obj) self.cleaned_data['authors'].append(author_obj)
@ -309,6 +321,13 @@ class MediaAdminForm(ModelForm):
return self.cleaned_data return self.cleaned_data
def _clean_fields(self): 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(): for name, field in self.fields.items():
# value_from_datadict() gets the data from the data dictionaries. # value_from_datadict() gets the data from the data dictionaries.
# Each widget type knows how to retrieve its own data, because some # Each widget type knows how to retrieve its own data, because some
@ -318,7 +337,6 @@ class MediaAdminForm(ModelForm):
else: else:
value = field.widget.value_from_datadict( value = field.widget.value_from_datadict(
self.data, self.files, self.add_prefix(name)) self.data, self.files, self.add_prefix(name))
from django.core.exceptions import ValidationError
try: try:
# We don't want to check a field when we enter an ISBN. # We don't want to check a field when we enter an ISBN.
if "isbn" not in self.data \ if "isbn" not in self.data \
@ -332,5 +350,7 @@ class MediaAdminForm(ModelForm):
self.add_error(name, e) self.add_error(name, e)
class Meta: class Meta:
model = BD model = Comic
fields = '__all__' fields = ('isbn', 'title', 'subtitle', 'external_url',
'side_identifier', 'authors', 'number_of_pages',
'publish_date', 'present', )

View File

@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-10-02 13:02+0200\n" "POT-Creation-Date: 2021-11-14 14:25+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -13,245 +13,269 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: admin.py:34 admin.py:89 admin.py:100 models.py:29 models.py:65 models.py:130 #: admin.py:46 admin.py:102 admin.py:114 models.py:30 models.py:85
#: models.py:192 models.py:243 models.py:274
msgid "authors" msgid "authors"
msgstr "auteurs" msgstr "auteurs"
#: admin.py:44 #: admin.py:56
msgid "external url" msgid "external url"
msgstr "URL externe" msgstr "URL externe"
#: admin.py:127
msgid "Turn back"
msgstr "Rendre"
#: admin.py:130 models.py:407
msgid "given back to"
msgstr "rendu à"
#: fields.py:17 #: fields.py:17
msgid "ISBN-10 or ISBN-13" msgid "ISBN-10 or ISBN-13"
msgstr "ISBN-10 ou ISBN-13" msgstr "ISBN-10 ou ISBN-13"
#: forms.py:244 #: forms.py:302
msgid "This ISBN is not found." msgid "This ISBN is not found."
msgstr "L'ISBN n'a pas été trouvé." msgstr "L'ISBN n'a pas été trouvé."
#: models.py:16 models.py:431 #: management/commands/migrate_to_new_format.py:57 models.py:156
msgid "name"
msgstr "nom"
#: models.py:21
msgid "note"
msgstr "note"
#: models.py:28
msgid "author"
msgstr "auteur"
#: models.py:35 models.py:100 models.py:162 models.py:345
msgid "ISBN"
msgstr "ISBN"
#: models.py:36 models.py:101 models.py:163 models.py:346
msgid "You may be able to scan it from a bar code."
msgstr "Peut souvent être scanné à partir du code barre."
#: models.py:43 models.py:108 models.py:170 models.py:224 models.py:263
#: models.py:294
msgid "title"
msgstr "titre"
#: models.py:48 models.py:113 models.py:175
msgid "subtitle"
msgstr "sous-titre"
#: models.py:54 models.py:119 models.py:181
msgid "external URL"
msgstr "URL externe"
#: models.py:59 models.py:124 models.py:186 models.py:229 models.py:268
msgid "side identifier"
msgstr "côte"
#: models.py:69 models.py:134 models.py:196
msgid "number of pages"
msgstr "nombre de pages"
#: models.py:75 models.py:140 models.py:202
msgid "publish date"
msgstr "date de publication"
#: models.py:81 models.py:146 models.py:208 models.py:247 models.py:278
#: models.py:329 models.py:363
msgid "present"
msgstr "présent"
#: models.py:82 models.py:147 models.py:209 models.py:248 models.py:279
#: models.py:330 models.py:364
msgid "Tell that the medium is present in the Mediatek."
msgstr "Indique que le medium est présent à la Mediatek."
#: models.py:93 models.py:355
msgid "BD"
msgstr "BD"
#: models.py:94
msgid "BDs"
msgstr "BDs"
#: models.py:155
msgid "manga"
msgstr "manga"
#: models.py:156
msgid "mangas"
msgstr "mangas"
#: models.py:217
msgid "roman"
msgstr "roman"
#: models.py:218
msgid "romans"
msgstr "romans"
#: models.py:234
msgid "rounds per minute"
msgstr "tours par minute"
#: models.py:236
msgid "33 RPM"
msgstr "33 TPM"
#: models.py:237
msgid "45 RPM"
msgstr "45 TPM"
#: models.py:256
msgid "vinyle"
msgstr "vinyle"
#: models.py:257
msgid "vinyles"
msgstr "vinyle"
#: models.py:287
msgid "CD"
msgstr "CD"
#: models.py:288
msgid "CDs" msgid "CDs"
msgstr "CDs" msgstr "CDs"
#: models.py:299 #: management/commands/migrate_to_new_format.py:57 models.py:155
msgid "number" msgid "CD"
msgstr "nombre" msgstr "CD"
#: models.py:303 #: management/commands/migrate_to_new_format.py:73 models.py:149
msgid "year" msgid "vinyls"
msgstr "année" msgstr "vinyles"
#: models.py:310 #: management/commands/migrate_to_new_format.py:73 models.py:148
msgid "month" msgid "vinyl"
msgstr "mois" msgstr "vinyle"
#: models.py:317 #: management/commands/migrate_to_new_format.py:91 models.py:196
msgid "day" msgid "reviews"
msgstr "jour"
#: models.py:324
msgid "double"
msgstr "double"
#: models.py:338
msgid "revue"
msgstr "revue"
#: models.py:339
msgid "revues"
msgstr "revues" msgstr "revues"
#: models.py:353 #: management/commands/migrate_to_new_format.py:91 models.py:195
msgid "type" msgid "review"
msgstr "type" msgstr "revue"
#: models.py:356 #: management/commands/migrate_to_new_format.py:111 models.py:315
msgid "Manga" msgid "games"
msgstr "Manga" msgstr "jeux"
#: models.py:357 #: management/commands/migrate_to_new_format.py:111 models.py:314
msgid "Roman"
msgstr "Roman"
#: models.py:369
msgid "future medium"
msgstr "medium à importer"
#: models.py:370
msgid "future media"
msgstr "medias à importer"
#: models.py:384
msgid "borrower"
msgstr "emprunteur"
#: models.py:387
msgid "borrowed on"
msgstr "emprunté le"
#: models.py:392
msgid "given back on"
msgstr "rendu le"
#: models.py:398
msgid "borrowed with"
msgstr "emprunté avec"
#: models.py:399
msgid "The keyholder that registered this borrowed item."
msgstr "Le permanencier qui enregistre cet emprunt."
#: models.py:408
msgid "The keyholder to whom this item was given back."
msgstr "Le permanencier à qui l'emprunt a été rendu."
#: models.py:415
msgid "borrowed item"
msgstr "emprunt"
#: models.py:416
msgid "borrowed items"
msgstr "emprunts"
#: models.py:436
msgid "owner"
msgstr "propriétaire"
#: models.py:441
msgid "duration"
msgstr "durée"
#: models.py:445
msgid "minimum number of players"
msgstr "nombre minimum de joueurs"
#: models.py:449
msgid "maximum number of players"
msgstr "nombre maximum de joueurs"
#: models.py:454
msgid "comment"
msgstr "commentaire"
#: models.py:461
msgid "game" msgid "game"
msgstr "jeu" msgstr "jeu"
#: models.py:462 #: models.py:17
msgid "games" msgid "name"
msgstr "jeux" 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 #: templates/media/generate_side_identifier.html:3
msgid "Generate side identifier" msgid "Generate side identifier"
@ -277,12 +301,6 @@ msgstr "ISBN invalide : mauvaise longueur"
msgid "Invalid ISBN: Only upper case allowed" msgid "Invalid ISBN: Only upper case allowed"
msgstr "ISBN invalide : seulement les majuscules sont autorisées" msgstr "ISBN invalide : seulement les majuscules sont autorisées"
#: views.py:50 #: views.py:25
msgid "Welcome to the Mediatek database" msgid "Welcome to the Mediatek database"
msgstr "Bienvenue sur la base de données de la Mediatek" msgstr "Bienvenue sur la base de données de la Mediatek"
#~ msgid "medium"
#~ msgstr "medium"
#~ msgid "media"
#~ msgstr "media"

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

View File

@ -2,7 +2,7 @@ from argparse import FileType
from sys import stdin from sys import stdin
from django.core.management import BaseCommand from django.core.management import BaseCommand
from media.models import Auteur, CD from media.models import Author, CD
class Command(BaseCommand): class Command(BaseCommand):
@ -29,7 +29,7 @@ class Command(BaseCommand):
title = cd[0] title = cd[0]
side = cd[1] side = cd[1]
authors_str = cd[2].split('|') 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] for author in authors_str]
cd, created = CD.objects.get_or_create( cd, created = CD.objects.get_or_create(
title=title, title=title,

View File

@ -1,23 +1,22 @@
from random import random from random import random
from time import sleep from time import sleep
from django.core.exceptions import ValidationError
from django.core.management import BaseCommand from django.core.management import BaseCommand
from media.forms import MediaAdminForm from media.forms import MediaAdminForm
from media.models import BD, FutureMedia, Manga, Roman from media.models import Comic, FutureMedium, Manga, Novel
class Command(BaseCommand): class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
for future_medium in FutureMedia.objects.all(): for future_medium in FutureMedium.objects.all():
isbn = future_medium.isbn isbn = future_medium.isbn
type_str = future_medium.type type_str = future_medium.type
if type_str == 'bd': if type_str == 'bd':
cl = BD cl = Comic
elif type_str == 'manga': elif type_str == 'manga':
cl = Manga cl = Manga
elif type_str == 'roman': elif type_str == 'roman':
cl = Roman cl = Novel
else: else:
self.stderr.write(self.style.WARNING( self.stderr.write(self.style.WARNING(
"Unknown medium type: {type}. Ignoring..." "Unknown medium type: {type}. Ignoring..."
@ -25,7 +24,9 @@ class Command(BaseCommand):
continue continue
if cl.objects.filter(isbn=isbn).exists(): if cl.objects.filter(isbn=isbn).exists():
self.stderr.write(self.style.WARNING(f"ISBN {isbn} for type {type_str} already exists, remove it")) self.stderr.write(self.style.WARNING(
f"ISBN {isbn} for type {type_str} already exists, "
f"remove it"))
future_medium.delete() future_medium.delete()
continue continue
@ -36,7 +37,8 @@ class Command(BaseCommand):
try: try:
form.full_clean() form.full_clean()
if hasattr(form.instance, "subtitle") and not form.instance.subtitle: if hasattr(form.instance, "subtitle") and \
not form.instance.subtitle:
form.instance.subtitle = "" form.instance.subtitle = ""
form.save() form.save()
future_medium.delete() future_medium.delete()
@ -46,4 +48,5 @@ class Command(BaseCommand):
except Exception as e: except Exception as e:
self.stderr.write(self.style.WARNING( self.stderr.write(self.style.WARNING(
"An error occured while importing ISBN {isbn}: {error}" "An error occured while importing ISBN {isbn}: {error}"
.format(isbn=isbn, error=str(e.__class__) + "(" + str(e) + ")"))) .format(isbn=isbn,
error=str(e.__class__) + "(" + str(e) + ")")))

View File

@ -3,7 +3,7 @@ from sys import stdin
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.management import BaseCommand 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 from media.validators import isbn_validator
@ -27,7 +27,7 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
type_str = options["media_type"] type_str = options["media_type"]
media_classes = [BD, Manga, Roman, FutureMedia] media_classes = [Comic, Manga, Novel, FutureMedium]
file = options["input"] file = options["input"]
isbns = [] isbns = []
@ -70,7 +70,7 @@ class Command(BaseCommand):
if isbn_exists: if isbn_exists:
continue 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" self.stdout.write(self.style.SUCCESS("ISBN {isbn} imported"
.format(isbn=isbn))) .format(isbn=isbn)))
imported += 1 imported += 1

View File

@ -2,7 +2,7 @@ from argparse import FileType
from sys import stdin from sys import stdin
from django.core.management import BaseCommand from django.core.management import BaseCommand
from media.models import Auteur, BD from media.models import Author, Comic
class Command(BaseCommand): class Command(BaseCommand):
@ -28,9 +28,9 @@ class Command(BaseCommand):
title = revue[0] title = revue[0]
number = revue[1] 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('|')] for n in revue[2].split('|')]
bd = BD.objects.create( bd = Comic.objects.create(
title=title, title=title,
subtitle=number, subtitle=number,
side_identifier="{:.3} {:.3} {:0>2}" side_identifier="{:.3} {:.3} {:0>2}"

View File

@ -2,9 +2,8 @@ from argparse import FileType
from sys import stdin from sys import stdin
from django.core.management import BaseCommand from django.core.management import BaseCommand
from media.forms import generate_side_identifier from media.forms import generate_side_identifier
from media.models import Roman, Auteur from media.models import Novel, Author
class Command(BaseCommand): class Command(BaseCommand):
@ -29,10 +28,10 @@ class Command(BaseCommand):
continue continue
title = book[1] title = book[1]
authors = [Auteur.objects.get_or_create(name=n)[0] authors = [Author.objects.get_or_create(name=n)[0]
for n in book[0].split(';')] for n in book[0].split(';')]
side_identifier = generate_side_identifier(title, authors) side_identifier = generate_side_identifier(title, authors)
roman = Roman.objects.create( roman = Novel.objects.create(
title=title, title=title,
side_identifier=side_identifier, side_identifier=side_identifier,
) )

View File

@ -2,7 +2,7 @@ from argparse import FileType
from sys import stdin from sys import stdin
from django.core.management import BaseCommand from django.core.management import BaseCommand
from media.models import Revue from media.models import Review
class Command(BaseCommand): class Command(BaseCommand):
@ -37,7 +37,7 @@ class Command(BaseCommand):
year = revue[4] year = revue[4]
if not year: if not year:
year = None year = None
revue, created = Revue.objects.get_or_create( revue, created = Review.objects.get_or_create(
title=title, title=title,
number=number.replace('*', ''), number=number.replace('*', ''),
year=year, year=year,

View File

@ -2,7 +2,7 @@ from argparse import FileType
from sys import stdin from sys import stdin
from django.core.management import BaseCommand from django.core.management import BaseCommand
from media.models import Auteur, Vinyle from media.models import Author, Vinyl
class Command(BaseCommand): class Command(BaseCommand):
@ -36,9 +36,9 @@ class Command(BaseCommand):
title = vinyle[1 if rpm == 33 else 2] title = vinyle[1 if rpm == 33 else 2]
authors_str = vinyle[2 if rpm == 33 else 1]\ authors_str = vinyle[2 if rpm == 33 else 1]\
.split('|' if rpm == 33 else ';') .split('|' if rpm == 33 else ';')
authors = [Auteur.objects.get_or_create(name=author)[0] authors = [Author.objects.get_or_create(name=author)[0]
for author in authors_str] for author in authors_str]
vinyle, created = Vinyle.objects.get_or_create( vinyle, created = Vinyl.objects.get_or_create(
title=title, title=title,
side_identifier=side, side_identifier=side,
rpm=rpm, rpm=rpm,

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

View File

@ -1,8 +1,7 @@
from django.core.management import BaseCommand from django.core.management import BaseCommand
from django.db import transaction from django.db import transaction
from media.forms import generate_side_identifier from media.forms import generate_side_identifier
from media.models import BD, Manga, Roman from media.models import Comic, Manga, Novel
class Command(BaseCommand): class Command(BaseCommand):
@ -11,22 +10,25 @@ class Command(BaseCommand):
type=str, type=str,
default='bd', default='bd',
choices=['bd', 'manga', 'roman'], choices=['bd', 'manga', 'roman'],
help="Type of medium where the sides need to be regenerated.") help="Type of medium where the "
"sides need to be regenerated.")
parser.add_argument('--noninteractivemode', '-ni', action="store_true", parser.add_argument('--noninteractivemode', '-ni', action="store_true",
help="Disable the interaction mode and replace existing side identifiers.") help="Disable the interaction mode and replace "
"existing side identifiers.")
parser.add_argument('--no-commit', '-nc', action="store_true", parser.add_argument('--no-commit', '-nc', action="store_true",
help="Only show modifications, don't commit them to database.") help="Only show modifications, don't commit "
"them to database.")
@transaction.atomic @transaction.atomic
def handle(self, *args, **options): def handle(self, *args, **options):
t = options["type"] t = options["type"]
medium_class = None medium_class = None
if t == "bd": if t == "bd":
medium_class = BD medium_class = Comic
elif t == "manga": elif t == "manga":
medium_class = Manga medium_class = Manga
elif t == "roman": elif t == "roman":
medium_class = Roman medium_class = Novel
interactive_mode = not options["noninteractivemode"] interactive_mode = not options["noninteractivemode"]
@ -37,23 +39,30 @@ class Command(BaseCommand):
if not obj.authors.all(): if not obj.authors.all():
self.stdout.write(str(obj)) self.stdout.write(str(obj))
subtitle = obj.subtitle if hasattr(obj, "subtitle") else None subtitle = obj.subtitle if hasattr(obj, "subtitle") else None
generated_side_identifier = generate_side_identifier(obj.title, obj.authors.all(), subtitle) generated_side_identifier = generate_side_identifier(
obj.title, obj.authors.all(), subtitle)
if current_side_identifier != generated_side_identifier: if current_side_identifier != generated_side_identifier:
answer = 'y' answer = 'y'
if interactive_mode: if interactive_mode:
answer = '' answer = ''
while answer != 'y' and answer != 'n': while answer != 'y' and answer != 'n':
answer = input(f"For medium {obj}, current side: {current_side_identifier}, generated side: " answer = input(f"For medium {obj}, current side: "
f"{generated_side_identifier}, would you like to replace ? [y/n]").lower()[0] f"{current_side_identifier}, "
f"generated side: "
f"{generated_side_identifier}, "
f"would you like to replace ? [y/n]")\
.lower()[0]
if answer == 'y': if answer == 'y':
self.stdout.write(self.style.WARNING(f"Replace side of {obj} from {current_side_identifier} " self.stdout.write(self.style.WARNING(
f"to {generated_side_identifier}...")) f"Replace side of {obj} from {current_side_identifier}"
f" to {generated_side_identifier}..."))
obj.side_identifier = generated_side_identifier obj.side_identifier = generated_side_identifier
if not options["no_commit"]: if not options["no_commit"]:
obj.save() obj.save()
replaced += 1 replaced += 1
if replaced: if replaced:
self.stdout.write(self.style.SUCCESS(f"{replaced} side identifiers were replaced.")) self.stdout.write(self.style.SUCCESS(
f"{replaced} side identifiers were replaced."))
else: else:
self.stdout.write(self.style.WARNING("Nothing changed.")) self.stdout.write(self.style.WARNING("Nothing changed."))

View File

@ -2,7 +2,7 @@ from time import sleep
from django.core.management import BaseCommand from django.core.management import BaseCommand
from media.forms import MediaAdminForm from media.forms import MediaAdminForm
from media.models import BD, Manga from media.models import Comic, Manga
class Command(BaseCommand): class Command(BaseCommand):
@ -14,7 +14,7 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
converted = 0 converted = 0
for media in BD.objects.all(): for media in Comic.objects.all():
if media.pk < 3400: if media.pk < 3400:
continue continue
# We sleep 5 seconds to avoid a ban from Bedetheque # We sleep 5 seconds to avoid a ban from Bedetheque

View 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'},
),
]

View 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'),
),
]

View 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'},
),
]

View 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',),
),
]

View 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',
),
]

View 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',
),
]

View File

@ -1,15 +1,16 @@
# -*- mode: python; coding: utf-8 -*- # -*- 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 # SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from polymorphic.models import PolymorphicModel
from .fields import ISBNField from .fields import ISBNField
class Auteur(models.Model): class Author(models.Model):
name = models.CharField( name = models.CharField(
max_length=255, max_length=255,
unique=True, unique=True,
@ -30,7 +31,7 @@ class Auteur(models.Model):
ordering = ['name'] ordering = ['name']
class BD(models.Model): class Borrowable(PolymorphicModel):
isbn = ISBNField( isbn = ISBNField(
_('ISBN'), _('ISBN'),
help_text=_('You may be able to scan it from a bar code.'), help_text=_('You may be able to scan it from a bar code.'),
@ -40,16 +41,35 @@ class BD(models.Model):
) )
title = models.CharField( title = models.CharField(
verbose_name=_('title'),
max_length=255, max_length=255,
verbose_name=_("title"),
) )
subtitle = models.CharField( present = models.BooleanField(
verbose_name=_('subtitle'), verbose_name=_("present"),
max_length=255, help_text=_("Tell that the medium is present in the Mediatek."),
blank=True, 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( external_url = models.URLField(
verbose_name=_('external URL'), verbose_name=_('external URL'),
blank=True, blank=True,
@ -61,10 +81,22 @@ class BD(models.Model):
) )
authors = models.ManyToManyField( authors = models.ManyToManyField(
'Auteur', 'Author',
verbose_name=_('authors'), 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( number_of_pages = models.PositiveIntegerField(
verbose_name=_('number of pages'), verbose_name=_('number of pages'),
blank=True, blank=True,
@ -77,159 +109,33 @@ class BD(models.Model):
null=True, null=True,
) )
present = models.BooleanField(
verbose_name=_("present"),
help_text=_("Tell that the medium is present in the Mediatek."),
default=False,
)
def __str__(self):
if self.subtitle:
return "{} : {}".format(self.title, self.subtitle)
else:
return self.title
class Meta: class Meta:
verbose_name = _("BD") verbose_name = _("book")
verbose_name_plural = _("BDs") verbose_name_plural = _("books")
class Comic(Book):
class Meta:
verbose_name = _("comic")
verbose_name_plural = _("comics")
ordering = ['title', 'subtitle'] ordering = ['title', 'subtitle']
class Manga(models.Model): class Manga(Book):
isbn = ISBNField(
_('ISBN'),
help_text=_('You may be able to scan it from a bar code.'),
unique=True,
blank=True,
null=True,
)
title = models.CharField(
verbose_name=_('title'),
max_length=255,
)
subtitle = models.CharField(
verbose_name=_('subtitle'),
max_length=255,
blank=True,
)
external_url = models.URLField(
verbose_name=_('external URL'),
blank=True,
)
side_identifier = models.CharField(
verbose_name=_('side identifier'),
max_length=255,
)
authors = models.ManyToManyField(
'Auteur',
verbose_name=_('authors'),
)
number_of_pages = models.PositiveIntegerField(
verbose_name=_('number of pages'),
blank=True,
null=True,
)
publish_date = models.DateField(
verbose_name=_('publish date'),
blank=True,
null=True,
)
present = models.BooleanField(
verbose_name=_("present"),
help_text=_("Tell that the medium is present in the Mediatek."),
default=False,
)
def __str__(self):
return self.title
class Meta: class Meta:
verbose_name = _("manga") verbose_name = _("manga")
verbose_name_plural = _("mangas") verbose_name_plural = _("mangas")
ordering = ['title']
class Roman(models.Model):
isbn = ISBNField(
_('ISBN'),
help_text=_('You may be able to scan it from a bar code.'),
unique=True,
blank=True,
null=True,
)
title = models.CharField(
verbose_name=_('title'),
max_length=255,
)
subtitle = models.CharField(
verbose_name=_('subtitle'),
max_length=255,
blank=True,
)
external_url = models.URLField(
verbose_name=_('external URL'),
blank=True,
)
side_identifier = models.CharField(
verbose_name=_('side identifier'),
max_length=255,
)
authors = models.ManyToManyField(
'Auteur',
verbose_name=_('authors'),
)
number_of_pages = models.PositiveIntegerField(
verbose_name=_('number of pages'),
blank=True,
null=True,
)
publish_date = models.DateField(
verbose_name=_('publish date'),
blank=True,
null=True,
)
present = models.BooleanField(
verbose_name=_("present"),
help_text=_("Tell that the medium is present in the Mediatek."),
default=False,
)
def __str__(self):
return self.title
class Meta:
verbose_name = _("roman")
verbose_name_plural = _("romans")
ordering = ['title', 'subtitle'] ordering = ['title', 'subtitle']
class Vinyle(models.Model): class Novel(Book):
title = models.CharField( class Meta:
verbose_name=_('title'), verbose_name = _("novel")
max_length=255, verbose_name_plural = _("novels")
) ordering = ['title', 'subtitle']
side_identifier = models.CharField(
verbose_name=_('side identifier'),
max_length=255,
)
class Vinyl(Medium):
rpm = models.PositiveIntegerField( rpm = models.PositiveIntegerField(
verbose_name=_('rounds per minute'), verbose_name=_('rounds per minute'),
choices=[ choices=[
@ -238,63 +144,20 @@ class Vinyle(models.Model):
], ],
) )
authors = models.ManyToManyField(
'Auteur',
verbose_name=_('authors'),
)
present = models.BooleanField(
verbose_name=_("present"),
help_text=_("Tell that the medium is present in the Mediatek."),
default=False,
)
def __str__(self):
return self.title
class Meta: class Meta:
verbose_name = _("vinyle") verbose_name = _("vinyl")
verbose_name_plural = _("vinyles") verbose_name_plural = _("vinyls")
ordering = ['title'] ordering = ['title']
class CD(models.Model): class CD(Medium):
title = models.CharField(
verbose_name=_('title'),
max_length=255,
)
side_identifier = models.CharField(
verbose_name=_('side identifier'),
max_length=255,
)
authors = models.ManyToManyField(
'Auteur',
verbose_name=_('authors'),
)
present = models.BooleanField(
verbose_name=_("present"),
help_text=_("Tell that the medium is present in the Mediatek."),
default=False,
)
def __str__(self):
return self.title
class Meta: class Meta:
verbose_name = _("CD") verbose_name = _("CD")
verbose_name_plural = _("CDs") verbose_name_plural = _("CDs")
ordering = ['title'] ordering = ['title']
class Revue(models.Model): class Review(Borrowable):
title = models.CharField(
verbose_name=_('title'),
max_length=255,
)
number = models.PositiveIntegerField( number = models.PositiveIntegerField(
verbose_name=_('number'), verbose_name=_('number'),
) )
@ -325,22 +188,16 @@ class Revue(models.Model):
default=False, default=False,
) )
present = models.BooleanField(
verbose_name=_("present"),
help_text=_("Tell that the medium is present in the Mediatek."),
default=False,
)
def __str__(self): def __str__(self):
return self.title + "" + str(self.number) return self.title + "" + str(self.number)
class Meta: class Meta:
verbose_name = _("revue") verbose_name = _("review")
verbose_name_plural = _("revues") verbose_name_plural = _("reviews")
ordering = ['title', 'number'] ordering = ['title', 'number']
class FutureMedia(models.Model): class FutureMedium(models.Model):
isbn = ISBNField( isbn = ISBNField(
_('ISBN'), _('ISBN'),
help_text=_('You may be able to scan it from a bar code.'), help_text=_('You may be able to scan it from a bar code.'),
@ -352,7 +209,7 @@ class FutureMedia(models.Model):
type = models.CharField( type = models.CharField(
_('type'), _('type'),
choices=[ choices=[
('bd', _('BD')), ('bd', _('Comic')),
('manga', _('Manga')), ('manga', _('Manga')),
('roman', _('Roman')), ('roman', _('Roman')),
], ],
@ -373,35 +230,36 @@ class FutureMedia(models.Model):
return "Future medium (ISBN: {isbn})".format(isbn=self.isbn, ) return "Future medium (ISBN: {isbn})".format(isbn=self.isbn, )
class Emprunt(models.Model): class Borrow(models.Model):
media = models.ForeignKey( borrowable = models.ForeignKey(
'BD', 'media.Borrowable',
on_delete=models.PROTECT, on_delete=models.PROTECT,
verbose_name=_('object'),
) )
user = models.ForeignKey( user = models.ForeignKey(
'users.User', settings.AUTH_USER_MODEL,
on_delete=models.PROTECT, on_delete=models.PROTECT,
verbose_name=_("borrower"), verbose_name=_("borrower"),
) )
date_emprunt = models.DateTimeField( borrow_date = models.DateTimeField(
verbose_name=_('borrowed on'), verbose_name=_('borrowed on'),
) )
date_rendu = models.DateTimeField( given_back = models.DateTimeField(
blank=True, blank=True,
null=True, null=True,
verbose_name=_('given back on'), verbose_name=_('given back on'),
) )
permanencier_emprunt = models.ForeignKey( borrowed_with = models.ForeignKey(
'users.User', settings.AUTH_USER_MODEL,
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='user_permanencier_emprunt', related_name='+',
verbose_name=_('borrowed with'), verbose_name=_('borrowed with'),
help_text=_('The keyholder that registered this borrowed item.') help_text=_('The keyholder that registered this borrowed item.')
) )
permanencier_rendu = models.ForeignKey( given_back_to = models.ForeignKey(
'users.User', settings.AUTH_USER_MODEL,
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='user_permanencier_rendu', related_name='+',
blank=True, blank=True,
null=True, null=True,
verbose_name=_('given back to'), verbose_name=_('given back to'),
@ -409,42 +267,37 @@ class Emprunt(models.Model):
) )
def __str__(self): def __str__(self):
return str(self.media) + str(self.user) return str(self.borrowable) + str(self.user)
class Meta: class Meta:
verbose_name = _("borrowed item") verbose_name = _("borrowed item")
verbose_name_plural = _("borrowed items") verbose_name_plural = _("borrowed items")
ordering = ['-date_emprunt'] ordering = ['-borrow_date']
class Jeu(models.Model): class Game(Borrowable):
DUREE = ( DURATIONS = (
('-1h', '-1h'), ('-1h', '-1h'),
('1-2h', '1-2h'), ('1-2h', '1-2h'),
('2-3h', '2-3h'), ('2-3h', '2-3h'),
('3-4h', '3-4h'), ('3-4h', '3-4h'),
('4h+', '4h+'), ('4h+', '4h+'),
) )
owner = models.ForeignKey(
name = models.CharField(
max_length=255,
verbose_name=_("name"),
)
proprietaire = models.ForeignKey(
'users.User', 'users.User',
on_delete=models.PROTECT, on_delete=models.PROTECT,
verbose_name=_("owner"), verbose_name=_("owner"),
) )
duree = models.CharField( duration = models.CharField(
choices=DUREE, choices=DURATIONS,
max_length=255, max_length=255,
verbose_name=_("duration"), verbose_name=_("duration"),
) )
nombre_joueurs_min = models.IntegerField( players_min = models.IntegerField(
validators=[MinValueValidator(1)], validators=[MinValueValidator(1)],
verbose_name=_("minimum number of players"), verbose_name=_("minimum number of players"),
) )
nombre_joueurs_max = models.IntegerField( players_max = models.IntegerField(
validators=[MinValueValidator(1)], validators=[MinValueValidator(1)],
verbose_name=_('maximum number of players'), verbose_name=_('maximum number of players'),
) )
@ -455,9 +308,9 @@ class Jeu(models.Model):
) )
def __str__(self): def __str__(self):
return str(self.name) return str(self.title)
class Meta: class Meta:
verbose_name = _("game") verbose_name = _("game")
verbose_name_plural = _("games") verbose_name_plural = _("games")
ordering = ['name'] ordering = ['title']

View File

@ -4,7 +4,7 @@
import re import re
import requests import requests
from media.models import Auteur from media.models import Author
class BedetequeScraper: class BedetequeScraper:
@ -23,7 +23,7 @@ class BedetequeScraper:
:return: CSRF token :return: CSRF token
""" """
response = self.session.get(self.referer).content.decode() 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) return re.search(regex, response).group(1)
def search_by_isbn(self, isbn: str) -> [str]: def search_by_isbn(self, isbn: str) -> [str]:
@ -44,7 +44,7 @@ class BedetequeScraper:
regex = r'href=\"(https://www\.bedetheque\.com/BD.*.html)\"' regex = r'href=\"(https://www\.bedetheque\.com/BD.*.html)\"'
return re.findall(regex, content) 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 Load BD web page and scrap data
:param bd_url: URL where to find BD data :param bd_url: URL where to find BD data
@ -99,12 +99,12 @@ class BedetequeScraper:
if 'author' not in data: if 'author' not in data:
data['authors'] = list() data['authors'] = list()
if author: if author:
author_obj = Auteur.objects.get_or_create( author_obj = Author.objects.get_or_create(
name=author.group(1))[0] name=author.group(1))[0]
data['authors'].append(author_obj) data['authors'].append(author_obj)
illustrator = re.search(regex_illustrator, content) illustrator = re.search(regex_illustrator, content)
if illustrator: if illustrator:
author_obj = Auteur.objects.get_or_create( author_obj = Author.objects.get_or_create(
name=illustrator.group(1))[0] name=illustrator.group(1))[0]
data['authors'].append(author_obj) data['authors'].append(author_obj)

View File

@ -1,17 +1,18 @@
from rest_framework import serializers from rest_framework import serializers
from .models import Auteur, BD, CD, FutureMedia, Manga, Emprunt, Jeu, Revue, Roman, Vinyle from .models import Author, Borrow, CD, Comic, FutureMedium, Manga, Game, \
Novel, Review, Vinyl
class AuteurSerializer(serializers.ModelSerializer): class AuthorSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Auteur model = Author
fields = ['url', 'name'] fields = ['url', 'name']
class BDSerializer(serializers.ModelSerializer): class ComicSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = BD model = Comic
fields = '__all__' fields = '__all__'
@ -27,39 +28,37 @@ class CDSerializer(serializers.ModelSerializer):
fields = '__all__' fields = '__all__'
class VinyleSerializer(serializers.ModelSerializer): class VinylSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Vinyle model = Vinyl
fields = '__all__' fields = '__all__'
class RomanSerializer(serializers.ModelSerializer): class NovelSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Roman model = Novel
fields = '__all__' fields = '__all__'
class RevueSerializer(serializers.ModelSerializer): class ReviewSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Revue model = Review
fields = '__all__' fields = '__all__'
class FutureMediaSerializer(serializers.ModelSerializer): class FutureMediumSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = FutureMedia model = FutureMedium
fields = '__all__' fields = '__all__'
class EmpruntSerializer(serializers.HyperlinkedModelSerializer): class BorrowSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = Emprunt model = Borrow
fields = ['url', 'media', 'user', 'date_emprunt', 'date_rendu', fields = '__all__'
'permanencier_emprunt', 'permanencier_rendu']
class JeuSerializer(serializers.HyperlinkedModelSerializer): class GameSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = Jeu model = Game
fields = ['url', 'name', 'proprietaire', 'duree', 'nombre_joueurs_min', fields = '__all__'
'nombre_joueurs_max', 'comment']

View File

@ -36,22 +36,22 @@
document.getElementById("isbn").focus(); document.getElementById("isbn").focus();
let bd_request = new XMLHttpRequest(); let bd_request = new XMLHttpRequest();
bd_request.open('GET', '/api/media/bd/?search=' + isbn, true); bd_request.open('GET', '/api/media/comic/?search=' + isbn, true);
bd_request.onload = function () { bd_request.onload = function () {
let data = JSON.parse(this.response); let data = JSON.parse(this.response);
data.results.forEach(bd => { data.results.forEach(comic => {
let present = bd.present; let present = comic.present;
if (markAsPresent && isbn === bd.isbn) { if (markAsPresent && isbn === comic.isbn) {
present = true; present = true;
let presentRequest = new XMLHttpRequest(); let presentRequest = new XMLHttpRequest();
presentRequest.open("GET", "/media/mark-as-present/bd/" + bd.id + "/", true); presentRequest.open("GET", "/media/mark-as-present/bd/" + comic.id + "/", true);
presentRequest.send(); presentRequest.send();
} }
result_div.innerHTML += "<li id='bd_" + bd.id + "'>" + result_div.innerHTML += "<li id='comic_" + comic.id + "'>" +
"<a href='/database/media/bd/" + bd.id + "/change/'>BD : " "<a href='/database/media/comic/" + comic.id + "/change/'>BD : "
+ bd.title + (bd.subtitle ? " - " + bd.subtitle : "") + "</a>" + comic.title + (comic.subtitle ? " - " + comic.subtitle : "") + "</a>"
+ (present ? " (<a class='absent' href='#' onclick=\"markAsPresent('bd', " + bd.id + ", false)\">marquer comme absent</a>)" + (present ? " (<a class='absent' href='#' onclick=\"markAsPresent('bd', " + comic.id + ", false)\">marquer comme absent</a>)"
: " (absent, <a class='present' href='#' onclick=\"markAsPresent('bd', " + bd.id + ")\">marquer comme présent</a>)") + "</li>"; : " (absent, <a class='present' href='#' onclick=\"markAsPresent('bd', " + comic.id + ")\">marquer comme présent</a>)") + "</li>";
}); });
} }
bd_request.send(); bd_request.send();
@ -92,35 +92,35 @@
cd_request.send(); cd_request.send();
let vinyle_request = new XMLHttpRequest(); let vinyle_request = new XMLHttpRequest();
vinyle_request.open('GET', '/api/media/vinyle/?search=' + isbn, true); vinyle_request.open('GET', '/api/media/vinyl/?search=' + isbn, true);
vinyle_request.onload = function () { vinyle_request.onload = function () {
let data = JSON.parse(this.response); let data = JSON.parse(this.response);
data.results.forEach(vinyle => { data.results.forEach(vinyl => {
let present = markAsPresent || vinyle.present; let present = markAsPresent || vinyl.present;
result_div.innerHTML += "<li id='vinyle_" + vinyle.id + "'>" + result_div.innerHTML += "<li id='vinyl_" + vinyl.id + "'>" +
"<a href='/database/media/vinyle/" + vinyle.id + "/change/'>Vinyle : " + vinyle.title + "</a>" "<a href='/database/media/vinyl/" + vinyl.id + "/change/'>Vinyle : " + vinyl.title + "</a>"
+ (present ? " (<a class='absent' href='#' onclick=\"markAsPresent('vinyle', " + vinyle.id + ", false)\">marquer comme absent</a>)" + (present ? " (<a class='absent' href='#' onclick=\"markAsPresent('vinyl', " + vinyl.id + ", false)\">marquer comme absent</a>)"
: " (absent, <a class='present' href='#' onclick=\"markAsPresent('vinyle', " + vinyle.id + ")\">marquer comme présent</a>)") + "</li>"; : " (absent, <a class='present' href='#' onclick=\"markAsPresent('vinyl', " + vinyl.id + ")\">marquer comme présent</a>)") + "</li>";
}); });
} }
vinyle_request.send(); vinyle_request.send();
let roman_request = new XMLHttpRequest(); let roman_request = new XMLHttpRequest();
roman_request.open('GET', '/api/media/roman/?search=' + isbn, true); roman_request.open('GET', '/api/media/novel/?search=' + isbn, true);
roman_request.onload = function () { roman_request.onload = function () {
let data = JSON.parse(this.response); let data = JSON.parse(this.response);
data.results.forEach(roman => { data.results.forEach(novel => {
let present = roman.present; let present = novel.present;
if (markAsPresent && isbn === roman.isbn) { if (markAsPresent && isbn === novel.isbn) {
present = true; present = true;
let presentRequest = new XMLHttpRequest(); let presentRequest = new XMLHttpRequest();
presentRequest.open("GET", "/media/mark-as-present/roman/" + roman.id + "/", true); presentRequest.open("GET", "/media/mark-as-present/novel/" + novel.id + "/", true);
presentRequest.send(); presentRequest.send();
} }
result_div.innerHTML += "<li id='roman_" + roman.id + "'>" + result_div.innerHTML += "<li id='roman_" + novel.id + "'>" +
"<a href='/database/media/roman/" + roman.id + "/change/'>Roman : " + roman.title + "</a>" "<a href='/database/media/roman/" + novel.id + "/change/'>Roman : " + novel.title + "</a>"
+ (present ? " (<a class='absent' href='#' onclick=\"markAsPresent('roman', " + roman.id + ", false)\">marquer comme absent</a>)" + (present ? " (<a class='absent' href='#' onclick=\"markAsPresent('novel', " + novel.id + ", false)\">marquer comme absent</a>)"
: " (absent, <a class='present' href='#' onclick=\"markAsPresent('roman', " + roman.id + ")\">marquer comme présent</a>)") + "</li>"; : " (absent, <a class='present' href='#' onclick=\"markAsPresent('novel', " + novel.id + ")\">marquer comme présent</a>)") + "</li>";
}); });
} }
roman_request.send(); roman_request.send();

View File

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

View File

@ -3,7 +3,7 @@
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from media.models import Auteur, BD from media.models import Author, Comic
from users.models import User from users.models import User
""" """
@ -21,44 +21,44 @@ class TemplateTests(TestCase):
self.client.force_login(self.user) self.client.force_login(self.user)
# Create an author # Create an author
self.dummy_author = Auteur.objects.create(name="Test author") self.dummy_author = Author.objects.create(name="Test author")
# Create media # Create media
self.dummy_bd1 = BD.objects.create( self.dummy_bd1 = Comic.objects.create(
title="Test media", title="Test media",
side_identifier="T M", side_identifier="T M",
) )
self.dummy_bd1.authors.add(self.dummy_author) self.dummy_bd1.authors.add(self.dummy_author)
self.dummy_bd2 = BD.objects.create( self.dummy_bd2 = Comic.objects.create(
title="Test media bis", title="Test media bis",
side_identifier="T M 2", side_identifier="T M 2",
external_url="https://example.com/", external_url="https://example.com/",
) )
self.dummy_bd2.authors.add(self.dummy_author) self.dummy_bd2.authors.add(self.dummy_author)
def test_bd_bd_changelist(self): def test_comic_bd_changelist(self):
response = self.client.get(reverse('admin:media_bd_changelist')) response = self.client.get(reverse('admin:media_comic_changelist'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_bd_bd_add(self): def test_comic_bd_add(self):
response = self.client.get(reverse('admin:media_bd_add')) response = self.client.get(reverse('admin:media_comic_add'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_bd_isbn_download(self): def test_comic_isbn_download(self):
data = { data = {
'_isbn': True, '_isbn': True,
'isbn': "0316358525", 'isbn': "0316358525",
} }
response = self.client.post(reverse( response = self.client.post(reverse(
'admin:media_bd_change', 'admin:media_comic_change',
args=[self.dummy_bd1.id], args=[self.dummy_bd1.id],
), data=data) ), data=data)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
def test_bd_emprunt_changelist(self): def test_comic_borrow_changelist(self):
response = self.client.get(reverse('admin:media_emprunt_changelist')) response = self.client.get(reverse('admin:media_borrow_changelist'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_bd_emprunt_add(self): def test_comic_borrow_add(self):
response = self.client.get(reverse('admin:media_emprunt_add')) response = self.client.get(reverse('admin:media_borrow_add'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)

View File

@ -2,21 +2,32 @@
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay # Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.conf.urls import url
from django.urls import path from django.urls import path
from . import views from . import views
app_name = 'media' app_name = 'media'
urlpatterns = [ urlpatterns = [
url(r'^retour_emprunt/(?P<empruntid>[0-9]+)$', views.retour_emprunt,
name='retour-emprunt'),
path('find/', views.FindMediumView.as_view(), name="find"), path('find/', views.FindMediumView.as_view(), name="find"),
path('mark-as-present/bd/<int:pk>/', views.MarkBDAsPresent.as_view(), name="mark_bd_as_present"), path('mark-as-present/comic/<int:pk>/',
path('mark-as-present/manga/<int:pk>/', views.MarkMangaAsPresent.as_view(), name="mark_manga_as_present"), views.MarkComicAsPresent.as_view(),
path('mark-as-present/cd/<int:pk>/', views.MarkCDAsPresent.as_view(), name="mark_cd_as_present"), name="mark_comic_as_present"),
path('mark-as-present/vinyle/<int:pk>/', views.MarkVinyleAsPresent.as_view(), name="mark_vinyle_as_present"), path('mark-as-present/manga/<int:pk>/',
path('mark-as-present/roman/<int:pk>/', views.MarkRomanAsPresent.as_view(), name="mark_roman_as_present"), views.MarkMangaAsPresent.as_view(),
path('mark-as-present/revue/<int:pk>/', views.MarkRevueAsPresent.as_view(), name="mark_revue_as_present"), name="mark_manga_as_present"),
path('mark-as-present/future/<int:pk>/', views.MarkFutureAsPresent.as_view(), name="mark_future_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"),
] ]

View File

@ -1,54 +1,34 @@
# -*- mode: python; coding: utf-8 -*- # -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay # Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from http.client import NO_CONTENT
from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse from django.http import HttpResponse
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from django.db import transaction from django.shortcuts import redirect
from django.shortcuts import redirect, render
from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView, DetailView from django.views.generic import TemplateView, DetailView
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.filters import SearchFilter from rest_framework.filters import SearchFilter
from reversion import revisions as reversion
from .models import Auteur, BD, CD, Emprunt, FutureMedia, Jeu, Manga, Revue, Roman, Vinyle from .models import Author, Borrow, CD, Comic, FutureMedium, Game, Manga,\
from .serializers import AuteurSerializer, BDSerializer, CDSerializer, EmpruntSerializer, FutureMediaSerializer, \ Novel, Review, Vinyl
JeuSerializer, MangaSerializer, RevueSerializer, RomanSerializer, VinyleSerializer from .serializers import AuthorSerializer, BorrowSerializer, ComicSerializer, \
CDSerializer, FutureMediumSerializer, GameSerializer, MangaSerializer, \
NovelSerializer, ReviewSerializer, VinylSerializer
@login_required class IndexView(TemplateView):
@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):
""" """
Home page which redirect to admin when logged in Home page which redirect to admin when logged in
""" """
if request.user.is_authenticated: extra_context = {'title': _('Welcome to the Mediatek database')}
return redirect('admin:index') template_name = 'admin/index.html'
else:
return render(request, 'admin/index.html', { def dispatch(self, request, *args, **kwargs):
'title': _('Welcome to the Mediatek database'), if request.user.is_authenticated:
}) return redirect('admin:index')
return super().dispatch(request, *args, **kwargs)
class FindMediumView(LoginRequiredMixin, TemplateView): class FindMediumView(LoginRequiredMixin, TemplateView):
@ -63,8 +43,8 @@ class MarkMediumAsPresent(LoginRequiredMixin, DetailView):
return HttpResponse("", content_type=204) return HttpResponse("", content_type=204)
class MarkBDAsPresent(MarkMediumAsPresent): class MarkComicAsPresent(MarkMediumAsPresent):
model = BD model = Comic
class MarkMangaAsPresent(MarkMediumAsPresent): class MarkMangaAsPresent(MarkMediumAsPresent):
@ -75,39 +55,40 @@ class MarkCDAsPresent(MarkMediumAsPresent):
model = CD model = CD
class MarkVinyleAsPresent(MarkMediumAsPresent): class MarkVinylAsPresent(MarkMediumAsPresent):
model = Vinyle model = Vinyl
class MarkRomanAsPresent(MarkMediumAsPresent): class MarkNovelAsPresent(MarkMediumAsPresent):
model = Roman model = Novel
class MarkRevueAsPresent(MarkMediumAsPresent): class MarkReviewAsPresent(MarkMediumAsPresent):
model = Revue model = Review
class MarkFutureAsPresent(MarkMediumAsPresent): class MarkFutureAsPresent(MarkMediumAsPresent):
model = FutureMedia model = FutureMedium
class AuteurViewSet(viewsets.ModelViewSet): class AuthorViewSet(viewsets.ModelViewSet):
""" """
API endpoint that allows authors to be viewed or edited. API endpoint that allows authors to be viewed or edited.
""" """
queryset = Auteur.objects.all() queryset = Author.objects.all()
serializer_class = AuteurSerializer serializer_class = AuthorSerializer
class BDViewSet(viewsets.ModelViewSet): class ComicViewSet(viewsets.ModelViewSet):
""" """
API endpoint that allows media to be viewed or edited. API endpoint that allows media to be viewed or edited.
""" """
queryset = BD.objects.all() queryset = Comic.objects.all()
serializer_class = BDSerializer serializer_class = ComicSerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ["isbn", "side_identifier"] filterset_fields = ["isbn", "side_identifier"]
search_fields = ["=isbn", "title", "subtitle", "side_identifier", "authors__name"] search_fields = ["=isbn", "title", "subtitle", "side_identifier",
"authors__name"]
class MangaViewSet(viewsets.ModelViewSet): class MangaViewSet(viewsets.ModelViewSet):
@ -118,7 +99,8 @@ class MangaViewSet(viewsets.ModelViewSet):
serializer_class = MangaSerializer serializer_class = MangaSerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ["isbn", "side_identifier"] filterset_fields = ["isbn", "side_identifier"]
search_fields = ["=isbn", "title", "subtitle", "side_identifier", "authors__name"] search_fields = ["=isbn", "title", "subtitle", "side_identifier",
"authors__name"]
class CDViewSet(viewsets.ModelViewSet): class CDViewSet(viewsets.ModelViewSet):
@ -132,61 +114,62 @@ class CDViewSet(viewsets.ModelViewSet):
search_fields = ["title", "side_identifier", "authors__name"] search_fields = ["title", "side_identifier", "authors__name"]
class VinyleViewSet(viewsets.ModelViewSet): class VinylViewSet(viewsets.ModelViewSet):
""" """
API endpoint that allows media to be viewed or edited. API endpoint that allows media to be viewed or edited.
""" """
queryset = Vinyle.objects.all() queryset = Vinyl.objects.all()
serializer_class = VinyleSerializer serializer_class = VinylSerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ["side_identifier", "rpm"] filterset_fields = ["side_identifier", "rpm"]
search_fields = ["title", "side_identifier", "authors__name"] search_fields = ["title", "side_identifier", "authors__name"]
class RomanViewSet(viewsets.ModelViewSet): class NovelViewSet(viewsets.ModelViewSet):
""" """
API endpoint that allows media to be viewed or edited. API endpoint that allows media to be viewed or edited.
""" """
queryset = Roman.objects.all() queryset = Novel.objects.all()
serializer_class = RomanSerializer serializer_class = NovelSerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ["isbn", "side_identifier", "number_of_pages"] filterset_fields = ["isbn", "side_identifier", "number_of_pages"]
search_fields = ["=isbn", "title", "subtitle", "side_identifier", "authors__name"] search_fields = ["=isbn", "title", "subtitle", "side_identifier",
"authors__name"]
class RevueViewSet(viewsets.ModelViewSet): class ReviewViewSet(viewsets.ModelViewSet):
""" """
API endpoint that allows media to be viewed or edited. API endpoint that allows media to be viewed or edited.
""" """
queryset = Revue.objects.all() queryset = Review.objects.all()
serializer_class = RevueSerializer serializer_class = ReviewSerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ["number", "year", "month", "day", "double"] filterset_fields = ["number", "year", "month", "day", "double"]
search_fields = ["title"] search_fields = ["title"]
class FutureMediaViewSet(viewsets.ModelViewSet): class FutureMediumViewSet(viewsets.ModelViewSet):
""" """
API endpoint that allows media to be viewed or edited. API endpoint that allows media to be viewed or edited.
""" """
queryset = FutureMedia.objects.all() queryset = FutureMedium.objects.all()
serializer_class = FutureMediaSerializer serializer_class = FutureMediumSerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ["isbn"] filterset_fields = ["isbn"]
search_fields = ["=isbn"] search_fields = ["=isbn"]
class EmpruntViewSet(viewsets.ModelViewSet): class BorrowViewSet(viewsets.ModelViewSet):
""" """
API endpoint that allows borrowed items to be viewed or edited. API endpoint that allows borrowed items to be viewed or edited.
""" """
queryset = Emprunt.objects.all() queryset = Borrow.objects.all()
serializer_class = EmpruntSerializer serializer_class = BorrowSerializer
class JeuViewSet(viewsets.ModelViewSet): class GameViewSet(viewsets.ModelViewSet):
""" """
API endpoint that allows games to be viewed or edited. API endpoint that allows games to be viewed or edited.
""" """
queryset = Jeu.objects.all() queryset = Game.objects.all()
serializer_class = JeuSerializer serializer_class = GameSerializer

View File

@ -1,12 +1,9 @@
Django~=2.2.10 authlib~=0.15
docutils~=0.14 docutils~=0.16 # for Django-admin docs
Pillow>=5.4.1 Django~=2.2
pytz~=2019.1 django-filter~=2.4
six~=1.12.0 django-polymorphic~=3.0
sqlparse~=0.2.4 django-reversion~=3.0
django-filter~=2.1.0 djangorestframework~=3.12
django-reversion~=3.0.3 django_extensions~=3.0
python-stdnum~=1.10 requests~=2.25 # for scrapping
djangorestframework~=3.9.0
pyyaml~=3.13
coreapi~=2.3.3

View File

@ -54,7 +54,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if user.is_authenticated %} {% if user.is_authenticated %}
<a href="{% url 'logout' %}">{% trans 'Log out' %}</a> <a href="{% url 'logout' %}">{% trans 'Log out' %}</a>
{% else %} {% else %}
<a href="{% url 'login' %}">{% trans 'Log in' %}</a> <a href="{% url 'users:login' %}">{% trans 'Log in' %}</a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
</div> </div>
@ -96,7 +96,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
</noscript> </noscript>
</form> </form>
<p> <p>
Mediatek 2017-2020 &mdash; Mediatek 2017-2021 &mdash;
<a href="mailto:club-med@crans.org">Nous contacter</a> &mdash; <a href="mailto:club-med@crans.org">Nous contacter</a> &mdash;
<a href="{% url "api-root" %}">Explorer l'API</a> <a href="{% url "api-root" %}">Explorer l'API</a>
</p> </p>

View File

@ -56,9 +56,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
<h3> <h3>
{% trans 'My profile' %} {% trans 'My profile' %}
<small><a class="changelink" href="{% url 'users:edit-info' %}">
{% trans 'Edit' %}
</a></small>
</h3> </h3>
<ul> <ul>
<li><strong>{% trans 'username' %}</strong> : {{ user.username }}</li> <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 'date joined' %}</strong> : {{ user.date_joined }}</li>
<li><strong>{% trans 'last login' %}</strong> : {{ user.last_login }}</li> <li><strong>{% trans 'last login' %}</strong> : {{ user.last_login }}</li>
<li><strong>{% trans 'address' %}</strong> : {{ user.address }}</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><strong>{% trans 'groups' %}</strong> : {% for g in user.groups.all %}{{ g.name }} {% endfor %}
</li> </li>
<li><strong>{% trans 'maximum borrowed' %}</strong> : {{ user.maxemprunt }}</li>
<li> <li>
<strong>{% trans 'membership for current year' %}</strong> : <strong>{% trans 'membership for current year' %}</strong> :
{% if user.is_member %} {% if user.is_member %}
@ -84,8 +80,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
<h3>{% trans 'Current borrowed items' %}</h3> <h3>{% trans 'Current borrowed items' %}</h3>
{% if borrowed_items %} {% if borrowed_items %}
<ul> <ul>
{% for emprunt in borrowed_items %} {% for borrow in borrowed_items %}
<li>{{ emprunt.media }} ({% trans 'since' %} {{ emprunt.date_emprunt }})</li> <li>{{ borrow.object }} ({% trans 'since' %} {{ borrow.borrow_date }})</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% else %} {% else %}

View File

@ -1,5 +1,5 @@
[tox] [tox]
envlist = py37,py38,linters envlist = py37,py38,py39,linters
skipsdist = True skipsdist = True
[testenv] [testenv]

View File

@ -3,22 +3,14 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin from django.contrib import admin
from django.contrib import messages
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import PasswordResetForm from django.utils import timezone
from django.urls import reverse from django.utils.safestring import mark_safe
from django.utils.html import format_html
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from med.admin import admin_site from med.admin import admin_site
from .forms import UserCreationAdminForm from .models import User
from .models import Adhesion, User
class AdhesionAdmin(VersionAdmin):
list_display = ('starting_in', 'ending_in')
autocomplete_fields = ('members',)
class IsMemberFilter(admin.SimpleListFilter): class IsMemberFilter(admin.SimpleListFilter):
@ -31,12 +23,12 @@ class IsMemberFilter(admin.SimpleListFilter):
) )
def queryset(self, request, queryset): def queryset(self, request, queryset):
value = self.value() if self.parameter_name in request.GET:
if value == 'Yes': queryset = queryset.filter(
# Get current membership year and list all members membership__date_start__lte=timezone.now(),
last_adh_year = Adhesion.objects.all().order_by('starting_in') \ membership__date_end__gte=timezone.now(),
.reverse().first() ).distinct()
return last_adh_year.members
return queryset return queryset
@ -45,62 +37,32 @@ class UserAdmin(VersionAdmin, BaseUserAdmin):
fieldsets = ( fieldsets = (
(None, {'fields': ('username', 'password')}), (None, {'fields': ('username', 'password')}),
(_('Personal info'), {'fields': ('first_name', 'last_name', 'email', (_('Personal info'), {'fields': ('first_name', 'last_name', 'email',
'telephone', 'address', 'comment')}), 'phone_number', 'address',
'comment')}),
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions', 'groups', 'user_permissions')}),
'maxemprunt')}),
(_('Important dates'), {'fields': ('last_login', 'date_joined')}), (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
) )
list_display = ('username', 'email', 'first_name', 'last_name', 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', list_filter = (IsMemberFilter, 'is_staff', 'is_superuser', 'is_active',
'groups') 'groups')
# Customize required initial fields def has_add_permission(self, request):
add_form_template = 'admin/change_form.html' # Only add users through Note Kfet login
add_form = UserCreationAdminForm return False
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 is_member(self, obj): def is_member(self, obj):
""" """
Get current membership year and check if user is there Get current membership year and check if user is there
""" """
last_adh_year = Adhesion.objects.all().order_by('starting_in') \ if obj.is_member:
.reverse().first() return mark_safe(
is_member = last_adh_year and obj in last_adh_year.members.all()
if is_member:
return format_html(
'<img src="/static/admin/img/icon-yes.svg" alt="True">' '<img src="/static/admin/img/icon-yes.svg" alt="True">'
) )
else: else:
return format_html( return mark_safe(
'<img src="/static/admin/img/icon-no.svg" alt="False"> ' '<img src="/static/admin/img/icon-no.svg" alt="False">'
'<a class="button" href="{}">{}</a>',
reverse('users:adherer', args=[obj.pk]),
_('Adhere')
) )
is_member.short_description = _('is member') is_member.short_description = _('is member')
@ -108,4 +70,3 @@ class UserAdmin(VersionAdmin, BaseUserAdmin):
admin_site.register(User, UserAdmin) admin_site.register(User, UserAdmin)
admin_site.register(Adhesion, AdhesionAdmin)

View File

@ -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}

View 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',
),
]

View 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',
},
),
]

View 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',
},
),
]

View 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',
),
]

View 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'),
),
]

View File

@ -1,16 +1,21 @@
# -*- mode: python; coding: utf-8 -*- # -*- 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 # 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.contrib.auth.models import AbstractUser
from django.db import models from django.db import models
from django.db.models import Q
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from med.settings import MAX_EMPRUNT
class User(AbstractUser): class User(AbstractUser):
telephone = models.CharField( phone_number = models.CharField(
verbose_name=_('phone number'), verbose_name=_('phone number'),
max_length=15, max_length=15,
blank=True, blank=True,
@ -20,12 +25,6 @@ class User(AbstractUser):
max_length=255, max_length=255,
blank=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( comment = models.CharField(
verbose_name=_('comment'), verbose_name=_('comment'),
help_text=_('Promotion...'), help_text=_('Promotion...'),
@ -33,7 +32,7 @@ class User(AbstractUser):
blank=True, blank=True,
) )
date_joined = models.DateTimeField( date_joined = models.DateTimeField(
_('date joined'), verbose_name=_('date joined'),
default=timezone.now, default=timezone.now,
null=True, null=True,
) )
@ -42,31 +41,171 @@ class User(AbstractUser):
@property @property
def is_member(self): def is_member(self):
last_year = Adhesion.objects.all().order_by( """
'starting_in').reverse().first() Return True if user is member of the club.
return last_year and self in last_year.members.all() """
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): class Membership(models.Model):
starting_in = models.IntegerField( user = models.ForeignKey(
verbose_name=_('starting in'), User,
help_text=_('Year in which the membership year starts.'), on_delete=models.CASCADE,
unique=True, verbose_name=_('user'),
)
ending_in = models.IntegerField(
verbose_name=_('ending in'),
help_text=_('Year in which the membership year ends.'),
unique=True,
)
members = models.ManyToManyField(
'User',
verbose_name=_('members'),
blank=True,
) )
class Meta: date_start = models.DateField(
verbose_name = _('membership year') verbose_name=_('start date'),
verbose_name_plural = _('membership years') )
date_end = models.DateField(
verbose_name=_('start date'),
)
def __str__(self): def __str__(self):
return f"{self.starting_in} - {self.ending_in}" return f'{self.user}: {self.date_start} to {self.date_end}'
class Meta:
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')

View File

@ -8,7 +8,7 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = User model = User
fields = ['url', 'username', 'first_name', 'last_name', 'email', fields = ['url', 'username', 'first_name', 'last_name', 'email',
'groups', 'telephone', 'address', 'maxemprunt', 'comment', 'groups', 'phone_number', 'address', 'comment',
'date_joined'] 'date_joined']

View File

@ -1,7 +1,6 @@
# -*- mode: python; coding: utf-8 -*- # -*- mode: python; coding: utf-8 -*-
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.core import mail
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from users.models import User from users.models import User
@ -20,30 +19,6 @@ class TemplateTests(TestCase):
) )
self.client.force_login(self.user) 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): def test_users_user_changelist(self):
response = self.client.get(reverse('admin:users_user_changelist')) response = self.client.get(reverse('admin:users_user_changelist'))
self.assertEqual(response.status_code, 200) 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)

View File

@ -8,6 +8,6 @@ from . import views
app_name = 'users' app_name = 'users'
urlpatterns = [ urlpatterns = [
url(r'^edit_info/$', views.edit_info, name='edit-info'), url('login/', views.LoginView.as_view(), name='login'),
url(r'^adherer/(?P<userid>[0-9]+)$', views.adherer, name='adherer'), url('authorize/', views.AuthorizeView.as_view(), name='auth'),
] ]

View File

@ -1,68 +1,47 @@
# -*- mode: python; coding: utf-8 -*- # -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay # Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from datetime import datetime
from django.contrib import messages from authlib.integrations.django_client import OAuth
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth import login
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.db import transaction from django.urls import reverse
from django.shortcuts import redirect, render from django.utils import timezone
from django.template.context_processors import csrf from django.views.generic import RedirectView
from django.utils.translation import ugettext_lazy as _
from rest_framework import viewsets from rest_framework import viewsets
from reversion import revisions as reversion from users.models import User, AccessToken
from users.forms import BaseInfoForm
from users.models import Adhesion, User
from .serializers import GroupSerializer, UserSerializer from .serializers import GroupSerializer, UserSerializer
def form(ctx, template, request): class LoginView(RedirectView):
c = ctx def get_redirect_url(self, *args, **kwargs):
c.update(csrf(request)) oauth = OAuth()
return render(request, template, c) 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 class AuthorizeView(RedirectView):
def edit_info(request): def get_redirect_url(self, *args, **kwargs):
""" oauth = OAuth()
Edite son utilisateur oauth.register('notekfet')
""" token = oauth.notekfet.authorize_access_token(self.request)
user = BaseInfoForm(request.POST or None, instance=request.user) token_obj = AccessToken.objects.create(
if user.is_valid(): access_token=token['access_token'],
with transaction.atomic(), reversion.create_revision(): expires_in=token['expires_in'],
user.save() scopes=token['scope'],
reversion.set_user(request.user) refresh_token=token['refresh_token'],
reversion.set_comment("Champs modifié(s) : %s" % ', '.join( expires_at=timezone.utc.fromutc(
field for field in user.changed_data)) datetime.fromtimestamp(token['expires_at'])),
messages.success(request, "L'user a bien été modifié") )
return redirect("index") user = token_obj.fetch_user(True)
return form({ self.request.session['access_token_id'] = token_obj.id
'form': user, self.request.session.save()
'password_change': True, login(self.request, user)
'title': _('Edit user profile'), return reverse('index')
}, '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 UserViewSet(viewsets.ModelViewSet): class UserViewSet(viewsets.ModelViewSet):