1
0
mirror of https://gitlab.crans.org/mediatek/med.git synced 2025-02-25 12:26:33 +00:00

Compare commits

..

No commits in common. "145806b5ce73a5449a2b7690ceebbfd4571373b0" and "faf697d3cfede753dac956147b851da889792183" have entirely different histories.

26 changed files with 353 additions and 501 deletions

View File

@ -7,7 +7,7 @@ from django.contrib.auth.admin import Group, GroupAdmin
from django.contrib.sites.admin import Site, SiteAdmin from django.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 Borrow from media.models import Emprunt
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 = Borrow.objects.filter(user=request.user, user_borrowed = Emprunt.objects.filter(user=request.user,
given_back=None) date_rendu=None)
response.context_data["borrowed_items"] = user_borrowed response.context_data["borrowed_items"] = user_borrowed
return response return response

View File

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

View File

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

View File

@ -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.BorrowViewSet) router.register(r'borrowed_items', media.views.EmpruntViewSet)
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)

View File

@ -2,6 +2,7 @@
# 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, \
@ -10,7 +11,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, Borrow, Borrowable, CD, Comic, FutureMedium, \ from .models import Author, Borrowable, CD, Comic, Emprunt, FutureMedium, \
Game, Manga, Novel, Review, Vinyl Game, Manga, Novel, Review, Vinyl
@ -119,15 +120,30 @@ class ReviewAdmin(VersionAdmin, PolymorphicChildModelAdmin):
show_in_index = True show_in_index = True
class BorrowAdmin(VersionAdmin): class EmpruntAdmin(VersionAdmin):
list_display = ('borrowable', 'user', 'borrow_date', 'borrowed_with', list_display = ('media', 'user', 'date_emprunt', 'date_rendu',
'given_back_to') 'permanencier_emprunt', 'permanencier_rendu_custom')
search_fields = ('borrowable__isbn', 'borrowable__title', search_fields = ('media__title', 'media__side_identifier',
'borrowable__medium__side_identifier', 'user__username', 'date_emprunt', 'date_rendu')
'user__username', 'borrow_date', 'given_back') date_hierarchy = 'date_emprunt'
date_hierarchy = 'borrow_date' autocomplete_fields = ('media', 'user', 'permanencier_emprunt',
autocomplete_fields = ('borrowable', 'user', 'borrowed_with', 'permanencier_rendu')
'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):
""" """
@ -135,7 +151,7 @@ class BorrowAdmin(VersionAdmin):
""" """
# Make GET data mutable # Make GET data mutable
data = request.GET.copy() data = request.GET.copy()
data['borrowed_with'] = request.user data['permanencier_emprunt'] = 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)
@ -157,5 +173,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(Borrow, BorrowAdmin) admin_site.register(Emprunt, EmpruntAdmin)
admin_site.register(Game, GameAdmin) admin_site.register(Game, GameAdmin)

View File

