Isbns, and minor fixes.
authorRadek Czajka <rczajka@rczajka.pl>
Tue, 26 Sep 2023 11:08:48 +0000 (13:08 +0200)
committerRadek Czajka <rczajka@rczajka.pl>
Tue, 26 Sep 2023 11:08:48 +0000 (13:08 +0200)
18 files changed:
src/cover/utils.py
src/depot/publishers/base.py
src/documents/views.py
src/isbn/__init__.py [new file with mode: 0644]
src/isbn/admin.py [new file with mode: 0644]
src/isbn/apps.py [new file with mode: 0644]
src/isbn/management/commands/isbn_import_wl.py [new file with mode: 0644]
src/isbn/migrations/0001_initial.py [new file with mode: 0644]
src/isbn/migrations/0002_isbn_bn_data_isbn_wl_data_alter_isbn_book_and_more.py [new file with mode: 0644]
src/isbn/migrations/0003_isbn_notes_alter_isbn_bn_data_alter_isbn_wl_data.py [new file with mode: 0644]
src/isbn/migrations/0004_alter_isbn_form.py [new file with mode: 0644]
src/isbn/migrations/__init__.py [new file with mode: 0644]
src/isbn/models.py [new file with mode: 0644]
src/isbn/product_forms.py [new file with mode: 0644]
src/isbn/templates/isbn/list.html [new file with mode: 0644]
src/isbn/urls.py [new file with mode: 0644]
src/isbn/views.py [new file with mode: 0644]
src/redakcja/urls.py

index 56fb24a..6815d29 100644 (file)
@@ -99,7 +99,10 @@ def get_wikimedia_data(url):
         entity = client.get(qitem)
         meta['title'] = entity.label.get('pl', str(entity.label))
         author = entity.get(client.get(WIKIDATA.CREATOR))
-        meta['author'] = author.label.get('pl', str(author.label))
+        if author is not None:
+            meta['author'] = author.label.get('pl', str(author.label))
+        else:
+            meta['author'] = ''
 
     if meta['license_name'] == 'Public domain':
         meta['license_name'] = 'domena publiczna'
index 5cfcbef..88ee56b 100644 (file)
@@ -41,7 +41,7 @@ class BasePublisher:
             '<a href="https://wolnelektury.pl/katalog/autor/{}/">{}</a>'.format(
                 slugify(p.readable()),
                 p.readable(),
-            )
+            ) if p is not None else ''
             for p in wlbook.meta.authors
         ) + '<br>'
         description += '<a href="https://wolnelektury.pl/katalog/lektura/{}/">{}</a><br>'.format(
@@ -54,21 +54,21 @@ class BasePublisher:
             '<a href="https://wolnelektury.pl/katalog/epoka/{}/">{}</a>'.format(
                 slugify(p),
                 p,
-            )
+            ) if p is not None else ''
             for p in wlbook.meta.epochs
         ) + ' '
         description += 'Rodzaj: ' + ', '.join(
             '<a href="https://wolnelektury.pl/katalog/rodzaj/{}/">{}</a>'.format(
                 slugify(p),
                 p,
-            )
+            ) if p is not None else ''
             for p in wlbook.meta.kinds
         ) + ' '
         description += 'Gatunek: ' + ', '.join(
             '<a href="https://wolnelektury.pl/katalog/gatunek/{}/">{}</a>'.format(
                 slugify(p),
                 p,
-            )
+            ) if p is not None else ''
             for p in wlbook.meta.genres
         ) + '</p>'
 
index 0fbb3b9..ecc9219 100644 (file)
@@ -369,7 +369,7 @@ def book(request, slug):
         doc = None
     else:
         try:
-            stats = doc.get_statistic()
+            stats = doc.get_statistics()
         except:
             pass
 
