Experimetal Woblink.
authorRadek Czajka <rczajka@rczajka.pl>
Thu, 10 Aug 2023 19:36:24 +0000 (21:36 +0200)
committerRadek Czajka <rczajka@rczajka.pl>
Thu, 10 Aug 2023 19:36:24 +0000 (21:36 +0200)
26 files changed:
requirements/requirements.txt
src/catalogue/models.py
src/catalogue/views.py
src/depot/admin.py
src/depot/legimi.py [deleted file]
src/depot/management/commands/depot.py
src/depot/migrations/0003_shop_rename_legimibookpublish_shopbookpublish_and_more.py [new file with mode: 0644]
src/depot/migrations/0004_shopbookpublish_shop.py [new file with mode: 0644]
src/depot/migrations/0005_shop_name_shop_publisher_handle.py [new file with mode: 0644]
src/depot/migrations/0006_shop_description_add.py [new file with mode: 0644]
src/depot/models.py
src/depot/publishers/base.py [new file with mode: 0644]
src/depot/publishers/legimi.py [new file with mode: 0644]
src/depot/publishers/woblink.py [new file with mode: 0644]
src/depot/publishers/woblink_constants.py [new file with mode: 0644]
src/depot/templatetags/depot.py [new file with mode: 0644]
src/depot/urls.py
src/depot/views.py
src/depot/woblink.py [deleted file]
src/documents/forms.py
src/documents/migrations/0011_book_woblink_id.py [new file with mode: 0644]
src/documents/models/book.py
src/documents/templates/documents/book_detail.html
src/documents/templates/documents/book_stats.html
src/documents/views.py
src/wlxml/views.py

index 1f07c4f..346dad4 100644 (file)
@@ -10,7 +10,7 @@ python-slugify==8.0.1
 python-docx==0.8.11
 Wikidata==0.7
 
-librarian==23.07.1
+librarian==23.08
 
 ## Django
 Django==4.1.9
index 85757a6..be63708 100644 (file)
@@ -121,6 +121,16 @@ class Author(WikidataModel):
     def get_absolute_url(self):
         return reverse("catalogue_author", args=[self.slug])
 
+    @classmethod
+    def get_by_literal(cls, literal):
+        names = literal.split(',', 1)
+        names = [n.strip() for n in names]
+        if len(names) == 2:
+            return cls.objects.filter(last_name=names[0], first_name=names[1]).first()
+        else:
+            return cls.objects.filter(last_name=names[0], first_name='').first() or \
+                cls.objects.filter(first_name=names[0], last_name='').first()
+
     @property
     def name(self):
         return f"{self.last_name}, {self.first_name}"
index 697a5ac..7301580 100644 (file)
@@ -20,7 +20,7 @@ from rest_framework.permissions import IsAdminUser
 from rest_framework.response import Response
 from rest_framework.views import APIView
 from rest_framework import serializers
-from depot.woblink import get_woblink_session
+import depot.models
 
 
 
@@ -384,11 +384,14 @@ def publish_collection(request, pk):
 
 @login_required
 def woblink_author_autocomplete(request):
-    session = get_woblink_session()
+    shop = depot.models.Shop.objects.filter(shop='woblink').first()
+    if shop is None:
+        return JsonResponse({})
+    woblink = shop.get_publisher()
     term = request.GET.get('term')
     if not term:
         return JsonResponse({})