@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-14 14:25+0100\n" "POT-Creation-Date: 2021-10-26 15:14+0200\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,7 +13,8 @@ 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:46 admin.py:102 admin.py:114 models.py:30 models.py:85 #: 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
msgid "authors" msgid "authors"
msgstr "auteurs" msgstr "auteurs"
@ -21,47 +22,55 @@ msgstr "auteurs"
msgid "external url" msgid "external url"
msgstr "URL externe" 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 #: 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:302 #: forms.py:301
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é."
#: management/commands/migrate_to_new_format.py:57 models.py:156 #: management/commands/migrate_to_new_format.py:52 models.py:408 models.py:415
msgid "CDs" msgid "CDs"
msgstr "CDs" msgstr "CDs"
#: management/commands/migrate_to_new_format.py:57 models.py:155 #: management/commands/migrate_to_new_format.py:52 models.py:407 models.py:414
msgid "CD" msgid "CD"
msgstr "CD" msgstr "CD"
#: management/commands/migrate_to_new_format.py:73 models.py:149 #: management/commands/migrate_to_new_format.py:68 models.py:362 models.py:377
msgid "vinyls" msgid "vinyls"
msgstr "vinyles" msgstr "vinyles"
#: management/commands/migrate_to_new_format.py:73 models.py:148 #: management/commands/migrate_to_new_format.py:68 models.py:361 models.py:376
msgid "vinyl" msgid "vinyl"
msgstr "vinyle" msgstr "vinyle"
#: management/commands/migrate_to_new_format.py:91 models.py:196 #: management/commands/migrate_to_new_format.py:86 models.py:466 models.py:506
msgid "reviews" msgid "reviews"
msgstr "revues" msgstr "revues"
#: management/commands/migrate_to_new_format.py:91 models.py:195 #: management/commands/migrate_to_new_format.py:86 models.py:465 models.py:505
msgid "review" msgid "review"
msgstr "revue" msgstr "revue"
#: management/commands/migrate_to_new_format.py:111 models.py:315 #: management/commands/migrate_to_new_format.py:106 models.py:629 models.py:670
msgid "games" msgid "games"
msgstr "jeux" msgstr "jeux"
#: management/commands/migrate_to_new_format.py:111 models.py:314 #: management/commands/migrate_to_new_format.py:106 models.py:628 models.py:669
msgid "game" msgid "game"
msgstr "jeu" msgstr "jeu"
#: models.py:17 #: models.py:17 models.py:598
msgid "name" msgid "name"
msgstr "nom" msgstr "nom"
@ -73,59 +82,63 @@ msgstr "note"
msgid "author" msgid "author"
msgstr "auteur" msgstr "auteur"
#: models.py:36 models.py:202 #: models.py:37 models.py:127 models.py:199 models.py:268 models.py:329
msgid "ISBN" #: models.py:383 models.py:421
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" msgid "title"
msgstr "titre" msgstr "titre"
#: models.py:49 models.py:220 #: 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" msgid "present"
msgstr "présent" msgstr "présent"
#: models.py:50 models.py:221 #: 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." msgid "Tell that the medium is present in the Mediatek."
msgstr "Indique que le medium est présent à la Mediatek." msgstr "Indique que le medium est présent à la Mediatek."
#: models.py:68 #: models.py:60
msgid "borrowable" msgid "borrowable"
msgstr "empruntable" msgstr "empruntable"
#: models.py:69 #: models.py:61
msgid "borrowables" msgid "borrowables"
msgstr "empruntables" msgstr "empruntables"
#: models.py:74 #: models.py:66 models.py:138 models.py:210 models.py:279
msgid "external URL" msgid "external URL"
msgstr "URL externe" msgstr "URL externe"
#: models.py:79 #: models.py:71 models.py:143 models.py:215 models.py:284 models.py:334
#: models.py:388
msgid "side identifier" msgid "side identifier"
msgstr "côte" msgstr "côte"
#: models.py:89 #: models.py:81
msgid "medium" msgid "medium"
msgstr "medium" msgstr "medium"
#: models.py:90 #: models.py:82
msgid "media" msgid "media"
msgstr "media" msgstr "media"
#: models.py:95 #: models.py:87 models.py:119 models.py:191 models.py:260 models.py:512
msgid "ISBN"
msgstr "ISBN"
#: models.py:88 models.py:120 models.py:192 models.py:261 models.py:513
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
msgid "subtitle" msgid "subtitle"
msgstr "sous-titre" msgstr "sous-titre"
#: models.py:101 #: models.py:101 models.py:153 models.py:225 models.py:294
msgid "number of pages" msgid "number of pages"
msgstr "nombre de pages" msgstr "nombre de pages"
#: models.py:107 #: models.py:107 models.py:159 models.py:231 models.py:300
msgid "publish date" msgid "publish date"
msgstr "date de publication" msgstr "date de publication"
@ -137,143 +150,135 @@ msgstr "livre"
msgid "books" msgid "books"
msgstr "livres" msgstr "livres"
#: models.py:119 #: models.py:177 models.py:184
msgid "comic" msgid "comic"
msgstr "BD" msgstr "BD"
#: models.py:120 #: models.py:178 models.py:185
msgid "comics" msgid "comics"
msgstr "BDs" msgstr "BDs"
#: models.py:126 #: models.py:246 models.py:253
msgid "manga" msgid "manga"
msgstr "manga" msgstr "manga"
#: models.py:127 #: models.py:247 models.py:254
msgid "mangas" msgid "mangas"
msgstr "mangas" msgstr "mangas"
#: models.py:133 #: models.py:315 models.py:322
msgid "novel" msgid "novel"
msgstr "roman" msgstr "roman"
#: models.py:134 #: models.py:316 models.py:323
msgid "novels" msgid "novels"
msgstr "romans" msgstr "romans"
#: models.py:140 #: models.py:339 models.py:368
msgid "rounds per minute" msgid "rounds per minute"
msgstr "tours par minute" msgstr "tours par minute"
#: models.py:142 #: models.py:341 models.py:370
msgid "33 RPM" msgid "33 RPM"
msgstr "33 TPM" msgstr "33 TPM"
#: models.py:143 #: models.py:342 models.py:371
msgid "45 RPM" msgid "45 RPM"
msgstr "45 TPM" msgstr "45 TPM"
#: models.py:162 #: models.py:426 models.py:472
msgid "number" msgid "number"
msgstr "nombre" msgstr "nombre"
#: models.py:166 #: models.py:430 models.py:476
msgid "year" msgid "year"
msgstr "année" msgstr "année"
#: models.py:173 #: models.py:437 models.py:483
msgid "month" msgid "month"
msgstr "mois" msgstr "mois"
#: models.py:180 #: models.py:444 models.py:490
msgid "day" msgid "day"
msgstr "jour" msgstr "jour"
#: models.py:187 #: models.py:451 models.py:497
msgid "double" msgid "double"
msgstr "double" msgstr "double"
#: models.py:210 #: models.py:520
msgid "type" msgid "type"
msgstr "type" msgstr "type"
#: models.py:212 #: models.py:522
msgid "Comic" msgid "Comic"
msgstr "BD" msgstr "BD"
#: models.py:213 #: models.py:523
msgid "Manga" msgid "Manga"
msgstr "Manga" msgstr "Manga"
#: models.py:214 #: models.py:524
msgid "Roman" msgid "Roman"
msgstr "Roman" msgstr "Roman"
#: models.py:226 #: models.py:536
msgid "future medium" msgid "future medium"
msgstr "medium à importer" msgstr "medium à importer"
#: models.py:227 #: models.py:537
msgid "future media" msgid "future media"
msgstr "medias à importer" msgstr "medias à importer"
#: models.py:237 #: models.py:551
msgid "object"
msgstr "objet"
#: models.py:242
msgid "borrower" msgid "borrower"
msgstr "emprunteur" msgstr "emprunteur"
#: models.py:245 #: models.py:554
msgid "borrowed on" msgid "borrowed on"
msgstr "emprunté le" msgstr "emprunté le"
#: models.py:250 #: models.py:559
msgid "given back on" msgid "given back on"
msgstr "rendu le" msgstr "rendu le"
#: models.py:256 #: models.py:565
msgid "borrowed with" msgid "borrowed with"
msgstr "emprunté avec" msgstr "emprunté avec"
#: models.py:257 #: models.py:566
msgid "The keyholder that registered this borrowed item." msgid "The keyholder that registered this borrowed item."
msgstr "Le permanencier qui enregistre cet emprunt." msgstr "Le permanencier qui enregistre cet emprunt."
#: models.py:265 #: models.py:575
msgid "given back to"
msgstr "rendu à"
#: models.py:266
msgid "The keyholder to whom this item was given back." msgid "The keyholder to whom this item was given back."
msgstr "Le permanencier à qui l'emprunt a été rendu." msgstr "Le permanencier à qui l'emprunt a été rendu."
#: models.py:273 #: models.py:582
msgid "borrowed item" msgid "borrowed item"
msgstr "emprunt" msgstr "emprunt"
#: models.py:274 #: models.py:583
msgid "borrowed items" msgid "borrowed items"
msgstr "emprunts" msgstr "emprunts"
#: models.py:289 #: models.py:603 models.py:644
msgid "owner" msgid "owner"
msgstr "propriétaire" msgstr "propriétaire"
#: models.py:294 #: models.py:608 models.py:649
msgid "duration" msgid "duration"
msgstr "durée" msgstr "durée"
#: models.py:298 #: models.py:612 models.py:653
msgid "minimum number of players" msgid "minimum number of players"
msgstr "nombre minimum de joueurs" msgstr "nombre minimum de joueurs"
#: models.py:302 #: models.py:616 models.py:657
msgid "maximum number of players" msgid "maximum number of players"
msgstr "nombre maximum de joueurs" msgstr "nombre maximum de joueurs"
#: models.py:307 #: models.py:621 models.py:662
msgid "comment" msgid "comment"
msgstr "commentaire" msgstr "commentaire"
@ -301,6 +306,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:25 #: views.py:47
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"

