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
 
 python-docx==0.8.11
 Wikidata==0.7
 
-librarian==23.07.1
+librarian==23.08
 
 ## Django
 Django==4.1.9
 
 ## 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])
 
     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}"
     @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 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):
 
 @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({})
     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({
         '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
 
 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']
 @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 django.core.management.base import BaseCommand
-from depot.models import LegimiBookPublish
+from depot.models import ShopBookPublish
 
 
 class Command(BaseCommand):
     def handle(self, **options):
 
 
 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()
 
             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 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):
 
 
 class Package(models.Model):
@@ -27,14 +28,14 @@ class Package(models.Model):
             self.set_status(self.get_status())
         except:
             pass
             self.set_status(self.get_status())
         except:
             pass
-        
+
         try:
             self.set_definition(self.get_definition())
         except:
             pass
 
         super().save(*args, **kwargs)
         try:
             self.set_definition(self.get_definition())
         except:
             pass
 
         super().save(*args, **kwargs)
-    
+
     def get_status(self):
         return json.loads(self.status_json)
 
     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)
     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)
     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
     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)
         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:
         for change in changes:
-            me.legimichunkpublish_set.create(change=change)
+            me.shopchunkpublish_set.create(change=change)
         return me
 
     def publish(self):
         return me
 
     def publish(self):
@@ -151,10 +153,11 @@ class LegimiBookPublish(models.Model):
         try:
             changes = [
                 p.change for p in
         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()
         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'])
 
 
         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)
     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 = [
 
 
 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
 
 
 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:
         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())
         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
 
     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)
 
     def __init__(self, *args, **kwargs):
         super(DocumentCreateForm, self).__init__(*args, **kwargs)
@@ -151,7 +153,7 @@ class BookForm(forms.ModelForm):
 
     class Meta:
         model = Book
 
     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)
 
     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)
         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
 
     class NoTextError(BaseException):
         pass
@@ -282,9 +283,6 @@ class Book(models.Model):
         except IndexError:
             return None
 
         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:
     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 %}
 {% extends "documents/base.html" %}
 {% load book_list i18n %}
 {% load bootstrap4 %}
+{% load depot %}
 
 
 {% block titleextra %}{{ book.title }}{% endblock %}
 
 
 {% block titleextra %}{{ book.title }}{% endblock %}
        <div class="col-md-8">
 
 
        <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 %}
         {% 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>
     <div class="card-body">
 
     </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>
   </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>
-    <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 %}
                   {% 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 %}
                   {% 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 %}
                   {% 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 %}
                   {% endif %}
-                {% endwith %}
-              </form>
+                  {% endwith %}
+                {% endfor %}
+              {% endif %}
+
             {% else %}
             {% 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 %}
             {% 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>
-</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>
           {% endwith %}
         </tbody>
       </table>
index 5ce14de..8f652cd 100644 (file)
@@ -1,11 +1,19 @@
 <tr>
 <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 %}
 </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:
     publishable = publish_error is None
 
     try:
-        doc = book.wldocument()
+        doc = book.wldocument(librarian2=True)
     except:
         doc = None
     except:
         doc = None
-    
+
     return render(request, "documents/book_detail.html", {
         "book": book,
         "doc": doc,
     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 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):
 
 
 class XslView(TemplateView):
@@ -48,7 +48,7 @@ class TagView(DetailView):
 VALUE_TYPES = {
     LegimiCategory: {
         'widget': 'select',
 VALUE_TYPES = {
     LegimiCategory: {
         'widget': 'select',
-        'options': [''] + list(legimi.CATEGORIES.keys()),
+        'options': [''] + list(Legimi.CATEGORIES.keys()),
     },
     Audience: {
         'autocomplete': {
     },
     Audience: {
         'autocomplete': {