-    response = session.get(
+    response = woblink.session.get(
         'https://publisher.woblink.com/author/autocomplete/' + term
     ).json()
     return JsonResponse({
index eebf010..3ad1fa3 100644 (file)
@@ -1,3 +1,4 @@
+from admin_ordering.admin import OrderableAdmin
 from django.contrib import admin
 from . import models
 
@@ -5,3 +6,21 @@ from . import models
 @admin.register(models.Package)
 class PackageAdmin(admin.ModelAdmin):
     raw_id_fields = ['books']
+
+
+class MediaInsertTextInline(OrderableAdmin, admin.TabularInline):
+    model = models.MediaInsertText
+    extra = 0
+
+
+class PriceLevelInline(OrderableAdmin, admin.TabularInline):
+    model = models.PriceLevel
+    extra = 0
+
+
+@admin.register(models.Shop)
+class ShopAdmin(admin.ModelAdmin):
+    inlines = [
+        MediaInsertTextInline,
+        PriceLevelInline,
+    ]
diff --git a/src/depot/legimi.py b/src/depot/legimi.py
deleted file mode 100644 (file)
index 464eeca..0000000
+++ /dev/null
@@ -1,442 +0,0 @@
-from datetime import date
-import re
-from django.conf import settings
-from librarian.functions import lang_code_3to2
-from librarian.html import transform_abstrakt
-from librarian.builders import EpubBuilder, MobiBuilder
-from librarian.covers.marquise import MarquiseCover, LabelMarquiseCover
-import requests
-from slugify import slugify
-
-
-
-fundraising=[
-            "Książka, którą czytasz, pochodzi z <a href=\"https://wolnelektury.pl/\">Wolnych Lektur</a>. Naszą misją jest wspieranie dzieciaków w dostępie do lektur szkolnych oraz zachęcanie ich do czytania. Miło Cię poznać!",
-            "Podoba Ci się to, co robimy? Jesteśmy organizacją pożytku publicznego. Wesprzyj Wolne Lektury drobną wpłatą: <a href=\"https://wolnelektury.pl/towarzystwo/\">wolnelektury.pl/towarzystwo/</a>",
-            "Przyjaciele Wolnych Lektur otrzymują dostęp do prapremier wcześniej niż inni. Zadeklaruj stałą wpłatę i dołącz do Towarzystwa Przyjaciół Wolnych Lektur: <a href=\"https://wolnelektury.pl/towarzystwo/\">wolnelektury.pl/towarzystwo/</a>",
-            "Informacje o nowościach w naszej bibliotece w Twojej skrzynce mailowej? Nic prostszego, zapisz się do newslettera. Kliknij, by pozostawić swój adres e-mail: <a href=\"https://wolnelektury.pl/newsletter/zapisz-sie/\">wolnelektury.pl/newsletter/zapisz-sie/</a>",
-            "Przekaż 1% podatku na Wolne Lektury.<br/>\nKRS: 0000070056<br/>\nNazwa organizacji: Fundacja Nowoczesna Polska<br/>\nKażda wpłacona kwota zostanie przeznaczona na rozwój Wolnych Lektur."
-]
-
-description_add = '<p>Książkę polecają <a href="https://wolnelektury.pl">Wolne Lektury</a> — najpopularniejsza biblioteka on-line.</p>'
-
-
-class Legimi:
-    #BASE_URL = 'https://wydawca.legimi.com'
-    BASE_URL = 'https://panel.legimi.pl'
-    LOGIN_URL = BASE_URL + '/publishers/membership'
-    UPLOAD_URL = BASE_URL + '/administration/upload/start'
-    CREATE_URL = BASE_URL + '/publishers/publications/create'
-    EDIT_URL = BASE_URL + '/publishers/publications/edit/%s'
-    EDIT_FILES_URL = BASE_URL + '/publishers/publications/editfiles/%s'
-    EDIT_SALE_URL = BASE_URL + '/publishers/publications/editsale/%s'
-
-    CATEGORIES = {
-        'Dla dzieci i młodzieży': 94,
-        'Książki dla dzieci': 15,
-        'Literatura młodzieżowa': 24,
-        'Kryminał': 29,
-        'Kryminał klasyczny': 31,
-        'Kryminał współczesny': 32,
-        'Kryminał historyczny': 30,
-        'default': 8886,
-        'Edukacja': 10,
-        'Słowniki i leksykony': 14,
-        'Encyklopedie': 13,
-        'Lektury': 11,
-        'Starożytność': 80,
-        'Barok': 83,
-        'Oświecenie': 84,
-        'Dwudziestolecie międzywojenne': 88,
-        'Średniowiecze': 81,
-        'Współczesność': 90,
-        'Modernizm': 87,
-        'Pozytywizm': 86,
-        'Renesans': 82,
-        'Romantyzm': 85,
-        'Młoda Polska': 89,
-        'Podręczniki': 52,
-        'Fantastyka i sci-fi': 25,
-        'Fantastyka': 26,
-        'Science fiction': 27,
-        'Języki obce': 59,
-        'Antyki i kolekcjonerstwo': 53,
-        'Astrologia i wróżbiarstwo': 54,
-        'Zdrowie i rodzina': 57,
-        'Hobby': 55,
-        'Medycyna i zdrowie': 58,
-        'Psychologiczne': 78,
-        'Styl': 56,
-        'Humanistyka': 97,
-        'Kultura i sztuka': 64,
-        'Film': 66,
-        'Muzyka': 65,
-        'Eseje literackie': 49,
-        'Historia': 60,
-        'Styl życia': 73,
-        'Wakacje i podróże': 69,
-        'Dla mężczyzn': 79,
-        'Sport': 76,
-        'Obyczajowe i romanse': 93,
-        'Humor': 68,
-        'Obyczajowe': 35,
-        'Powieść': 41,
-        'Powieść przygodowa': 42,
-        'Współczesna powieść przygodowa': 44,
-        'Historyczna powieść przygodowa': 43,
-        'Powieść historyczna': 46,
-        'Powieść psychologiczna': 47,
-        'Powieść religijna': 45,
-        'Romans': 36,
-        'Romans klasyczny': 38,
-        'Romans współczesny': 39,
-        'Literatura erotyczna': 40,
-        'Romans historyczny': 37,
-        'Dla kobiet': 77,
-        'Sensacja, thriller, horror': 91,
-        'Horror': 28,
-        'Sensacja': 33,
-        'Thriller': 34,
-        'Aktualności': 70,
-        'Czasopisma': 71,
-        'Literatura faktu, reportaże, biografie': 92,
-        'Literatura faktu': 16,
-        'Biografie': 17,
-        'Publicystyka': 20,
-        'Dzienniki': 19,
-        'Dokument, esej': 18,
-        'Historia literatury i krytyka literacka': 23,
-        'Literatura popularnonaukowa': 22,
-        'Reportaż': 21,
-        'Społeczno-polityczne': 72,
-        'Poezja i dramat': 95,
-        'Dramat': 48,
-        'Poezja': 50,
-        'Religia i duchowość': 51,
-        'Nauka i nowe technologie': 98,
-        'Nauka i technika': 61,
-        'Nauki ścisłe': 62,
-        'Nauki humanistyczne': 63,
-        'Technologia i Internet': 75,
-        'Specjalistyczne': 99,
-        'Biznes i finanse': 1,
-        'Ekonomia': 5,
-        'Finanse': 6,
-        'Zarządzanie': 3,
-        'Marketing': 2,
-        'Rozwój osobisty': 7,
-        'Kariera i sukces zawodowy': 8,
-        'Psychologia, motywacja': 9,
-        'PR': 4,
-        'Prawo': 67,
-        'Branżowe': 74,
-    }
-    
-    def __init__(self, username, password, publisher_id):
-        self.username = username
-        self.password = password
-        self.publisher_id = publisher_id
-        self._session = None
-
-    @property
-    def session(self):
-        if self._session is None:
-            session = requests.Session()
-            response = session.post(
-                self.LOGIN_URL,
-                data={
-                    'ValidationTrue': 'true',
-                    'UserName': self.username,
-                    'Password': self.password,
-                })
-            self._session = session
-        return self._session
-        
-    def list(self):
-        return self.session.get('https://wydawca.legimi.com/publishers/publications')
-        
-    def upload(self, content):
-        response = self.session.post(
-            self.UPLOAD_URL,
-            files={
-                "files": content,
-            })
-        model = response.json()['model']
-        return {
-            "name": model['Name'],
-            "token": model['Token'],
-            "url": model['Url'],
-        }
-
-    def send_book(self, book, changes=None):
-        wlbook = book.wldocument(librarian2=True, changes=changes)
-        meta = wlbook.meta
-
-        cover = LabelMarquiseCover(meta, width=1200).output_file()
-        epub_file = EpubBuilder(
-            cover=MarquiseCover,
-            fundraising=fundraising,
-            base_url='file://' + book.gallery_path() + '/'
-        ).build(wlbook).get_file()
-        mobi_file = MobiBuilder(
-            cover=MarquiseCover,
-            fundraising=fundraising,
-            base_url='file://' + book.gallery_path() + '/'
-        ).build(wlbook).get_file()
-
-        thema = []
-        if meta.thema_main:
-            thema.append(meta.thema_main)
-        thema.extend(meta.thema)
-        
-        book_data = {
-            "Title": meta.title,
-            "Author": ", ".join(p.readable() for p in meta.authors),
-            "Year": str(date.today().year),
-
-            'GenreId': str(self.get_genre(wlbook)),
-            'themaCategories': ';'.join(thema),
-            'thema-search': '',
-            'Isbn': '',
-            'LanguageLocale': lang_code_3to2(meta.language),
-
-            'Description': self.get_description(wlbook),
-        }
-        if meta.isbn_html:
-            isbn = meta.isbn_html
-            if isbn.upper().startswith(('ISBN ', 'ISBN-')):
-                isbn = isbn[5:]
-            isbn = isbn.strip()
-            book_data['Isbn'] = isbn
-
-        files_data = {}
-
-        cover_data = self.upload(
-            (meta.url.slug + '.jpg', cover.get_file(), 'image/jpeg')
-        )
-        book_data.update({
-            "Cover.Name": cover_data['name'],
-            "Cover.Token": cover_data['token'],
-            "Cover.Url": cover_data['url'],
-        })
-
-        epub_data = self.upload(
-            (meta.url.slug + '.epub', epub_file, 'application/epub+zip')
-        )
-        files_data.update({
-            'BookEpub.Token': epub_data['token'],
-            'BookEpub.Name': epub_data['name'],
-            'SampleEpubType': 'Generation',
-        })
-
-        mobi_data = self.upload(
-            (meta.url.slug + '.mobi', mobi_file, 'application/x-mobipocket-ebook')
-        )
-        files_data.update({
-            'BookMobi.Token': mobi_data['token'],
-            'BookMobi.Name': mobi_data['name'],
-        })
-        
-        if book.legimi_id:
-            self.edit(
-                book.legimi_id,
-                book_data
-            )
-            self.edit_files(
-                book.legimi_id,
-                files_data
-            )
-        else:
-            legimi_id = self.create_book(book_data, files_data)
-            if legimi_id:
-                book.legimi_id = legimi_id
-                book.save(update_fields=['legimi_id'])
-
-    def get_description(self, wlbook):
-        description = ''
-        abstract = wlbook.tree.find('.//abstrakt')
-        if abstract is not None:
-            description = transform_abstrakt(abstract)
-        description += description_add
-        description += '<p>'
-        description += ', '.join(
-            '<a href="https://wolnelektury.pl/katalog/autor/{}/">{}</a>'.format(
-                slugify(p.readable()),
-                p.readable(),
-            )
-            for p in wlbook.meta.authors
-        ) + '<br>'
-        description += '<a href="https://wolnelektury.pl/katalog/lektura/{}/">{}</a><br>'.format(
-            wlbook.meta.url.slug,
-            wlbook.meta.title
-        )
-        if wlbook.meta.translators:
-            description += 'tłum. ' + ', '.join(p.readable() for p in wlbook.meta.translators) + '<br>'
-        description += 'Epoka: ' + ', '.join(
-            '<a href="https://wolnelektury.pl/katalog/epoka/{}/">{}</a>'.format(
-                slugify(p),
-                p,
-            )
-            for p in wlbook.meta.epochs
-        ) + ' '
-        description += 'Rodzaj: ' + ', '.join(
-            '<a href="https://wolnelektury.pl/katalog/rodzaj/{}/">{}</a>'.format(
-                slugify(p),
-                p,
-            )
-            for p in wlbook.meta.kinds
-        ) + ' '
-        description += 'Gatunek: ' + ', '.join(
-            '<a href="https://wolnelektury.pl/katalog/gatunek/{}/">{}</a>'.format(
-                slugify(p),
-                p,
-            )
-            for p in wlbook.meta.genres
-        ) + '</p>'
-
-        # TODO: Move away from using audiences for this.
-        if wlbook.meta.audience in ('L', 'SP1', 'SP2', 'SP3', 'SP4'):
-            description += '<p><em>{}</em> to lektura szkolna.'.format(wlbook.meta.title)
-            if wlbook.tree.find('//pe') is not None:
-                description += '<br>Ebook <em>{title}</em> zawiera przypisy opracowane specjalnie dla uczennic i uczniów {school}.'.format(
-                    title=wlbook.meta.title,
-                    school='szkoły podstawowej' if wlbook.meta.audience.startswith('SP') else 'liceum i technikum'
-                )
-            description += '</p>'
-        return description
-
-    def get_genre(self, wlbook):
-        if wlbook.meta.legimi and wlbook.meta.legimi in self.CATEGORIES:
-            return self.CATEGORIES[wlbook.meta.legimi]
-        for epoch in wlbook.meta.epochs:
-            if epoch in self.CATEGORIES:
-                return self.CATEGORIES[epoch]
-        return self.CATEGORIES['Lektury']
-    
-    def create_book(self, book_data, files_data):
-        data = {
-            'createValidationTrue': 'true',
-            'PublisherId': self.publisher_id,#3609954
-            'IsLibraryPass': 'False',
-
-            'SamplesGenerationType': 'Quantity',
-            'SamplesGenerationPercent': '10',
-
-            'EnterToTheMarketType': 'No',
-            'EnterToTheMarketDate': '',
-            'HidingDate': '',
-            'SalesNoLimitOption': 'false',
-            'SalesNoLimitKindle': 'false',
-            'SalesInStoreEbookGrossValue': '0,00',
-            'SalesPromotion': 'False',
-            'SalesPromotionGrossValue': '0,00',
-            'SalesPromotionDatesRange.DateStart': '',
-            'SalesPromotionDatesRange.DateEnd': '',
-        }
-
-        for form in 'Epub', 'Mobi', 'Pdf':
-            data.update({
-                f'Book{form}.Token': '',
-                f'Book{form}.Name': '',
-                f'Book{form}.StorageName': '',
-                f'Book{form}.Order': '',
-
-                f'Sample{form}Type': 'Files',
-                f'Sample{form}.Token': '',
-                f'Sample{form}.Name': '',
-                f'Sample{form}.StorageName': '',
-                f'Sample{form}.Order': '',
-            })
-
-        data.update(book_data)
-        data.update(files_data)
-
-        response = self.session.post(self.CREATE_URL, data=data)
-        m = re.search(r'/(\d+)$', response.url)
-        if m is not None:
-            return m.group(1)
-
-    def edit(self, legimi_id, data):
-        current = {
-            'ValidationTrue': 'true',
-            'Id': legimi_id
-        }
-
-        current.update(data)
-        
-        self.session.post(
-            self.EDIT_URL % legimi_id,
-            data=current
-        )
-
-    def edit_files(self, legimi_id, files_data):
-        current = {
-            'ValidationTrue': 'true',
-            'Id': legimi_id,
-            'SamplesGenerationType': 'Quantity',
-            'SamplesGenerationPercent': '10',
-        }
-
-        for form in 'Epub', 'Mobi', 'Pdf':
-            current.update({
-                f'Book{form}.Token': '',
-                f'Book{form}.Name': '',
-                f'Book{form}.StorageName': '',
-                f'Book{form}.Order': '',
-
-                f'Sample{form}.Type': 'Files',
-                f'Sample{form}.Token': '',
-                f'Sample{form}.Name': '',
-                f'Sample{form}.StorageName': '',
-                f'Sample{form}.Order': '',
-            })
-
-        current.update(files_data)
-        response = self.session.post(
-            self.EDIT_FILES_URL % legimi_id,
-            data=current
-        )
-
-    def edit_sale(self, book):
-        assert book.legimi_id
-
-        words = book.wldocument().get_statistics()['total']['words_with_fn']
-
-        price = settings.LEGIMI_SMALL_PRICE
-        if words > settings.LEGIMI_SMALL_WORDS:
-            price = settings.LEGIMI_BIG_PRICE
-
-        abo = 'true' if words > settings.LEGIMI_BIG_WORDS else 'false'
-
-        data = {
-            'ValidationTrue': 'true',
-            'Id': book.legimi_id,
-            'SalesPromotionId': "0",
-            'IsLibraryPass': "False",
-            'OriginalEnterToTheMarketType': "No",
-            'OriginalHidingDate': "",
-            'OriginalEnterToTheMarketDate': "",
-            'EnterToTheMarketType': "Yes",
-            'EnterToTheMarketDate': "",
-            'HidingDate': "",
-            'SalesNoLimitOption': abo,
-            'SalesNoLimitKindle': abo,
-            'SalesInStoreEbookGrossValue': f'{price},00',
-            'SalesPromotion': "False",
-            'SalesPromotionGrossValue': "0,00",
-            'SalesPromotionDatesRange.DateStart': "",
-            'SalesPromotionDatesRange.DateEnd': "",
-        }
-
-        self.session.post(
-            self.EDIT_SALE_URL % book.legimi_id,
-            data=data
-        )
-
-
-legimi = Legimi(
-    settings.LEGIMI_USERNAME,
-    settings.LEGIMI_PASSWORD,
-    settings.LEGIMI_PUBLISHER_ID,
-)
index 4b8b99a..6e64e24 100644 (file)
@@ -1,10 +1,10 @@
 from django.core.management.base import BaseCommand
-from depot.models import LegimiBookPublish
+from depot.models import ShopBookPublish
 
 
 class Command(BaseCommand):
     def handle(self, **options):
-        for p in LegimiBookPublish.objects.filter(status=0).order_by('created_at'):
-            print(p, p.book.slug, p.created_at)
+        for p in ShopBookPublish.objects.filter(status=0).order_by('created_at'):
+            print(p.id, p.shop, p.book.slug, p.created_at)
             p.publish()
 
diff --git a/src/depot/migrations/0003_shop_rename_legimibookpublish_shopbookpublish_and_more.py b/src/depot/migrations/0003_shop_rename_legimibookpublish_shopbookpublish_and_more.py
new file mode 100644 (file)
index 0000000..bcf0af4
--- /dev/null
@@ -0,0 +1,99 @@
+# Generated by Django 4.1.9 on 2023-08-10 18:36
+
+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),
+        ("documents", "0011_book_woblink_id"),
+        ("depot", "0002_legimibookpublish_legimichunkpublish"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="Shop",
+            fields=[
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "shop",
+                    models.CharField(
+                        choices=[("legimi", "Legimi"), ("woblink", "Woblink")],
+                        max_length=32,
+                    ),
+                ),
+                ("username", models.CharField(max_length=255)),
+                ("password", models.CharField(max_length=255)),
+            ],
+        ),
+        migrations.RenameModel(
+            old_name="LegimiBookPublish",
+            new_name="ShopBookPublish",
+        ),
+        migrations.RenameModel(
+            old_name="LegimiChunkPublish",
+            new_name="ShopChunkPublish",
+        ),
+        migrations.CreateModel(
+            name="PriceLevel",
+            fields=[
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("min_pages", models.IntegerField(blank=True, null=True)),
+                ("min_words", models.IntegerField(blank=True, null=True)),
+                ("price", models.IntegerField()),
+                (
+                    "shop",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE, to="depot.shop"
+                    ),
+                ),
+            ],
+            options={
+                "ordering": ("price",),
+            },
+        ),
+        migrations.CreateModel(
+            name="MediaInsertText",
+            fields=[
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("ordering", models.IntegerField()),
+                ("text", models.TextField()),
+                (
+                    "shop",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE, to="depot.shop"
+                    ),
+                ),
+            ],
+            options={
+                "ordering": ("ordering",),
+            },
+        ),
+    ]
diff --git a/src/depot/migrations/0004_shopbookpublish_shop.py b/src/depot/migrations/0004_shopbookpublish_shop.py
new file mode 100644 (file)
index 0000000..d908f75
--- /dev/null
@@ -0,0 +1,21 @@
+# Generated by Django 4.1.9 on 2023-08-10 18:36
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("depot", "0003_shop_rename_legimibookpublish_shopbookpublish_and_more"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="shopbookpublish",
+            name="shop",
+            field=models.ForeignKey(
+                null=True, on_delete=django.db.models.deletion.SET_NULL, to="depot.shop"
+            ),
+        ),
+    ]
diff --git a/src/depot/migrations/0005_shop_name_shop_publisher_handle.py b/src/depot/migrations/0005_shop_name_shop_publisher_handle.py
new file mode 100644 (file)
index 0000000..b50632d
--- /dev/null
@@ -0,0 +1,24 @@
+# Generated by Django 4.1.9 on 2023-08-10 18:55
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("depot", "0004_shopbookpublish_shop"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="shop",
+            name="name",
+            field=models.CharField(default="", max_length=255),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name="shop",
+            name="publisher_handle",
+            field=models.CharField(blank=True, max_length=255),
+        ),
+    ]
diff --git a/src/depot/migrations/0006_shop_description_add.py b/src/depot/migrations/0006_shop_description_add.py
new file mode 100644 (file)
index 0000000..cecf244
--- /dev/null
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.9 on 2023-08-10 20:18
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("depot", "0005_shop_name_shop_publisher_handle"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="shop",
+            name="description_add",
+            field=models.TextField(blank=True),
+        ),
+    ]
index 23b6d9f..4b5bf65 100644 (file)
@@ -9,7 +9,8 @@ from django.db import models
 from django.utils.timezone import now
 from librarian.cover import make_cover
 from librarian.builders import EpubBuilder, MobiBuilder
-from .legimi import legimi
+from .publishers.legimi import Legimi
+from .publishers.woblink import Woblink
 
 
 class Package(models.Model):
@@ -27,14 +28,14 @@ class Package(models.Model):
             self.set_status(self.get_status())
         except:
             pass
-        
+
         try:
             self.set_definition(self.get_definition())
         except:
             pass
 
         super().save(*args, **kwargs)
-    
+
     def get_status(self):
         return json.loads(self.status_json)
 
@@ -121,9 +122,10 @@ class Package(models.Model):
             )
 
 
-class LegimiBookPublish(models.Model):
+class ShopBookPublish(models.Model):
     book = models.ForeignKey('documents.Book', models.CASCADE)
     user = models.ForeignKey(settings.AUTH_USER_MODEL, models.SET_NULL, null=True)
+    shop = models.ForeignKey('Shop', models.SET_NULL, null=True)
     created_at = models.DateTimeField()
     started_at = models.DateTimeField(null=True, blank=True)
     finished_at = models.DateTimeField(null=True, blank=True)
