diff --git a/logs/__init__.py b/logs/__init__.py index e69de29..d567d34 100644 --- a/logs/__init__.py +++ b/logs/__init__.py @@ -0,0 +1,5 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +default_app_config = 'logs.apps.LogsConfig' diff --git a/logs/apps.py b/logs/apps.py index 7f3a755..1d8770d 100644 --- a/logs/apps.py +++ b/logs/apps.py @@ -1,3 +1,7 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + from django.apps import AppConfig diff --git a/media/__init__.py b/media/__init__.py index e69de29..78b3a89 100644 --- a/media/__init__.py +++ b/media/__init__.py @@ -0,0 +1,5 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +default_app_config = 'media.apps.MediaConfig' diff --git a/media/admin.py b/media/admin.py index 3d0d846..69d603d 100644 --- a/media/admin.py +++ b/media/admin.py @@ -17,18 +17,21 @@ class AuteurAdmin(VersionAdmin): class MediaAdmin(VersionAdmin): - list_display = ('titre', 'authors', 'cote') - search_fields = ('titre', 'auteur__nom', 'cote') - autocomplete_fields = ('auteur',) + list_display = ('title', 'authors_list', 'side_title', 'isbn') + search_fields = ('title', 'authors__nom', 'side_title', 'subtitle', 'isbn') + autocomplete_fields = ('authors',) + date_hierarchy = 'publish_date' - def authors(self, obj): - return ", ".join([a.nom for a in obj.auteur.all()]) + def authors_list(self, obj): + return ", ".join([a.nom for a in obj.authors.all()]) + + authors_list.short_description = _('authors') class EmpruntAdmin(VersionAdmin): list_display = ('media', 'user', 'date_emprunt', 'date_rendu', 'permanencier_emprunt', 'permanencier_rendu_custom') - search_fields = ('media__titre', 'media__cote', 'user__username', + search_fields = ('media__title', 'media__side_title', 'user__username', 'date_emprunt', 'date_rendu') date_hierarchy = 'date_emprunt' autocomplete_fields = ('media', 'user', 'permanencier_emprunt', diff --git a/media/apps.py b/media/apps.py index 0550795..1c83375 100644 --- a/media/apps.py +++ b/media/apps.py @@ -1,3 +1,7 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + from django.apps import AppConfig diff --git a/media/fields.py b/media/fields.py new file mode 100644 index 0000000..58ad2f6 --- /dev/null +++ b/media/fields.py @@ -0,0 +1,55 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +""" +Based on https://github.com/secnot/django-isbn-field +""" + +from django.core.validators import EMPTY_VALUES +from django.db.models import CharField +from django.utils.translation import gettext_lazy as _ + +from .validators import isbn_validator + + +class ISBNField(CharField): + description = _("ISBN-10 or ISBN-13") + + def __init__(self, clean_isbn=True, *args, **kwargs): + self.clean_isbn = clean_isbn + kwargs['max_length'] = kwargs[ + 'max_length'] if 'max_length' in kwargs else 28 + kwargs['verbose_name'] = kwargs[ + 'verbose_name'] if 'verbose_name' in kwargs else u'ISBN' + kwargs['validators'] = [isbn_validator] + super(ISBNField, self).__init__(*args, **kwargs) + + def formfield(self, **kwargs): + defaults = { + 'min_length': 10, + 'validators': [isbn_validator], + } + defaults.update(kwargs) + return super(ISBNField, self).formfield(**defaults) + + def deconstruct(self): + name, path, args, kwargs = super(ISBNField, self).deconstruct() + # Only include clean_isbn in kwarg if it's not the default value + if not self.clean_isbn: + kwargs['clean_isbn'] = self.clean_isbn + return name, path, args, kwargs + + def pre_save(self, model_instance, add): + """ + Remove dashes, spaces, and convert isbn to uppercase before saving + when clean_isbn is enabled + """ + value = getattr(model_instance, self.attname) + if self.clean_isbn and value not in EMPTY_VALUES: + cleaned_isbn = value.replace(' ', '').replace('-', '').upper() + setattr(model_instance, self.attname, cleaned_isbn) + return super(ISBNField, self).pre_save(model_instance, add) + + def __unicode__(self): + return self.value diff --git a/media/migrations/0010_auto_20190811_0901.py b/media/migrations/0010_auto_20190811_0901.py new file mode 100644 index 0000000..fb9ef72 --- /dev/null +++ b/media/migrations/0010_auto_20190811_0901.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-08-11 07:01 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('media', '0009_auto_20190802_1455'), + ] + + operations = [ + migrations.RenameField( + model_name='media', + old_name='titre', + new_name='title', + ), + ] diff --git a/media/migrations/0011_auto_20190811_0903.py b/media/migrations/0011_auto_20190811_0903.py new file mode 100644 index 0000000..3576b34 --- /dev/null +++ b/media/migrations/0011_auto_20190811_0903.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.4 on 2019-08-11 07:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('media', '0010_auto_20190811_0901'), + ] + + operations = [ + migrations.AddField( + model_name='media', + name='subtitle', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='subtitle'), + ), + migrations.AlterField( + model_name='media', + name='title', + field=models.CharField(max_length=255, verbose_name='title'), + ), + ] diff --git a/media/migrations/0012_media_external_url.py b/media/migrations/0012_media_external_url.py new file mode 100644 index 0000000..3876612 --- /dev/null +++ b/media/migrations/0012_media_external_url.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-08-11 07:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('media', '0011_auto_20190811_0903'), + ] + + operations = [ + migrations.AddField( + model_name='media', + name='external_url', + field=models.URLField(blank=True, null=True, verbose_name='external URL'), + ), + ] diff --git a/media/migrations/0013_auto_20190811_0907.py b/media/migrations/0013_auto_20190811_0907.py new file mode 100644 index 0000000..f24dffc --- /dev/null +++ b/media/migrations/0013_auto_20190811_0907.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-08-11 07:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('media', '0012_media_external_url'), + ] + + operations = [ + migrations.RenameField( + model_name='media', + old_name='auteur', + new_name='authors', + ), + ] diff --git a/media/migrations/0014_auto_20190811_0908.py b/media/migrations/0014_auto_20190811_0908.py new file mode 100644 index 0000000..c31e122 --- /dev/null +++ b/media/migrations/0014_auto_20190811_0908.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.4 on 2019-08-11 07:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('media', '0013_auto_20190811_0907'), + ] + + operations = [ + migrations.AddField( + model_name='media', + name='number_of_pages', + field=models.PositiveIntegerField(blank=True, null=True, verbose_name='number of pages'), + ), + migrations.AlterField( + model_name='media', + name='authors', + field=models.ManyToManyField(to='media.Auteur', verbose_name='authors'), + ), + ] diff --git a/media/migrations/0015_media_publish_date.py b/media/migrations/0015_media_publish_date.py new file mode 100644 index 0000000..59a8184 --- /dev/null +++ b/media/migrations/0015_media_publish_date.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-08-11 07:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('media', '0014_auto_20190811_0908'), + ] + + operations = [ + migrations.AddField( + model_name='media', + name='publish_date', + field=models.DateField(blank=True, null=True, verbose_name='publish date'), + ), + ] diff --git a/media/migrations/0016_media_isbn.py b/media/migrations/0016_media_isbn.py new file mode 100644 index 0000000..3c33234 --- /dev/null +++ b/media/migrations/0016_media_isbn.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.4 on 2019-08-11 07:17 + +from django.db import migrations +import media.fields +import media.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('media', '0015_media_publish_date'), + ] + + operations = [ + migrations.AddField( + model_name='media', + name='isbn', + field=media.fields.ISBNField(blank=True, max_length=28, null=True, validators=[media.validators.isbn_validator], verbose_name='ISBN'), + ), + ] diff --git a/media/migrations/0017_auto_20190811_0918.py b/media/migrations/0017_auto_20190811_0918.py new file mode 100644 index 0000000..c4a1885 --- /dev/null +++ b/media/migrations/0017_auto_20190811_0918.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.4 on 2019-08-11 07:18 + +from django.db import migrations +import media.fields +import media.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('media', '0016_media_isbn'), + ] + + operations = [ + migrations.AlterField( + model_name='media', + name='isbn', + field=media.fields.ISBNField(blank=True, help_text='You may be able to scan it from a bar code.', max_length=28, null=True, validators=[media.validators.isbn_validator], verbose_name='ISBN'), + ), + ] diff --git a/media/migrations/0018_auto_20190811_0918.py b/media/migrations/0018_auto_20190811_0918.py new file mode 100644 index 0000000..5683b67 --- /dev/null +++ b/media/migrations/0018_auto_20190811_0918.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-08-11 07:18 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('media', '0017_auto_20190811_0918'), + ] + + operations = [ + migrations.RenameField( + model_name='media', + old_name='cote', + new_name='side_title', + ), + ] diff --git a/media/migrations/0019_auto_20190811_0919.py b/media/migrations/0019_auto_20190811_0919.py new file mode 100644 index 0000000..d78c70a --- /dev/null +++ b/media/migrations/0019_auto_20190811_0919.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-08-11 07:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('media', '0018_auto_20190811_0918'), + ] + + operations = [ + migrations.AlterField( + model_name='media', + name='side_title', + field=models.CharField(max_length=255, verbose_name='side title'), + ), + ] diff --git a/media/models.py b/media/models.py index 3149def..a4a8776 100644 --- a/media/models.py +++ b/media/models.py @@ -6,6 +6,8 @@ from django.core.validators import MinValueValidator from django.db import models from django.utils.translation import gettext_lazy as _ +from .fields import ISBNField + class Auteur(models.Model): nom = models.CharField(max_length=255, unique=True) @@ -19,12 +21,48 @@ class Auteur(models.Model): class Media(models.Model): - titre = models.CharField(max_length=255) - cote = models.CharField(max_length=31) - auteur = models.ManyToManyField('Auteur') + isbn = ISBNField( + _('ISBN'), + help_text=_('You may be able to scan it from a bar code.'), + blank=True, + null=True, + ) + title = models.CharField( + verbose_name=_('title'), + max_length=255, + ) + subtitle = models.CharField( + verbose_name=_('subtitle'), + max_length=255, + blank=True, + null=True, + ) + external_url = models.URLField( + verbose_name=_('external URL'), + blank=True, + null=True, + ) + side_title = models.CharField( + verbose_name=_('side title'), + max_length=255, + ) + authors = models.ManyToManyField( + 'Auteur', + verbose_name=_('authors'), + ) + number_of_pages = models.PositiveIntegerField( + verbose_name=_('number of pages'), + blank=True, + null=True, + ) + publish_date = models.DateField( + verbose_name=_('publish date'), + blank=True, + null=True, + ) def __str__(self): - return str(self.titre) + ' - ' + str(self.auteur.all().first()) + return str(self.title) + ' - ' + str(self.authors.all().first()) class Meta: verbose_name = _("medium") diff --git a/media/static/media/isbn_fetcher.js b/media/static/media/isbn_fetcher.js new file mode 100644 index 0000000..7a492e4 --- /dev/null +++ b/media/static/media/isbn_fetcher.js @@ -0,0 +1,46 @@ +// curl 'https://openlibrary.org/api/books?bibkeys=ISBN:0201558025&format=json&jscmd=data' +a = { + "ISBN:0201558025": { + "publishers": [{"name": "Addison-Wesley"}], + "pagination": "xiii, 657 p. :", + "identifiers": { + "lccn": ["93040325"], + "openlibrary": ["OL1429049M"], + "isbn_10": ["0201558025"], + "wikidata": ["Q15303722"], + "goodreads": ["112243"], + "librarything": ["45844"] + }, + //"subtitle": "a foundation for computer science", + //"title": "Concrete mathematics", + //"url": "https://openlibrary.org/books/OL1429049M/Concrete_mathematics", + "classifications": {"dewey_decimal_class": ["510"], "lc_classifications": ["QA39.2 .G733 1994"]}, + "notes": "Includes bibliographical references (p. 604-631) and index.", + "number_of_pages": 657, + "cover": { + "small": "https://covers.openlibrary.org/b/id/135182-S.jpg", + "large": "https://covers.openlibrary.org/b/id/135182-L.jpg", + "medium": "https://covers.openlibrary.org/b/id/135182-M.jpg" + }, + "subjects": [{ + "url": "https://openlibrary.org/subjects/computer_science", + "name": "Computer science" + }, {"url": "https://openlibrary.org/subjects/mathematics", "name": "Mathematics"}], + "publish_date": "1994", + "key": "/books/OL1429049M", + "authors": [{ + "url": "https://openlibrary.org/authors/OL720958A/Ronald_L._Graham", + "name": "Ronald L. Graham" + }, { + "url": "https://openlibrary.org/authors/OL229501A/Donald_Knuth", + "name": "Donald Knuth" + }, {"url": "https://openlibrary.org/authors/OL2669938A/Oren_Patashnik", "name": "Oren Patashnik"}], + "by_statement": "Ronald L. Graham, Donald E. Knuth, Oren Patashnik.", + "publish_places": [{"name": "Reading, Mass"}], + "ebooks": [{ + "formats": {}, + "preview_url": "https://archive.org/details/concretemathemat00grah_444", + "availability": "restricted" + }] + } +} \ No newline at end of file diff --git a/media/validators.py b/media/validators.py new file mode 100644 index 0000000..c678608 --- /dev/null +++ b/media/validators.py @@ -0,0 +1,31 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright (C) 2017-2019 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +""" +Based on https://github.com/secnot/django-isbn-field +""" + +from django.core.exceptions import ValidationError +from django.utils.translation import ugettext_lazy as _ +from six import string_types +from stdnum import isbn + + +def isbn_validator(raw_isbn): + """Check string is a valid ISBN number""" + isbn_to_check = raw_isbn.replace('-', '').replace(' ', '') + + if not isinstance(isbn_to_check, string_types): + raise ValidationError(_(u'Invalid ISBN: Not a string')) + + if len(isbn_to_check) != 10 and len(isbn_to_check) != 13: + raise ValidationError(_(u'Invalid ISBN: Wrong length')) + + if not isbn.is_valid(isbn_to_check): + raise ValidationError(_(u'Invalid ISBN: Failed checksum')) + + if isbn_to_check != isbn_to_check.upper(): + raise ValidationError(_(u'Invalid ISBN: Only upper case allowed')) + + return True diff --git a/requirements.txt b/requirements.txt index 87bf784..e6c3d57 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ Pillow==5.4.1 pytz==2019.1 six==1.12.0 sqlparse==0.2.4 -django-reversion==3.0.3 \ No newline at end of file +django-reversion==3.0.3 +python-stdnum==1.10 \ No newline at end of file