diff --git a/med/admin.py b/med/admin.py
index acb795d..78a27b2 100644
--- a/med/admin.py
+++ b/med/admin.py
@@ -7,7 +7,7 @@ from django.contrib.auth.admin import Group, GroupAdmin
from django.contrib.sites.admin import Site, SiteAdmin
from django.utils.translation import gettext_lazy as _
from django.views.decorators.cache import never_cache
-from media.models import Emprunt
+from media.models import Borrow
class DatabaseAdmin(AdminSite):
@@ -22,8 +22,8 @@ class DatabaseAdmin(AdminSite):
# User is always authenticated
# Get currently borrowed items
- user_borrowed = Emprunt.objects.filter(user=request.user,
- date_rendu=None)
+ user_borrowed = Borrow.objects.filter(user=request.user,
+ given_back=None)
response.context_data["borrowed_items"] = user_borrowed
return response
diff --git a/med/settings.py b/med/settings.py
index be9456a..1c90644 100644
--- a/med/settings.py
+++ b/med/settings.py
@@ -167,9 +167,26 @@ PAGINATION_NUMBER = 25
AUTH_USER_MODEL = 'users.User'
-MAX_EMPRUNT = 5 # Max emprunts
+NOTE_KFET_URL = 'https://note.crans.org'
+NOTE_KFET_CLIENT_ID = 'CHANGE_ME'
+NOTE_KFET_CLIENT_SECRET = 'CHANGE_ME'
+NOTE_KFET_SCOPES = '1_1 2_1 48_1'
try:
from .settings_local import *
except ImportError:
pass
+
+AUTHLIB_OAUTH_CLIENTS = {
+ 'notekfet': {
+ 'client_id': f'{NOTE_KFET_CLIENT_ID}',
+ 'client_secret': f'{NOTE_KFET_CLIENT_SECRET}',
+ 'access_token_url': f'{NOTE_KFET_URL}/o/token/',
+ 'refresh_token_url': f'{NOTE_KFET_URL}/o/token/',
+ 'authorize_url': f'{NOTE_KFET_URL}/o/authorize/',
+ 'userinfo_endpoint': f'{NOTE_KFET_URL}/api/me/',
+ 'client_kwargs': {
+ 'scope': NOTE_KFET_SCOPES,
+ }
+ }
+}
diff --git a/med/settings_local.example.py b/med/settings_local.example.py
index 51fb051..c28549b 100644
--- a/med/settings_local.example.py
+++ b/med/settings_local.example.py
@@ -40,3 +40,8 @@ DATABASES = {
'PORT': '',
}
}
+
+NOTE_KFET_URL = 'https://note.crans.org'
+NOTE_KFET_CLIENT_ID = 'CHANGE_ME'
+NOTE_KFET_CLIENT_SECRET = 'CHANGE_ME'
+NOTE_KFET_SCOPES = '1_1 2_1 48_1'
diff --git a/med/urls.py b/med/urls.py
index e286a50..e8fe86d 100644
--- a/med/urls.py
+++ b/med/urls.py
@@ -21,7 +21,7 @@ router.register(r'media/vinyl', media.views.VinylViewSet)
router.register(r'media/novel', media.views.NovelViewSet)
router.register(r'media/review', media.views.ReviewViewSet)
router.register(r'media/future', media.views.FutureMediumViewSet)
-router.register(r'borrowed_items', media.views.EmpruntViewSet)
+router.register(r'borrowed_items', media.views.BorrowViewSet)
router.register(r'games', media.views.GameViewSet)
router.register(r'users', users.views.UserViewSet)
router.register(r'groups', users.views.GroupViewSet)
diff --git a/media/admin.py b/media/admin.py
index 8a11026..19e93bb 100644
--- a/media/admin.py
+++ b/media/admin.py
@@ -2,7 +2,6 @@
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
-from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import ugettext_lazy as _
from polymorphic.admin import PolymorphicChildModelAdmin, \
@@ -11,7 +10,7 @@ from med.admin import admin_site
from reversion.admin import VersionAdmin
from .forms import MediaAdminForm
-from .models import Author, Borrowable, CD, Comic, Emprunt, FutureMedium, \
+from .models import Author, Borrow, Borrowable, CD, Comic, FutureMedium, \
Game, Manga, Novel, Review, Vinyl
@@ -120,30 +119,15 @@ class ReviewAdmin(VersionAdmin, PolymorphicChildModelAdmin):
show_in_index = True
-class EmpruntAdmin(VersionAdmin):
- list_display = ('media', 'user', 'date_emprunt', 'date_rendu',
- 'permanencier_emprunt', 'permanencier_rendu_custom')
- search_fields = ('media__title', 'media__side_identifier',
- 'user__username', 'date_emprunt', 'date_rendu')
- date_hierarchy = 'date_emprunt'
- autocomplete_fields = ('media', 'user', 'permanencier_emprunt',
- 'permanencier_rendu')
-
- def permanencier_rendu_custom(self, obj):
- """
- Show a button if item has not been returned yet
- """
- if obj.permanencier_rendu:
- return obj.permanencier_rendu
- else:
- return format_html(
- '{}',
- reverse('media:retour-emprunt', args=[obj.pk]),
- _('Turn back')
- )
-
- permanencier_rendu_custom.short_description = _('given back to')
- permanencier_rendu_custom.allow_tags = True
+class BorrowAdmin(VersionAdmin):
+ list_display = ('borrowable', 'user', 'borrow_date', 'borrowed_with',
+ 'given_back_to')
+ search_fields = ('borrowable__isbn', 'borrowable__title',
+ 'borrowable__medium__side_identifier',
+ 'user__username', 'borrow_date', 'given_back')
+ date_hierarchy = 'borrow_date'
+ autocomplete_fields = ('borrowable', 'user', 'borrowed_with',
+ 'given_back_to')
def add_view(self, request, form_url='', extra_context=None):
"""
@@ -151,7 +135,7 @@ class EmpruntAdmin(VersionAdmin):
"""
# Make GET data mutable
data = request.GET.copy()
- data['permanencier_emprunt'] = request.user
+ data['borrowed_with'] = request.user
request.GET = data
return super().add_view(request, form_url, extra_context)
@@ -173,5 +157,5 @@ admin_site.register(CD, CDAdmin)
admin_site.register(Vinyl, VinylAdmin)
admin_site.register(Review, ReviewAdmin)
admin_site.register(FutureMedium, FutureMediumAdmin)
-admin_site.register(Emprunt, EmpruntAdmin)
+admin_site.register(Borrow, BorrowAdmin)
admin_site.register(Game, GameAdmin)
diff --git a/media/locale/fr/LC_MESSAGES/django.po b/media/locale/fr/LC_MESSAGES/django.po
index 7e2ba46..6c0d5bc 100644
--- a/media/locale/fr/LC_MESSAGES/django.po
+++ b/media/locale/fr/LC_MESSAGES/django.po
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-10-26 15:14+0200\n"
+"POT-Creation-Date: 2021-11-14 14:25+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -13,8 +13,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
-#: admin.py:46 admin.py:102 admin.py:114 models.py:30 models.py:77
-#: models.py:149 models.py:221 models.py:290 models.py:348 models.py:394
+#: admin.py:46 admin.py:102 admin.py:114 models.py:30 models.py:85
msgid "authors"
msgstr "auteurs"
@@ -22,55 +21,47 @@ msgstr "auteurs"
msgid "external url"
msgstr "URL externe"
-#: admin.py:142
-msgid "Turn back"
-msgstr "Rendre"
-
-#: admin.py:145 models.py:574
-msgid "given back to"
-msgstr "rendu à"
-
#: fields.py:17
msgid "ISBN-10 or ISBN-13"
msgstr "ISBN-10 ou ISBN-13"
-#: forms.py:301
+#: forms.py:302
msgid "This ISBN is not found."
msgstr "L'ISBN n'a pas été trouvé."
-#: management/commands/migrate_to_new_format.py:52 models.py:408 models.py:415
+#: management/commands/migrate_to_new_format.py:57 models.py:156
msgid "CDs"
msgstr "CDs"
-#: management/commands/migrate_to_new_format.py:52 models.py:407 models.py:414
+#: management/commands/migrate_to_new_format.py:57 models.py:155
msgid "CD"
msgstr "CD"
-#: management/commands/migrate_to_new_format.py:68 models.py:362 models.py:377
+#: management/commands/migrate_to_new_format.py:73 models.py:149
msgid "vinyls"
msgstr "vinyles"
-#: management/commands/migrate_to_new_format.py:68 models.py:361 models.py:376
+#: management/commands/migrate_to_new_format.py:73 models.py:148
msgid "vinyl"
msgstr "vinyle"
-#: management/commands/migrate_to_new_format.py:86 models.py:466 models.py:506
+#: management/commands/migrate_to_new_format.py:91 models.py:196
msgid "reviews"
msgstr "revues"
-#: management/commands/migrate_to_new_format.py:86 models.py:465 models.py:505
+#: management/commands/migrate_to_new_format.py:91 models.py:195
msgid "review"
msgstr "revue"
-#: management/commands/migrate_to_new_format.py:106 models.py:629 models.py:670
+#: management/commands/migrate_to_new_format.py:111 models.py:315
msgid "games"
msgstr "jeux"
-#: management/commands/migrate_to_new_format.py:106 models.py:628 models.py:669
+#: management/commands/migrate_to_new_format.py:111 models.py:314
msgid "game"
msgstr "jeu"
-#: models.py:17 models.py:598
+#: models.py:17
msgid "name"
msgstr "nom"
@@ -82,63 +73,59 @@ msgstr "note"
msgid "author"
msgstr "auteur"
-#: models.py:37 models.py:127 models.py:199 models.py:268 models.py:329
-#: models.py:383 models.py:421
-msgid "title"
-msgstr "titre"
-
-#: models.py:41 models.py:165 models.py:237 models.py:306 models.py:352
-#: models.py:398 models.py:456 models.py:530
-msgid "present"
-msgstr "présent"
-
-#: models.py:42 models.py:166 models.py:238 models.py:307 models.py:353
-#: models.py:399 models.py:457 models.py:531
-msgid "Tell that the medium is present in the Mediatek."
-msgstr "Indique que le medium est présent à la Mediatek."
-
-#: models.py:60
-msgid "borrowable"
-msgstr "empruntable"
-
-#: models.py:61
-msgid "borrowables"
-msgstr "empruntables"
-
-#: models.py:66 models.py:138 models.py:210 models.py:279
-msgid "external URL"
-msgstr "URL externe"
-
-#: models.py:71 models.py:143 models.py:215 models.py:284 models.py:334
-#: models.py:388
-msgid "side identifier"
-msgstr "côte"
-
-#: models.py:81
-msgid "medium"
-msgstr "medium"
-
-#: models.py:82
-msgid "media"
-msgstr "media"
-
-#: models.py:87 models.py:119 models.py:191 models.py:260 models.py:512
+#: models.py:36 models.py:202
msgid "ISBN"
msgstr "ISBN"
-#: models.py:88 models.py:120 models.py:192 models.py:261 models.py:513
+#: 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:95 models.py:132 models.py:204 models.py:273
+#: 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 models.py:153 models.py:225 models.py:294
+#: models.py:101
msgid "number of pages"
msgstr "nombre de pages"
-#: models.py:107 models.py:159 models.py:231 models.py:300
+#: models.py:107
msgid "publish date"
msgstr "date de publication"
@@ -150,135 +137,143 @@ msgstr "livre"
msgid "books"
msgstr "livres"
-#: models.py:177 models.py:184
+#: models.py:119
msgid "comic"
msgstr "BD"
-#: models.py:178 models.py:185
+#: models.py:120
msgid "comics"
msgstr "BDs"
-#: models.py:246 models.py:253
+#: models.py:126
msgid "manga"
msgstr "manga"
-#: models.py:247 models.py:254
+#: models.py:127
msgid "mangas"
msgstr "mangas"
-#: models.py:315 models.py:322
+#: models.py:133
msgid "novel"
msgstr "roman"
-#: models.py:316 models.py:323
+#: models.py:134
msgid "novels"
msgstr "romans"
-#: models.py:339 models.py:368
+#: models.py:140
msgid "rounds per minute"
msgstr "tours par minute"
-#: models.py:341 models.py:370
+#: models.py:142
msgid "33 RPM"
msgstr "33 TPM"
-#: models.py:342 models.py:371
+#: models.py:143
msgid "45 RPM"
msgstr "45 TPM"
-#: models.py:426 models.py:472
+#: models.py:162
msgid "number"
msgstr "nombre"
-#: models.py:430 models.py:476
+#: models.py:166
msgid "year"
msgstr "année"
-#: models.py:437 models.py:483
+#: models.py:173
msgid "month"
msgstr "mois"
-#: models.py:444 models.py:490
+#: models.py:180
msgid "day"
msgstr "jour"
-#: models.py:451 models.py:497
+#: models.py:187
msgid "double"
msgstr "double"
-#: models.py:520
+#: models.py:210
msgid "type"
msgstr "type"
-#: models.py:522
+#: models.py:212
msgid "Comic"
msgstr "BD"
-#: models.py:523
+#: models.py:213
msgid "Manga"
msgstr "Manga"
-#: models.py:524
+#: models.py:214
msgid "Roman"
msgstr "Roman"
-#: models.py:536
+#: models.py:226
msgid "future medium"
msgstr "medium à importer"
-#: models.py:537
+#: models.py:227
msgid "future media"
msgstr "medias à importer"
-#: models.py:551
+#: models.py:237
+msgid "object"
+msgstr "objet"
+
+#: models.py:242
msgid "borrower"
msgstr "emprunteur"
-#: models.py:554
+#: models.py:245
msgid "borrowed on"
msgstr "emprunté le"
-#: models.py:559
+#: models.py:250
msgid "given back on"
msgstr "rendu le"
-#: models.py:565
+#: models.py:256
msgid "borrowed with"
msgstr "emprunté avec"
-#: models.py:566
+#: models.py:257
msgid "The keyholder that registered this borrowed item."
msgstr "Le permanencier qui enregistre cet emprunt."
-#: models.py:575
+#: 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:582
+#: models.py:273
msgid "borrowed item"
msgstr "emprunt"
-#: models.py:583
+#: models.py:274
msgid "borrowed items"
msgstr "emprunts"
-#: models.py:603 models.py:644
+#: models.py:289
msgid "owner"
msgstr "propriétaire"
-#: models.py:608 models.py:649
+#: models.py:294
msgid "duration"
msgstr "durée"
-#: models.py:612 models.py:653
+#: models.py:298
msgid "minimum number of players"
msgstr "nombre minimum de joueurs"
-#: models.py:616 models.py:657
+#: models.py:302
msgid "maximum number of players"
msgstr "nombre maximum de joueurs"
-#: models.py:621 models.py:662
+#: models.py:307
msgid "comment"
msgstr "commentaire"
@@ -306,6 +301,6 @@ msgstr "ISBN invalide : mauvaise longueur"
msgid "Invalid ISBN: Only upper case allowed"
msgstr "ISBN invalide : seulement les majuscules sont autorisées"
-#: views.py:47
+#: views.py:25
msgid "Welcome to the Mediatek database"
msgstr "Bienvenue sur la base de données de la Mediatek"
diff --git a/media/management/commands/migrate_to_new_format.py b/media/management/commands/migrate_to_new_format.py
index c718b3f..cb3d34e 100644
--- a/media/management/commands/migrate_to_new_format.py
+++ b/media/management/commands/migrate_to_new_format.py
@@ -20,7 +20,7 @@ class Command(BaseCommand):
"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, \
+ from media.models import OldCD, OldComic, OldGame, OldManga, OldNovel,\
OldReview, OldVinyl
# Migrate books
diff --git a/media/migrations/0045_auto_20211114_1423.py b/media/migrations/0045_auto_20211114_1423.py
new file mode 100644
index 0000000..a719a07
--- /dev/null
+++ b/media/migrations/0045_auto_20211114_1423.py
@@ -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',
+ ),
+ ]
diff --git a/media/models.py b/media/models.py
index 2d5ccfb..eda6597 100644
--- a/media/models.py
+++ b/media/models.py
@@ -1,7 +1,7 @@
# -*- mode: python; coding: utf-8 -*-
-# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
+# Copyright (C) 2017-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
-
+from django.conf import settings
from django.core.validators import MinValueValidator
from django.db import models
from django.utils.translation import gettext_lazy as _
@@ -230,35 +230,36 @@ class FutureMedium(models.Model):
return "Future medium (ISBN: {isbn})".format(isbn=self.isbn, )
-class Emprunt(models.Model):
- media = models.ForeignKey(
+class Borrow(models.Model):
+ borrowable = models.ForeignKey(
'media.Borrowable',
on_delete=models.PROTECT,
+ verbose_name=_('object'),
)
user = models.ForeignKey(
- 'users.User',
+ settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
verbose_name=_("borrower"),
)
- date_emprunt = models.DateTimeField(
+ borrow_date = models.DateTimeField(
verbose_name=_('borrowed on'),
)
- date_rendu = models.DateTimeField(
+ given_back = models.DateTimeField(
blank=True,
null=True,
verbose_name=_('given back on'),
)
- permanencier_emprunt = models.ForeignKey(
- 'users.User',
+ borrowed_with = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
- related_name='user_permanencier_emprunt',
+ related_name='+',
verbose_name=_('borrowed with'),
help_text=_('The keyholder that registered this borrowed item.')
)
- permanencier_rendu = models.ForeignKey(
- 'users.User',
+ given_back_to = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
- related_name='user_permanencier_rendu',
+ related_name='+',
blank=True,
null=True,
verbose_name=_('given back to'),
@@ -266,12 +267,12 @@ class Emprunt(models.Model):
)
def __str__(self):
- return str(self.media) + str(self.user)
+ return str(self.borrowable) + str(self.user)
class Meta:
verbose_name = _("borrowed item")
verbose_name_plural = _("borrowed items")
- ordering = ['-date_emprunt']
+ ordering = ['-borrow_date']
class Game(Borrowable):
diff --git a/media/serializers.py b/media/serializers.py
index 97c0ff5..aea4f08 100644
--- a/media/serializers.py
+++ b/media/serializers.py
@@ -1,6 +1,6 @@
from rest_framework import serializers
-from .models import Author, CD, Comic, FutureMedium, Manga, Emprunt, Game, \
+from .models import Author, Borrow, CD, Comic, FutureMedium, Manga, Game, \
Novel, Review, Vinyl
@@ -52,15 +52,13 @@ class FutureMediumSerializer(serializers.ModelSerializer):
fields = '__all__'
-class EmpruntSerializer(serializers.HyperlinkedModelSerializer):
+class BorrowSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
- model = Emprunt
- fields = ['url', 'media', 'user', 'date_emprunt', 'date_rendu',
- 'permanencier_emprunt', 'permanencier_rendu']
+ model = Borrow
+ fields = '__all__'
class GameSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Game
- fields = ['url', 'name', 'proprietaire', 'duree', 'nombre_joueurs_min',
- 'nombre_joueurs_max', 'comment']
+ fields = '__all__'
diff --git a/media/tests/test_templates.py b/media/tests/test_templates.py
index d6bf8b9..15b6df9 100644
--- a/media/tests/test_templates.py
+++ b/media/tests/test_templates.py
@@ -55,10 +55,10 @@ class TemplateTests(TestCase):
), data=data)
self.assertEqual(response.status_code, 302)
- def test_comic_emprunt_changelist(self):
- response = self.client.get(reverse('admin:media_emprunt_changelist'))
+ def test_comic_borrow_changelist(self):
+ response = self.client.get(reverse('admin:media_borrow_changelist'))
self.assertEqual(response.status_code, 200)
- def test_comic_emprunt_add(self):
- response = self.client.get(reverse('admin:media_emprunt_add'))
+ def test_comic_borrow_add(self):
+ response = self.client.get(reverse('admin:media_borrow_add'))
self.assertEqual(response.status_code, 200)
diff --git a/media/urls.py b/media/urls.py
index 261d559..973bf45 100644
--- a/media/urls.py
+++ b/media/urls.py
@@ -2,15 +2,12 @@
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
-from django.conf.urls import url
from django.urls import path
from . import views
app_name = 'media'
urlpatterns = [
- url(r'^retour_emprunt/(?P[0-9]+)$', views.retour_emprunt,
- name='retour-emprunt'),
path('find/', views.FindMediumView.as_view(), name="find"),
path('mark-as-present/comic//',
views.MarkComicAsPresent.as_view(),
diff --git a/media/views.py b/media/views.py
index bd0e2f4..4934079 100644
--- a/media/views.py
+++ b/media/views.py
@@ -2,42 +2,20 @@
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
-from django.contrib import messages
-from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse
from django_filters.rest_framework import DjangoFilterBackend
-from django.db import transaction
from django.shortcuts import redirect
-from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView, DetailView
from rest_framework import viewsets
from rest_framework.filters import SearchFilter
-from reversion import revisions as reversion
-from .models import Author, CD, Comic, Emprunt, FutureMedium, Game, Manga,\
+from .models import Author, Borrow, CD, Comic, FutureMedium, Game, Manga,\
Novel, Review, Vinyl
-from .serializers import AuthorSerializer, ComicSerializer, CDSerializer,\
- EmpruntSerializer, FutureMediumSerializer, GameSerializer, \
- MangaSerializer, NovelSerializer, ReviewSerializer, VinylSerializer
-
-
-@login_required
-@permission_required('media.change_emprunt')
-def retour_emprunt(request, empruntid):
- try:
- emprunt_instance = Emprunt.objects.get(pk=empruntid)
- except Emprunt.DoesNotExist:
- messages.error(request, u"Entrée inexistante")
- return redirect("admin:media_emprunt_changelist")
- with transaction.atomic(), reversion.create_revision():
- emprunt_instance.permanencier_rendu = request.user
- emprunt_instance.date_rendu = timezone.now()
- emprunt_instance.save()
- reversion.set_user(request.user)
- messages.success(request, "Retour enregistré")
- return redirect("admin:media_emprunt_changelist")
+from .serializers import AuthorSerializer, BorrowSerializer, ComicSerializer, \
+ CDSerializer, FutureMediumSerializer, GameSerializer, MangaSerializer, \
+ NovelSerializer, ReviewSerializer, VinylSerializer
class IndexView(TemplateView):
@@ -181,12 +159,12 @@ class FutureMediumViewSet(viewsets.ModelViewSet):
search_fields = ["=isbn"]
-class EmpruntViewSet(viewsets.ModelViewSet):
+class BorrowViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows borrowed items to be viewed or edited.
"""
- queryset = Emprunt.objects.all()
- serializer_class = EmpruntSerializer
+ queryset = Borrow.objects.all()
+ serializer_class = BorrowSerializer
class GameViewSet(viewsets.ModelViewSet):
diff --git a/requirements.txt b/requirements.txt
index 7f2237e..19a6e17 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
+authlib~=0.15
docutils~=0.16 # for Django-admin docs
Django~=2.2
django-filter~=2.4
diff --git a/theme/templates/admin/base_site.html b/theme/templates/admin/base_site.html
index 4adb2d8..5a4d054 100644
--- a/theme/templates/admin/base_site.html
+++ b/theme/templates/admin/base_site.html
@@ -54,7 +54,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if user.is_authenticated %}
{% trans 'Log out' %}
{% else %}
- {% trans 'Log in' %}
+ {% trans 'Log in' %}
{% endif %}
{% endblock %}
diff --git a/theme/templates/admin/index.html b/theme/templates/admin/index.html
index 08c3498..4b285ff 100644
--- a/theme/templates/admin/index.html
+++ b/theme/templates/admin/index.html
@@ -56,9 +56,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
- {% trans 'username' %} : {{ user.username }}
@@ -67,10 +64,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
- {% trans 'date joined' %} : {{ user.date_joined }}
- {% trans 'last login' %} : {{ user.last_login }}
- {% trans 'address' %} : {{ user.address }}
- - {% trans 'phone number' %} : {{ user.telephone }}
+ - {% trans 'phone number' %} : {{ user.phone_number }}
- {% trans 'groups' %} : {% for g in user.groups.all %}{{ g.name }} {% endfor %}
- - {% trans 'maximum borrowed' %} : {{ user.maxemprunt }}
-
{% trans 'membership for current year' %} :
{% if user.is_member %}
@@ -84,8 +80,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% trans 'Current borrowed items' %}
{% if borrowed_items %}
- {% for emprunt in borrowed_items %}
- - {{ emprunt.media }} ({% trans 'since' %} {{ emprunt.date_emprunt }})
+ {% for borrow in borrowed_items %}
+ - {{ borrow.object }} ({% trans 'since' %} {{ borrow.borrow_date }})
{% endfor %}
{% else %}
diff --git a/users/admin.py b/users/admin.py
index 6849ef6..bfd0fa9 100644
--- a/users/admin.py
+++ b/users/admin.py
@@ -3,16 +3,13 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin
-from django.contrib import messages
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
-from django.contrib.auth.forms import PasswordResetForm
-from django.urls import reverse
-from django.utils.html import format_html
+from django.utils import timezone
+from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from reversion.admin import VersionAdmin
from med.admin import admin_site
-from .forms import UserCreationAdminForm
from .models import User
@@ -26,7 +23,12 @@ class IsMemberFilter(admin.SimpleListFilter):
)
def queryset(self, request, queryset):
- # FIXME Replace with imported Note Kfet memberships
+ if self.parameter_name in request.GET:
+ queryset = queryset.filter(
+ membership__date_start__lte=timezone.now(),
+ membership__date_end__gte=timezone.now(),
+ ).distinct()
+
return queryset
@@ -35,61 +37,32 @@ class UserAdmin(VersionAdmin, BaseUserAdmin):
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {'fields': ('first_name', 'last_name', 'email',
- 'telephone', 'address', 'comment')}),
+ 'phone_number', 'address',
+ 'comment')}),
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
- 'groups', 'user_permissions',
- 'maxemprunt')}),
+ 'groups', 'user_permissions')}),
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
)
list_display = ('username', 'email', 'first_name', 'last_name',
- 'maxemprunt', 'is_member', 'is_staff')
+ 'is_member', 'is_staff')
list_filter = (IsMemberFilter, 'is_staff', 'is_superuser', 'is_active',
'groups')
- # Customize required initial fields
- add_form_template = 'admin/change_form.html'
- add_form = UserCreationAdminForm
- add_fieldsets = (
- (None, {
- 'classes': ('wide',),
- 'fields': ("username", "email", "first_name", "last_name",
- "address", "telephone"),
- }),
- )
-
- def save_model(self, request, obj, form, change):
- """
- On creation, send a password init mail
- """
- super().save_model(request, obj, form, change)
-
- if not change:
- # Virtually fill the password reset form
- password_reset = PasswordResetForm(data={'email': obj.email})
- if password_reset.is_valid():
- password_reset.save(request=request,
- use_https=request.is_secure())
- messages.success(request, _("An email to set the password"
- " was sent."))
- else:
- messages.error(request, _("The email is invalid."))
+ def has_add_permission(self, request):
+ # Only add users through Note Kfet login
+ return False
def is_member(self, obj):
"""
Get current membership year and check if user is there
"""
- # FIXME Use NK20
- is_member = True
- if is_member:
- return format_html(
+ if obj.is_member:
+ return mark_safe(
''
)
else:
- return format_html(
- ' '
- '{}',
- reverse('users:adherer', args=[obj.pk]),
- _('Adhere')
+ return mark_safe(
+ ''
)
is_member.short_description = _('is member')
diff --git a/users/forms.py b/users/forms.py
deleted file mode 100644
index 41f0a9b..0000000
--- a/users/forms.py
+++ /dev/null
@@ -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}
diff --git a/users/migrations/0043_accesstoken.py b/users/migrations/0043_accesstoken.py
new file mode 100644
index 0000000..60b8436
--- /dev/null
+++ b/users/migrations/0043_accesstoken.py
@@ -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',
+ },
+ ),
+ ]
diff --git a/users/migrations/0044_membership.py b/users/migrations/0044_membership.py
new file mode 100644
index 0000000..188a697
--- /dev/null
+++ b/users/migrations/0044_membership.py
@@ -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',
+ },
+ ),
+ ]
diff --git a/users/migrations/0045_auto_20211114_1423.py b/users/migrations/0045_auto_20211114_1423.py
new file mode 100644
index 0000000..6bca1cd
--- /dev/null
+++ b/users/migrations/0045_auto_20211114_1423.py
@@ -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',
+ ),
+ ]
diff --git a/users/models.py b/users/models.py
index 3b5cf51..36f4d60 100644
--- a/users/models.py
+++ b/users/models.py
@@ -1,16 +1,21 @@
# -*- mode: python; coding: utf-8 -*-
-# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
+# Copyright (C) 2017-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
+from datetime import datetime
+
+import requests
+from authlib.integrations.django_client import OAuth
+from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.db import models
+from django.db.models import Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
-from med.settings import MAX_EMPRUNT
class User(AbstractUser):
- telephone = models.CharField(
+ phone_number = models.CharField(
verbose_name=_('phone number'),
max_length=15,
blank=True,
@@ -20,12 +25,6 @@ class User(AbstractUser):
max_length=255,
blank=True,
)
- maxemprunt = models.IntegerField(
- verbose_name=_('maximum borrowed'),
- help_text=_('Maximal amount of simultaneous borrowed item '
- 'authorized.'),
- default=MAX_EMPRUNT,
- )
comment = models.CharField(
verbose_name=_('comment'),
help_text=_('Promotion...'),
@@ -33,7 +32,7 @@ class User(AbstractUser):
blank=True,
)
date_joined = models.DateTimeField(
- _('date joined'),
+ verbose_name=_('date joined'),
default=timezone.now,
null=True,
)
@@ -42,5 +41,173 @@ class User(AbstractUser):
@property
def is_member(self):
- # FIXME Use NK20
- return True
+ """
+ Return True if user is member of the club.
+ """
+ return Membership.objects.filter(
+ user=self,
+ date_start__lte=timezone.now(),
+ date_end__gte=timezone.now()).exists()
+
+ def update_data(self, data: dict):
+ """
+ Update user data from given dictionary.
+ Useful when we want to update user data from Note Kfet.
+
+ Parameters
+ ----------
+ data : dict
+ Dictionary with user data to update.
+ """
+ self.email = data['email']
+ self.first_name = data['first_name']
+ self.last_name = data['last_name']
+ self.phone_number = data['profile']['phone_number']
+ self.address = data['profile']['address']
+ self.comment = data['profile']['section']
+
+ 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(
+ auto_now_add=True,
+ verbose_name=_('start date'),
+ )
+
+ date_end = models.DateField(
+ auto_now_add=True,
+ 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')
diff --git a/users/serializers.py b/users/serializers.py
index e1b5faf..6dfd05a 100644
--- a/users/serializers.py
+++ b/users/serializers.py
@@ -8,7 +8,7 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ['url', 'username', 'first_name', 'last_name', 'email',
- 'groups', 'telephone', 'address', 'maxemprunt', 'comment',
+ 'groups', 'phone_number', 'address', 'comment',
'date_joined']
diff --git a/users/tests/test_templates.py b/users/tests/test_templates.py
index f94e9b7..66879e5 100644
--- a/users/tests/test_templates.py
+++ b/users/tests/test_templates.py
@@ -1,7 +1,6 @@
# -*- mode: python; coding: utf-8 -*-
# SPDX-License-Identifier: GPL-3.0-or-later
-from django.core import mail
from django.test import TestCase
from django.urls import reverse
from users.models import User
@@ -20,30 +19,6 @@ class TemplateTests(TestCase):
)
self.client.force_login(self.user)
- def test_users_edit_info(self):
- response = self.client.get(reverse('users:edit-info'))
- self.assertEqual(response.status_code, 200)
-
def test_users_user_changelist(self):
response = self.client.get(reverse('admin:users_user_changelist'))
self.assertEqual(response.status_code, 200)
-
- def test_users_user_creation_form(self):
- response = self.client.get(reverse('admin:users_user_add'))
- self.assertEqual(response.status_code, 200)
-
- def test_users_user_add_init_mail(self):
- """
- Test that an initialization mail is send when a new user is added
- """
- data = {
- 'username': "test_user",
- 'email': "test@example.com",
- 'first_name': "Test",
- 'last_name': "User",
- }
- response = self.client.post(reverse(
- 'admin:users_user_add',
- ), data=data)
- self.assertEqual(len(mail.outbox), 1)
- self.assertEqual(response.status_code, 302)
diff --git a/users/urls.py b/users/urls.py
index 457e218..54fb5f1 100644
--- a/users/urls.py
+++ b/users/urls.py
@@ -8,5 +8,6 @@ from . import views
app_name = 'users'
urlpatterns = [
- url(r'^edit_info/$', views.edit_info, name='edit-info'),
+ url('login/', views.LoginView.as_view(), name='login'),
+ url('authorize/', views.AuthorizeView.as_view(), name='auth'),
]
diff --git a/users/views.py b/users/views.py
index 0f5b17a..025961a 100644
--- a/users/views.py
+++ b/users/views.py
@@ -1,47 +1,47 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
+from datetime import datetime
-from django.contrib import messages
-from django.contrib.auth.decorators import login_required
+from authlib.integrations.django_client import OAuth
+from django.contrib.auth import login
from django.contrib.auth.models import Group
-from django.db import transaction
-from django.shortcuts import redirect, render
-from django.template.context_processors import csrf
-from django.utils.translation import ugettext_lazy as _
+from django.urls import reverse
+from django.utils import timezone
+from django.views.generic import RedirectView
from rest_framework import viewsets
-from reversion import revisions as reversion
-from users.forms import BaseInfoForm
-from users.models import User
+from users.models import User, AccessToken
from .serializers import GroupSerializer, UserSerializer
-def form(ctx, template, request):
- c = ctx
- c.update(csrf(request))
- return render(request, template, c)
+class LoginView(RedirectView):
+ def get_redirect_url(self, *args, **kwargs):
+ oauth = OAuth()
+ oauth.register('notekfet')
+ redirect_url = self.request.build_absolute_uri(reverse('users:auth'))
+ return oauth.notekfet.authorize_redirect(self.request,
+ redirect_url).url
-@login_required
-def edit_info(request):
- """
- Edite son utilisateur
- """
- user = BaseInfoForm(request.POST or None, instance=request.user)
- if user.is_valid():
- with transaction.atomic(), reversion.create_revision():
- user.save()
- reversion.set_user(request.user)
- reversion.set_comment("Champs modifié(s) : %s" % ', '.join(
- field for field in user.changed_data))
- messages.success(request, "L'user a bien été modifié")
- return redirect("index")
- return form({
- 'form': user,
- 'password_change': True,
- 'title': _('Edit user profile'),
- }, 'users/user.html', request)
+class AuthorizeView(RedirectView):
+ def get_redirect_url(self, *args, **kwargs):
+ oauth = OAuth()
+ oauth.register('notekfet')
+ token = oauth.notekfet.authorize_access_token(self.request)
+ token_obj = AccessToken.objects.create(
+ access_token=token['access_token'],
+ expires_in=token['expires_in'],
+ scopes=token['scope'],
+ refresh_token=token['refresh_token'],
+ expires_at=timezone.utc.fromutc(
+ datetime.fromtimestamp(token['expires_at'])),
+ )
+ user = token_obj.fetch_user(True)
+ self.request.session['access_token_id'] = token_obj.id
+ self.request.session.save()
+ login(self.request, user)
+ return reverse('index')
class UserViewSet(viewsets.ModelViewSet):