@@ -136,12 +138,12 @@ class LegimiBookPublish(models.Model):
     error = models.TextField(blank=True)
 
     @classmethod
-    def create_for(cls, book, user):
+    def create_for(cls, book, user, shop):
         book.assert_publishable()
         changes = book.get_current_changes(publishable=True)
-        me = cls.objects.create(book=book, user=user, created_at=now())
+        me = cls.objects.create(book=book, user=user, shop=shop, created_at=now())
         for change in changes:
-            me.legimichunkpublish_set.create(change=change)
+            me.shopchunkpublish_set.create(change=change)
         return me
 
     def publish(self):
@@ -151,10 +153,11 @@ class LegimiBookPublish(models.Model):
         try:
             changes = [
                 p.change for p in
-                self.legimichunkpublish_set.order_by('change__chunk__number')
+                self.shopchunkpublish_set.order_by('change__chunk__number')
             ]
-            legimi.send_book(self.book, changes=changes)
-            legimi.edit_sale(self.book)
+
+            self.shop.publish(self.book, changes=changes)
+
         except Exception:
             self.status = 110
             self.error = traceback.format_exc()
@@ -165,6 +168,71 @@ class LegimiBookPublish(models.Model):
         self.save(update_fields=['status', 'finished_at', 'error'])
 
 
-class LegimiChunkPublish(models.Model):
-    book_publish = models.ForeignKey(LegimiBookPublish, models.CASCADE)
+class ShopChunkPublish(models.Model):
+    book_publish = models.ForeignKey(ShopBookPublish, models.CASCADE)
     change = models.ForeignKey('documents.ChunkChange', models.CASCADE)
