1
0
mirror of https://gitlab.crans.org/mediatek/med.git synced 2025-07-05 23:23:56 +02:00

Compare commits

..

25 Commits

Author SHA1 Message Date
3be0c99811 Separate comics per first letter in exported Markdown site
Signed-off-by: Yohann D'Anello <ynerant@crans.org>
2022-04-12 16:26:48 +02: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
32 changed files with 871 additions and 907 deletions

View File

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

View File

@ -167,9 +167,26 @@ PAGINATION_NUMBER = 25
AUTH_USER_MODEL = 'users.User'
MAX_EMPRUNT = 5 # Max emprunts
NOTE_KFET_URL = 'https://note.crans.org'
NOTE_KFET_CLIENT_ID = 'CHANGE_ME'
NOTE_KFET_CLIENT_SECRET = 'CHANGE_ME'
NOTE_KFET_SCOPES = '1_1 2_1 48_1'
try:
from .settings_local import *
except ImportError:
pass
AUTHLIB_OAUTH_CLIENTS = {
'notekfet': {
'client_id': f'{NOTE_KFET_CLIENT_ID}',
'client_secret': f'{NOTE_KFET_CLIENT_SECRET}',
'access_token_url': f'{NOTE_KFET_URL}/o/token/',
'refresh_token_url': f'{NOTE_KFET_URL}/o/token/',
'authorize_url': f'{NOTE_KFET_URL}/o/authorize/',
'userinfo_endpoint': f'{NOTE_KFET_URL}/api/me/',
'client_kwargs': {
'scope': NOTE_KFET_SCOPES,
}
}
}

View File

@ -40,3 +40,8 @@ DATABASES = {
'PORT': '',
}
}
NOTE_KFET_URL = 'https://note.crans.org'
NOTE_KFET_CLIENT_ID = 'CHANGE_ME'
NOTE_KFET_CLIENT_SECRET = 'CHANGE_ME'
NOTE_KFET_SCOPES = '1_1 2_1 48_1'

View File

@ -21,7 +21,7 @@ router.register(r'media/vinyl', media.views.VinylViewSet)
router.register(r'media/novel', media.views.NovelViewSet)
router.register(r'media/review', media.views.ReviewViewSet)
router.register(r'media/future', media.views.FutureMediumViewSet)
router.register(r'borrowed_items', media.views.EmpruntViewSet)
router.register(r'borrowed_items', media.views.BorrowViewSet)
router.register(r'games', media.views.GameViewSet)
router.register(r'users', users.views.UserViewSet)
router.register(r'groups', users.views.GroupViewSet)

View File

@ -2,7 +2,6 @@
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import ugettext_lazy as _
from polymorphic.admin import PolymorphicChildModelAdmin, \
@ -11,7 +10,7 @@ from med.admin import admin_site
from reversion.admin import VersionAdmin
from .forms import MediaAdminForm
from .models import Author, Borrowable, CD, Comic, Emprunt, FutureMedium, \
from .models import Author, Borrow, Borrowable, CD, Comic, FutureMedium, \
Game, Manga, Novel, Review, Vinyl
@ -120,30 +119,15 @@ class ReviewAdmin(VersionAdmin, PolymorphicChildModelAdmin):
show_in_index = True
class EmpruntAdmin(VersionAdmin):
list_display = ('media', 'user', 'date_emprunt', 'date_rendu',
'permanencier_emprunt', 'permanencier_rendu_custom')
search_fields = ('media__title', 'media__side_identifier',
'user__username', 'date_emprunt', 'date_rendu')
date_hierarchy = 'date_emprunt'
autocomplete_fields = ('media', 'user', 'permanencier_emprunt',
'permanencier_rendu')
def permanencier_rendu_custom(self, obj):
"""
Show a button if item has not been returned yet
"""
if obj.permanencier_rendu:
return obj.permanencier_rendu
else:
return format_html(
'<a class="button" href="{}">{}</a>',
reverse('media:retour-emprunt', args=[obj.pk]),
_('Turn back')
)
permanencier_rendu_custom.short_description = _('given back to')
permanencier_rendu_custom.allow_tags = True
class BorrowAdmin(VersionAdmin):
list_display = ('borrowable', 'user', 'borrow_date', 'borrowed_with',
'given_back_to')
search_fields = ('borrowable__isbn', 'borrowable__title',
'borrowable__medium__side_identifier',
'user__username', 'borrow_date', 'given_back')
date_hierarchy = 'borrow_date'
autocomplete_fields = ('borrowable', 'user', 'borrowed_with',
'given_back_to')
def add_view(self, request, form_url='', extra_context=None):
"""
@ -151,16 +135,17 @@ class EmpruntAdmin(VersionAdmin):
"""
# Make GET data mutable
data = request.GET.copy()
data['permanencier_emprunt'] = request.user
data['borrowed_with'] = request.user
request.GET = data
return super().add_view(request, form_url, extra_context)
class GameAdmin(VersionAdmin, PolymorphicChildModelAdmin):
list_display = ('title', 'owner', 'duration', 'players_min',
'players_max', 'comment')
search_fields = ('name', 'owner__username', 'duration', 'comment')
'players_max', 'comment', 'isbn')
search_fields = ('isbn', 'title', 'owner__username', 'duration', 'comment')
autocomplete_fields = ('owner',)
show_in_index = True
admin_site.register(Author, AuthorAdmin)
@ -172,5 +157,5 @@ admin_site.register(CD, CDAdmin)
admin_site.register(Vinyl, VinylAdmin)
admin_site.register(Review, ReviewAdmin)
admin_site.register(FutureMedium, FutureMediumAdmin)
admin_site.register(Emprunt, EmpruntAdmin)
admin_site.register(Borrow, BorrowAdmin)
admin_site.register(Game, GameAdmin)