View File

@ -20,7 +20,7 @@ class Command(BaseCommand):
"Old data structure has been deleted. This script won't work " "Old data structure has been deleted. This script won't work "
"anymore (and is now useless)")) "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 OldReview, OldVinyl
# Migrate books # Migrate books

View File

@ -1,36 +0,0 @@
# Generated by Django 2.2.24 on 2021-11-14 13:23
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('media', '0044_auto_20211102_1254'),
]
operations = [
migrations.CreateModel(
name='Borrow',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('borrow_date', models.DateTimeField(verbose_name='borrowed on')),
('given_back', models.DateTimeField(blank=True, null=True, verbose_name='given back on')),
('borrowable', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='media.Borrowable', verbose_name='object')),
('borrowed_with', models.ForeignKey(help_text='The keyholder that registered this borrowed item.', on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='borrowed with')),
('given_back_to', models.ForeignKey(blank=True, help_text='The keyholder to whom this item was given back.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='given back to')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='borrower')),
],
options={
'verbose_name': 'borrowed item',
'verbose_name_plural': 'borrowed items',
'ordering': ['-borrow_date'],
},
),
migrations.DeleteModel(
name='Emprunt',
),
]

View File

@ -1,7 +1,7 @@
# -*- mode: python; coding: utf-8 -*- # -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2017-2021 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 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 _
@ -230,36 +230,35 @@ class FutureMedium(models.Model):
return "Future medium (ISBN: {isbn})".format(isbn=self.isbn, ) return "Future medium (ISBN: {isbn})".format(isbn=self.isbn, )
class Borrow(models.Model): class Emprunt(models.Model):
borrowable = models.ForeignKey( media = models.ForeignKey(
'media.Borrowable', 'media.Borrowable',
on_delete=models.PROTECT, on_delete=models.PROTECT,
verbose_name=_('object'),
) )
user = models.ForeignKey( user = models.ForeignKey(
settings.AUTH_USER_MODEL, 'users.User',
on_delete=models.PROTECT, on_delete=models.PROTECT,
verbose_name=_("borrower"), verbose_name=_("borrower"),
) )
borrow_date = models.DateTimeField( date_emprunt = models.DateTimeField(
verbose_name=_('borrowed on'), verbose_name=_('borrowed on'),
) )
given_back = models.DateTimeField( date_rendu = models.DateTimeField(
blank=True, blank=True,
null=True, null=True,
verbose_name=_('given back on'), verbose_name=_('given back on'),
) )
borrowed_with = models.ForeignKey( permanencier_emprunt = models.ForeignKey(
settings.AUTH_USER_MODEL, 'users.User',
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='+', related_name='user_permanencier_emprunt',
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.')
) )
given_back_to = models.ForeignKey( permanencier_rendu = models.ForeignKey(
settings.AUTH_USER_MODEL, 'users.User',
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='+', related_name='user_permanencier_rendu',
blank=True, blank=True,
null=True, null=True,
verbose_name=_('given back to'), verbose_name=_('given back to'),
@ -267,12 +266,12 @@ class Borrow(models.Model):
) )
def __str__(self): def __str__(self):
return str(self.borrowable) + str(self.user) return str(self.media) + 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 = ['-borrow_date'] ordering = ['-date_emprunt']
class Game(Borrowable): class Game(Borrowable):