+
+
+class Shop(models.Model):
+    name = models.CharField(max_length=255)
+    shop = models.CharField(max_length=32, choices=[
+        ('legimi', 'Legimi'),
+        ('woblink', 'Woblink'),
+    ])
+    username = models.CharField(max_length=255)
+    password = models.CharField(max_length=255)
+    publisher_handle = models.CharField(max_length=255, blank=True)
+    description_add = models.TextField(blank=True)
+
+    def __str__(self):
+        return self.shop
+
+    def get_texts(self):
+        return [t.text for t in self.mediainserttext_set.all()]
+
+    def get_price(self, words, pages):
+        price_obj = self.pricelevel_set.exclude(
+            min_pages__gt=pages
+        ).exclude(
+            min_words__gt=words
+        ).order_by('-price').first()
+        if price_obj is None:
+            return None
+        return price_obj.price
+
+    def get_publisher(self):
+        if self.shop == 'legimi':
+            pub_class = Legimi
+        elif self.shop == 'woblink':
+            pub_class = Woblink
+        return pub_class(self.username, self.password, self.publisher_handle)
+
+    def publish(self, book, changes):
+        self.get_publisher().send_book(
+            self, book, changes=changes,
+        )
+
+    def can_publish(self, book):
+        return self.get_publisher().can_publish(self, book)
+
+    def get_last(self, book):
+        return self.shopbookpublish_set.filter(book=book).order_by('-created_at').first()
+
+
+class PriceLevel(models.Model):
+    shop = models.ForeignKey(Shop, models.CASCADE)
+    min_pages = models.IntegerField(null=True, blank=True)
+    min_words = models.IntegerField(null=True, blank=True)
+    price = models.IntegerField()
+
+    class Meta:
+        ordering = ('price',)
+
+
+class MediaInsertText(models.Model):
+    shop = models.ForeignKey(Shop, models.CASCADE)
+    ordering = models.IntegerField()
+    text = models.TextField()
+
+    class Meta:
+        ordering = ('ordering',)
diff --git a/src/depot/publishers/base.py b/src/depot/publishers/base.py
new file mode 100644 (file)
index 0000000..b728cf2
--- /dev/null
@@ -0,0 +1,75 @@
+import requests
+from librarian.html import transform_abstrakt
+from slugify import slugify
+
+
+class BasePublisher:
+    def __init__(self, username, password, publisher_handle):
+        self.username = username
+        self.password = password
+        self.publisher_handle = publisher_handle
+        self._session = None
+
+    @property
+    def session(self):
+        if self._session is None:
+            self._session = requests.Session()
+            self.login()
+        return self._session
+
+    def send_book(self, shop, book, changes=None):
+        raise NotImplementedError()
+
+    def get_description(self, wlbook, description_add=''):
+        description = ''
+        abstract = wlbook.tree.find('.//abstrakt')
+        if abstract is not None:
+            description = transform_abstrakt(abstract)
+        description += description_add
+        description += '<p>'
+        description += ', '.join(
+            '<a href="https://wolnelektury.pl/katalog/autor/{}/">{}</a>'.format(
+                slugify(p.readable()),
+                p.readable(),
+            )
+            for p in wlbook.meta.authors
+        ) + '<br>'
+        description += '<a href="https://wolnelektury.pl/katalog/lektura/{}/">{}</a><br>'.format(
+            wlbook.meta.url.slug,
+            wlbook.meta.title
+        )
+        if wlbook.meta.translators:
+            description += 'tłum. ' + ', '.join(p.readable() for p in wlbook.meta.translators) + '<br>'
+        description += 'Epoka: ' + ', '.join(
+            '<a href="https://wolnelektury.pl/katalog/epoka/{}/">{}</a>'.format(
+                slugify(p),
+                p,
+            )
+            for p in wlbook.meta.epochs
+        ) + ' '
+        description += 'Rodzaj: ' + ', '.join(
+            '<a href="https://wolnelektury.pl/katalog/rodzaj/{}/">{}</a>'.format(
+                slugify(p),
+                p,
+            )
+            for p in wlbook.meta.kinds
+        ) + ' '
+        description += 'Gatunek: ' + ', '.join(
+            '<a href="https://wolnelektury.pl/katalog/gatunek/{}/">{}</a>'.format(
+                slugify(p),
+                p,
+            )
+            for p in wlbook.meta.genres
+        ) + '</p>'
+
+        # TODO: Move away from using audiences for this.
+        if wlbook.meta.audience in ('L', 'SP1', 'SP2', 'SP3', 'SP4'):
+            description += '<p><em>{}</em> to lektura szkolna.'.format(wlbook.meta.title)
+            if wlbook.tree.find('//pe') is not None:
+                description += '<br>Ebook <em>{title}</em> zawiera przypisy opracowane specjalnie dla uczennic i uczniów {school}.'.format(
+                    title=wlbook.meta.title,
+                    school='szkoły podstawowej' if wlbook.meta.audience.startswith('SP') else 'liceum i technikum'
+                )
+            description += '</p>'
+        return description
+
diff --git a/src/depot/publishers/legimi.py b/src/depot/publishers/legimi.py
new file mode 100644 (file)
index 0000000..c7fa26d
--- /dev/null
@@ -0,0 +1,390 @@
+from datetime import date
+import re
+from django.conf import settings
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
+from librarian.functions import lang_code_3to2
+from librarian.builders import EpubBuilder, MobiBuilder
+from librarian.covers.marquise import MarquiseCover, LabelMarquiseCover
+from .base import BasePublisher
+
+
+class Legimi(BasePublisher):
+    BASE_URL = 'https://panel.legimi.pl'
+    LOGIN_URL = BASE_URL + '/publishers/membership'
+    UPLOAD_URL = BASE_URL + '/administration/upload/start'
+    CREATE_URL = BASE_URL + '/publishers/publications/create'
+    EDIT_URL = BASE_URL + '/publishers/publications/edit/%s'
+    EDIT_FILES_URL = BASE_URL + '/publishers/publications/editfiles/%s'
+    EDIT_SALE_URL = BASE_URL + '/publishers/publications/editsale/%s'
+
+    CATEGORIES = {
+        'Dla dzieci i młodzieży': 94,
+        'Książki dla dzieci': 15,
+        'Literatura młodzieżowa': 24,
+        'Kryminał': 29,
+        'Kryminał klasyczny': 31,
+        'Kryminał współczesny': 32,
+        'Kryminał historyczny': 30,
+        'default': 8886,
+        'Edukacja': 10,
+        'Słowniki i leksykony': 14,
+        'Encyklopedie': 13,
+        'Lektury': 11,
+        'Starożytność': 80,
+        'Barok': 83,
+        'Oświecenie': 84,
+        'Dwudziestolecie międzywojenne': 88,
+        'Średniowiecze': 81,
+        'Współczesność': 90,
+        'Modernizm': 87,
+        'Pozytywizm': 86,
+        'Renesans': 82,
+        'Romantyzm': 85,
+        'Młoda Polska': 89,
+        'Podręczniki': 52,
+        'Fantastyka i sci-fi': 25,
+        'Fantastyka': 26,
+        'Science fiction': 27,
+        'Języki obce': 59,
+        'Antyki i kolekcjonerstwo': 53,
+        'Astrologia i wróżbiarstwo': 54,
+        'Zdrowie i rodzina': 57,
+        'Hobby': 55,
+        'Medycyna i zdrowie': 58,
+        'Psychologiczne': 78,
+        'Styl': 56,
+        'Humanistyka': 97,
+        'Kultura i sztuka': 64,
+        'Film': 66,
+        'Muzyka': 65,
+        'Eseje literackie': 49,
+        'Historia': 60,
+        'Styl życia': 73,
+        'Wakacje i podróże': 69,
+        'Dla mężczyzn': 79,
+        'Sport': 76,
+        'Obyczajowe i romanse': 93,
+        'Humor': 68,
+        'Obyczajowe': 35,
+        'Powieść': 41,
+        'Powieść przygodowa': 42,
+        'Współczesna powieść przygodowa': 44,
+        'Historyczna powieść przygodowa': 43,
+        'Powieść historyczna': 46,
+        'Powieść psychologiczna': 47,
+        'Powieść religijna': 45,
+        'Romans': 36,
+        'Romans klasyczny': 38,
+        'Romans współczesny': 39,
+        'Literatura erotyczna': 40,
+        'Romans historyczny': 37,
+        'Dla kobiet': 77,
+        'Sensacja, thriller, horror': 91,
+        'Horror': 28,
+        'Sensacja': 33,
+        'Thriller': 34,
+        'Aktualności': 70,
+        'Czasopisma': 71,
+        'Literatura faktu, reportaże, biografie': 92,
+        'Literatura faktu': 16,
+        'Biografie': 17,
+        'Publicystyka': 20,
+        'Dzienniki': 19,
+        'Dokument, esej': 18,
+        'Historia literatury i krytyka literacka': 23,
+        'Literatura popularnonaukowa': 22,
+        'Reportaż': 21,
+        'Społeczno-polityczne': 72,
+        'Poezja i dramat': 95,
+        'Dramat': 48,
+        'Poezja': 50,
+        'Religia i duchowość': 51,
+        'Nauka i nowe technologie': 98,
+        'Nauka i technika': 61,
+        'Nauki ścisłe': 62,
+        'Nauki humanistyczne': 63,
+        'Technologia i Internet': 75,
+        'Specjalistyczne': 99,
+        'Biznes i finanse': 1,
+        'Ekonomia': 5,
+        'Finanse': 6,
+        'Zarządzanie': 3,
+        'Marketing': 2,
+        'Rozwój osobisty': 7,
+        'Kariera i sukces zawodowy': 8,
+        'Psychologia, motywacja': 9,
+        'PR': 4,
+        'Prawo': 67,
+        'Branżowe': 74,
+    }
+
+    def login(self):
+        self._session.post(
+            self.LOGIN_URL,
+            data={
+                'ValidationTrue': 'true',
+                'UserName': self.username,
+                'Password': self.password,
+            })
+
+    def can_publish(self, shop, book):
+        meta = book.wldocument(librarian2=True).meta
+        d = {
+            'errors': [],
+            'warnings': [],
+        }
+        if meta.thema_main or meta.thema:
+            if meta.thema_main:
+                comment = "w kategorii <b><tt>{code}</tt></b>".format(
+                    code=escape(meta.thema_main)
+                )
+                if meta.thema:
+                    comment += " oraz: " + ", ".join(
+                        "<b><tt>{code}</tt></b>".format(code=escape(t))
+                        for t in meta.thema
+                    )
+                d['comment'] = mark_safe(comment)
+            elif meta.thema:
+                d['comment'] = mark_safe(
+                    "w kategorii " + ", ".join(
+                        "<b><tt>{code}</tt></b>".format(code=escape(t))
+                        for t in meta.thema
+                    )
+                )
+                d['warnings'].append('Brak głównej kategorii Thema')
+        else:
+            d['errors'].append('Brak kategorii Thema.')
+        return d
+
+    def list(self):
+        return self.session.get('https://wydawca.legimi.com/publishers/publications')
+
+    def upload(self, content):
+        response = self.session.post(
+            self.UPLOAD_URL,
+            files={
+                "files": content,
+            })
+        model = response.json()['model']
+        return {
+            "name": model['Name'],
+            "token": model['Token'],
+            "url": model['Url'],
+        }
+
+    def send_book(self, shop, book, changes=None):
+        wlbook = book.wldocument(librarian2=True, changes=changes)
+        meta = wlbook.meta
+
+        cover = LabelMarquiseCover(meta, width=1200).output_file()
+        texts = shop.get_texts()
+        epub_file = EpubBuilder(
+            cover=MarquiseCover,
+            fundraising=texts,
+            base_url='file://' + book.gallery_path() + '/'
+        ).build(wlbook).get_file()
+        mobi_file = MobiBuilder(
+            cover=MarquiseCover,
+            fundraising=texts,
+            base_url='file://' + book.gallery_path() + '/'
+        ).build(wlbook).get_file()
+
+        thema = []
+        if meta.thema_main:
+            thema.append(meta.thema_main)
+        thema.extend(meta.thema)
+
+        book_data = {
+            "Title": meta.title,
+            "Author": ", ".join(p.readable() for p in meta.authors),
+            "Year": str(date.today().year),
+
+            'GenreId': str(self.get_genre(wlbook)),
+            'themaCategories': ';'.join(thema),
+            'thema-search': '',
+            'Isbn': '',
+            'LanguageLocale': lang_code_3to2(meta.language),
+
+            'Description': self.get_description(wlbook, shop.description_add),
+        }
+        if meta.isbn_html:
+            isbn = meta.isbn_html
+            if isbn.upper().startswith(('ISBN ', 'ISBN-')):
+                isbn = isbn[5:]
+            isbn = isbn.strip()
+            book_data['Isbn'] = isbn
+
+        files_data = {}
+
+        cover_data = self.upload(
+            (meta.url.slug + '.jpg', cover.get_file(), 'image/jpeg')
+        )
+        book_data.update({
+            "Cover.Name": cover_data['name'],
+            "Cover.Token": cover_data['token'],
+            "Cover.Url": cover_data['url'],
+        })
+
+        epub_data = self.upload(
+            (meta.url.slug + '.epub', epub_file, 'application/epub+zip')
+        )
+        files_data.update({
+            'BookEpub.Token': epub_data['token'],
+            'BookEpub.Name': epub_data['name'],
+            'SampleEpubType': 'Generation',
+        })
+
+        mobi_data = self.upload(
+            (meta.url.slug + '.mobi', mobi_file, 'application/x-mobipocket-ebook')
+        )
+        files_data.update({
+            'BookMobi.Token': mobi_data['token'],
+            'BookMobi.Name': mobi_data['name'],
+        })
+
+        if book.legimi_id:
+            self.edit(
+                book.legimi_id,
+                book_data
+            )
+            self.edit_files(
+                book.legimi_id,
+                files_data
+            )
+        else:
+            legimi_id = self.create_book(book_data, files_data)
+            if legimi_id:
+                book.legimi_id = legimi_id
+                book.save(update_fields=['legimi_id'])
+
+        self.edit_sale(book)
+
+    def get_genre(self, wlbook):
+        if wlbook.meta.legimi and wlbook.meta.legimi in self.CATEGORIES:
+            return self.CATEGORIES[wlbook.meta.legimi]
+        for epoch in wlbook.meta.epochs:
+            if epoch in self.CATEGORIES:
+                return self.CATEGORIES[epoch]
+        return self.CATEGORIES['Lektury']
+
+    def create_book(self, book_data, files_data):
+        data = {
+            'createValidationTrue': 'true',
+            'PublisherId': self.publisher_handle,
+            'IsLibraryPass': 'False',
+
+            'SamplesGenerationType': 'Quantity',
+            'SamplesGenerationPercent': '10',
+
+            'EnterToTheMarketType': 'No',
+            'EnterToTheMarketDate': '',
+            'HidingDate': '',
+            'SalesNoLimitOption': 'false',
+            'SalesNoLimitKindle': 'false',
+            'SalesInStoreEbookGrossValue': '0,00',
+            'SalesPromotion': 'False',
+            'SalesPromotionGrossValue': '0,00',
+            'SalesPromotionDatesRange.DateStart': '',
+            'SalesPromotionDatesRange.DateEnd': '',
+        }
+
+        for form in 'Epub', 'Mobi', 'Pdf':
+            data.update({
+                f'Book{form}.Token': '',
+                f'Book{form}.Name': '',
+                f'Book{form}.StorageName': '',
+                f'Book{form}.Order': '',
+
+                f'Sample{form}Type': 'Files',
+                f'Sample{form}.Token': '',
+                f'Sample{form}.Name': '',
+                f'Sample{form}.StorageName': '',
+                f'Sample{form}.Order': '',
+            })
+
+        data.update(book_data)
+        data.update(files_data)
+
+        response = self.session.post(self.CREATE_URL, data=data)
+        m = re.search(r'/(\d+)$', response.url)
+        if m is not None:
+            return m.group(1)
+
+    def edit(self, legimi_id, data):
+        current = {
+            'ValidationTrue': 'true',
+            'Id': legimi_id
+        }
+
+        current.update(data)
+
+        self.session.post(
+            self.EDIT_URL % legimi_id,
+            data=current
+        )
+
+    def edit_files(self, legimi_id, files_data):
+        current = {
+            'ValidationTrue': 'true',
+            'Id': legimi_id,
+            'SamplesGenerationType': 'Quantity',
+            'SamplesGenerationPercent': '10',
+        }
+
+        for form in 'Epub', 'Mobi', 'Pdf':
+            current.update({
+                f'Book{form}.Token': '',
+                f'Book{form}.Name': '',
+                f'Book{form}.StorageName': '',
+                f'Book{form}.Order': '',
+
+                f'Sample{form}.Type': 'Files',
+                f'Sample{form}.Token': '',
+                f'Sample{form}.Name': '',
+                f'Sample{form}.StorageName': '',
+                f'Sample{form}.Order': '',
+            })
+
+        current.update(files_data)
+
+        response = self.session.post(
+            self.EDIT_FILES_URL % legimi_id,
+            data=current
+        )
+
+    def edit_sale(self, book):
+        assert book.legimi_id
+
+        words = book.wldocument().get_statistics()['total']['words_with_fn']
+
+        price = settings.LEGIMI_SMALL_PRICE
+        if words > settings.LEGIMI_SMALL_WORDS:
+            price = settings.LEGIMI_BIG_PRICE
+
+        abo = 'true' if words > settings.LEGIMI_BIG_WORDS else 'false'
+
+        data = {
+            'ValidationTrue': 'true',
+            'Id': book.legimi_id,
+            'SalesPromotionId': "0",
+            'IsLibraryPass': "False",
+            'OriginalEnterToTheMarketType': "No",
+            'OriginalHidingDate': "",
+            'OriginalEnterToTheMarketDate': "",
+            'EnterToTheMarketType': "Yes",
+            'EnterToTheMarketDate': "",
+            'HidingDate': "",
+            'SalesNoLimitOption': abo,
+            'SalesNoLimitKindle': abo,
+            'SalesInStoreEbookGrossValue': f'{price},00',
+            'SalesPromotion': "False",
+            'SalesPromotionGrossValue': "0,00",
+            'SalesPromotionDatesRange.DateStart': "",
+            'SalesPromotionDatesRange.DateEnd': "",
+        }
+
+        self.session.post(
+            self.EDIT_SALE_URL % book.legimi_id,
+            data=data
+        )
diff --git a/src/depot/publishers/woblink.py b/src/depot/publishers/woblink.py
new file mode 100644 (file)
index 0000000..00dca5e
--- /dev/null
@@ -0,0 +1,534 @@
+from datetime import date
+import io
+import json
+import re
+from time import sleep
+from django.conf import settings
+from django.utils.html import escape, format_html
+from django.utils.safestring import mark_safe
+from librarian.builders.html import SnippetHtmlBuilder
+from librarian.functions import lang_code_3to2
+from catalogue.models import Author, Thema
+from .. import models
+from .base import BasePublisher
+from .woblink_constants import WOBLINK_CATEGORIES
+
+
+class WoblinkError(ValueError):
+    pass
+
+class NoPrice(WoblinkError):
+    def as_html(self):
+        return format_html(
+            'Brak <a href="/admin/depot/shop/{price}">określonej ceny</a>.',
+            price=self.args[0].id
+        )
+
+class NoIsbn(WoblinkError):
+    def as_html(self):
+        return 'Brak ISBN.'
+
+class AuthorLiteralForeign(WoblinkError):
+    def as_html(self):
+        return format_html(
+            'Nie obsługiwane: autor „{author}” w języku {lang}.',
+            author=str(self.args[0]),
+            lang=self.args[0].lang,
+        )
+
+class AuthorNotInCatalogue(WoblinkError):
+    def as_html(self):
+        return format_html(
+            'Brak autora „{author}” w katalogu.',
+            author=str(self.args[0])
+        )
+
+class AuthorNoWoblink(WoblinkError):
+    def as_html(self):
+        return format_html(
+            'Autor <a href="/admin/catalogue/author/{author_id}/">{author}</a> bez identyfikatora Woblink.',
+            author_id=self.args[0].id,
+            author=self.args[0].name
+        )
+
+class NoThema(WoblinkError):
+    def as_html(self):
+        return format_html('Brak Thema.')
+
+class UnknownThema(WoblinkError):
+    def as_html(self):
+        return format_html(
+            'Nieznana Thema {code}.',
+            code=self.args[0]
+        )
+
+
+class ThemaUnknownWoblink(WoblinkError):
+    def as_html(self):
+        return format_html(
+            'Thema <a href="/admin/catalogue/thema/{id}/">{code}</a> przypisana do nieznanej kategorii Woblink.',
+            id=self.args[0].id,
+            code=self.args[0].code,
+        )
+
+class NoWoblinkCategory(WoblinkError):
+    def as_html(self):
+        return 'Brak kategorii Woblink.'
+
+class WoblinkWarning(Warning):
+    pass
+
+class NoMainThemaWarning(WoblinkWarning):
+    def as_html(self):
+        return format_html(
+            'Brak głównej kategorii Thema.'
+        )
+
+class ThemaNoWoblink(WoblinkWarning):
+    def as_html(self):
+        return format_html(
+            'Thema <a href="/admin/catalogue/thema/{id}/">{code}</a> nie przypisana do kategorii Woblink.',
+            id=self.args[0].id,
+            code=self.args[0].code,
+        )
+
+class AuthorLiteralForeignWarning(WoblinkWarning):
+    def as_html(self):
+        return format_html(
+            'Nie obsługiwane: autor „{author}” w języku {lang}.',
+            author=str(self.args[0]),
+            lang=self.args[0].lang,
+        )
+
+class AuthorNotInCatalogueWarning(WoblinkWarning):
+    def as_html(self):
+        return format_html(
+            'Brak autora „{author}” w katalogu.',
+            author=str(self.args[0])
+        )
+
+class AuthorNoWoblinkWarning(WoblinkWarning):
+    def as_html(self):
+        return format_html(
+            'Autor <a href="/admin/catalogue/author/{author_id}/">{author}</a> bez identyfikatora Woblink.',
+            author_id=self.args[0].id,
+            author=self.args[0].name
+        )
+
+
+
+
+class Woblink(BasePublisher):
+    BASE_URL = 'https://publisher.woblink.com/'
+    ADD_URL = BASE_URL + 'catalog/add'
+    STEP1_URL = BASE_URL + 'catalog/edit/%s'
+    STEP2_URL = BASE_URL + 'catalog/edit/%s/2'
+    STEP3_URL = BASE_URL + 'catalog/edit/%s/3'
+    UPLOAD_URL = BASE_URL + 'file/upload-%s'
+    JOB_STATUS_URL = BASE_URL + 'task/status'
+    GENERATE_DEMO_URL = BASE_URL + 'task/run/generate-%s-demo/%s/%d'
+    CHECK_DEMO_URL = BASE_URL + 'task/run/check-%s-demo/%s'
+
+    ROLE_AUTHOR = 1
+    ROLE_TRANSLATOR = 4
+
+    def login(self):
+        response = self.session.get('https://publisher.woblink.com/login')
+        token = re.search(
+            r'name="_csrf_token" value="([^"]+)"',
+            response.text
+        ).group(1)
+        data = {
+            '_csrf_token': token,
+            '_username': self.username,
+            '_password': self.password,
+        }
+        response = self.session.post(
+            'https://publisher.woblink.com/login_check',
+            data=data,
+        )
+
+    def get_isbn(self, meta, errors=None):
+        if not meta.isbn_epub:
+            if errors is not None:
+                errors.append(NoIsbn())
+        return meta.isbn_epub
+
+    def get_authors_data(self, meta, errors=None):
+        authors = []
+        for role, items, obligatory in [
+                (self.ROLE_AUTHOR, meta.authors, True),
+                (self.ROLE_TRANSLATOR, meta.translators, False)
+        ]:
+            for person_literal in items:
+                if person_literal.lang != 'pl':
+                    if errors is not None:
+                        if obligatory:
+                             errors.append(AuthorLiteralForeign(person_literal))
+                        else:
+                            errors.append(AuthorLiteralForeignWarning(person_literal))
+                    continue
+                aobj = Author.get_by_literal(str(person_literal))
+                if aobj is None:
+                    if errors is not None:
+                        if obligatory:
+                             errors.append(AuthorNotInCatalogue(person_literal))
+                        else:
+                            errors.append(AuthorNotInCatalogueWarning(person_literal))
+                    continue
+                if not aobj.woblink:
+                    if errors is not None:
+                        if obligatory:
+                             errors.append(AuthorNoWoblink(aobj))
+                        else:
+                            errors.append(AuthorNoWoblinkWarning(aobj))
+                    continue
+                authors.append((role, aobj.woblink))
+        return authors
+
+    def get_genres(self, meta, errors=None):
+        thema_codes = []
+        if meta.thema_main:
+            thema_codes.append(meta.thema_main)
+        else:
+            if errors is not None:
+                errors.append(NoMainThemaWarning())
+        thema_codes.extend(meta.thema)
+        if not thema_codes:
+            if errors is not None:
+                errors.append(NoThema())
+        category_ids = []
+        for code in thema_codes:
+            try:
+                thema = Thema.objects.get(code=code)
+            except Thema.DoesNotExist:
+                if errors is not None:
+                    errors.append(UnknownThema(code))
+            else:
+                if thema.woblink_category is None:
+                    if errors is not None:
+                        errors.append(ThemaNoWoblink(thema))
+                elif thema.woblink_category not in WOBLINK_CATEGORIES:
+                    if errors is not None:
+                        errors.append(ThemaUnknownWoblink(thema))
+                elif thema.woblink_category not in category_ids:
+                    category_ids.append(thema.woblink_category)
+        if not category_ids:
+            if errors is not None:
+                errors.append(NoWoblinkCategory())
+        return category_ids
+
+    def get_series(self, meta, errors=None):
+        pass
+
+    def get_abstract(self, wldoc, errors=None, description_add=None):
+        description = self.get_description(wldoc, description_add)
+        parts = description.split('\n', 1)
+        if len(parts) == 1 or len(parts[0]) > 200:
+            p1 = description[:200].rsplit(' ', 1)[0]
+            p2 = description[len(p1):]
+            p1 += '…'
+            p2 = '…' + p2
+            parts = [p1, p2]
+
+        m = re.search(r'<[^>]+$', parts[0])
+        if m is not None:
+            parts[0] = parts[:-len(m.group(0))]
+            parts[1] = m.group(0) + parts[1]
+
+        opened = []
+        for tag in re.findall(r'<[^>]+[^/>]>', parts[0]):
+            if tag[1] == '/':
+                opened.pop()
+            else:
+                opened.append(tag)
+        for tag in reversed(opened):
+            parts[0] += '</' + tag[1:-1].split()[0] + '>'
+            parts[1] = tag + parts[1]
+        return {
+            'header': parts[0],
+            'rest': parts[1],
+        }
+
+    def get_lang2code(self, meta, errors=None):
+        return lang_code_3to2(meta.language)
+
+    def get_price(self, shop, wldoc, errors=None):
+        stats = wldoc.get_statistics()['total']
+        words = stats['words_with_fn']
+        pages = stats['chars_with_fn'] / 1800
+        price = shop.get_price(words, pages)
+        if price is None:
+            if errors:
+                errors.append(NoPrice(shop))
+            return 0
+
+        return price
+
+    def can_publish(self, shop, book):
+        wldoc = book.wldocument(librarian2=True)
+        d = {
+            'warnings': [],
+            'errors': [],
+        }
+        errors = []
+        book_data = self.get_book_data(shop, wldoc, errors)
+        for error in errors:
+            if not isinstance(error, Warning):
+                errlist = d['errors']
+            else:
+                errlist = d['warnings']
+            errlist.append(error.as_html())
+
+        if book_data.get('genres'):
+            d['comment'] = format_html(
+                'W kategoriach: {cat} ({price} zł)',
+                cat=', '.join(self.describe_category(g) for g in book_data['genres']),
+                price=book_data['price']
+            )
+
+        return d
+
+    def describe_category(self, category):
+        t = []
+        while category:
+            c = WOBLINK_CATEGORIES[category]
+            t.append(c['name'])
+            category = c.get('parent')
+        return ' / '.join(reversed(t))
+
+    def create_book(self, isbn):
+        isbn = ''.join(c for c in isbn if c.isdigit())
+        assert len(isbn) == 13
+        response = self.session.post(
+            self.ADD_URL,
+            data={
+                'AddPublication[pubType]': 'ebook',
+                'AddPublication[pubHasIsbn]': '1',
+                'AddPublication[pubIsbn]': isbn,
+                 ##AddPubation[save]
+            }
+        )
+        m = re.search(r'/(\d+)$', response.url)
+        if m is not None:
+            return m.group(1)
+
+    def send_book(self, shop, book, changes=None):
+        wldoc = book.wldocument(librarian2=True, changes=changes, publishable=False) # TODO pub
+        meta = wldoc.meta
+
+        book_data = self.get_book_data(shop, wldoc)
+
+        if not book.woblink_id:
+            #book.woblink_id = 2959868
+            woblink_id = self.create_book(book_data['isbn'])
+            assert woblink_id
+            book.woblink_id = woblink_id
+            book.save(update_fields=['woblink_id'])
+
+        self.edit_step1(book.woblink_id, book_data)
+        self.edit_step2(book.woblink_id, book_data)
+        self.edit_step3(book.woblink_id, book_data)
+        self.send_cover(book.woblink_id, wldoc)
+        texts = shop.get_texts()
+        self.send_epub(
+            book.woblink_id, wldoc, book.gallery_path(),
+            fundraising=texts
+        )
+        self.send_mobi(
+            book.woblink_id, wldoc, book.gallery_path(),
+            fundraising=texts
+        )
+
+    def get_book_data(self, shop, wldoc, errors=None):
+        return {
+            "title": wldoc.meta.title,
+            "isbn": self.get_isbn(wldoc.meta, errors=errors),
+            "authors": self.get_authors_data(wldoc.meta, errors=errors),
+            "abstract": self.get_abstract(
+                wldoc, errors=errors, description_add=shop.description_add
+            ),
+            "lang2code": self.get_lang2code(wldoc.meta, errors=errors),
+            "genres": self.get_genres(wldoc.meta, errors=errors),
+            "price": self.get_price(shop, wldoc, errors=errors),
+        }
+
+    def with_form_name(self, data, name):
+        return {
+            f"{name}[{k}]": v
+            for (k, v) in data.items()
+        }
+
+    def edit_step1(self, woblink_id, book_data):
+        data = book_data
+
+        authors_data = [
+            {
+                "AhpPubId": woblink_id,
+                "AhpAutId": author_id,
+                "AhpType": author_type,
+            }
+            for (author_type, author_id) in data['authors']
+        ]
+
+        d = {
+            'pubTitle': book_data['title'],
+            'npwAuthorHasPublications': json.dumps(authors_data),
+            'pubShortNote': data['abstract']['header'],
+            'pubNote': data['abstract']['rest'],
+            'pubCulture': data['lang2code'],
+            'npwPublicationHasAwards': '[]',
+            'npwPublicationHasSeriess': '[]', # TODO
+                # "[{\"Id\":6153,\"PublicationId\":73876,\"SeriesId\":1615,\"Tome\":null}]"
+        }
+        d = self.with_form_name(d, 'EditPublicationStep1')
+        d['roles'] = [author_type for (author_type, author_id) in data['authors']]
+        r = self.session.post(self.STEP1_URL % woblink_id, data=d)
+        return r
+
+
+    def edit_step2(self, woblink_id, book_data):
+        gd = {}
+        legacy = None
+        for i, g in enumerate(book_data['genres']):
+            gdata = WOBLINK_CATEGORIES[g]
+            if legacy is None:
+                legacy = gdata.get('legacy')
+            if p := gdata.get('parent'):
+                gd.setdefault(p, {'isMain': False})
+                gd[p].setdefault('children', [])
+                gd[p]['children'].append(str(g))
+                gd[p].setdefault('mainChild', str(g))
+                if legacy is None:
+                    legacy = WOBLINK_CATEGORIES[p].get('legacy')
+            else:
+                gd.setdefault(p, {})
+                ds[p]['isMain'] = True
+        gd = [
+            {
+                "pubId": woblink_id,
+                "category": str(k),
+                **v
+            }
+            for k, v in gd.items()
+        ]
+
+        data = {
+            'npwPublicationHasNewGenres': json.dumps(gd),
+            'genre': legacy or '',
+        }
+        data = self.with_form_name(data, 'AddPublicationStep2')
+        return self.session.post(self.STEP2_URL % woblink_id, data=data)
+
+    def edit_step3(self, woblink_id, book_data):
+        d = {
+            'pubBasePrice': book_data['price'],
+            'pubPremiereDate': '2023-08-09', #date.today().isoformat(),
+            'pubIsLicenseIndefinite': '1',
+            'pubFileFormat': 'epub+mobi',
+            'pubIsAcs': '0',
+            'pubPublisherIndex': '',
+        }
+        d = self.with_form_name(d, 'EditPublicationStep3')
+        return self.session.post(self.STEP3_URL % woblink_id, data=d)
+
+    def wait_for_job(self, job_id):
+        while True:
+            response = self.session.post(
+                self.JOB_STATUS_URL,
+                data={'ids[]': job_id}
+            )
+            data = response.json()[job_id]
+            if data['ready']:
+                assert data['successful']
+                return
+            sleep(2)
+
+    def upload_file(self, woblink_id, filename, content, form_name, field_name, mime_type):
+        data = {
+            'pubId': woblink_id,
+        }
+        files = {
+            field_name: (filename, content, mime_type)
+        }
+        response = self.session.post(
+            self.UPLOAD_URL % field_name,
+            data=self.with_form_name(data, form_name),
+            files=self.with_form_name(files, form_name),
+        )
+        resp_data = response.json()
+        assert resp_data['success'] is True
+        if 'jobId' in resp_data:
+            self.wait_for_job(resp_data['jobId'])
+
+    def generate_demo(self, woblink_id, file_format, check=True):
+        percent = 10
+        while True:
+            job_id = self.session.get(
+                self.GENERATE_DEMO_URL % (file_format, woblink_id, percent),
+            ).json()['jobId']
+            try:
+                self.wait_for_job(job_id)
+            except AssertionError:
+                if percent < 50:
+                    percent += 10
+                else:
+                    raise
+            else:
+                break
+
+        if check:
+            self.wait_for_job(
+                self.session.get(
+                    self.CHECK_DEMO_URL % (file_format, woblink_id)
+                ).json()['jobId']
+            )
+
+    def send_epub(self, woblink_id, doc, gallery_path, fundraising=None):
+        from librarian.builders import EpubBuilder
+        content = EpubBuilder(
+            base_url='file://' + gallery_path + '/',
+            fundraising=fundraising or [],
+        ).build(doc).get_file()
+        self.upload_file(
+            woblink_id,
+            doc.meta.url.slug + '.epub',
+            content,
+            'UploadEpub',
+            'epub',
+            'application/epub+zip'
+        )
+        self.generate_demo(woblink_id, 'epub')
+
+    def send_mobi(self, woblink_id, doc, gallery_path, fundraising=None):
+        from librarian.builders import MobiBuilder
+        content = MobiBuilder(
+            base_url='file://' + gallery_path + '/',
+            fundraising=fundraising or [],
+        ).build(doc).get_file()
+        self.upload_file(
+            woblink_id,
+            doc.meta.url.slug + '.mobi',
+            content,
+            'UploadMobi',
+            'mobi',
+            'application/x-mobipocket-ebook'
+        )
+        self.generate_demo(woblink_id, 'mobi', check=False)
+
+    def send_cover(self, woblink_id, doc):
+        from librarian.cover import make_cover
+        # TODO Labe
+        # A5 @ 300ppi.
+        cover = make_cover(doc.meta, cover_class='m-label', width=1748, height=2480)
+        content = io.BytesIO()
+        cover.final_image().save(content, cover.format)
+        content.seek(0)
+        self.upload_file(
+            woblink_id,
+            doc.meta.url.slug + '.jpeg',
+            content,
+            'UploadCover',
+            'cover',
+            cover.mime_type()
+        )
diff --git a/src/depot/publishers/woblink_constants.py b/src/depot/publishers/woblink_constants.py
new file mode 100644 (file)
index 0000000..0fad595
--- /dev/null
@@ -0,0 +1,232 @@
+# This comes from Woblink publisher panel.
+
+WOBLINK_CATEGORIES = {
+    2: {"name": "Dla dzieci", "legacy": 459},
+    3: {"parent": 2, "name": "Baśnie dla dzieci", "legacy": 459},
+    4: {"parent": 2, "name": "Edukacyjne, popularnonaukowe i encyklopedyczne", "legacy": 459},
+    5: {"parent": 2, "name": "Lektury szkolne", "legacy": 459},
+    6: {"parent": 2, "name": "Literatura religijna", "legacy": 459},
+    7: {"parent": 2, "name": "Opowiadania", "legacy": 459},
+    8: {"parent": 2, "name": "Powieść", "legacy": 459},
+    9: {"parent": 2, "name": "Wiersze, rymowanki i piosenki", "legacy": 459},
+    10: {"parent": 2, "name": "Zagadki, rozrywka", "legacy": 459},
+    11: {"parent": 2, "name": "Inne", "legacy": 459},
+    465: {"parent": 2, "name": "Lektury szkolne podstawówka"},
+    469: {"parent": 2, "name": "Klasyka literatury dla dzieci"},
+    470: {"parent": 2, "name": "Bajki i opowiadania dla dzieci"},
+    474: {"parent": 2, "name": "Książki dla 6,7 i 8-latka"},
+    475: {"parent": 2, "name": "Książki dla 9,10,11 i 12-latka"},
+    478: {"parent": 2, "name": "Detektywistyczne książki dla dzieci"},
+    480: {"parent": 2, "name": "Książki edukacyjne dla dzieci"},
+    482: {"parent": 2, "name": "Książki dla 3,4 i 5-latka"},
+    501: {"parent": 2, "name": "Książki przygodowe dla dzieci"},
+    503: {"parent": 2, "name": "Książki o zwierzętach dla dzieci"},
+    508: {"parent": 2, "name": "Książki fantastyczne dla dzieci"},
+    512: {"parent": 2, "name": "Obcojęzyczne książki dla dzieci"},
+    12: {"name": "Dla młodzieży", "legacy": 459},
+    212: {"parent": 12, "name": "Poradniki  Edukacja, Hobby", "legacy": 473},
+    213: {"parent": 12, "name": "Powieści i lektury", "legacy": 453},
+    24: {"parent": 12, "name": "Pozostałe", "legacy": 459},
+    25: {"name": "Podręczniki  i Encyklopedie", "legacy": 474},
+    27: {"parent": 25, "name": "Słowniki i informatory tematyczne", "legacy": 474},
+    198: {"parent": 25, "name": "Podręczniki i materiały edukacyjne", "legacy": 474},
+    208: {"parent": 25, "name": "Inne", "legacy": 474},
+    26: {"parent": 25, "name": "Encyklopedie", "legacy": 474},
+    200: {"parent": 25, "name": "Podręczniki akademickie", "legacy": 474},
+    199: {"parent": 25, "name": "Podręczniki szkolne", "legacy": 474},
+    219: {"name": "Nauka języków obcych", "legacy": 474},
+    201: {"parent": 219, "name": "Książki do nauki angielskiego", "legacy": 474},
+    202: {"parent": 219, "name": "Książki do nauki niemieckiego", "legacy": 474},
+    203: {"parent": 219, "name": "Książki do nauki francuskiego", "legacy": 474},
+    204: {"parent": 219, "name": "Książki do nauki hiszpańskiego", "legacy": 474},
+    205: {"parent": 219, "name": "Książki do nauki włoskiego", "legacy": 474},
+    206: {"parent": 219, "name": "Książki do nauki rosyjskiego", "legacy": 474},
+    207: {"parent": 219, "name": "Pozostałe książki do nauki języków", "legacy": 474},
+    28: {"parent": 219, "name": "Słowniki językowe", "legacy": 474},
+    29: {"parent": 219, "name": "Pozostałe", "legacy": 474},
+    498: {"parent": 219, "name": "Książki do nauki ukraińskiego"},
+    499: {"parent": 219, "name": "Język polski dla obcokrajowców"},
+    30: {"name": "Książki o filozofii", "legacy": 461},
+    31: {"parent": 30, "name": "Epistemologia", "legacy": 461},
+    32: {"parent": 30, "name": "Etyka", "legacy": 461},
+    33: {"parent": 30, "name": "Historia filozofii", "legacy": 461},
+    34: {"parent": 30, "name": "Logika", "legacy": 461},
+    35: {"parent": 30, "name": "Ontologia", "legacy": 461},
+    36: {"parent": 30, "name": "Szkoły i kierunki w filozofii", "legacy": 461},
+    37: {"parent": 30, "name": "Inne", "legacy": 461},
+    473: {"parent": 30, "name": "Klasycy filozofii"},
+    38: {"name": "Książki historyczne", "legacy": 464},
+    214: {"parent": 38, "name": "Książki o średniowieczu", "legacy": 464},
+    215: {"parent": 38, "name": "Książki o nowożytności", "legacy": 464},
+    216: {"parent": 38, "name": "Książki o historii najnowszej", "legacy": 464},
+    43: {"parent": 38, "name": "II wojna światowa", "legacy": 464},
+    45: {"parent": 38, "name": "Pozostałe", "legacy": 464},
+    489: {"parent": 38, "name": "Książki o historii Polski"},
+    490: {"parent": 38, "name": "Historiografia"},
+    491: {"parent": 38, "name": "Książki o historii Europy i świata"},
+    492: {"parent": 38, "name": "Książki o militariach i historii wojskowości"},
+    493: {"parent": 38, "name": "Książki o archeologii"},
+    40: {"parent": 38, "name": "Książki o starożytności", "legacy": 464},
+    46: {"name": "Komiks i książka graficzna", "legacy": 452},
+    47: {"name": "Internet, komputery, informatyka", "legacy": 485},
+    48: {"parent": 47, "name": "Informatyka - zagadnienia ogólne", "legacy": 485},
+    49: {"parent": 47, "name": "Bazy danych, analiza danych", "legacy": 485},
+    51: {"parent": 47, "name": "Internet, media elektroniczne", "legacy": 485},
+    50: {"parent": 47, "name": "Cyfrowy styl życia", "legacy": 485},
+    52: {"parent": 47, "name": "Języki programowania i programowanie", "legacy": 485},
+    53: {"parent": 47, "name": "Komputery - hardware", "legacy": 485},
+    54: {"parent": 47, "name": "Komputery - oprogramowanie", "legacy": 485},
+    55: {"parent": 47, "name": "Systemy operacyjne", "legacy": 485},
+    56: {"parent": 47, "name": "Inne", "legacy": 485},
+    57: {"name": "Książki obcojęzyczne", "legacy": 482},
+    58: {"parent": 57, "name": "Język angielski", "legacy": 482},
+    59: {"parent": 57, "name": "Inne", "legacy": 482},
+    60: {"name": "Kultura, Sztuka, Design", "legacy": 477},
+    70: {"parent": 60, "name": "Design / Architektura", "legacy": 477},
+    62: {"parent": 60, "name": "Architektura", "legacy": 477},
+    67: {"parent": 60, "name": "Kino / Teatr / Telewizja", "legacy": 477},
+    64: {"parent": 60, "name": "Fotografia / Malarstwo / Rzeźba", "legacy": 477},
+    66: {"parent": 60, "name": "Rzeźba", "legacy": 477},
+    63: {"parent": 60, "name": "Fotografia", "legacy": 477},
+    61: {"parent": 60, "name": "Historia i teoria sztuki", "legacy": 477},
+    65: {"parent": 60, "name": "Muzyka / Taniec", "legacy": 477},
+    71: {"parent": 60, "name": "Taniec", "legacy": 477},
+    72: {"parent": 60, "name": "Pozostałe", "legacy": 477},
+    68: {"parent": 60, "name": "Konserwacja sztuki", "legacy": 477},
+    69: {"parent": 60, "name": "Wystawy i muzea", "legacy": 477},
+    73: {"name": "Literatura", "legacy": 478},
+    83: {"parent": 73, "name": "Romans", "legacy": 484},
+    74: {"parent": 73, "name": "Fantastyka i Horror", "legacy": 456},
+    77: {"parent": 73, "name": "Literatura piękna", "legacy": 478},
+    78: {"parent": 73, "name": "Poezja i Dramat", "legacy": 476},
+    79: {"parent": 73, "name": "Powieść historyczna", "legacy": 478},
+    80: {"parent": 73, "name": "Powieść obyczajowa", "legacy": 458},
+    82: {"parent": 73, "name": "Kryminał i Sensacja", "legacy": 457},
+    84: {"parent": 73, "name": "Pozostałe", "legacy": 485},
+    464: {"parent": 73, "name": "Erotyka", "legacy": 484},
+    85: {"name": "Literatura faktu", "legacy": 460},
+    89: {"parent": 85, "name": "Literatura popularnonaukowa", "legacy": 472},
+    186: {"parent": 85, "name": "Książki podróżnicze i Przewodniki", "legacy": 463},
+    92: {"parent": 85, "name": "Pozostałe", "legacy": 460},
+    93: {"name": "Marketing, Zarządzanie, Finanse", "legacy": 471},
+    94: {"parent": 93, "name": "Finanse i rachunkowość", "legacy": 485},
+    95: {"parent": 93, "name": "Biznes / Przedsiębiorczość / Rozwój osobisty", "legacy": 485},
+    96: {"parent": 93, "name": "E-biznes", "legacy": 485},
+    101: {"parent": 93, "name": "Zarządzanie i administracja", "legacy": 485},
+    99: {"parent": 93, "name": "Psychologia biznesu", "legacy": 485},
+    100: {"parent": 93, "name": "Szkolenia i kariera", "legacy": 485},
+    97: {"parent": 93, "name": "Ekonomia", "legacy": 485},
+    98: {"parent": 93, "name": "Marketing i sprzedaż", "legacy": 485},
+    102: {"parent": 93, "name": "Pozostałe", "legacy": 485},
+    110: {"name": "Psychologia, Społeczeństwo, Polityka", "legacy": 485},
+    111: {"parent": 110, "name": "Edukacja i pedagogika", "legacy": 485},
+    112: {"parent": 110, "name": "Polityka i administracja państwowa", "legacy": 485},
+    113: {"parent": 110, "name": "Psychologia", "legacy": 485},
+    114: {"parent": 110, "name": "Służby publiczne, socjalne, kryminologia, wojsko", "legacy": 485},
+    115: {"parent": 110, "name": "Socjologia i antropologia", "legacy": 485},
+    116: {"parent": 110, "name": "Gender\xa0studies", "legacy": 479},
+    117: {"parent": 110, "name": "Inne", "legacy": 485},
+    118: {"name": "Nauki ścisłe i Medycyna", "legacy": 485},
+    103: {"parent": 118, "name": "Medycyna", "legacy": 480},
+    104: {"parent": 118, "name": "Medycyna ogólnie", "legacy": 480},
+    105: {"parent": 118, "name": "Zagadnienia specjalistyczne", "legacy": 480},
+    106: {"parent": 118, "name": "Pielęgniarstwo", "legacy": 480},
+    107: {"parent": 118, "name": "Weterynaria", "legacy": 480},
+    108: {"parent": 118, "name": "Inne", "legacy": 480},
+    109: {"parent": 118, "name": "Nauki społeczne i społeczeństwo", "legacy": 485},
+    122: {"parent": 118, "name": "Biologia / Geografia / Ekologia", "legacy": 485},
+    125: {"parent": 118, "name": "Geografia i nauki o ziemi", "legacy": 485},
+    123: {"parent": 118, "name": "Ekologia i środowisko", "legacy": 485},
+    121: {"parent": 118, "name": "Biologia i nauki pokrewne", "legacy": 485},
+    124: {"parent": 118, "name": "Astronomia / Fizyka / Matematyka", "legacy": 485},
+    120: {"parent": 118, "name": "Astronomia", "legacy": 485},
+    126: {"parent": 118, "name": "Matematyka", "legacy": 485},
+    128: {"parent": 118, "name": "Pozostałe", "legacy": 485},
+    119: {"parent": 118, "name": "Nauka  - zagadnienia ogólne", "legacy": 485},
+    127: {"parent": 118, "name": "Planowanie przestrzenne i regionalne, urbanistyka", "legacy": 485},
+    217: {"name": "Poradniki", "legacy": 473},
+    129: {"name": "Prawo", "legacy": 470},
+    130: {"parent": 129, "name": "Akty prawne, komentarze i\xa0wzory dokumentów", "legacy": 470},
+    131: {"parent": 129, "name": "Prawo  - teoria i zagadnienia ogólne", "legacy": 470},
+    132: {"parent": 129, "name": "Prawo administracyjne", "legacy": 470},
+    133: {"parent": 129, "name": "Prawo cywilne", "legacy": 470},
+    134: {"parent": 129, "name": "Prawo finansowe", "legacy": 470},
+    135: {"parent": 129, "name": "Prawo handlowe i gospodarcze", "legacy": 470},
+    136: {"parent": 129, "name": "Prawo karne", "legacy": 470},
+    137: {"parent": 129, "name": "Prawo międzynarodowe i europejskie", "legacy": 470},
+    138: {"parent": 129, "name": "Prawo pracy i kodeks pracy", "legacy": 470},
+    139: {"parent": 129, "name": "Prawo własności intelektualnej", "legacy": 470},
+    140: {"parent": 129, "name": "Szczegółowe zagadnienia prawa - pozostałe", "legacy": 470},
+    141: {"parent": 129, "name": "Inne", "legacy": 470},
+    497: {"parent": 129, "name": "Prawo konstytucyjne"},
+    142: {"name": "Religia", "legacy": 467},
+    143: {"parent": 142, "name": "Literatura religijna", "legacy": 467},
+    144: {"parent": 142, "name": "Religioznawstwo", "legacy": 467},
+    145: {"parent": 142, "name": "Inne", "legacy": 467},
+    146: {"name": "Sport i Rekreacja", "legacy": 481},
+    147: {"parent": 146, "name": "Bieganie i lekkoatletyka", "legacy": 481},
+    148: {"parent": 146, "name": "Fitness i joga", "legacy": 481},
+    149: {"parent": 146, "name": "Jeździectwo", "legacy": 481},
+    150: {"parent": 146, "name": "Kolarstwo", "legacy": 481},
+    151: {"parent": 146, "name": "Koszykówka", "legacy": 481},
+    152: {"parent": 146, "name": "Narciarstwo, sporty zimowe", "legacy": 481},
+    153: {"parent": 146, "name": "Piłka nożna", "legacy": 481},
+    154: {"parent": 146, "name": "Siatkówka", "legacy": 481},
+    155: {"parent": 146, "name": "Aktywny wypoczynek", "legacy": 481},
+    156: {"parent": 146, "name": "Sporty powietrzne", "legacy": 481},
+    157: {"parent": 146, "name": "Sporty walki", "legacy": 481},
+    158: {"parent": 146, "name": "Sporty wodne", "legacy": 481},
+    159: {"parent": 146, "name": "Tenis", "legacy": 481},
+    160: {"parent": 146, "name": "Wspinaczka, turystyka górska", "legacy": 481},
+    161: {"parent": 146, "name": "Imprezy sportowe i współzawodnictwo", "legacy": 481},
+    162: {"parent": 146, "name": "Inne", "legacy": 481},
+    163: {"name": "Technika, Inżynieria, Rolnictwo", "legacy": 485},
+    164: {"parent": 163, "name": "Chemia przemysłowa i technologie przemysłowe", "legacy": 485},
+    165: {"parent": 163, "name": "Elektronika, automatyka", "legacy": 485},
+    166: {"parent": 163, "name": "Energetyka", "legacy": 485},
+    167: {"parent": 163, "name": "Inżynieria biochemiczna", "legacy": 485},
+    168: {"parent": 163, "name": "Inżynieria cywilna, wodna, bezpieczeństwa i transportu", "legacy": 485},
+    169: {"parent": 163, "name": "Inżynieria mechaniczna i materiałowa", "legacy": 485},
+    170: {"parent": 163, "name": "Inżynieria środowiska", "legacy": 485},
+    171: {"parent": 163, "name": "Rolnictwo", "legacy": 485},
+    172: {"parent": 163, "name": "Technologia - zagadnienia", "legacy": 485},
+    173: {"parent": 163, "name": "Telekomunikacja", "legacy": 485},
+    174: {"parent": 163, "name": "Transport i mechanika", "legacy": 485},
+    175: {"parent": 163, "name": "Inne technologie", "legacy": 485},
+    176: {"parent": 163, "name": "Inne", "legacy": 485},
+    177: {"name": "Dom, Moda, Hobby", "legacy": 485},
+    179: {"parent": 177, "name": "Dom, Wnętrze, Ogród", "legacy": 485},
+    188: {"parent": 177, "name": "Moda i styl", "legacy": 485},
+    220: {"parent": 177, "name": "Kolorowanki", "legacy": 485},
+    191: {"name": "Kuchnia , Diety i Fitness", "legacy": 480},
+    189: {"parent": 191, "name": "Zainteresowania przyrodnicze", "legacy": 485},
+    183: {"parent": 191, "name": "Kuchnia", "legacy": 469},
+    193: {"parent": 191, "name": "Diety / Fitness", "legacy": 480},
+    218: {"name": "Zdrowie, Rodzina, Związki", "legacy": 473},
+    192: {"parent": 218, "name": "Dziecko, rodzina i zdrowie", "legacy": 480},
+    194: {"parent": 218, "name": "Seks - poradniki", "legacy": 480},
+    195: {"parent": 218, "name": "Umysł, ciało, duch", "legacy": 480},
+    196: {"parent": 218, "name": "Zdrowie i rozwój osobisty - poradniki", "legacy": 480},
+    197: {"parent": 218, "name": "Inne", "legacy": 480},
+    209: {"name": "Prasa", "legacy": 483},
+    210: {"name": "Pozostałe", "legacy": 485},
+    462: {"name": "Minibooki", "legacy": 486},
+    91: {"name": "Reportaż"},
+    466: {"parent": 91, "name": "Polska szkoła reportażu"},
+    467: {"parent": 91, "name": "Książki true crime"},
+    468: {"parent": 91, "name": "Reportaż podróżniczy"},
+    476: {"parent": 91, "name": "Reportaż wojenny"},
+    477: {"parent": 91, "name": "Reportaż o Polsce"},
+    86: {"name": "Biografie i wspomnienia", "legacy": 454},
+    87: {"parent": 86, "name": "Eseistyka", "legacy": 460},
+    88: {"parent": 86, "name": "Listy", "legacy": 460},
+    90: {"parent": 86, "name": "Pamiętniki i\xa0dzienniki", "legacy": 460},
+    483: {"parent": 86, "name": "Biografie sławnych ludzi"},
+    484: {"parent": 86, "name": "Biografie muzyków"},
+    485: {"parent": 86, "name": "Biografie pisarzy"},
+    486: {"parent": 86, "name": "Autobiografie"},
+    488: {"parent": 86, "name": "Biografie sportowców"},
+    494: {"parent": 86, "name": "Biografie postaci historycznych"},
+    496: {"parent": 86, "name": "Biografie świętych (Hagiografia)"},
+    500: {"parent": 86, "name": "Biografie polityków"},
+}
diff --git a/src/depot/templatetags/depot.py b/src/depot/templatetags/depot.py
new file mode 100644 (file)
index 0000000..d1194f0
--- /dev/null
@@ -0,0 +1,20 @@
+from django.template import Library
+from depot.models import Shop
+
+
+register = Library()
+
+
+@register.simple_tag(takes_context=True)
+def depot_shops(context, book):
+    shops = []
+    for shop in Shop.objects.all():
+        d = {
+            'shop_id': shop.id,
+            'name': shop.name,
+        }
+        d.update(shop.can_publish(book))
+        d['last'] = shop.get_last(book)
+        d['id'] = getattr(book, shop.shop + '_id')
+        shops.append(d)
+    return shops
index 9804515..4901ca9 100644 (file)
@@ -3,9 +3,8 @@ from . import views
 
 
 urlpatterns = [
-    path(
-        'legimi-publish/<int:book_id>/',
-        views.LegimiPublishView.as_view(),
-        name='depot_legimi_publish'
+    path('shop-publish/<int:shop_id>/<int:book_id>/',
+         views.ShopPublishView.as_view(),
+         name='depot_shop_publish'
     )
 ]
index 5e468b6..7976086 100644 (file)
@@ -5,13 +5,14 @@ from documents.models import Book
 from . import models
 
 
-class LegimiPublishView(PermissionRequiredMixin, View):
-    permission_required = 'depot.add_legimibookpublish'
+class ShopPublishView(PermissionRequiredMixin, View):
+    permission_required = 'depot.add_shopbookpublish'
 
-    def post(self, request, book_id):
+    def post(self, request, shop_id, book_id):
+        shop = get_object_or_404(models.Shop, pk=shop_id)
         book = get_object_or_404(Book, pk=book_id)
         try:
-            publish = models.LegimiBookPublish.create_for(book, request.user)
+            publish = models.ShopBookPublish.create_for(book, request.user, shop)
         except AssertionError:
             pass
         return redirect(book.get_absolute_url())
diff --git a/src/depot/woblink.py b/src/depot/woblink.py
deleted file mode 100644 (file)
index ffd6b4d..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-import re
-from django.conf import settings
-import requests
-
-
-def get_woblink_session(*args, **kwargs):
-    session = requests.Session()
-    response = session.get('https://publisher.woblink.com/login')
-    token = re.search(
-        r'name="_csrf_token" value="([^"]+)"',
-        response.text
-    ).group(1)
-    data = {
-        '_csrf_token': token,
-    }
-    data.update(settings.WOBLINK_CREDENTIALS)
-    response = session.post(
-        'https://publisher.woblink.com/login_check',
-        data=data,
-    )
-    return session
-
-
-
index dccf0f0..4c97033 100644 (file)
@@ -20,7 +20,9 @@ class DocumentCreateForm(forms.ModelForm):
 
     class Meta:
         model = Book
-        exclude = ['parent', 'parent_number', 'project']
+        fields = [
+            'title', 'slug', 'public', 'gallery'
+        ]
 
     def __init__(self, *args, **kwargs):
         super(DocumentCreateForm, self).__init__(*args, **kwargs)
@@ -151,7 +153,7 @@ class BookForm(forms.ModelForm):
 
     class Meta:
         model = Book
-        exclude = ['project', 'cover', 'legimi_id']
+        exclude = ['project', 'cover', 'legimi_id', 'woblink_id']
 
     def __init__(self, *args, **kwargs):
         ret = super(BookForm, self).__init__(*args, **kwargs)
diff --git a/src/documents/migrations/0011_book_woblink_id.py b/src/documents/migrations/0011_book_woblink_id.py
new file mode 100644 (file)
index 0000000..a142d84
--- /dev/null
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.9 on 2023-08-04 14:25
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("documents", "0010_alter_book_cover"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="book",
+            name="woblink_id",
+            field=models.CharField(blank=True, max_length=255),
+        ),
+    ]
index f40af29..c11862b 100644 (file)
@@ -57,6 +57,7 @@ class Book(models.Model):
         related_query_name='document_book',
     )
     legimi_id = models.CharField(max_length=255, blank=True)
