diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a7e7bb5..700a1fa 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,8 +9,8 @@ py39-django22: - > apt-get update && apt-get install --no-install-recommends -y - python3-django python3-django-reversion python3-djangorestframework - python3-docutils python3-requests tox + python3-django python3-django-polymorphic python3-django-reversion + python3-djangorestframework python3-docutils python3-requests tox script: tox -e py39 linters: diff --git a/med/settings.py b/med/settings.py index 69904ef..be9456a 100644 --- a/med/settings.py +++ b/med/settings.py @@ -36,6 +36,7 @@ INSTALLED_APPS = [ 'reversion', 'rest_framework', 'django_extensions', + 'polymorphic', # Django contrib 'django.contrib.admin', diff --git a/media/admin.py b/media/admin.py index 4ee4522..8a11026 100644 --- a/media/admin.py +++ b/media/admin.py @@ -5,12 +5,14 @@ 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, \ + PolymorphicParentModelAdmin from med.admin import admin_site from reversion.admin import VersionAdmin from .forms import MediaAdminForm -from .models import Author, CD, Comic, Emprunt, FutureMedium, Game, Manga,\ - Novel, Review, Vinyl +from .models import Author, Borrowable, CD, Comic, Emprunt, FutureMedium, \ + Game, Manga, Novel, Review, Vinyl class AuthorAdmin(VersionAdmin): @@ -18,7 +20,17 @@ class AuthorAdmin(VersionAdmin): search_fields = ('name',) -class MediumAdmin(VersionAdmin): +class BorrowableAdmin(PolymorphicParentModelAdmin): + search_fields = ('title',) + child_models = (CD, Comic, Manga, Novel, Review, Vinyl,) + + def get_model_perms(self, request): + # We don't want that the borrowable items appear directly in + # main menu, but we still want search borrowable items. + return {} + + +class MediumAdmin(VersionAdmin, PolymorphicChildModelAdmin): list_display = ('__str__', 'authors_list', 'side_identifier', 'isbn', 'external_link') search_fields = ('title', 'authors__name', 'side_identifier', 'subtitle', @@ -26,6 +38,7 @@ class MediumAdmin(VersionAdmin): autocomplete_fields = ('authors',) date_hierarchy = 'publish_date' form = MediaAdminForm + show_in_index = True def authors_list(self, obj): return ", ".join([a.name for a in obj.authors.all()]) @@ -77,10 +90,11 @@ class FutureMediumAdmin(VersionAdmin): extra_context=extra_context) -class CDAdmin(VersionAdmin): +class CDAdmin(VersionAdmin, PolymorphicChildModelAdmin): list_display = ('title', 'authors_list', 'side_identifier',) search_fields = ('title', 'authors__name', 'side_identifier',) autocomplete_fields = ('authors',) + show_in_index = True def authors_list(self, obj): return ", ".join([a.name for a in obj.authors.all()]) @@ -88,10 +102,11 @@ class CDAdmin(VersionAdmin): authors_list.short_description = _('authors') -class VinylAdmin(VersionAdmin): +class VinylAdmin(VersionAdmin, PolymorphicChildModelAdmin): list_display = ('title', 'authors_list', 'side_identifier', 'rpm',) search_fields = ('title', 'authors__name', 'side_identifier', 'rpm',) autocomplete_fields = ('authors',) + show_in_index = True def authors_list(self, obj): return ", ".join([a.name for a in obj.authors.all()]) @@ -99,9 +114,10 @@ class VinylAdmin(VersionAdmin): authors_list.short_description = _('authors') -class ReviewAdmin(VersionAdmin): +class ReviewAdmin(VersionAdmin, PolymorphicChildModelAdmin): list_display = ('__str__', 'number', 'year', 'month', 'day', 'double',) search_fields = ('title', 'number', 'year',) + show_in_index = True class EmpruntAdmin(VersionAdmin): @@ -140,14 +156,16 @@ class EmpruntAdmin(VersionAdmin): return super().add_view(request, form_url, extra_context) -class GameAdmin(VersionAdmin): - list_display = ('name', 'owner', 'duration', 'players_min', +class GameAdmin(VersionAdmin, PolymorphicChildModelAdmin): + list_display = ('title', 'owner', 'duration', 'players_min', 'players_max', 'comment') search_fields = ('name', 'owner__username', 'duration', 'comment') autocomplete_fields = ('owner',) + show_in_index = True admin_site.register(Author, AuthorAdmin) +admin_site.register(Borrowable, BorrowableAdmin) admin_site.register(Comic, MediumAdmin) admin_site.register(Manga, MediumAdmin) admin_site.register(Novel, MediumAdmin) diff --git a/media/forms.py b/media/forms.py index 04ed691..0651764 100644 --- a/media/forms.py +++ b/media/forms.py @@ -9,6 +9,7 @@ import unicodedata from urllib.error import HTTPError import urllib.request +from django.core.exceptions import ValidationError from django.db.models import QuerySet from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ @@ -320,6 +321,13 @@ class MediaAdminForm(ModelForm): return self.cleaned_data def _clean_fields(self): + # First clean ISBN field + isbn_field = self.fields['isbn'] + isbn = isbn_field.widget.value_from_datadict( + self.data, self.files, self.add_prefix('isbn')) + isbn = isbn_field.clean(isbn) + self.cleaned_data['isbn'] = isbn + for name, field in self.fields.items(): # value_from_datadict() gets the data from the data dictionaries. # Each widget type knows how to retrieve its own data, because some @@ -329,7 +337,6 @@ class MediaAdminForm(ModelForm): else: value = field.widget.value_from_datadict( self.data, self.files, self.add_prefix(name)) - from django.core.exceptions import ValidationError try: # We don't want to check a field when we enter an ISBN. if "isbn" not in self.data \ diff --git a/media/locale/fr/LC_MESSAGES/django.po b/media/locale/fr/LC_MESSAGES/django.po index 4d4f603..7e2ba46 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-23 18:27+0200\n" +"POT-Creation-Date: 2021-10-26 15:14+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -13,20 +13,20 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: admin.py:33 admin.py:88 admin.py:99 models.py:29 models.py:65 models.py:130 -#: models.py:192 models.py:243 models.py:274 +#: admin.py:46 admin.py:102 admin.py:114 models.py:30 models.py:77 +#: models.py:149 models.py:221 models.py:290 models.py:348 models.py:394 msgid "authors" msgstr "auteurs" -#: admin.py:43 +#: admin.py:56 msgid "external url" msgstr "URL externe" -#: admin.py:126 +#: admin.py:142 msgid "Turn back" msgstr "Rendre" -#: admin.py:129 models.py:407 +#: admin.py:145 models.py:574 msgid "given back to" msgstr "rendu à" @@ -38,220 +38,249 @@ msgstr "ISBN-10 ou ISBN-13" msgid "This ISBN is not found." msgstr "L'ISBN n'a pas été trouvé." -#: models.py:16 models.py:431 -msgid "name" -msgstr "nom" - -#: models.py:21 -msgid "note" -msgstr "note" - -#: models.py:28 -msgid "author" -msgstr "auteur" - -#: models.py:35 models.py:100 models.py:162 models.py:345 -msgid "ISBN" -msgstr "ISBN" - -#: models.py:36 models.py:101 models.py:163 models.py:346 -msgid "You may be able to scan it from a bar code." -msgstr "Peut souvent être scanné à partir du code barre." - -#: models.py:43 models.py:108 models.py:170 models.py:224 models.py:263 -#: models.py:294 -msgid "title" -msgstr "titre" - -#: models.py:48 models.py:113 models.py:175 -msgid "subtitle" -msgstr "sous-titre" - -#: models.py:54 models.py:119 models.py:181 -msgid "external URL" -msgstr "URL externe" - -#: models.py:59 models.py:124 models.py:186 models.py:229 models.py:268 -msgid "side identifier" -msgstr "côte" - -#: models.py:69 models.py:134 models.py:196 -msgid "number of pages" -msgstr "nombre de pages" - -#: models.py:75 models.py:140 models.py:202 -msgid "publish date" -msgstr "date de publication" - -#: models.py:81 models.py:146 models.py:208 models.py:247 models.py:278 -#: models.py:329 models.py:363 -msgid "present" -msgstr "présent" - -#: models.py:82 models.py:147 models.py:209 models.py:248 models.py:279 -#: models.py:330 models.py:364 -msgid "Tell that the medium is present in the Mediatek." -msgstr "Indique que le medium est présent à la Mediatek." - -#: models.py:93 models.py:355 -msgid "comic" -msgstr "BD" - -#: models.py:94 -msgid "comics" -msgstr "BDs" - -#: models.py:155 -msgid "manga" -msgstr "manga" - -#: models.py:156 -msgid "mangas" -msgstr "mangas" - -#: models.py:217 -msgid "novel" -msgstr "roman" - -#: models.py:218 -msgid "novels" -msgstr "romans" - -#: models.py:234 -msgid "rounds per minute" -msgstr "tours par minute" - -#: models.py:236 -msgid "33 RPM" -msgstr "33 TPM" - -#: models.py:237 -msgid "45 RPM" -msgstr "45 TPM" - -#: models.py:256 -msgid "vinyl" -msgstr "vinyle" - -#: models.py:257 -msgid "vinyls" -msgstr "vinyles" - -#: models.py:287 -msgid "CD" -msgstr "CD" - -#: models.py:288 +#: management/commands/migrate_to_new_format.py:52 models.py:408 models.py:415 msgid "CDs" msgstr "CDs" -#: models.py:299 -msgid "number" -msgstr "nombre" +#: management/commands/migrate_to_new_format.py:52 models.py:407 models.py:414 +msgid "CD" +msgstr "CD" -#: models.py:303 -msgid "year" -msgstr "année" +#: management/commands/migrate_to_new_format.py:68 models.py:362 models.py:377 +msgid "vinyls" +msgstr "vinyles" -#: models.py:310 -msgid "month" -msgstr "mois" +#: management/commands/migrate_to_new_format.py:68 models.py:361 models.py:376 +msgid "vinyl" +msgstr "vinyle" -#: models.py:317 -msgid "day" -msgstr "jour" - -#: models.py:324 -msgid "double" -msgstr "double" - -#: models.py:338 -msgid "review" -msgstr "revue" - -#: models.py:339 +#: management/commands/migrate_to_new_format.py:86 models.py:466 models.py:506 msgid "reviews" msgstr "revues" -#: models.py:353 -msgid "type" -msgstr "type" +#: management/commands/migrate_to_new_format.py:86 models.py:465 models.py:505 +msgid "review" +msgstr "revue" -#: models.py:356 -msgid "Manga" -msgstr "Manga" +#: management/commands/migrate_to_new_format.py:106 models.py:629 models.py:670 +msgid "games" +msgstr "jeux" -#: models.py:357 -msgid "Roman" -msgstr "Roman" - -#: models.py:369 -msgid "future medium" -msgstr "medium à importer" - -#: models.py:370 -msgid "future media" -msgstr "medias à importer" - -#: models.py:384 -msgid "borrower" -msgstr "emprunteur" - -#: models.py:387 -msgid "borrowed on" -msgstr "emprunté le" - -#: models.py:392 -msgid "given back on" -msgstr "rendu le" - -#: models.py:398 -msgid "borrowed with" -msgstr "emprunté avec" - -#: models.py:399 -msgid "The keyholder that registered this borrowed item." -msgstr "Le permanencier qui enregistre cet emprunt." - -#: models.py:408 -msgid "The keyholder to whom this item was given back." -msgstr "Le permanencier à qui l'emprunt a été rendu." - -#: models.py:415 -msgid "borrowed item" -msgstr "emprunt" - -#: models.py:416 -msgid "borrowed items" -msgstr "emprunts" - -#: models.py:436 -msgid "owner" -msgstr "propriétaire" - -#: models.py:441 -msgid "duration" -msgstr "durée" - -#: models.py:445 -msgid "minimum number of players" -msgstr "nombre minimum de joueurs" - -#: models.py:449 -msgid "maximum number of players" -msgstr "nombre maximum de joueurs" - -#: models.py:454 -msgid "comment" -msgstr "commentaire" - -#: models.py:461 +#: management/commands/migrate_to_new_format.py:106 models.py:628 models.py:669 msgid "game" msgstr "jeu" -#: models.py:462 -msgid "games" -msgstr "jeux" +#: models.py:17 models.py:598 +msgid "name" +msgstr "nom" + +#: models.py:22 +msgid "note" +msgstr "note" + +#: models.py:29 +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 +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" +msgstr "sous-titre" + +#: models.py:101 models.py:153 models.py:225 models.py:294 +msgid "number of pages" +msgstr "nombre de pages" + +#: models.py:107 models.py:159 models.py:231 models.py:300 +msgid "publish date" +msgstr "date de publication" + +#: models.py:113 +msgid "book" +msgstr "livre" + +#: models.py:114 +msgid "books" +msgstr "livres" + +#: models.py:177 models.py:184 +msgid "comic" +msgstr "BD" + +#: models.py:178 models.py:185 +msgid "comics" +msgstr "BDs" + +#: models.py:246 models.py:253 +msgid "manga" +msgstr "manga" + +#: models.py:247 models.py:254 +msgid "mangas" +msgstr "mangas" + +#: models.py:315 models.py:322 +msgid "novel" +msgstr "roman" + +#: models.py:316 models.py:323 +msgid "novels" +msgstr "romans" + +#: models.py:339 models.py:368 +msgid "rounds per minute" +msgstr "tours par minute" + +#: models.py:341 models.py:370 +msgid "33 RPM" +msgstr "33 TPM" + +#: models.py:342 models.py:371 +msgid "45 RPM" +msgstr "45 TPM" + +#: models.py:426 models.py:472 +msgid "number" +msgstr "nombre" + +#: models.py:430 models.py:476 +msgid "year" +msgstr "année" + +#: models.py:437 models.py:483 +msgid "month" +msgstr "mois" + +#: models.py:444 models.py:490 +msgid "day" +msgstr "jour" + +#: models.py:451 models.py:497 +msgid "double" +msgstr "double" + +#: models.py:520 +msgid "type" +msgstr "type" + +#: models.py:522 +msgid "Comic" +msgstr "BD" + +#: models.py:523 +msgid "Manga" +msgstr "Manga" + +#: models.py:524 +msgid "Roman" +msgstr "Roman" + +#: models.py:536 +msgid "future medium" +msgstr "medium à importer" + +#: models.py:537 +msgid "future media" +msgstr "medias à importer" + +#: models.py:551 +msgid "borrower" +msgstr "emprunteur" + +#: models.py:554 +msgid "borrowed on" +msgstr "emprunté le" + +#: models.py:559 +msgid "given back on" +msgstr "rendu le" + +#: models.py:565 +msgid "borrowed with" +msgstr "emprunté avec" + +#: models.py:566 +msgid "The keyholder that registered this borrowed item." +msgstr "Le permanencier qui enregistre cet emprunt." + +#: models.py:575 +msgid "The keyholder to whom this item was given back." +msgstr "Le permanencier à qui l'emprunt a été rendu." + +#: models.py:582 +msgid "borrowed item" +msgstr "emprunt" + +#: models.py:583 +msgid "borrowed items" +msgstr "emprunts" + +#: models.py:603 models.py:644 +msgid "owner" +msgstr "propriétaire" + +#: models.py:608 models.py:649 +msgid "duration" +msgstr "durée" + +#: models.py:612 models.py:653 +msgid "minimum number of players" +msgstr "nombre minimum de joueurs" + +#: models.py:616 models.py:657 +msgid "maximum number of players" +msgstr "nombre maximum de joueurs" + +#: models.py:621 models.py:662 +msgid "comment" +msgstr "commentaire" #: templates/media/generate_side_identifier.html:3 msgid "Generate side identifier" @@ -277,6 +306,6 @@ msgstr "ISBN invalide : mauvaise longueur" msgid "Invalid ISBN: Only upper case allowed" msgstr "ISBN invalide : seulement les majuscules sont autorisées" -#: views.py:51 +#: views.py:47 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 new file mode 100644 index 0000000..da0a60a --- /dev/null +++ b/media/management/commands/migrate_to_new_format.py @@ -0,0 +1,127 @@ +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, \ + OldCD, OldComic, OldGame, OldManga, OldNovel, OldReview, OldVinyl +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 + # 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) diff --git a/media/migrations/0042_auto_20211023_1929.py b/media/migrations/0042_auto_20211023_1929.py new file mode 100644 index 0000000..720aa82 --- /dev/null +++ b/media/migrations/0042_auto_20211023_1929.py @@ -0,0 +1,70 @@ +# Generated by Django 2.2.17 on 2021-10-23 17:29 +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('media', '0041_auto_20211023_1838'), + ] + + operations = [ + migrations.RenameModel( + old_name='CD', + new_name='OldCD', + ), + migrations.RenameModel( + old_name='Manga', + new_name='OldManga', + ), + # Remove index before renaming the model + migrations.AlterField( + model_name='game', + name='owner', + field=models.ForeignKey(db_index=False, on_delete=models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='owner'), + ), + migrations.RenameModel( + old_name='Game', + new_name='OldGame', + ), + migrations.AlterField( + model_name='oldgame', + name='owner', + field=models.ForeignKey(db_index=True, on_delete=models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='owner'), + ), + migrations.RenameField( + model_name='oldgame', + old_name='name', + new_name='title', + ), + migrations.RenameModel( + old_name='Novel', + new_name='OldNovel', + ), + migrations.RenameModel( + old_name='Comic', + new_name='OldComic', + ), + migrations.RenameModel( + old_name='Review', + new_name='OldReview', + ), + migrations.RenameModel( + old_name='Vinyl', + new_name='OldVinyl', + ), + migrations.AlterModelOptions( + name='oldcomic', + options={'ordering': ['title', 'subtitle'], 'verbose_name': 'comic', 'verbose_name_plural': 'comics'}, + ), + migrations.AlterModelOptions( + name='oldmanga', + options={'ordering': ['title'], 'verbose_name': 'manga', 'verbose_name_plural': 'mangas'}, + ), + migrations.AlterModelOptions( + name='oldnovel', + options={'ordering': ['title', 'subtitle'], 'verbose_name': 'novel', 'verbose_name_plural': 'novels'}, + ), + ] diff --git a/media/migrations/0043_auto_20211023_2012.py b/media/migrations/0043_auto_20211023_2012.py new file mode 100644 index 0000000..618dba3 --- /dev/null +++ b/media/migrations/0043_auto_20211023_2012.py @@ -0,0 +1,166 @@ +# Generated by Django 2.2.17 on 2021-10-23 18:12 + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import media.fields +import media.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('media', '0042_auto_20211023_1929'), + ] + + operations = [ + migrations.CreateModel( + name='Borrowable', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('isbn', media.fields.ISBNField(blank=True, help_text='You may be able to scan it from a bar code.', max_length=28, null=True, unique=True, validators=[media.validators.isbn_validator], verbose_name='ISBN')), + ('title', models.CharField(max_length=255, verbose_name='title')), + ('present', models.BooleanField(default=False, help_text='Tell that the medium is present in the Mediatek.', verbose_name='present')), + ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_media.borrowable_set+', to='contenttypes.ContentType')), + ], + options={ + 'verbose_name': 'borrowable', + 'verbose_name_plural': 'borrowables', + }, + ), + migrations.AlterModelOptions( + name='oldgame', + options={'ordering': ['title'], 'verbose_name': 'game', 'verbose_name_plural': 'games'}, + ), + migrations.AlterField( + model_name='emprunt', + name='media', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='media.Borrowable'), + ), + migrations.CreateModel( + name='Medium', + fields=[ + ('borrowable_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Borrowable')), + ('external_url', models.URLField(blank=True, verbose_name='external URL')), + ('side_identifier', models.CharField(max_length=255, verbose_name='side identifier')), + ('authors', models.ManyToManyField(to='media.Author', verbose_name='authors')), + ], + options={ + 'verbose_name': 'medium', + 'verbose_name_plural': 'media', + }, + bases=('media.borrowable',), + ), + migrations.CreateModel( + name='Review', + fields=[ + ('borrowable_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Borrowable')), + ('number', models.PositiveIntegerField(verbose_name='number')), + ('year', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='year')), + ('month', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='month')), + ('day', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='day')), + ('double', models.BooleanField(default=False, verbose_name='double')), + ], + options={ + 'verbose_name': 'review', + 'verbose_name_plural': 'reviews', + 'ordering': ['title', 'number'], + }, + bases=('media.borrowable',), + ), + migrations.CreateModel( + name='Book', + fields=[ + ('medium_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Medium')), + ('subtitle', models.CharField(blank=True, max_length=255, verbose_name='subtitle')), + ('number_of_pages', models.PositiveIntegerField(blank=True, null=True, verbose_name='number of pages')), + ('publish_date', models.DateField(blank=True, null=True, verbose_name='publish date')), + ], + options={ + 'verbose_name': 'book', + 'verbose_name_plural': 'books', + }, + bases=('media.medium',), + ), + migrations.CreateModel( + name='CD', + fields=[ + ('medium_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Medium')), + ], + options={ + 'verbose_name': 'CD', + 'verbose_name_plural': 'CDs', + 'ordering': ['title'], + }, + bases=('media.medium',), + ), + migrations.CreateModel( + name='Vinyl', + fields=[ + ('medium_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Medium')), + ('rpm', models.PositiveIntegerField(choices=[(33, '33 RPM'), (45, '45 RPM')], verbose_name='rounds per minute')), + ], + options={ + 'verbose_name': 'vinyl', + 'verbose_name_plural': 'vinyls', + 'ordering': ['title'], + }, + bases=('media.medium',), + ), + migrations.CreateModel( + name='Game', + fields=[ + ('borrowable_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Borrowable')), + ('duration', models.CharField(choices=[('-1h', '-1h'), ('1-2h', '1-2h'), ('2-3h', '2-3h'), ('3-4h', '3-4h'), ('4h+', '4h+')], max_length=255, verbose_name='duration')), + ('players_min', models.IntegerField(validators=[django.core.validators.MinValueValidator(1)], verbose_name='minimum number of players')), + ('players_max', models.IntegerField(validators=[django.core.validators.MinValueValidator(1)], verbose_name='maximum number of players')), + ('comment', models.CharField(blank=True, max_length=255, verbose_name='comment')), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='owner')), + ], + options={ + 'verbose_name': 'game', + 'verbose_name_plural': 'games', + 'ordering': ['title'], + }, + bases=('media.borrowable',), + ), + migrations.CreateModel( + name='Comic', + fields=[ + ('book_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Book')), + ], + options={ + 'verbose_name': 'comic', + 'verbose_name_plural': 'comics', + 'ordering': ['title', 'subtitle'], + }, + bases=('media.book',), + ), + migrations.CreateModel( + name='Manga', + fields=[ + ('book_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Book')), + ], + options={ + 'verbose_name': 'manga', + 'verbose_name_plural': 'mangas', + 'ordering': ['title', 'subtitle'], + }, + bases=('media.book',), + ), + migrations.CreateModel( + name='Novel', + fields=[ + ('book_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='media.Book')), + ], + options={ + 'verbose_name': 'novel', + 'verbose_name_plural': 'novels', + 'ordering': ['title', 'subtitle'], + }, + bases=('media.book',), + ), + ] diff --git a/media/models.py b/media/models.py index a797cff..0932058 100644 --- a/media/models.py +++ b/media/models.py @@ -5,6 +5,7 @@ from django.core.validators import MinValueValidator from django.db import models from django.utils.translation import gettext_lazy as _ +from polymorphic.models import PolymorphicModel from .fields import ISBNField @@ -30,7 +31,90 @@ class Author(models.Model): ordering = ['name'] -class Comic(models.Model): +class Borrowable(PolymorphicModel): + isbn = ISBNField( + _('ISBN'), + help_text=_('You may be able to scan it from a bar code.'), + unique=True, + blank=True, + null=True, + ) + + title = models.CharField( + max_length=255, + verbose_name=_("title"), + ) + + present = models.BooleanField( + verbose_name=_("present"), + help_text=_("Tell that the medium is present in the Mediatek."), + default=False, + ) + + def __str__(self): + obj = self + if obj.__class__ == Borrowable: + # Get true object instance, useful for autocompletion + obj = Borrowable.objects.get(pk=obj.pk) + + title = obj.title + if hasattr(obj, 'subtitle'): + subtitle = obj.subtitle + if subtitle: + title = f"{title} : {subtitle}" + return title + + class Meta: + verbose_name = _('borrowable') + verbose_name_plural = _('borrowables') + + +class Medium(Borrowable): + external_url = models.URLField( + verbose_name=_('external URL'), + blank=True, + ) + + side_identifier = models.CharField( + verbose_name=_('side identifier'), + max_length=255, + ) + + authors = models.ManyToManyField( + 'Author', + verbose_name=_('authors'), + ) + + class Meta: + verbose_name = _("medium") + verbose_name_plural = _("media") + + +class Book(Medium): + subtitle = models.CharField( + verbose_name=_('subtitle'), + max_length=255, + blank=True, + ) + + number_of_pages = models.PositiveIntegerField( + verbose_name=_('number of pages'), + blank=True, + null=True, + ) + + publish_date = models.DateField( + verbose_name=_('publish date'), + blank=True, + null=True, + ) + + class Meta: + verbose_name = _("book") + verbose_name_plural = _("books") + + +class OldComic(models.Model): isbn = ISBNField( _('ISBN'), help_text=_('You may be able to scan it from a bar code.'), @@ -95,7 +179,14 @@ class Comic(models.Model): ordering = ['title', 'subtitle'] -class Manga(models.Model): +class Comic(Book): + class Meta: + verbose_name = _("comic") + verbose_name_plural = _("comics") + ordering = ['title', 'subtitle'] + + +class OldManga(models.Model): isbn = ISBNField( _('ISBN'), help_text=_('You may be able to scan it from a bar code.'), @@ -157,7 +248,14 @@ class Manga(models.Model): ordering = ['title'] -class Novel(models.Model): +class Manga(Book): + class Meta: + verbose_name = _("manga") + verbose_name_plural = _("mangas") + ordering = ['title', 'subtitle'] + + +class OldNovel(models.Model): isbn = ISBNField( _('ISBN'), help_text=_('You may be able to scan it from a bar code.'), @@ -219,7 +317,14 @@ class Novel(models.Model): ordering = ['title', 'subtitle'] -class Vinyl(models.Model): +class Novel(Book): + class Meta: + verbose_name = _("novel") + verbose_name_plural = _("novels") + ordering = ['title', 'subtitle'] + + +class OldVinyl(models.Model): title = models.CharField( verbose_name=_('title'), max_length=255, @@ -258,7 +363,22 @@ class Vinyl(models.Model): ordering = ['title'] -class CD(models.Model): +class Vinyl(Medium): + rpm = models.PositiveIntegerField( + verbose_name=_('rounds per minute'), + choices=[ + (33, _('33 RPM')), + (45, _('45 RPM')), + ], + ) + + class Meta: + verbose_name = _("vinyl") + verbose_name_plural = _("vinyls") + ordering = ['title'] + + +class OldCD(models.Model): title = models.CharField( verbose_name=_('title'), max_length=255, @@ -289,7 +409,14 @@ class CD(models.Model): ordering = ['title'] -class Review(models.Model): +class CD(Medium): + class Meta: + verbose_name = _("CD") + verbose_name_plural = _("CDs") + ordering = ['title'] + + +class OldReview(models.Model): title = models.CharField( verbose_name=_('title'), max_length=255, @@ -340,6 +467,46 @@ class Review(models.Model): ordering = ['title', 'number'] +class Review(Borrowable): + 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, + ) + + def __str__(self): + return self.title + " n°" + str(self.number) + + class Meta: + verbose_name = _("review") + verbose_name_plural = _("reviews") + ordering = ['title', 'number'] + + class FutureMedium(models.Model): isbn = ISBNField( _('ISBN'), @@ -375,7 +542,7 @@ class FutureMedium(models.Model): class Emprunt(models.Model): media = models.ForeignKey( - 'Comic', + 'media.Borrowable', on_delete=models.PROTECT, ) user = models.ForeignKey( @@ -417,7 +584,7 @@ class Emprunt(models.Model): ordering = ['-date_emprunt'] -class Game(models.Model): +class OldGame(models.Model): DURATIONS = ( ('-1h', '-1h'), ('1-2h', '1-2h'), @@ -426,7 +593,7 @@ class Game(models.Model): ('4h+', '4h+'), ) - name = models.CharField( + title = models.CharField( max_length=255, verbose_name=_("name"), ) @@ -455,9 +622,50 @@ class Game(models.Model): ) def __str__(self): - return str(self.name) + return str(self.title) class Meta: verbose_name = _("game") verbose_name_plural = _("games") - ordering = ['name'] + ordering = ['title'] + + +class Game(Borrowable): + DURATIONS = ( + ('-1h', '-1h'), + ('1-2h', '1-2h'), + ('2-3h', '2-3h'), + ('3-4h', '3-4h'), + ('4h+', '4h+'), + ) + 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'] diff --git a/media/templates/media/find_medium.html b/media/templates/media/find_medium.html index 1a79e2e..1ee7e4b 100644 --- a/media/templates/media/find_medium.html +++ b/media/templates/media/find_medium.html @@ -36,22 +36,22 @@ document.getElementById("isbn").focus(); let bd_request = new XMLHttpRequest(); - bd_request.open('GET', '/api/media/bd/?search=' + isbn, true); + bd_request.open('GET', '/api/media/comic/?search=' + isbn, true); bd_request.onload = function () { let data = JSON.parse(this.response); - data.results.forEach(bd => { - let present = bd.present; - if (markAsPresent && isbn === bd.isbn) { + data.results.forEach(comic => { + let present = comic.present; + if (markAsPresent && isbn === comic.isbn) { present = true; let presentRequest = new XMLHttpRequest(); - presentRequest.open("GET", "/media/mark-as-present/bd/" + bd.id + "/", true); + presentRequest.open("GET", "/media/mark-as-present/bd/" + comic.id + "/", true); presentRequest.send(); } - result_div.innerHTML += "
  • " + - "BD : " - + bd.title + (bd.subtitle ? " - " + bd.subtitle : "") + "" - + (present ? " (marquer comme absent)" - : " (absent, marquer comme présent)") + "
  • "; + result_div.innerHTML += "
  • " + + "BD : " + + comic.title + (comic.subtitle ? " - " + comic.subtitle : "") + "" + + (present ? " (marquer comme absent)" + : " (absent, marquer comme présent)") + "
  • "; }); } bd_request.send(); @@ -92,35 +92,35 @@ cd_request.send(); let vinyle_request = new XMLHttpRequest(); - vinyle_request.open('GET', '/api/media/vinyle/?search=' + isbn, true); + vinyle_request.open('GET', '/api/media/vinyl/?search=' + isbn, true); vinyle_request.onload = function () { let data = JSON.parse(this.response); - data.results.forEach(vinyle => { - let present = markAsPresent || vinyle.present; - result_div.innerHTML += "
  • " + - "Vinyle : " + vinyle.title + "" - + (present ? " (marquer comme absent)" - : " (absent, marquer comme présent)") + "
  • "; + data.results.forEach(vinyl => { + let present = markAsPresent || vinyl.present; + result_div.innerHTML += "
  • " + + "Vinyle : " + vinyl.title + "" + + (present ? " (marquer comme absent)" + : " (absent, marquer comme présent)") + "
  • "; }); } vinyle_request.send(); let roman_request = new XMLHttpRequest(); - roman_request.open('GET', '/api/media/roman/?search=' + isbn, true); + roman_request.open('GET', '/api/media/novel/?search=' + isbn, true); roman_request.onload = function () { let data = JSON.parse(this.response); - data.results.forEach(roman => { - let present = roman.present; - if (markAsPresent && isbn === roman.isbn) { + data.results.forEach(novel => { + let present = novel.present; + if (markAsPresent && isbn === novel.isbn) { present = true; let presentRequest = new XMLHttpRequest(); - presentRequest.open("GET", "/media/mark-as-present/roman/" + roman.id + "/", true); + presentRequest.open("GET", "/media/mark-as-present/novel/" + novel.id + "/", true); presentRequest.send(); } - result_div.innerHTML += "
  • " + - "Roman : " + roman.title + "" - + (present ? " (marquer comme absent)" - : " (absent, marquer comme présent)") + "
  • "; + result_div.innerHTML += "
  • " + + "Roman : " + novel.title + "" + + (present ? " (marquer comme absent)" + : " (absent, marquer comme présent)") + "
  • "; }); } roman_request.send(); diff --git a/media/urls.py b/media/urls.py index d11bbce..261d559 100644 --- a/media/urls.py +++ b/media/urls.py @@ -12,7 +12,7 @@ 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/bd//', + path('mark-as-present/comic//', views.MarkComicAsPresent.as_view(), name="mark_comic_as_present"), path('mark-as-present/manga//', @@ -21,15 +21,15 @@ urlpatterns = [ path('mark-as-present/cd//', views.MarkCDAsPresent.as_view(), name="mark_cd_as_present"), - path('mark-as-present/vinyle//', + path('mark-as-present/vinyl//', views.MarkVinylAsPresent.as_view(), name="mark_vinyle_as_present"), - path('mark-as-present/roman//', - views.MarkRomanAsPresent.as_view(), - name="mark_roman_as_present"), - path('mark-as-present/revue//', - views.MarkRevueAsPresent.as_view(), - name="mark_revue_as_present"), + path('mark-as-present/novel//', + views.MarkNovelAsPresent.as_view(), + name="mark_novel_as_present"), + path('mark-as-present/review//', + views.MarkReviewAsPresent.as_view(), + name="mark_review_as_present"), path('mark-as-present/future//', views.MarkFutureAsPresent.as_view(), name="mark_future_as_present"), diff --git a/media/views.py b/media/views.py index 08370c3..bd0e2f4 100644 --- a/media/views.py +++ b/media/views.py @@ -81,11 +81,11 @@ class MarkVinylAsPresent(MarkMediumAsPresent): model = Vinyl -class MarkRomanAsPresent(MarkMediumAsPresent): +class MarkNovelAsPresent(MarkMediumAsPresent): model = Novel -class MarkRevueAsPresent(MarkMediumAsPresent): +class MarkReviewAsPresent(MarkMediumAsPresent): model = Review diff --git a/requirements.txt b/requirements.txt index 3c3b10a..7f2237e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ docutils~=0.16 # for Django-admin docs Django~=2.2 django-filter~=2.4 +django-polymorphic~=3.0 django-reversion~=3.0 djangorestframework~=3.12 django_extensions~=3.0