View File

@ -9,6 +9,7 @@ import unicodedata
from urllib.error import HTTPError
import urllib.request
from django.core.exceptions import ValidationError
from django.db.models import QuerySet
from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
@ -320,6 +321,13 @@ class MediaAdminForm(ModelForm):
return self.cleaned_data
def _clean_fields(self):
# First clean ISBN field
isbn_field = self.fields['isbn']
isbn = isbn_field.widget.value_from_datadict(
self.data, self.files, self.add_prefix('isbn'))
isbn = isbn_field.clean(isbn)
self.cleaned_data['isbn'] = isbn
for name, field in self.fields.items():
# value_from_datadict() gets the data from the data dictionaries.
# Each widget type knows how to retrieve its own data, because some
@ -329,7 +337,6 @@ class MediaAdminForm(ModelForm):
else:
value = field.widget.value_from_datadict(
self.data, self.files, self.add_prefix(name))
from django.core.exceptions import ValidationError
try:
# We don't want to check a field when we enter an ISBN.
if "isbn" not in self.data \

View File

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-23 18:27+0200\n"
"POT-Creation-Date: 2021-11-14 14:25+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -13,245 +13,269 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: admin.py:33 admin.py:88 admin.py:99 models.py:29 models.py:65 models.py:130
#: models.py:192 models.py:243 models.py:274
#: admin.py:46 admin.py:102 admin.py:114 models.py:30 models.py:85
msgid "authors"
msgstr "auteurs"
#: admin.py:43
#: admin.py:56
msgid "external url"
msgstr "URL externe"
#: admin.py:126
msgid "Turn back"
msgstr "Rendre"
#: admin.py:129 models.py:407
msgid "given back to"
msgstr "rendu à"
#: fields.py:17
msgid "ISBN-10 or ISBN-13"
msgstr "ISBN-10 ou ISBN-13"
#: forms.py:301
#: forms.py:302
msgid "This ISBN is not found."
msgstr "L'ISBN n'a pas été trouvé."
#: models.py:16 models.py:431
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 "comic"
msgstr "BD"
#: models.py:94
msgid "comics"
msgstr "BDs"
#: models.py:155
msgid "manga"
msgstr "manga"
#: models.py:156
msgid "mangas"
msgstr "mangas"
#: models.py:217
msgid "novel"
msgstr "roman"
#: models.py:218
msgid "novels"
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 "vinyl"
msgstr "vinyle"
#: models.py:257
msgid "vinyls"
msgstr "vinyles"
#: models.py:287
msgid "CD"
msgstr "CD"
#: models.py:288
#: management/commands/migrate_to_new_format.py:57 models.py:156
msgid "CDs"
msgstr "CDs"
#: models.py:299
msgid "number"
msgstr "nombre"
#: management/commands/migrate_to_new_format.py:57 models.py:155
msgid "CD"
msgstr "CD"
#: models.py:303
msgid "year"
msgstr "année"
#: management/commands/migrate_to_new_format.py:73 models.py:149
msgid "vinyls"
msgstr "vinyles"
#: models.py:310
msgid "month"
msgstr "mois"
#: management/commands/migrate_to_new_format.py:73 models.py:148
msgid "vinyl"
msgstr "vinyle"
#: models.py:317
msgid "day"
msgstr "jour"
#: models.py:324
msgid "double"
msgstr "double"
#: models.py:338
msgid "review"
msgstr "revue"
#: models.py:339
#: management/commands/migrate_to_new_format.py:91 models.py:196
msgid "reviews"
msgstr "revues"
#: models.py:353
msgid "type"
msgstr "type"
#: management/commands/migrate_to_new_format.py:91 models.py:195
msgid "review"
msgstr "revue"
#: models.py:356
msgid "Manga"
msgstr "Manga"
#: management/commands/migrate_to_new_format.py:111 models.py:315
msgid "games"
msgstr "jeux"
#: models.py:357
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
#: management/commands/migrate_to_new_format.py:111 models.py:314
msgid "game"
msgstr "jeu"
#: models.py:462
msgid "games"
msgstr "jeux"
#: models.py:17
msgid "name"
msgstr "nom"
#: models.py:22
msgid "note"
msgstr "note"
#: models.py:29
msgid "author"
msgstr "auteur"
#: models.py:36 models.py:202
msgid "ISBN"
msgstr "ISBN"
#: models.py:37 models.py:203
msgid "You may be able to scan it from a bar code."
msgstr "Peut souvent être scanné à partir du code barre."
#: models.py:45
msgid "title"
msgstr "titre"
#: models.py:49 models.py:220
msgid "present"
msgstr "présent"
#: models.py:50 models.py:221
msgid "Tell that the medium is present in the Mediatek."
msgstr "Indique que le medium est présent à la Mediatek."
#: models.py:68
msgid "borrowable"
msgstr "empruntable"
#: models.py:69
msgid "borrowables"
msgstr "empruntables"
#: models.py:74
msgid "external URL"
msgstr "URL externe"
#: models.py:79
msgid "side identifier"
msgstr "côte"
#: models.py:89
msgid "medium"
msgstr "medium"
#: models.py:90
msgid "media"
msgstr "media"
#: models.py:95
msgid "subtitle"
msgstr "sous-titre"
#: models.py:101
msgid "number of pages"
msgstr "nombre de pages"
#: models.py:107
msgid "publish date"
msgstr "date de publication"
#: models.py:113
msgid "book"
msgstr "livre"
#: models.py:114
msgid "books"
msgstr "livres"
#: models.py:119
msgid "comic"
msgstr "BD"
#: models.py:120
msgid "comics"
msgstr "BDs"
#: models.py:126
msgid "manga"
msgstr "manga"
#: models.py:127
msgid "mangas"
msgstr "mangas"
#: models.py:133
msgid "novel"
msgstr "roman"
#: models.py:134
msgid "novels"
msgstr "romans"
#: models.py:140
msgid "rounds per minute"
msgstr "tours par minute"
#: models.py:142
msgid "33 RPM"
msgstr "33 TPM"
#: models.py:143
msgid "45 RPM"
msgstr "45 TPM"
#: models.py:162
msgid "number"
msgstr "nombre"
#: models.py:166
msgid "year"
msgstr "année"
#: models.py:173
msgid "month"
msgstr "mois"
#: models.py:180
msgid "day"
msgstr "jour"
#: models.py:187
msgid "double"
msgstr "double"
#: models.py:210
msgid "type"
msgstr "type"
#: models.py:212
msgid "Comic"
msgstr "BD"
#: models.py:213
msgid "Manga"
msgstr "Manga"
#: models.py:214
msgid "Roman"
msgstr "Roman"
#: models.py:226
msgid "future medium"
msgstr "medium à importer"
#: models.py:227
msgid "future media"
msgstr "medias à importer"
#: models.py:237
msgid "object"
msgstr "objet"
#: models.py:242
msgid "borrower"
msgstr "emprunteur"
#: models.py:245
msgid "borrowed on"
msgstr "emprunté le"
#: models.py:250
msgid "given back on"
msgstr "rendu le"
#: models.py:256
msgid "borrowed with"
msgstr "emprunté avec"
#: models.py:257
msgid "The keyholder that registered this borrowed item."
msgstr "Le permanencier qui enregistre cet emprunt."
#: models.py:265
msgid "given back to"
msgstr "rendu à"
#: models.py:266
msgid "The keyholder to whom this item was given back."
msgstr "Le permanencier à qui l'emprunt a été rendu."
#: models.py:273
msgid "borrowed item"
msgstr "emprunt"
#: models.py:274
msgid "borrowed items"
msgstr "emprunts"
#: models.py:289
msgid "owner"
msgstr "propriétaire"
#: models.py:294
msgid "duration"
msgstr "durée"
#: models.py:298
msgid "minimum number of players"
msgstr "nombre minimum de joueurs"
#: models.py:302
msgid "maximum number of players"
msgstr "nombre maximum de joueurs"
#: models.py:307
msgid "comment"
msgstr "commentaire"
#: templates/media/generate_side_identifier.html:3
msgid "Generate side identifier"
@ -277,6 +301,6 @@ msgstr "ISBN invalide : mauvaise longueur"
msgid "Invalid ISBN: Only upper case allowed"
msgstr "ISBN invalide : seulement les majuscules sont autorisées"
#: views.py:51
#: views.py:25
msgid "Welcome to the Mediatek database"
msgstr "Bienvenue sur la base de données de la Mediatek"