diff --git a/src/isbn/__init__.py b/src/isbn/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/isbn/admin.py b/src/isbn/admin.py
new file mode 100644 (file)
index 0000000..15947ab
--- /dev/null
@@ -0,0 +1,11 @@
+from django.contrib import admin
+from . import models
+
+@admin.register(models.Isbn)
+class IsbnAdmin(admin.ModelAdmin):
+    search_fields = ['pool__prefix', 'suffix']
+
+
+admin.site.register(models.IsbnPool)
+
+# Register your models here.
diff --git a/src/isbn/apps.py b/src/isbn/apps.py
new file mode 100644 (file)
index 0000000..befb64f
--- /dev/null
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class IsbnConfig(AppConfig):
+    default_auto_field = "django.db.models.BigAutoField"
+    name = "isbn"
diff --git a/src/isbn/management/commands/isbn_import_wl.py b/src/isbn/management/commands/isbn_import_wl.py
new file mode 100644 (file)
index 0000000..0ed8745
--- /dev/null
@@ -0,0 +1,29 @@
+import json
+from django.core.management.base import BaseCommand
+from isbn.models import IsbnPool
+
+
+class Command(BaseCommand):
+    def add_arguments(self, parser):
+        parser.add_argument('filename')
+    
+    def handle(self, filename, **options):
+        with open(filename) as f:
+            data = json.load(f)
+        pool_map = {}
+        for d in data:
+            f = d['fields']
+            if d['model'] == 'isbn.isbnpool':
+                pool_map[d['pk']], created = IsbnPool.objects.get_or_create(
+                    prefix=f['prefix'],
+                    suffix_from=f['suffix_from'],
+                    suffix_to=f['suffix_to'],
+                    ref_from=f['ref_from'],
+                )
+            if d['model'] == 'isbn.onixrecord':
+                pool = pool_map[f['isbn_pool']]
+                isbn, created = pool.isbn_set.get_or_create(
+                    suffix=f['suffix'],
+                )
+                isbn.wl_data = json.dumps(f, indent=4, ensure_ascii=False)
+                isbn.save(update_fields=['wl_data'])
diff --git a/src/isbn/migrations/0001_initial.py b/src/isbn/migrations/0001_initial.py
new file mode 100644 (file)
index 0000000..f4d3eea
--- /dev/null
@@ -0,0 +1,74 @@
+# Generated by Django 4.1.9 on 2023-09-12 18:10
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        ("catalogue", "0050_audience_woblink"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="IsbnPool",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("prefix", models.CharField(max_length=10)),
+                ("suffix_from", models.IntegerField()),
+                ("suffix_to", models.IntegerField()),
+                ("ref_from", models.IntegerField()),
+                (
+                    "purpose",
+                    models.CharField(
+                        choices=[("WL", "Wolne Lektury"), ("GENERAL", "Ogólne")],
+                        max_length=8,
+                    ),
+                ),
+            ],
+        ),
+        migrations.CreateModel(
+            name="Isbn",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("datestamp", models.DateField()),
+                ("suffix", models.IntegerField()),
+                ("form", models.CharField(choices=[], max_length=32)),
+                (
+                    "book",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.PROTECT, to="catalogue.book"
+                    ),
+                ),
+                (
+                    "pool",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.PROTECT, to="isbn.isbnpool"
+                    ),
+                ),
+            ],
+            options={
+                "ordering": ["pool", "suffix"],
+                "unique_together": {("pool", "suffix")},
+            },
+        ),
+    ]
diff --git a/src/isbn/migrations/0002_isbn_bn_data_isbn_wl_data_alter_isbn_book_and_more.py b/src/isbn/migrations/0002_isbn_bn_data_isbn_wl_data_alter_isbn_book_and_more.py
new file mode 100644 (file)
index 0000000..8a1c372
--- /dev/null
@@ -0,0 +1,49 @@
+# Generated by Django 4.1.9 on 2023-09-25 15:10
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("catalogue", "0050_audience_woblink"),
+        ("isbn", "0001_initial"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="isbn",
+            name="bn_data",
+            field=models.TextField(default=""),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name="isbn",
+            name="wl_data",
+            field=models.TextField(default=""),
+            preserve_default=False,
+        ),
+        migrations.AlterField(
+            model_name="isbn",
+            name="book",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.PROTECT,
+                to="catalogue.book",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="isbn",
+            name="datestamp",
+            field=models.DateField(blank=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name="isbn",
+            name="form",
+            field=models.CharField(
+                blank=True, choices=[("pdf", "pdf")], max_length=32, null=True
+            ),
+        ),
+    ]
diff --git a/src/isbn/migrations/0003_isbn_notes_alter_isbn_bn_data_alter_isbn_wl_data.py b/src/isbn/migrations/0003_isbn_notes_alter_isbn_bn_data_alter_isbn_wl_data.py
new file mode 100644 (file)
index 0000000..7c44c18
--- /dev/null
@@ -0,0 +1,28 @@
+# Generated by Django 4.1.9 on 2023-09-26 10:52
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("isbn", "0002_isbn_bn_data_isbn_wl_data_alter_isbn_book_and_more"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="isbn",
+            name="notes",
+            field=models.TextField(blank=True),
+        ),
+        migrations.AlterField(
+            model_name="isbn",
+            name="bn_data",
+            field=models.TextField(blank=True),
+        ),
+        migrations.AlterField(
+            model_name="isbn",
+            name="wl_data",
+            field=models.TextField(blank=True),
+        ),
+    ]
diff --git a/src/isbn/migrations/0004_alter_isbn_form.py b/src/isbn/migrations/0004_alter_isbn_form.py
new file mode 100644 (file)
index 0000000..fe2b0b4
--- /dev/null
@@ -0,0 +1,31 @@
+# Generated by Django 4.1.9 on 2023-09-26 13:08
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("isbn", "0003_isbn_notes_alter_isbn_bn_data_alter_isbn_wl_data"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="isbn",
+            name="form",
+            field=models.CharField(
+                blank=True,
+                choices=[
+                    ("html", "html"),
+                    ("txt", "txt"),
+                    ("pdf", "pdf"),
+                    ("epub", "epub"),
+                    ("mobi", "mobi"),
+                    ("mp3", "mp3"),
+                    ("paperback", "paperback"),
+                ],
+                max_length=32,
+                null=True,
+            ),
+        ),
+    ]
diff --git a/src/isbn/migrations/__init__.py b/src/isbn/migrations/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/isbn/models.py b/src/isbn/models.py
new file mode 100644 (file)
index 0000000..9db0513
--- /dev/null
@@ -0,0 +1,150 @@
+from django.apps import apps
+from django.db import models
+from lxml import etree
+import requests
+from .product_forms import FORMS
+
+
+class IsbnPool(models.Model):
+    PURPOSE_GENERAL = 'GENERAL'
+    PURPOSE_WL = 'WL'
+    PURPOSE_CHOICES = (
+        (PURPOSE_WL, 'Wolne Lektury'),
+        (PURPOSE_GENERAL, 'Ogólne'),
+    )
+
+    prefix = models.CharField(max_length=10)
+    suffix_from = models.IntegerField()
+    suffix_to = models.IntegerField()
+    ref_from = models.IntegerField()
+    purpose = models.CharField(max_length=8, choices=PURPOSE_CHOICES)
+
+    def __str__(self):
+        return '-'.join((
+            self.prefix[:3],
+            self.prefix[3:5],
+            self.prefix[5:],
+            'X' * (12 - len(self.prefix)),
+            'X'
+        ))
+
+    @staticmethod
+    def check_digit(prefix12):
+        digits = [int(d) for d in prefix12]
+        return str((-sum(digits[0::2]) + 7 * sum(digits[1::2])) % 10)
+    
+    def get_code(self, suffix, dashes=False):
+        suffix_length = 12 - len(self.prefix)
+        suffix_str = f'{suffix:0{suffix_length}d}'
+        prefix12 = self.prefix + suffix_str
+        check_digit = self.check_digit(prefix12)
+        if dashes:
+            isbn = '-'.join((
+                self.prefix[:3],
+                self.prefix[3:5],
+                self.prefix[5:],
+                suffix_str,
+                check_digit
+            ))
+        else:
+            isbn = ''.join((
+                prefix12, check_digit
+            ))
+        return isbn
+
+    
+    @property
+    def size(self):
+        return self.suffix_to - self.suffix_from + 1
+
+    @property
+    def entries(self):
+        return self.isbn_set.count()
+    
+    @property
+    def fill_percentage(self):
+        return 100 * self.entries / self.size
+
+    def bn_record_id_for(self, suffix):
+        return self.ref_from + suffix
+    
+    def import_all_bn_data(self):
+        for suffix in range(self.suffix_from, self.suffix_to + 1):
+            print(suffix)
+            self.import_bn_data_for(suffix)
+    
+    def import_bn_data_for(self, suffix):
+        record_id = self.bn_record_id_for(suffix)
+        content = requests.get(
+            f'https://e-isbn.pl/IsbnWeb/record/export_onix.xml?record_id={record_id}').content
+        elem = etree.fromstring(content)
+        product = elem.find('{http://ns.editeur.org/onix/3.0/reference}Product')
+        if product is not None:
+            isbn, created = self.isbn_set.get_or_create(
+                suffix=suffix
+            )
+            isbn.bn_data = etree.tostring(product, pretty_print=True, encoding='unicode')
+            isbn.save(update_fields=['bn_data'])
+
+
+class Isbn(models.Model):
+    pool = models.ForeignKey(IsbnPool, models.PROTECT)
+    suffix = models.IntegerField()
+    datestamp = models.DateField(blank=True, null=True)
+    book = models.ForeignKey(
+        'catalogue.Book', models.PROTECT, null=True, blank=True
+    )
+    form = models.CharField(
+        max_length=32, choices=[
+            (form, form)
+            for form, config in FORMS
+        ], null=True, blank=True
+    )
+    bn_data = models.TextField(blank=True)
+    wl_data = models.TextField(blank=True)
+    notes = models.TextField(blank=True)
+
+    class Meta:
+        ordering = ['pool', 'suffix']
+        unique_together = ['pool', 'suffix']
+
+    def __str__(self):
+        return self.get_code(True)
+        
+    def get_code(self, dashes=True):
+        return self.pool.get_code(self.suffix, dashes=dashes)
+
+    @classmethod
+    def import_from_documents(cls):
+        Book = apps.get_model('documents', 'Book')
+        for book in Book.objects.all():
+            try:
+                catalogue_book = book.catalogue_book
+                if catalogue_book is None:
+                    continue
+            except:
+                continue
+            try:
+                meta = book.wldocument(publishable=False, librarian2=True).meta
+            except:
+                continue
+            for form in ('html', 'txt', 'pdf', 'epub', 'mobi'):
+                isbn = getattr(meta, f'isbn_{form}')
+                if isbn is not None:
+                    parts = isbn.split('-')
+                    assert parts[0] == 'ISBN'
+                    suffix = int(parts[-2])
+                    prefix = ''.join(parts[1:-2])
+                    pool = IsbnPool.objects.get(prefix=prefix)
+                    isbn, created = pool.isbn_set.get_or_create(
+                        suffix=suffix,
+                    )
+                    if isbn.book is None:
+                        isbn.book = catalogue_book
+                    else:
+                        assert isbn.book is catalogue_book
+                    if isbn.form is None:
+                        isbn.form = form
+                    else:
+                        assert isbn.form == form
+                    isbn.save(update_fields=['book', 'form'])
diff --git a/src/isbn/product_forms.py b/src/isbn/product_forms.py
new file mode 100644 (file)
index 0000000..206ca1d
--- /dev/null
@@ -0,0 +1,10 @@
+FORMS = [
+    ('html', ('EC', 'E105')),
+    ('txt', ('EB', 'E112')),
+    ('pdf', ('EB', 'E107')),
+    ('epub', ('ED', 'E101')),
+    ('mobi', ('ED', 'E127')),
+    ('mp3', ('AN', 'A103')),
+    ('paperback', ('BC', '')),
+]
+
diff --git a/src/isbn/templates/isbn/list.html b/src/isbn/templates/isbn/list.html
new file mode 100644 (file)
index 0000000..e9f0012
--- /dev/null
@@ -0,0 +1,75 @@
+{% extends "documents/base.html" %}
+{% load pagination_tags %}
+{% load l10n %}
+
+
+{% block content %}
+  {% localize off %}
+  <table class="table">
+    {% for pool in pools %}
+      <tr>
+        <th>
+          {{ pool.get_purpose_display }}
+        </th>
+        <th>
+          ({{ pool }})
+        </th>
+        <th>
+          <div class="progress" style="height: 20px;">
+            {% with p=pool.fill_percentage %}
+              <div class="progress-bar" role="progressbar" style="width: {{ p }}%;" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100">{% if p > 30 %}{{ pool.entries }} / {{ pool.size }}{% endif %}</div>
+              {% if p <= 30 %}{{ pool.entries }} / {{ pool.size }}{% endif %}
+            {% endwith %}
+          </div>
+        </th>
+      </tr>
+    {% endfor %}
+  </table>
+  {% endlocalize %}
+
+  {% autopaginate list 100 %}
+  <table class="table">
+    <thead>
+      <tr>
+        <th>ISBN</th>
+        <th>Książka</th>
+        <th>Forma</th>
+        <th>Czas</th>
+      </tr>
+    </thead>
+    <tbody>
+      {% for isbn in list %}
+        <tr>
+          <td>
+            {{ isbn.get_code }}
+          </td>
+          <td>
+            {{ isbn.book|default_if_none:'—' }}
+          </td>
+          <td>
+            {{ isbn.form|default_if_none:'—' }}
+          </td>
+          <td>
+            {{ isbn.datestamp|default_if_none:'—' }}
+          </td>
+          <td>
+            {% if isbn.wl_data %}
+              <span class="badge badge-info" title="{{ isbn.wl_data }}">WL</span>
+            {% endif %}
+          </td>
+          <td>
+            {% if isbn.bn_data %}
+              <span class="badge badge-info" title="{{ isbn.bn_data }}">BN</span>
+            {% endif %}
+          </td>
+          <td>
+            {% if isbn.notes %}
+              <span class="badge badge-info" title="{{ isbn.notes }}">not.</span>
+            {% endif %}
+          </td>
+        </tr>
+      {% endfor %}
+    </tbody>
+  </table>
+  {% paginate %}
+{% endblock %}
diff --git a/src/isbn/urls.py b/src/isbn/urls.py
new file mode 100644 (file)
index 0000000..a27781a
--- /dev/null
@@ -0,0 +1,7 @@
+from django.urls import path
+from . import views
+
+
+urlpatterns = [
+    path('', views.isbn_list, name='isbn_list'),
+]
diff --git a/src/isbn/views.py b/src/isbn/views.py
new file mode 100644 (file)
index 0000000..70c101a
--- /dev/null
@@ -0,0 +1,9 @@
+from django.shortcuts import render
+from .models import Isbn, IsbnPool
+
+
+def isbn_list(request):
+    return render(request, 'isbn/list.html', {
+        'pools': IsbnPool.objects.all(),
+        'list': Isbn.objects.all(),
+    })
index e407885..6fb77fb 100644 (file)
@@ -27,6 +27,7 @@ urlpatterns = [
     path('cover/', include('cover.urls')),
     path('depot/', include('depot.urls')),
     path('wlxml/', include('wlxml.urls')),
+    path('isbn/', include('isbn.urls')),
 
     path('api/', include('redakcja.api.urls')),
 ]