mirror of
https://gitlab.crans.org/mediatek/med.git
synced 2025-07-07 01:04:02 +02:00
Compare commits
28 Commits
6789b9e3ac
...
main
Author | SHA1 | Date | |
---|---|---|---|
3be0c99811 | |||
cf544dc596
|
|||
c0521005ef
|
|||
09c61091d5
|
|||
145806b5ce | |||
49898143ce
|
|||
fe55a2a5ea
|
|||
cdcb743b55
|
|||
e75f04b530
|
|||
34766257b3
|
|||
d0877f5cdc
|
|||
aceb0d893c
|
|||
41435a6838
|
|||
d3c2441111
|
|||
d036ea6f27
|
|||
e2aa645bbf
|
|||
faf697d3cf
|
|||
b0a1602ea2 | |||
d2ad52c15a
|
|||
8c6828564c | |||
d75250f436
|
|||
c424c7c040
|
|||
a01d480dd2
|
|||
079ade9bbb
|
|||
4928b555b7
|
|||
ae0d1a080e
|
|||
1e6e033cdd
|
|||
d0805ebe8a
|
@ -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
|
||||||
|
@ -167,9 +167,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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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'
|
||||||
|
@ -21,7 +21,7 @@ router.register(r'media/vinyl', media.views.VinylViewSet)
|
|||||||
router.register(r'media/novel', media.views.NovelViewSet)
|
router.register(r'media/novel', media.views.NovelViewSet)
|
||||||
router.register(r'media/review', media.views.ReviewViewSet)
|
router.register(r'media/review', media.views.ReviewViewSet)
|
||||||
router.register(r'media/future', media.views.FutureMediumViewSet)
|
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'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)
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
# 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, \
|
from polymorphic.admin import PolymorphicChildModelAdmin, \
|
||||||
@ -11,7 +10,7 @@ from med.admin import admin_site
|
|||||||
from reversion.admin import VersionAdmin
|
from reversion.admin import VersionAdmin
|
||||||
|
|
||||||
from .forms import MediaAdminForm
|
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
|
Game, Manga, Novel, Review, Vinyl
|
||||||
|
|
||||||
|
|
||||||
@ -23,7 +22,11 @@ class AuthorAdmin(VersionAdmin):
|
|||||||
class BorrowableAdmin(PolymorphicParentModelAdmin):
|
class BorrowableAdmin(PolymorphicParentModelAdmin):
|
||||||
search_fields = ('title',)
|
search_fields = ('title',)
|
||||||
child_models = (CD, Comic, Manga, Novel, Review, Vinyl,)
|
child_models = (CD, Comic, Manga, Novel, Review, Vinyl,)
|
||||||
show_in_index = False
|
|
||||||
|
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):
|
class MediumAdmin(VersionAdmin, PolymorphicChildModelAdmin):
|
||||||
@ -116,30 +119,15 @@ class ReviewAdmin(VersionAdmin, PolymorphicChildModelAdmin):
|
|||||||
show_in_index = True
|
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):
|
||||||
"""
|
"""
|
||||||
@ -147,16 +135,17 @@ 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 GameAdmin(VersionAdmin, PolymorphicChildModelAdmin):
|
class GameAdmin(VersionAdmin, PolymorphicChildModelAdmin):
|
||||||
list_display = ('title', 'owner', 'duration', 'players_min',
|
list_display = ('title', 'owner', 'duration', 'players_min',
|
||||||
'players_max', 'comment')
|
'players_max', 'comment', 'isbn')
|
||||||
search_fields = ('name', 'owner__username', 'duration', 'comment')
|
search_fields = ('isbn', 'title', 'owner__username', 'duration', 'comment')
|
||||||
autocomplete_fields = ('owner',)
|
autocomplete_fields = ('owner',)
|
||||||
|
show_in_index = True
|
||||||
|
|
||||||
|
|
||||||
admin_site.register(Author, AuthorAdmin)
|
admin_site.register(Author, AuthorAdmin)
|
||||||
@ -168,5 +157,5 @@ admin_site.register(CD, CDAdmin)
|
|||||||
admin_site.register(Vinyl, VinylAdmin)
|
admin_site.register(Vinyl, VinylAdmin)
|
||||||
admin_site.register(Review, ReviewAdmin)
|
admin_site.register(Review, ReviewAdmin)
|
||||||
admin_site.register(FutureMedium, FutureMediumAdmin)
|
admin_site.register(FutureMedium, FutureMediumAdmin)
|
||||||
admin_site.register(Emprunt, EmpruntAdmin)
|
admin_site.register(Borrow, BorrowAdmin)
|
||||||
admin_site.register(Game, GameAdmin)
|
admin_site.register(Game, GameAdmin)
|
||||||
|
@ -9,6 +9,7 @@ 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 _
|
||||||
@ -320,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
|
||||||
@ -329,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 \
|
||||||
|
@ -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: 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"
|
"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:33 admin.py:88 admin.py:99 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:43
|
#: admin.py:56
|
||||||
msgid "external url"
|
msgid "external url"
|
||||||
msgstr "URL externe"
|
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
|
#: 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:301
|
#: 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 "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
|
|
||||||
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"
|
|
||||||
msgstr "jour"
|
|
||||||
|
|
||||||
#: models.py:324
|
|
||||||
msgid "double"
|
|
||||||
msgstr "double"
|
|
||||||
|
|
||||||
#: models.py:338
|
|
||||||
msgid "review"
|
|
||||||
msgstr "revue"
|
|
||||||
|
|
||||||
#: models.py:339
|
|
||||||
msgid "reviews"
|
msgid "reviews"
|
||||||
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,6 +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:51
|
#: 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"
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
|
import os.path
|
||||||
|
|
||||||
from django.core.management import BaseCommand
|
from django.core.management import BaseCommand
|
||||||
|
|
||||||
from media.models import Comic, CD, Manga, Review, Novel, Vinyl, Game
|
from media.models import Comic, CD, Manga, Review, Novel, Vinyl, Game
|
||||||
|
|
||||||
|
|
||||||
@ -13,18 +16,18 @@ class Command(BaseCommand):
|
|||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
directory = options["directory"]
|
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("# Media de la Mediatek\n\n\n")
|
||||||
f.write("Ce site répertorie l'intégralité des media présents "
|
f.write("Ce site répertorie l'intégralité des media présents "
|
||||||
"à la Mediatek de l'ENS Paris-Saclay.\n")
|
"à 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"),
|
(Novel, "romans.md"),
|
||||||
(CD, "cd.md"), (Vinyl, "vinyles.md")]:
|
(CD, "cd.md"), (Vinyl, "vinyles.md")]:
|
||||||
self.process_model_class(model_class, file_name, f, directory)
|
self.process_model_class(model_class, file_name, f, directory)
|
||||||
|
|
||||||
# Traitement différent pour les revues
|
# 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")
|
f.write("# Revues\n\n\n")
|
||||||
|
|
||||||
titles = list(set(obj["title"] for obj in
|
titles = list(set(obj["title"] for obj in
|
||||||
@ -48,11 +51,13 @@ class Command(BaseCommand):
|
|||||||
f.write("\n\n\n")
|
f.write("\n\n\n")
|
||||||
|
|
||||||
# Traitement différent pour les jeux
|
# 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")
|
f.write("# Jeux\n\n\n")
|
||||||
|
|
||||||
for game in Game.objects.order_by("name").all():
|
for game in Game.objects.order_by("title").all():
|
||||||
f.write(f"## {game.name}\n\n\n")
|
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"Durée : {game.duration}\n\n")
|
||||||
f.write(f"Nombre de joueurs : {game.players_min} "
|
f.write(f"Nombre de joueurs : {game.players_min} "
|
||||||
f"- {game.players_max}\n\n")
|
f"- {game.players_max}\n\n")
|
||||||
@ -62,8 +67,54 @@ class Command(BaseCommand):
|
|||||||
f.write(f"Commentaire : {game.comment}\n\n")
|
f.write(f"Commentaire : {game.comment}\n\n")
|
||||||
f.write("\n\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):
|
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)
|
f.write("# " + str(model_class._meta.verbose_name_plural)
|
||||||
.capitalize() + "\n\n\n")
|
.capitalize() + "\n\n\n")
|
||||||
|
|
||||||
|
133
media/management/commands/migrate_to_new_format.py
Normal file
133
media/management/commands/migrate_to_new_format.py
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
from django.core.management import BaseCommand
|
||||||
|
from django.db import transaction
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from media.models import CD, Comic, Game, Manga, Novel, Review, Vinyl
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""
|
||||||
|
Convert old format into new format
|
||||||
|
"""
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('--doit', action='store_true',
|
||||||
|
help="Actually do the mogration.")
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def handle(self, *args, **options): # noqa: C901
|
||||||
|
self.stderr.write(self.style.WARNING(
|
||||||
|
"Old data structure has been deleted. This script won't work "
|
||||||
|
"anymore (and is now useless)"))
|
||||||
|
|
||||||
|
from media.models import OldCD, OldComic, OldGame, OldManga, OldNovel,\
|
||||||
|
OldReview, OldVinyl
|
||||||
|
|
||||||
|
# Migrate books
|
||||||
|
for old_book_class, book_class in [(OldComic, Comic),
|
||||||
|
(OldManga, Manga),
|
||||||
|
(OldNovel, Novel)]:
|
||||||
|
name = book_class._meta.verbose_name
|
||||||
|
name_plural = book_class._meta.verbose_name_plural
|
||||||
|
for book in tqdm(old_book_class.objects.all(),
|
||||||
|
desc=name_plural, unit=str(name)):
|
||||||
|
try:
|
||||||
|
new_book = book_class.objects.create(
|
||||||
|
isbn=book.isbn,
|
||||||
|
title=book.title,
|
||||||
|
subtitle=book.subtitle,
|
||||||
|
external_url=book.external_url,
|
||||||
|
side_identifier=book.side_identifier,
|
||||||
|
number_of_pages=book.number_of_pages,
|
||||||
|
publish_date=book.publish_date,
|
||||||
|
present=book.present,
|
||||||
|
)
|
||||||
|
new_book.authors.set(book.authors.all())
|
||||||
|
new_book.save()
|
||||||
|
except Exception:
|
||||||
|
self.stderr.write(f"There was an error with {name} "
|
||||||
|
f"{book} ({book.pk})")
|
||||||
|
raise
|
||||||
|
|
||||||
|
self.stdout.write(f"{book_class.objects.count()} {name_plural} "
|
||||||
|
"migrated")
|
||||||
|
|
||||||
|
# Migrate CDs
|
||||||
|
for cd in tqdm(OldCD.objects.all(),
|
||||||
|
desc=_("CDs"), unit=str(_("CD"))):
|
||||||
|
try:
|
||||||
|
new_cd = CD.objects.create(
|
||||||
|
title=cd.title,
|
||||||
|
present=cd.present,
|
||||||
|
)
|
||||||
|
new_cd.authors.set(cd.authors.all())
|
||||||
|
new_cd.save()
|
||||||
|
except Exception:
|
||||||
|
self.stderr.write(f"There was an error with {cd} ({cd.pk})")
|
||||||
|
raise
|
||||||
|
|
||||||
|
self.stdout.write(f"{CD.objects.count()} {_('CDs')} migrated")
|
||||||
|
|
||||||
|
# Migrate vinyls
|
||||||
|
for vinyl in tqdm(OldVinyl.objects.all(),
|
||||||
|
desc=_("vinyls"), unit=str(_("vinyl"))):
|
||||||
|
try:
|
||||||
|
new_vinyl = Vinyl.objects.create(
|
||||||
|
title=vinyl.title,
|
||||||
|
present=vinyl.present,
|
||||||
|
rpm=vinyl.rpm,
|
||||||
|
)
|
||||||
|
new_vinyl.authors.set(vinyl.authors.all())
|
||||||
|
new_vinyl.save()
|
||||||
|
except Exception:
|
||||||
|
self.stderr.write(f"There was an error with {vinyl} "
|
||||||
|
f"({vinyl.pk})")
|
||||||
|
raise
|
||||||
|
|
||||||
|
self.stdout.write(f"{Vinyl.objects.count()} {_('vinyls')} migrated")
|
||||||
|
|
||||||
|
# Migrate reviews
|
||||||
|
for review in tqdm(OldReview.objects.all(),
|
||||||
|
desc=_("reviews"), unit=str(_("review"))):
|
||||||
|
try:
|
||||||
|
Review.objects.create(
|
||||||
|
title=review.title,
|
||||||
|
number=review.number,
|
||||||
|
year=review.year,
|
||||||
|
month=review.month,
|
||||||
|
day=review.day,
|
||||||
|
double=review.double,
|
||||||
|
present=review.present,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
self.stderr.write(f"There was an error with {review} "
|
||||||
|
f"({review.pk})")
|
||||||
|
raise
|
||||||
|
|
||||||
|
self.stdout.write(f"{Review.objects.count()} {_('reviews')} migrated")
|
||||||
|
|
||||||
|
# Migrate games
|
||||||
|
for game in tqdm(OldGame.objects.all(),
|
||||||
|
desc=_("games"), unit=str(_("game"))):
|
||||||
|
try:
|
||||||
|
Game.objects.create(
|
||||||
|
title=game.title,
|
||||||
|
owner=game.owner,
|
||||||
|
duration=game.duration,
|
||||||
|
players_min=game.players_min,
|
||||||
|
players_max=game.players_max,
|
||||||
|
comment=game.comment,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
self.stderr.write(f"There was an error with {game} "
|
||||||
|
f"({game.pk})")
|
||||||
|
raise
|
||||||
|
|
||||||
|
self.stdout.write(f"{Game.objects.count()} {_('games')} migrated")
|
||||||
|
|
||||||
|
if not options['doit']:
|
||||||
|
self.stdout.write(self.style.WARNING(
|
||||||
|
"Warning: Data were't saved. Please use --doit option "
|
||||||
|
"to really perform the migration."
|
||||||
|
))
|
||||||
|
exit(1)
|
@ -21,6 +21,7 @@ class Migration(migrations.Migration):
|
|||||||
name='Borrowable',
|
name='Borrowable',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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')),
|
('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')),
|
('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')),
|
('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',
|
name='Book',
|
||||||
fields=[
|
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')),
|
('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')),
|
('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')),
|
('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')),
|
('publish_date', models.DateField(blank=True, null=True, verbose_name='publish date')),
|
||||||
|
58
media/migrations/0044_auto_20211102_1254.py
Normal file
58
media/migrations/0044_auto_20211102_1254.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# Generated by Django 2.2.24 on 2021-11-02 11:54
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('media', '0043_auto_20211023_2012'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='oldcd',
|
||||||
|
name='authors',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='oldcomic',
|
||||||
|
name='authors',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='oldgame',
|
||||||
|
name='owner',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='oldmanga',
|
||||||
|
name='authors',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='oldnovel',
|
||||||
|
name='authors',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='OldReview',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='oldvinyl',
|
||||||
|
name='authors',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='OldCD',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='OldComic',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='OldGame',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='OldManga',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='OldNovel',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='OldVinyl',
|
||||||
|
),
|
||||||
|
]
|
36
media/migrations/0045_auto_20211114_1423.py
Normal file
36
media/migrations/0045_auto_20211114_1423.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Generated by Django 2.2.24 on 2021-11-14 13:23
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('media', '0044_auto_20211102_1254'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Borrow',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('borrow_date', models.DateTimeField(verbose_name='borrowed on')),
|
||||||
|
('given_back', models.DateTimeField(blank=True, null=True, verbose_name='given back on')),
|
||||||
|
('borrowable', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='media.Borrowable', verbose_name='object')),
|
||||||
|
('borrowed_with', models.ForeignKey(help_text='The keyholder that registered this borrowed item.', on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='borrowed with')),
|
||||||
|
('given_back_to', models.ForeignKey(blank=True, help_text='The keyholder to whom this item was given back.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='given back to')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='borrower')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'borrowed item',
|
||||||
|
'verbose_name_plural': 'borrowed items',
|
||||||
|
'ordering': ['-borrow_date'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='Emprunt',
|
||||||
|
),
|
||||||
|
]
|
421
media/models.py
421
media/models.py
@ -1,7 +1,7 @@
|
|||||||
# -*- 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 _
|
||||||
@ -32,6 +32,14 @@ class Author(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Borrowable(PolymorphicModel):
|
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(
|
title = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
verbose_name=_("title"),
|
verbose_name=_("title"),
|
||||||
@ -44,7 +52,17 @@ class Borrowable(PolymorphicModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
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:
|
class Meta:
|
||||||
verbose_name = _('borrowable')
|
verbose_name = _('borrowable')
|
||||||
@ -73,14 +91,6 @@ class Medium(Borrowable):
|
|||||||
|
|
||||||
|
|
||||||
class Book(Medium):
|
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(
|
subtitle = models.CharField(
|
||||||
verbose_name=_('subtitle'),
|
verbose_name=_('subtitle'),
|
||||||
max_length=255,
|
max_length=255,
|
||||||
@ -99,82 +109,11 @@ class Book(Medium):
|
|||||||
null=True,
|
null=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self.subtitle:
|
|
||||||
return "{} : {}".format(self.title, self.subtitle)
|
|
||||||
else:
|
|
||||||
return self.title
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("book")
|
verbose_name = _("book")
|
||||||
verbose_name_plural = _("books")
|
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 Comic(Book):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("comic")
|
verbose_name = _("comic")
|
||||||
@ -182,68 +121,6 @@ class Comic(Book):
|
|||||||
ordering = ['title', 'subtitle']
|
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 Manga(Book):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("manga")
|
verbose_name = _("manga")
|
||||||
@ -251,68 +128,6 @@ class Manga(Book):
|
|||||||
ordering = ['title', 'subtitle']
|
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 Novel(Book):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("novel")
|
verbose_name = _("novel")
|
||||||
@ -320,45 +135,6 @@ class Novel(Book):
|
|||||||
ordering = ['title', 'subtitle']
|
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):
|
class Vinyl(Medium):
|
||||||
rpm = models.PositiveIntegerField(
|
rpm = models.PositiveIntegerField(
|
||||||
verbose_name=_('rounds per minute'),
|
verbose_name=_('rounds per minute'),
|
||||||
@ -374,37 +150,6 @@ class Vinyl(Medium):
|
|||||||
ordering = ['title']
|
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 CD(Medium):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("CD")
|
verbose_name = _("CD")
|
||||||
@ -412,57 +157,6 @@ class CD(Medium):
|
|||||||
ordering = ['title']
|
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 + " n°" + str(self.number)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("review")
|
|
||||||
verbose_name_plural = _("reviews")
|
|
||||||
ordering = ['title', 'number']
|
|
||||||
|
|
||||||
|
|
||||||
class Review(Borrowable):
|
class Review(Borrowable):
|
||||||
number = models.PositiveIntegerField(
|
number = models.PositiveIntegerField(
|
||||||
verbose_name=_('number'),
|
verbose_name=_('number'),
|
||||||
@ -536,35 +230,36 @@ class FutureMedium(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(
|
||||||
'media.Borrowable',
|
'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'),
|
||||||
@ -572,58 +267,12 @@ 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 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']
|
|
||||||
|
|
||||||
|
|
||||||
class Game(Borrowable):
|
class Game(Borrowable):
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from rest_framework import serializers
|
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
|
Novel, Review, Vinyl
|
||||||
|
|
||||||
|
|
||||||
@ -52,15 +52,13 @@ class FutureMediumSerializer(serializers.ModelSerializer):
|
|||||||
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 GameSerializer(serializers.HyperlinkedModelSerializer):
|
class GameSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Game
|
model = Game
|
||||||
fields = ['url', 'name', 'proprietaire', 'duree', 'nombre_joueurs_min',
|
fields = '__all__'
|
||||||
'nombre_joueurs_max', 'comment']
|
|
||||||
|
@ -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();
|
||||||
|
@ -55,10 +55,10 @@ class TemplateTests(TestCase):
|
|||||||
), data=data)
|
), data=data)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
def test_comic_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_comic_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)
|
||||||
|
@ -2,17 +2,14 @@
|
|||||||
# 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>/',
|
path('mark-as-present/comic/<int:pk>/',
|
||||||
views.MarkComicAsPresent.as_view(),
|
views.MarkComicAsPresent.as_view(),
|
||||||
name="mark_comic_as_present"),
|
name="mark_comic_as_present"),
|
||||||
path('mark-as-present/manga/<int:pk>/',
|
path('mark-as-present/manga/<int:pk>/',
|
||||||
@ -21,15 +18,15 @@ urlpatterns = [
|
|||||||
path('mark-as-present/cd/<int:pk>/',
|
path('mark-as-present/cd/<int:pk>/',
|
||||||
views.MarkCDAsPresent.as_view(),
|
views.MarkCDAsPresent.as_view(),
|
||||||
name="mark_cd_as_present"),
|
name="mark_cd_as_present"),
|
||||||
path('mark-as-present/vinyle/<int:pk>/',
|
path('mark-as-present/vinyl/<int:pk>/',
|
||||||
views.MarkVinylAsPresent.as_view(),
|
views.MarkVinylAsPresent.as_view(),
|
||||||
name="mark_vinyle_as_present"),
|
name="mark_vinyle_as_present"),
|
||||||
path('mark-as-present/roman/<int:pk>/',
|
path('mark-as-present/novel/<int:pk>/',
|
||||||
views.MarkRomanAsPresent.as_view(),
|
views.MarkNovelAsPresent.as_view(),
|
||||||
name="mark_roman_as_present"),
|
name="mark_novel_as_present"),
|
||||||
path('mark-as-present/revue/<int:pk>/',
|
path('mark-as-present/review/<int:pk>/',
|
||||||
views.MarkRevueAsPresent.as_view(),
|
views.MarkReviewAsPresent.as_view(),
|
||||||
name="mark_revue_as_present"),
|
name="mark_review_as_present"),
|
||||||
path('mark-as-present/future/<int:pk>/',
|
path('mark-as-present/future/<int:pk>/',
|
||||||
views.MarkFutureAsPresent.as_view(),
|
views.MarkFutureAsPresent.as_view(),
|
||||||
name="mark_future_as_present"),
|
name="mark_future_as_present"),
|
||||||
|
@ -2,42 +2,20 @@
|
|||||||
# 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.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
|
||||||
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 Author, CD, Comic, Emprunt, FutureMedium, Game, Manga,\
|
from .models import Author, Borrow, CD, Comic, FutureMedium, Game, Manga,\
|
||||||
Novel, Review, Vinyl
|
Novel, Review, Vinyl
|
||||||
from .serializers import AuthorSerializer, ComicSerializer, CDSerializer,\
|
from .serializers import AuthorSerializer, BorrowSerializer, ComicSerializer, \
|
||||||
EmpruntSerializer, FutureMediumSerializer, GameSerializer, \
|
CDSerializer, FutureMediumSerializer, GameSerializer, MangaSerializer, \
|
||||||
MangaSerializer, NovelSerializer, ReviewSerializer, VinylSerializer
|
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")
|
|
||||||
|
|
||||||
|
|
||||||
class IndexView(TemplateView):
|
class IndexView(TemplateView):
|
||||||
@ -81,11 +59,11 @@ class MarkVinylAsPresent(MarkMediumAsPresent):
|
|||||||
model = Vinyl
|
model = Vinyl
|
||||||
|
|
||||||
|
|
||||||
class MarkRomanAsPresent(MarkMediumAsPresent):
|
class MarkNovelAsPresent(MarkMediumAsPresent):
|
||||||
model = Novel
|
model = Novel
|
||||||
|
|
||||||
|
|
||||||
class MarkRevueAsPresent(MarkMediumAsPresent):
|
class MarkReviewAsPresent(MarkMediumAsPresent):
|
||||||
model = Review
|
model = Review
|
||||||
|
|
||||||
|
|
||||||
@ -181,12 +159,12 @@ class FutureMediumViewSet(viewsets.ModelViewSet):
|
|||||||
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 GameViewSet(viewsets.ModelViewSet):
|
class GameViewSet(viewsets.ModelViewSet):
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
authlib~=0.15
|
||||||
docutils~=0.16 # for Django-admin docs
|
docutils~=0.16 # for Django-admin docs
|
||||||
Django~=2.2
|
Django~=2.2
|
||||||
django-filter~=2.4
|
django-filter~=2.4
|
||||||
|
@ -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>
|
||||||
|
@ -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 %}
|
||||||
|
@ -3,16 +3,13 @@
|
|||||||
# 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 User
|
||||||
|
|
||||||
|
|
||||||
@ -26,7 +23,12 @@ class IsMemberFilter(admin.SimpleListFilter):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
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
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
@ -35,61 +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
|
||||||
"""
|
"""
|
||||||
# FIXME Use NK20
|
if obj.is_member:
|
||||||
is_member = True
|
return mark_safe(
|
||||||
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')
|
||||||
|
@ -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}
|
|
31
users/migrations/0043_accesstoken.py
Normal file
31
users/migrations/0043_accesstoken.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 2.2.24 on 2021-11-02 15:11
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0042_delete_adhesion'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AccessToken',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('access_token', models.CharField(max_length=32, verbose_name='access token')),
|
||||||
|
('expires_in', models.PositiveIntegerField(verbose_name='expires in')),
|
||||||
|
('scopes', models.CharField(max_length=255, verbose_name='scopes')),
|
||||||
|
('refresh_token', models.CharField(max_length=32, verbose_name='refresh token')),
|
||||||
|
('expires_at', models.DateTimeField(verbose_name='expires at')),
|
||||||
|
('owner', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='owner')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'access token',
|
||||||
|
'verbose_name_plural': 'access tokens',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
28
users/migrations/0044_membership.py
Normal file
28
users/migrations/0044_membership.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 2.2.24 on 2021-11-04 13:20
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0043_accesstoken'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Membership',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('date_start', models.DateField(auto_now_add=True, verbose_name='start date')),
|
||||||
|
('date_end', models.DateField(auto_now_add=True, verbose_name='start date')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'membership',
|
||||||
|
'verbose_name_plural': 'memberships',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
22
users/migrations/0045_auto_20211114_1423.py
Normal file
22
users/migrations/0045_auto_20211114_1423.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 2.2.24 on 2021-11-14 13:23
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0044_membership'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='user',
|
||||||
|
old_name='telephone',
|
||||||
|
new_name='phone_number',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='maxemprunt',
|
||||||
|
),
|
||||||
|
]
|
23
users/migrations/0046_auto_20211114_1624.py
Normal file
23
users/migrations/0046_auto_20211114_1624.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 2.2.24 on 2021-11-14 15:24
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0045_auto_20211114_1423'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='membership',
|
||||||
|
name='date_end',
|
||||||
|
field=models.DateField(verbose_name='start date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='membership',
|
||||||
|
name='date_start',
|
||||||
|
field=models.DateField(verbose_name='start date'),
|
||||||
|
),
|
||||||
|
]
|
189
users/models.py
189
users/models.py
@ -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,5 +41,171 @@ class User(AbstractUser):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_member(self):
|
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')
|
||||||
|
@ -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']
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
|
||||||
|
@ -8,5 +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('authorize/', views.AuthorizeView.as_view(), name='auth'),
|
||||||
]
|
]
|
||||||
|
@ -1,47 +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
|
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 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)
|
|
||||||
|
|
||||||
|
|
||||||
class UserViewSet(viewsets.ModelViewSet):
|
class UserViewSet(viewsets.ModelViewSet):
|
||||||
|
Reference in New Issue
Block a user