View File

@ -1,4 +1,7 @@
import os.path
from django.core.management import BaseCommand
from media.models import Comic, CD, Manga, Review, Novel, Vinyl, Game
@ -13,18 +16,18 @@ class Command(BaseCommand):
def handle(self, *args, **options):
directory = options["directory"]
with open(directory + "/docs/index.md", "w") as f:
with open(os.path.join(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"),
for model_class, file_name in [(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:
with open(os.path.join(directory, 'docs', 'revues.md'), "w") as f:
f.write("# Revues\n\n\n")
titles = list(set(obj["title"] for obj in
@ -48,11 +51,13 @@ class Command(BaseCommand):
f.write("\n\n\n")
# Traitement différent pour les jeux
with open(directory + "/docs/jeux.md", "w") as f:
with open(os.path.join(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")
for game in Game.objects.order_by("title").all():
f.write(f"## {game.title}\n\n\n")
if hasattr(medium, "isbn"):
f.write(f"ISBN : {game.isbn}\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")
@ -62,8 +67,54 @@ class Command(BaseCommand):
f.write(f"Commentaire : {game.comment}\n\n")
f.write("\n\n\n")
# Traitement différent pour les bds
# Regroupement par le premier caractère
titles = list(set(obj["title"] for obj in Comic.objects
.values("title").distinct().all()))
category = lambda s: s[0].lower() if 'a' <= s[0].lower() <= 'z' else '#'
db = {}
for title in titles:
c = category(title)
if c not in db:
db[c] = []
db[c].append(title)
for letter, titles in db.items():
with open(os.path.join(directory, 'docs', 'bds', f'{letter}.md'), 'w') as f:
f.write("# " + str(Comic._meta.verbose_name_plural)
.capitalize() + f" - {letter.upper()}\n\n\n")
titles.sort()
for title in titles:
f.write(f"## {title}\n\n\n")
for medium in Comic.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")
def process_model_class(self, model_class, file_name, f, directory):
with open(directory + "/docs/" + file_name, "w") as f:
with open(os.path.join(directory, 'docs', file_name), "w") as f:
f.write("# " + str(model_class._meta.verbose_name_plural)
.capitalize() + "\n\n\n")

View File

@ -1,11 +1,9 @@
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
from media.models import CD, Comic, Game, Manga, Novel, Review, Vinyl, \
OldCD, OldComic, OldGame, OldManga, OldNovel, OldReview, OldVinyl
class Command(BaseCommand):
"""
@ -17,7 +15,14 @@ class Command(BaseCommand):
help="Actually do the mogration.")
@transaction.atomic
def handle(self, *args, **options):
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),
@ -39,7 +44,7 @@ class Command(BaseCommand):
)
new_book.authors.set(book.authors.all())
new_book.save()
except:
except Exception:
self.stderr.write(f"There was an error with {name} "
f"{book} ({book.pk})")
raise
@ -57,7 +62,7 @@ class Command(BaseCommand):
)
new_cd.authors.set(cd.authors.all())
new_cd.save()
except:
except Exception:
self.stderr.write(f"There was an error with {cd} ({cd.pk})")
raise
@ -74,7 +79,7 @@ class Command(BaseCommand):
)
new_vinyl.authors.set(vinyl.authors.all())
new_vinyl.save()
except:
except Exception:
self.stderr.write(f"There was an error with {vinyl} "
f"({vinyl.pk})")
raise
@ -94,7 +99,7 @@ class Command(BaseCommand):
double=review.double,
present=review.present,
)
except:
except Exception:
self.stderr.write(f"There was an error with {review} "
f"({review.pk})")
raise
@ -113,7 +118,7 @@ class Command(BaseCommand):
players_max=game.players_max,
comment=game.comment,
)
except:
except Exception:
self.stderr.write(f"There was an error with {game} "
f"({game.pk})")
raise

View File

@ -21,6 +21,7 @@ class Migration(migrations.Migration):
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')),
@ -74,7 +75,6 @@ class Migration(migrations.Migration):
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')),
('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')),
('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')),

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,7 +1,7 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# Copyright (C) 2017-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
from django.core.validators import MinValueValidator
from django.db import models
from django.utils.translation import gettext_lazy as _
@ -32,6 +32,14 @@ class Author(models.Model):
class Borrowable(PolymorphicModel):
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(
max_length=255,
verbose_name=_("title"),
@ -83,14 +91,6 @@ class Medium(Borrowable):
class Book(Medium):
isbn = ISBNField(
_('ISBN'),
help_text=_('You may be able to scan it from a bar code.'),
unique=True,
blank=True,
null=True,
)
subtitle = models.CharField(
verbose_name=_('subtitle'),
max_length=255,
@ -114,71 +114,6 @@ class Book(Medium):
verbose_name_plural = _("books")
class OldComic(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(
'Author',
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):
if self.subtitle:
return "{} : {}".format(self.title, self.subtitle)
else:
return self.title
class Meta:
verbose_name = _("comic")
verbose_name_plural = _("comics")
ordering = ['title', 'subtitle']
class Comic(Book):
class Meta:
verbose_name = _("comic")
@ -186,68 +121,6 @@ class Comic(Book):
ordering = ['title', 'subtitle']
class OldManga(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(
'Author',
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 = _("manga")
verbose_name_plural = _("mangas")
ordering = ['title']
class Manga(Book):
class Meta:
verbose_name = _("manga")
@ -255,68 +128,6 @@ class Manga(Book):
ordering = ['title', 'subtitle']
class OldNovel(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(
'Author',
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 = _("novel")
verbose_name_plural = _("novels")
ordering = ['title', 'subtitle']
class Novel(Book):
class Meta:
verbose_name = _("novel")
@ -324,45 +135,6 @@ class Novel(Book):
ordering = ['title', 'subtitle']
class OldVinyl(models.Model):
title = models.CharField(
verbose_name=_('title'),
max_length=255,
)
side_identifier = models.CharField(
verbose_name=_('side identifier'),
max_length=255,
)
rpm = models.PositiveIntegerField(
verbose_name=_('rounds per minute'),
choices=[
(33, _('33 RPM')),
(45, _('45 RPM')),
],
)
authors = models.ManyToManyField(
'Author',
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:
verbose_name = _("vinyl")
verbose_name_plural = _("vinyls")
ordering = ['title']
class Vinyl(Medium):
rpm = models.PositiveIntegerField(
verbose_name=_('rounds per minute'),
@ -378,37 +150,6 @@ class Vinyl(Medium):
ordering = ['title']
class OldCD(models.Model):
title = models.CharField(
verbose_name=_('title'),
max_length=255,
)
side_identifier = models.CharField(
verbose_name=_('side identifier'),
max_length=255,
)
authors = models.ManyToManyField(
'Author',
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:
verbose_name = _("CD")
verbose_name_plural = _("CDs")
ordering = ['title']
class CD(Medium):
class Meta:
verbose_name = _("CD")
@ -416,57 +157,6 @@ class CD(Medium):
ordering = ['title']
class OldReview(models.Model):
title = models.CharField(
verbose_name=_('title'),
max_length=255,
)
number = models.PositiveIntegerField(
verbose_name=_('number'),
)
year = models.PositiveIntegerField(
verbose_name=_('year'),
null=True,
blank=True,
default=None,
)
month = models.PositiveIntegerField(
verbose_name=_('month'),
null=True,
blank=True,
default=None,
)
day = models.PositiveIntegerField(
verbose_name=_('day'),
null=True,
blank=True,
default=None,
)
double = models.BooleanField(
verbose_name=_('double'),
default=False,
)
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 + "" + str(self.number)
class Meta:
verbose_name = _("review")
verbose_name_plural = _("reviews")
ordering = ['title', 'number']
class Review(Borrowable):
number = models.PositiveIntegerField(
verbose_name=_('number'),
@ -540,35 +230,36 @@ class FutureMedium(models.Model):
return "Future medium (ISBN: {isbn})".format(isbn=self.isbn, )
class Emprunt(models.Model):
media = models.ForeignKey(
class Borrow(models.Model):
borrowable = models.ForeignKey(
'media.Borrowable',
on_delete=models.PROTECT,
verbose_name=_('object'),
)
user = models.ForeignKey(
'users.User',
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
verbose_name=_("borrower"),
)
date_emprunt = models.DateTimeField(
borrow_date = models.DateTimeField(
verbose_name=_('borrowed on'),
)
date_rendu = models.DateTimeField(
given_back = models.DateTimeField(
blank=True,
null=True,
verbose_name=_('given back on'),
)
permanencier_emprunt = models.ForeignKey(
'users.User',
borrowed_with = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name='user_permanencier_emprunt',
related_name='+',
verbose_name=_('borrowed with'),
help_text=_('The keyholder that registered this borrowed item.')
)
permanencier_rendu = models.ForeignKey(
'users.User',
given_back_to = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name='user_permanencier_rendu',
related_name='+',
blank=True,
null=True,
verbose_name=_('given back to'),
@ -576,58 +267,12 @@ class Emprunt(models.Model):
)
def __str__(self):
return str(self.media) + str(self.user)
return str(self.borrowable) + str(self.user)
class Meta:
verbose_name = _("borrowed item")
verbose_name_plural = _("borrowed items")
ordering = ['-date_emprunt']
class OldGame(models.Model):
DURATIONS = (
('-1h', '-1h'),
('1-2h', '1-2h'),
('2-3h', '2-3h'),
('3-4h', '3-4h'),
('4h+', '4h+'),
)
title = models.CharField(
max_length=255,
verbose_name=_("name"),
)
owner = models.ForeignKey(
'users.User',
on_delete=models.PROTECT,
verbose_name=_("owner"),
)
duration = models.CharField(
choices=DURATIONS,
max_length=255,
verbose_name=_("duration"),
)
players_min = models.IntegerField(
validators=[MinValueValidator(1)],
verbose_name=_("minimum number of players"),
)
players_max = models.IntegerField(
validators=[MinValueValidator(1)],
verbose_name=_('maximum number of players'),
)
comment = models.CharField(
max_length=255,
blank=True,
verbose_name=_('comment'),
)
def __str__(self):
return str(self.title)
class Meta:
verbose_name = _("game")
verbose_name_plural = _("games")
ordering = ['title']
ordering = ['-borrow_date']
class Game(Borrowable):

View File

@ -1,6 +1,6 @@
from rest_framework import serializers
from .models import Author, CD, Comic, FutureMedium, Manga, Emprunt, Game, \
from .models import Author, Borrow, CD, Comic, FutureMedium, Manga, Game, \
Novel, Review, Vinyl
@ -52,15 +52,13 @@ class FutureMediumSerializer(serializers.ModelSerializer):
fields = '__all__'
class EmpruntSerializer(serializers.HyperlinkedModelSerializer):
class BorrowSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Emprunt
fields = ['url', 'media', 'user', 'date_emprunt', 'date_rendu',
'permanencier_emprunt', 'permanencier_rendu']
model = Borrow
fields = '__all__'
class GameSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Game
fields = ['url', 'name', 'proprietaire', 'duree', 'nombre_joueurs_min',
'nombre_joueurs_max', 'comment']
fields = '__all__'

View File

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

View File

@ -55,10 +55,10 @@ class TemplateTests(TestCase):
), data=data)
self.assertEqual(response.status_code, 302)
def test_comic_emprunt_changelist(self):
response = self.client.get(reverse('admin:media_emprunt_changelist'))
def test_comic_borrow_changelist(self):
response = self.client.get(reverse('admin:media_borrow_changelist'))
self.assertEqual(response.status_code, 200)
def test_comic_emprunt_add(self):
response = self.client.get(reverse('admin:media_emprunt_add'))
def test_comic_borrow_add(self):
response = self.client.get(reverse('admin:media_borrow_add'))
self.assertEqual(response.status_code, 200)

View File

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

@ -2,42 +2,20 @@
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse
from django_filters.rest_framework import DjangoFilterBackend
from django.db import transaction
from django.shortcuts import redirect
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView, DetailView
from rest_framework import viewsets
from rest_framework.filters import SearchFilter
from reversion import revisions as reversion
from .models import Author, CD, Comic, Emprunt, FutureMedium, Game, Manga,\
from .models import Author, Borrow, CD, Comic, FutureMedium, Game, Manga,\
Novel, Review, Vinyl
from .serializers import AuthorSerializer, ComicSerializer, CDSerializer,\
EmpruntSerializer, FutureMediumSerializer, GameSerializer, \
MangaSerializer, NovelSerializer, ReviewSerializer, VinylSerializer
@login_required
@permission_required('media.change_emprunt')
def retour_emprunt(request, empruntid):
try:
emprunt_instance = Emprunt.objects.get(pk=empruntid)
except Emprunt.DoesNotExist:
messages.error(request, u"Entrée inexistante")
return redirect("admin:media_emprunt_changelist")
with transaction.atomic(), reversion.create_revision():
emprunt_instance.permanencier_rendu = request.user
emprunt_instance.date_rendu = timezone.now()
emprunt_instance.save()
reversion.set_user(request.user)
messages.success(request, "Retour enregistré")
return redirect("admin:media_emprunt_changelist")
from .serializers import AuthorSerializer, BorrowSerializer, ComicSerializer, \
CDSerializer, FutureMediumSerializer, GameSerializer, MangaSerializer, \
NovelSerializer, ReviewSerializer, VinylSerializer
class IndexView(TemplateView):
@ -81,11 +59,11 @@ class MarkVinylAsPresent(MarkMediumAsPresent):
model = Vinyl
class MarkRomanAsPresent(MarkMediumAsPresent):
class MarkNovelAsPresent(MarkMediumAsPresent):
model = Novel
class MarkRevueAsPresent(MarkMediumAsPresent):
class MarkReviewAsPresent(MarkMediumAsPresent):
model = Review
@ -181,12 +159,12 @@ class FutureMediumViewSet(viewsets.ModelViewSet):
search_fields = ["=isbn"]
class EmpruntViewSet(viewsets.ModelViewSet):
class BorrowViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows borrowed items to be viewed or edited.
"""
queryset = Emprunt.objects.all()
serializer_class = EmpruntSerializer
queryset = Borrow.objects.all()
serializer_class = BorrowSerializer
class GameViewSet(viewsets.ModelViewSet):

View File

@ -1,3 +1,4 @@
authlib~=0.15
docutils~=0.16 # for Django-admin docs
Django~=2.2
django-filter~=2.4

View File

@ -54,7 +54,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if user.is_authenticated %}
<a href="{% url 'logout' %}">{% trans 'Log out' %}</a>
{% else %}
<a href="{% url 'login' %}">{% trans 'Log in' %}</a>
<a href="{% url 'users:login' %}">{% trans 'Log in' %}</a>
{% endif %}
{% endblock %}
</div>

View File

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

View File

@ -3,16 +3,13 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin
from django.contrib import messages
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import PasswordResetForm
from django.urls import reverse
from django.utils.html import format_html
from django.utils import timezone
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from reversion.admin import VersionAdmin
from med.admin import admin_site
from .forms import UserCreationAdminForm
from .models import User
@ -26,7 +23,12 @@ class IsMemberFilter(admin.SimpleListFilter):
)
def queryset(self, request, queryset):
# FIXME Replace with imported Note Kfet memberships
if self.parameter_name in request.GET:
queryset = queryset.filter(
membership__date_start__lte=timezone.now(),
membership__date_end__gte=timezone.now(),
).distinct()
return queryset
@ -35,61 +37,32 @@ class UserAdmin(VersionAdmin, BaseUserAdmin):
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {'fields': ('first_name', 'last_name', 'email',
'telephone', 'address', 'comment')}),
'phone_number', 'address',
'comment')}),
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions',
'maxemprunt')}),
'groups', 'user_permissions')}),
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
)
list_display = ('username', 'email', 'first_name', 'last_name',
'maxemprunt', 'is_member', 'is_staff')
'is_member', 'is_staff')
list_filter = (IsMemberFilter, 'is_staff', 'is_superuser', 'is_active',
'groups')
# Customize required initial fields
add_form_template = 'admin/change_form.html'
add_form = UserCreationAdminForm
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ("username", "email", "first_name", "last_name",
"address", "telephone"),
}),
)
def save_model(self, request, obj, form, change):
"""
On creation, send a password init mail
"""
super().save_model(request, obj, form, change)
if not change:
# Virtually fill the password reset form
password_reset = PasswordResetForm(data={'email': obj.email})
if password_reset.is_valid():
password_reset.save(request=request,
use_https=request.is_secure())
messages.success(request, _("An email to set the password"
" was sent."))
else:
messages.error(request, _("The email is invalid."))
def has_add_permission(self, request):
# Only add users through Note Kfet login
return False
def is_member(self, obj):
"""
Get current membership year and check if user is there
"""
# FIXME Use NK20
is_member = True
if is_member:
return format_html(
if obj.is_member:
return mark_safe(
'<img src="/static/admin/img/icon-yes.svg" alt="True">'
)
else:
return format_html(
'<img src="/static/admin/img/icon-no.svg" alt="False"> '
'<a class="button" href="{}">{}</a>',
reverse('users:adherer', args=[obj.pk]),
_('Adhere')
return mark_safe(
'<img src="/static/admin/img/icon-no.svg" alt="False">'
)
is_member.short_description = _('is member')

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,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 -*-
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# Copyright (C) 2017-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import datetime
import requests
from authlib.integrations.django_client import OAuth
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.db.models import Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from med.settings import MAX_EMPRUNT
class User(AbstractUser):
telephone = models.CharField(
phone_number = models.CharField(
verbose_name=_('phone number'),
max_length=15,
blank=True,
@ -20,12 +25,6 @@ class User(AbstractUser):
max_length=255,
blank=True,
)
maxemprunt = models.IntegerField(
verbose_name=_('maximum borrowed'),
help_text=_('Maximal amount of simultaneous borrowed item '
'authorized.'),
default=MAX_EMPRUNT,
)
comment = models.CharField(
verbose_name=_('comment'),
help_text=_('Promotion...'),
@ -33,7 +32,7 @@ class User(AbstractUser):
blank=True,
)
date_joined = models.DateTimeField(
_('date joined'),
verbose_name=_('date joined'),
default=timezone.now,
null=True,
)
@ -42,5 +41,171 @@ class User(AbstractUser):
@property
def is_member(self):
# FIXME Use NK20
return True
"""
Return True if user is member of the club.
"""
return Membership.objects.filter(
user=self,
date_start__lte=timezone.now(),
date_end__gte=timezone.now()).exists()
def update_data(self, data: dict):
"""
Update user data from given dictionary.
Useful when we want to update user data from Note Kfet.
Parameters
----------
data : dict
Dictionary with user data to update.
"""
self.email = data['email'] or ''
self.first_name = data['first_name'] or ''
self.last_name = data['last_name'] or ''
self.phone_number = data['profile']['phone_number'] or ''
self.address = data['profile']['address'] or ''
self.comment = data['profile']['section'] or ''
for membership_dict in data['memberships']:
if membership_dict['club'] != 22: # Med
continue
# Add membership if not exists
Membership.objects.get_or_create(
user=self,
date_start=membership_dict['date_start'],
date_end=membership_dict['date_end'],
)
# Only members or old members are allow to connect to the website
self.is_active = Membership.objects.filter(user=self).exists()
class Membership(models.Model):
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
verbose_name=_('user'),
)
date_start = models.DateField(
verbose_name=_('start date'),
)
date_end = models.DateField(
verbose_name=_('start date'),
)
def __str__(self):
return f'{self.user}: {self.date_start} to {self.date_end}'
class Meta:
verbose_name = _('membership')
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:
model = User
fields = ['url', 'username', 'first_name', 'last_name', 'email',
'groups', 'telephone', 'address', 'maxemprunt', 'comment',
'groups', 'phone_number', 'address', 'comment',
'date_joined']

View File

@ -1,7 +1,6 @@
# -*- mode: python; coding: utf-8 -*-
# SPDX-License-Identifier: GPL-3.0-or-later
from django.core import mail
from django.test import TestCase
from django.urls import reverse
from users.models import User
@ -20,30 +19,6 @@ class TemplateTests(TestCase):
)
self.client.force_login(self.user)
def test_users_edit_info(self):
response = self.client.get(reverse('users:edit-info'))
self.assertEqual(response.status_code, 200)
def test_users_user_changelist(self):
response = self.client.get(reverse('admin:users_user_changelist'))
self.assertEqual(response.status_code, 200)
def test_users_user_creation_form(self):
response = self.client.get(reverse('admin:users_user_add'))
self.assertEqual(response.status_code, 200)
def test_users_user_add_init_mail(self):
"""
Test that an initialization mail is send when a new user is added
"""
data = {
'username': "test_user",
'email': "test@example.com",
'first_name': "Test",
'last_name': "User",
}
response = self.client.post(reverse(
'admin:users_user_add',
), data=data)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(response.status_code, 302)

View File

@ -8,5 +8,6 @@ from . import views
app_name = 'users'
urlpatterns = [
url(r'^edit_info/$', views.edit_info, name='edit-info'),
url('login/', views.LoginView.as_view(), name='login'),
url('authorize/', views.AuthorizeView.as_view(), name='auth'),
]

View File

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