+    woblink_id = models.CharField(max_length=255, blank=True)
 
     class NoTextError(BaseException):
         pass
@@ -282,9 +283,6 @@ class Book(models.Model):
         except IndexError:
             return None
 
-    def last_legimi_publish(self):
-        return self.legimibookpublish_set.order_by('-created_at').first()
-
     def assert_publishable(self):
         assert self.chunk_set.exists(), _('No chunks in the book.')
         try:
index caa3de5..a2dea02 100644 (file)
@@ -1,6 +1,7 @@
 {% extends "documents/base.html" %}
 {% load book_list i18n %}
 {% load bootstrap4 %}
+{% load depot %}
 
 
 {% block titleextra %}{{ book.title }}{% endblock %}
        <div class="col-md-8">
 
 
-{% if editable %}<form method='POST'>{% csrf_token %}{% endif %}
-    {% bootstrap_form form %}
-        {% if editable %}
-        {% buttons %}
-        <button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
+          {% if editable %}<form method='POST'>{% csrf_token %}{% endif %}
+            {% bootstrap_form form %}
+            {% if editable %}
+              {% buttons %}
+              <button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
         {% endbuttons %}
-    {% endif %}
-{% if editable %}</form>{% endif %}
+            {% endif %}
+            {% if editable %}</form>{% endif %}
 