View File

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

View File

@ -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_borrow_changelist(self): def test_comic_emprunt_changelist(self):
response = self.client.get(reverse('admin:media_borrow_changelist')) response = self.client.get(reverse('admin:media_emprunt_changelist'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_comic_borrow_add(self): def test_comic_emprunt_add(self):
response = self.client.get(reverse('admin:media_borrow_add')) response = self.client.get(reverse('admin:media_emprunt_add'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)

View File

@ -2,12 +2,15 @@
# 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/comic/<int:pk>/', path('mark-as-present/comic/<int:pk>/',
views.MarkComicAsPresent.as_view(), views.MarkComicAsPresent.as_view(),

View File

@ -2,20 +2,42 @@
# 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, Borrow, CD, Comic, FutureMedium, Game, Manga,\ from .models import Author, CD, Comic, Emprunt, FutureMedium, Game, Manga,\
Novel, Review, Vinyl Novel, Review, Vinyl
from .serializers import AuthorSerializer, BorrowSerializer, ComicSerializer, \ from .serializers import AuthorSerializer, ComicSerializer, CDSerializer,\
CDSerializer, FutureMediumSerializer, GameSerializer, MangaSerializer, \ EmpruntSerializer, FutureMediumSerializer, GameSerializer, \
NovelSerializer, ReviewSerializer, VinylSerializer 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")
class IndexView(TemplateView): class IndexView(TemplateView):
@ -159,12 +181,12 @@ class FutureMediumViewSet(viewsets.ModelViewSet):
search_fields = ["=isbn"] search_fields = ["=isbn"]
class BorrowViewSet(viewsets.ModelViewSet): class EmpruntViewSet(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 = Borrow.objects.all() queryset = Emprunt.objects.all()
serializer_class = BorrowSerializer serializer_class = EmpruntSerializer
class GameViewSet(viewsets.ModelViewSet): class GameViewSet(viewsets.ModelViewSet):

View File

@ -1,4 +1,3 @@
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

View File

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

View File

@ -56,6 +56,9 @@ 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>
@ -64,9 +67,10 @@ 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.phone_number }}</li> <li><strong>{% trans 'phone number' %}</strong> : {{ user.telephone }}</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 %}
@ -80,8 +84,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 borrow in borrowed_items %} {% for emprunt in borrowed_items %}
<li>{{ borrow.object }} ({% trans 'since' %} {{ borrow.borrow_date }})</li> <li>{{ emprunt.media }} ({% trans 'since' %} {{ emprunt.date_emprunt }})</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% else %} {% else %}