-{% if editable %}
-    {% if book.gallery %}
-    <p><a href="{% url 'documents_book_gallery' book.slug %}">{% trans "Edit gallery" %}</a></p>
-    {% endif %}
+            {% if editable %}
+              {% if book.gallery %}
+                <p><a href="{% url 'documents_book_gallery' book.slug %}">{% trans "Edit gallery" %}</a></p>
+              {% endif %}
 
-    <p style="text-align:right"><a class="btn btn-sm btn-danger" href="{% url 'documents_book_append' book.slug %}">{% trans "Append to other book" %}</a></p>
-{% endif %}
+              <p style="text-align:right"><a class="btn btn-sm btn-danger" href="{% url 'documents_book_append' book.slug %}">{% trans "Append to other book" %}</a></p>
+            {% endif %}
 
 
        </div>
     </div>
     <div class="card-body">
 
-    <table class='single-book-list table'><tbody>
-    {% for chunk in book %}
-        {% include 'documents/book_list/chunk.html' %}
-    {% endfor %}
-    </tbody></table>
+      <table class='single-book-list table'><tbody>
+        {% for chunk in book %}
+          {% include 'documents/book_list/chunk.html' %}
+        {% endfor %}
+      </tbody></table>
     </div>
   </div>
 
 
 
 
-<div class='card mt-4'>
+  <div class='card mt-4'>
 
-<div class="card-header">
-  <h2>{% trans "Publication" %}</h2>
-  </div>
-<div class="card-body">
-  <div class="row">
-    <div class="col-lg-3">
-      <img class="cover-preview" src="{% url 'cover_preview' book.slug %}" />
-      {% if book.dc_cover_image %}
-        <a href="{{ book.dc_cover_image.get_absolute_url }}">{{ book.dc_cover_image }}</a>
-      {% endif %}
-      <br><br>
-
-      <form action="{% url 'cover_quick_import' book.pk %}" method="post">
-        {% csrf_token %}
-        <div class="input-group">
-          <input type="url" name="url" class="form-control" placeholder="URL okładki">
-          <button type="submit" class="btn btn-sm btn-info">Ustaw</button>
-        </div>
-      </form>
-
-
-      <br><br>
-      <form action="{% url 'cover_preview' book.slug %}">
-        <input type="hidden" name="download" value="1">
-        Pobierz okładkę:
-        <div class="input-group">
-          <select class="form-control" name='cover_class'>
-           <option value="default">nowa</option>
-           <option value="m-label">nowa + label</option>
-            <option value="legacy">dawna</option>
-          </select><br>
-          <input class="form-control" name="width" type="number" required value="600" size="3" placeholder="szer.">
-          <input class="form-control" name="height" type="number" size="3" placeholder="wys.">
-          <button type="submit" class="btn btn-sm btn-primary">🡇</button>
-        </div>
-      </form>
+    <div class="card-header">
+      <h2>{% trans "Publication" %}</h2>
     </div>