View File

@ -3,13 +3,16 @@
# 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.utils import timezone from django.contrib.auth.forms import PasswordResetForm
from django.utils.safestring import mark_safe from django.urls import reverse
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
@ -23,12 +26,7 @@ class IsMemberFilter(admin.SimpleListFilter):
) )
def queryset(self, request, queryset): def queryset(self, request, queryset):
if self.parameter_name in request.GET: # FIXME Replace with imported Note Kfet memberships
queryset = queryset.filter(
membership__date_start__lte=timezone.now(),
membership__date_end__gte=timezone.now(),
).distinct()
return queryset return queryset
@ -37,32 +35,61 @@ 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',
'phone_number', 'address', 'telephone', 'address', 'comment')}),
'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',
'is_member', 'is_staff') 'maxemprunt', 'is_member', 'is_staff')
list_filter = (IsMemberFilter, 'is_staff', 'is_superuser', 'is_active', list_filter = (IsMemberFilter, 'is_staff', 'is_superuser', 'is_active',
'groups') 'groups')
def has_add_permission(self, request): # Customize required initial fields
# Only add users through Note Kfet login add_form_template = 'admin/change_form.html'
return False 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 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
""" """
if obj.is_member: # FIXME Use NK20
return mark_safe( is_member = True
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 mark_safe( return format_html(
'<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')

57
users/forms.py Normal file
View File

@ -0,0 +1,57 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django import forms
from django.contrib.auth.forms import UsernameField
from django.core.validators import MinLengthValidator
from django.forms import ModelForm
from .models import User
class PassForm(forms.Form):
passwd1 = forms.CharField(
label=u'Nouveau mot de passe',
max_length=255,
validators=[MinLengthValidator(8)],
widget=forms.PasswordInput,
)
passwd2 = forms.CharField(
label=u'Saisir à nouveau le mot de passe',
max_length=255,
validators=[MinLengthValidator(8)],
widget=forms.PasswordInput
)
class BaseInfoForm(ModelForm):
class Meta:
model = User
fields = [
'username',
'email',
'first_name',
'last_name',
'address',
'telephone',
]
class UserCreationAdminForm(ModelForm):
"""
A form that creates a user, with no privileges,
from the given information.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['email'].required = True
self.fields['first_name'].required = True
self.fields['last_name'].required = True
class Meta:
model = User
fields = ("username", "email", "first_name", "last_name", "address",
"telephone")
field_classes = {'username': UsernameField}

View File

@ -1,31 +0,0 @@
# Generated by Django 2.2.24 on 2021-11-02 15:11
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('users', '0042_delete_adhesion'),
]
operations = [
migrations.CreateModel(
name='AccessToken',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('access_token', models.CharField(max_length=32, verbose_name='access token')),
('expires_in', models.PositiveIntegerField(verbose_name='expires in')),
('scopes', models.CharField(max_length=255, verbose_name='scopes')),
('refresh_token', models.CharField(max_length=32, verbose_name='refresh token')),
('expires_at', models.DateTimeField(verbose_name='expires at')),
('owner', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='owner')),
],
options={
'verbose_name': 'access token',
'verbose_name_plural': 'access tokens',
},
),
]

View File

@ -1,28 +0,0 @@
# Generated by Django 2.2.24 on 2021-11-04 13:20
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('users', '0043_accesstoken'),
]
operations = [
migrations.CreateModel(
name='Membership',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date_start', models.DateField(auto_now_add=True, verbose_name='start date')),
('date_end', models.DateField(auto_now_add=True, verbose_name='start date')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'membership',
'verbose_name_plural': 'memberships',
},
),
]

View File

@ -1,22 +0,0 @@
# Generated by Django 2.2.24 on 2021-11-14 13:23
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('users', '0044_membership'),
]
operations = [
migrations.RenameField(
model_name='user',
old_name='telephone',
new_name='phone_number',
),
migrations.RemoveField(
model_name='user',
name='maxemprunt',
),
]

View File

@ -1,21 +1,16 @@
# -*- mode: python; coding: utf-8 -*- # -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2017-2021 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
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):
phone_number = models.CharField( telephone = models.CharField(
verbose_name=_('phone number'), verbose_name=_('phone number'),
max_length=15, max_length=15,
blank=True, blank=True,
@ -25,6 +20,12 @@ 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...'),
@ -32,7 +33,7 @@ class User(AbstractUser):
blank=True, blank=True,
) )
date_joined = models.DateTimeField( date_joined = models.DateTimeField(
verbose_name=_('date joined'), _('date joined'),
default=timezone.now, default=timezone.now,
null=True, null=True,
) )
@ -41,173 +42,5 @@ class User(AbstractUser):
@property @property
def is_member(self): def is_member(self):
""" # FIXME Use NK20
Return True if user is member of the club. return True
"""
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')

View File

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

View File

@ -1,6 +1,7 @@
# -*- 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
@ -19,6 +20,30 @@ class TemplateTests(TestCase):
) )
self.client.force_login(self.user) self.client.force_login(self.user)
def test_users_edit_info(self):
response = self.client.get(reverse('users:edit-info'))
self.assertEqual(response.status_code, 200)
def test_users_user_changelist(self): def test_users_user_changelist(self):
response = self.client.get(reverse('admin:users_user_changelist')) response = self.client.get(reverse('admin:users_user_changelist'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_users_user_creation_form(self):
response = self.client.get(reverse('admin:users_user_add'))
self.assertEqual(response.status_code, 200)
def test_users_user_add_init_mail(self):
"""
Test that an initialization mail is send when a new user is added
"""
data = {
'username': "test_user",
'email': "test@example.com",
'first_name': "Test",
'last_name': "User",
}
response = self.client.post(reverse(
'admin:users_user_add',
), data=data)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(response.status_code, 302)

View File

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

View File

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