-    <div class="col-lg-9">
-<p>{% trans "Last published" %}:
-    {% if book.last_published %}
-        {{ book.last_published }}
-    {% else %}
-        &mdash;
-    {% endif %}
-</p>
-
-{% if publishable %}
-    <p>
-    <a href="{% url 'documents_book_xml' book.slug %}" rel="nofollow">{% trans "Full XML" %}</a><br/>
-    <a target="_blank" href="{% url 'documents_book_html' book.slug %}" rel="nofollow">{% trans "HTML version" %}</a><br/>
-    <a href="{% url 'documents_book_txt' book.slug %}" rel="nofollow">{% trans "TXT version" %}</a><br/>
-    <a href="{% url 'documents_book_pdf' book.slug %}" rel="nofollow">{% trans "PDF version" %}</a><br/>
-    <a href="{% url 'documents_book_pdf_mobile' book.slug %}" rel="nofollow">{% trans "PDF version for mobiles" %}</a><br/>
-    <a href="{% url 'documents_book_epub' book.slug %}" rel="nofollow">{% trans "EPUB version" %}</a><br/>
-    <a href="{% url 'documents_book_mobi' book.slug %}" rel="nofollow">{% trans "MOBI version" %}</a><br/>
-    </p>
-
-    {% if user.is_authenticated %}
-        <!--
-        Angel photos:
-        Angels in Ely Cathedral (http://www.flickr.com/photos/21804434@N02/4483220595/) /
-        mira66 (http://www.flickr.com/photos/21804434@N02/) /
-        CC BY 2.0 (http://creativecommons.org/licenses/by/2.0/)
-        -->
-        <form method="POST" action="{% url 'documents_publish' book.slug %}">{% csrf_token %}
-            {{ publish_options_form.as_p }}
-            <img src="{{ STATIC_URL }}img/angel-left.png" style="vertical-align: middle" />
-            <button id="publish-button" type="submit">
-              <span>{% trans "Publish" %}</span></button>
-            <img src="{{ STATIC_URL }}img/angel-right.png" style="vertical-align: middle" />
-        </form>
-
-        {% if perms.depot.add_legimibookpublish %}
-          <hr>
-          {% with thema_main=doc.book_info.thema_main thema=doc.book_info.thema %}
-            {% if thema_main or thema %}
-              <form method="post" action="{% url 'depot_legimi_publish' book.pk %}">
-                {% csrf_token %}
-                <button class="btn btn-primary" type="submit">
-                  Opublikuj na Legimi<br><small>w kategorii:
-                  {% if thema_main %}
-                    <tt>{{ thema_main }}</tt>
+    <div class="card-body">
+      <div class="row">
+        <div class="col-lg-3">
+          <img class="cover-preview" src="{% url 'cover_preview' book.slug %}" />
+          {% if book.dc_cover_image %}
+            <a href="{{ book.dc_cover_image.get_absolute_url }}">{{ book.dc_cover_image }}</a>
+          {% endif %}
+          <br><br>
+
+          <form action="{% url 'cover_quick_import' book.pk %}" method="post">
+            {% csrf_token %}
+            <div class="input-group">
+              <input type="url" name="url" class="form-control" placeholder="URL okładki">
+              <button type="submit" class="btn btn-sm btn-info">Ustaw</button>
+            </div>
+          </form>
+
+
+          <br><br>
+          <form action="{% url 'cover_preview' book.slug %}">
+            <input type="hidden" name="download" value="1">
+            Pobierz okładkę:
+            <div class="input-group">
+              <select class="form-control" name='cover_class'>
+               <option value="default">nowa</option>
+               <option value="m-label">nowa + label</option>
+                <option value="legacy">dawna</option>
+              </select><br>
+              <input class="form-control" name="width" type="number" required value="600" size="3" placeholder="szer.">
+              <input class="form-control" name="height" type="number" size="3" placeholder="wys.">
+              <button type="submit" class="btn btn-sm btn-primary">🡇</button>
+            </div>
+          </form>
+        </div>
+        <div class="col-lg-9">
+          <p>{% trans "Last published" %}:
+            {% if book.last_published %}
+              {{ book.last_published }}
+            {% else %}
+              &mdash;
+            {% endif %}
+          </p>
+
+          {% if publishable %}
+            <p>
+              <a href="{% url 'documents_book_xml' book.slug %}" rel="nofollow">{% trans "Full XML" %}</a><br/>
+              <a target="_blank" href="{% url 'documents_book_html' book.slug %}" rel="nofollow">{% trans "HTML version" %}</a><br/>
+              <a href="{% url 'documents_book_txt' book.slug %}" rel="nofollow">{% trans "TXT version" %}</a><br/>
+              <a href="{% url 'documents_book_pdf' book.slug %}" rel="nofollow">{% trans "PDF version" %}</a><br/>
+              <a href="{% url 'documents_book_pdf_mobile' book.slug %}" rel="nofollow">{% trans "PDF version for mobiles" %}</a><br/>
+              <a href="{% url 'documents_book_epub' book.slug %}" rel="nofollow">{% trans "EPUB version" %}</a><br/>
+              <a href="{% url 'documents_book_mobi' book.slug %}" rel="nofollow">{% trans "MOBI version" %}</a><br/>
+            </p>
+
+            {% if user.is_authenticated %}
+              <!--
+                   Angel photos:
+                   Angels in Ely Cathedral (http://www.flickr.com/photos/21804434@N02/4483220595/) /
+                   mira66 (http://www.flickr.com/photos/21804434@N02/) /
+                   CC BY 2.0 (http://creativecommons.org/licenses/by/2.0/)
+              -->
+              <form method="POST" action="{% url 'documents_publish' book.slug %}">{% csrf_token %}
+                {{ publish_options_form.as_p }}
+                <img src="{{ STATIC_URL }}img/angel-left.png" style="vertical-align: middle" />
+                <button id="publish-button" type="submit">
+                  <span>{% trans "Publish" %}</span></button>
+                <img src="{{ STATIC_URL }}img/angel-right.png" style="vertical-align: middle" />
+              </form>
+
+
+              {% if perms.depot.add_shopbookpublish %}
+                {% depot_shops book as shops %}
+                {% for shop in shops %}
+                  <hr>
+                  <h3 class="mb-3">{{ shop.name }}</h3>
+                  {% if not shop.errors %}
+                    <form method="post" action="{% url 'depot_shop_publish' shop.shop_id book.pk %}">
+                      {% csrf_token %}
+                      <button class="btn btn-primary mb-3" type="submit">
+                        Opublikuj na {{ shop.name }}
+                      </button>
+                    </form>
+                    {% if shop.comment %}
+                      <div class="alert alert-info">
+                        {{ shop.comment }}
+                      </div>
+                    {% endif %}
+                  {% else %}
+                    {% for error in shop.errors %}
+                      <div class="alert alert-danger">
+                        {{ error }}
+                      </div>
+                    {% endfor %}
                   {% endif %}
-                  {% for t in thema %}
-                    {% if forloop.first and thema_main %}oraz: {% endif %}
-                    <tt>{{ t }}</tt>
-                    {% if not forloop.last %}, {% endif %}
+                  {% for warning in shop.warnings %}
+                    <div class="alert alert-warning">
+                      {{ warning }}
+                    </div>
                   {% endfor %}
-                  {% if not thema_main %}
-                    <span class="badge badge-secondary" title="Nie ustalono głównej kategorii Thema">&nbsp;*&nbsp;</small>
+                  {% if shop.id %}
+                    id:{{ shop.id }}
                   {% endif %}
-                  </small></button>
-                {% with llp=book.last_legimi_publish %}
-                  {% if llp %}
-                    {{ llp.created_at }} &rarr;
-                    {{ llp.started_at }} &rarr;
-                    {{ llp.finished_at }}
-                    ({{ llp.get_status_display }})
-                    <!-- {{ llp.id }} -->
+                  {% with last=shop.last %}
+                  {% if last %}
+                    {{ last.created_at }} &rarr;
+                    {{ last.started_at }} &rarr;
+                    {{ last.finished_at }}
+                    <span title="{{ last.error }}">
+                      ({{ last.get_status_display }})
+                    </span>
+                    <!-- {{ shop.last.id }} -->
                   {% endif %}
-                {% endwith %}
-              </form>
+                  {% endwith %}
+                {% endfor %}
+              {% endif %}
+
             {% else %}
-              <div class="alert alert-warning">Nie można opublikować na Legimi, ponieważ nie ustalono kategorii Thema.</div>
+              <a href="{% url 'cas_ng_login' %}">{% trans "Log in to publish." %}</a>
             {% endif %}
-          {% endwith %}
-        {% endif %}
-
-    {% else %}
-        <a href="{% url 'cas_ng_login' %}">{% trans "Log in to publish." %}</a>
-    {% endif %}
-{% else %}
-    <p>{% trans "This book can't be published yet, because:" %}</p>
-    <ul><li>{{ publishable_error }}</li></ul>
-{% endif %}
-
-
-
+          {% else %}
+            <p>{% trans "This book can't be published yet, because:" %}</p>
+            <ul><li>{{ publishable_error }}</li></ul>
+          {% endif %}
+        </div>
+      </div>
     </div>
   </div>
-</div>
-</div>
 
 
-{% if doc %}
-  <div class="card mt-4">
-    <div class="card-header">
-      <h2>{% trans "Statistics" %}</h2>
-    </div>
-    <div class="card-body">
-      <table class="table">
-        <thead>
-          <tr>
-            <th>
-              {% trans "book" %}
-            </th>
-            <th>{% trans "characters" %}</th>
-            <th>{% trans "characters (with footnotes)" %}</th>
-            <th>{% trans "words" %}</th>
-            <th>{% trans "words (with footnotes)" %}</th>
-            <th>{% trans "wiersze (+ inne znaki)" %}</th>
-            <th>{% trans "wiersze (+ inne znaki, z przypisami)" %}</th>
-          </tr>
-        </thead>
-        <tbody>
-          {% with stats=doc.get_statistics %}
-            {% include 'documents/book_stats.html' with book=book stats=stats depth=0 %}
+  {% if doc %}
+    <div class="card mt-4">
+      <div class="card-header">
+        <h2>{% trans "Statistics" %}</h2>
+      </div>
+      <div class="card-body">
+        <table class="table">
+          <thead>
+            <tr>
+              <th>
+                {% trans "book" %}
+              </th>
+              <th>{% trans "characters" %}</th>
+              <th>{% trans "characters (with footnotes)" %}</th>
+              <th>{% trans "words" %}</th>
+              <th>{% trans "words (with footnotes)" %}</th>
+              <th>{% trans "wiersze (+ inne znaki)" %}</th>
+              <th>{% trans "wiersze (+ inne znaki, z przypisami)" %}</th>
+            </tr>
+          </thead>
+          <tbody>
+            {% with stats=doc.get_statistics %}
+              {% include 'documents/book_stats.html' with book=doc stats=stats depth=0 %}
           {% endwith %}
         </tbody>
       </table>
index 5ce14de..8f652cd 100644 (file)
@@ -1,11 +1,19 @@
 <tr>
-  <td style="padding-left: {{ depth}}.75em;">{{ book.book_info.title|default:"???" }}</td>
-  <td>{{ stats.self.chars|default_if_none:"?" }}</td>
-  <td>{{ stats.self.chars_with_fn|default_if_none:"?" }}</td>
-  <td>{{ stats.self.words|default_if_none:"?" }}</td>
-  <td>{{ stats.self.words_with_fn|default_if_none:"?" }}</td>
-  <td>{{ stats.self.verses|default_if_none:"?" }} <small>+ {{ stats.self.chars_out_verse|default_if_none:"?" }} zn.</small></td>
-  <td>{{ stats.self.verses_with_fn|default_if_none:"?" }} <small> + {{ stats.self.chars_out_verse_with_fn|default_if_none:"?" }} zn.</small></td>
+  <td style="padding-left: {{ depth}}.75em;">
+    {{ book.meta.title|default:"???" }}
+  </td>
+  {% if stats.error %}
+    <td colspan="6">
+      {{ stats.error }}
+    </td>
+  {% else %}
+    <td>{{ stats.self.chars|default_if_none:"?" }}</td>
+    <td>{{ stats.self.chars_with_fn|default_if_none:"?" }}</td>
+    <td>{{ stats.self.words|default_if_none:"?" }}</td>
+    <td>{{ stats.self.words_with_fn|default_if_none:"?" }}</td>
+    <td>{{ stats.self.verses|default_if_none:"?" }} <small>+ {{ stats.self.chars_out_verse|default_if_none:"?" }} zn.</small></td>
+    <td>{{ stats.self.verses_with_fn|default_if_none:"?" }} <small> + {{ stats.self.chars_out_verse_with_fn|default_if_none:"?" }} zn.</small></td>
+  {% endif %}
 </tr>
 {% if stats.parts %}
   {% for part, partstats in stats.parts %}
index 225ad55..f4c8561 100644 (file)
@@ -363,10 +363,10 @@ def book(request, slug):
     publishable = publish_error is None
 
     try:
-        doc = book.wldocument()
+        doc = book.wldocument(librarian2=True)
     except:
         doc = None
-    
+
     return render(request, "documents/book_detail.html", {
         "book": book,
         "doc": doc,
index 64a49dc..3375b0d 100644 (file)
@@ -8,7 +8,7 @@ from librarian.document import WLDocument
 from librarian.builders import StandaloneHtmlBuilder
 from librarian.meta.types.wluri import WLURI
 from librarian.meta.types.text import LegimiCategory, Epoch, Kind, Genre, Audience, ThemaCategory, MainThemaCategory
-from depot.legimi import legimi
+from depot.publishers.legimi import Legimi
 
 
 class XslView(TemplateView):
@@ -48,7 +48,7 @@ class TagView(DetailView):
 VALUE_TYPES = {
     LegimiCategory: {
         'widget': 'select',
-        'options': [''] + list(legimi.CATEGORIES.keys()),
+        'options': [''] + list(Legimi.CATEGORIES.keys()),
     },
     Audience: {
         'autocomplete': {