From 2d538a50605add2666172861744229599487f1b2 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Thu, 10 Aug 2023 21:36:24 +0200 Subject: [PATCH] Experimetal Woblink. --- requirements/requirements.txt | 2 +- src/catalogue/models.py | 10 + src/catalogue/views.py | 9 +- src/depot/admin.py | 19 + src/depot/management/commands/depot.py | 6 +- ...imibookpublish_shopbookpublish_and_more.py | 99 ++++ .../migrations/0004_shopbookpublish_shop.py | 21 + .../0005_shop_name_shop_publisher_handle.py | 24 + .../migrations/0006_shop_description_add.py | 18 + src/depot/models.py | 92 ++- src/depot/publishers/base.py | 75 +++ src/depot/{ => publishers}/legimi.py | 164 ++---- src/depot/publishers/woblink.py | 534 ++++++++++++++++++ src/depot/publishers/woblink_constants.py | 232 ++++++++ src/depot/templatetags/depot.py | 20 + src/depot/urls.py | 7 +- src/depot/views.py | 9 +- src/depot/woblink.py | 24 - src/documents/forms.py | 6 +- .../migrations/0011_book_woblink_id.py | 18 + src/documents/models/book.py | 4 +- .../templates/documents/book_detail.html | 319 ++++++----- .../templates/documents/book_stats.html | 22 +- src/documents/views.py | 4 +- src/wlxml/views.py | 4 +- 25 files changed, 1412 insertions(+), 330 deletions(-) create mode 100644 src/depot/migrations/0003_shop_rename_legimibookpublish_shopbookpublish_and_more.py create mode 100644 src/depot/migrations/0004_shopbookpublish_shop.py create mode 100644 src/depot/migrations/0005_shop_name_shop_publisher_handle.py create mode 100644 src/depot/migrations/0006_shop_description_add.py create mode 100644 src/depot/publishers/base.py rename src/depot/{ => publishers}/legimi.py (69%) create mode 100644 src/depot/publishers/woblink.py create mode 100644 src/depot/publishers/woblink_constants.py create mode 100644 src/depot/templatetags/depot.py delete mode 100644 src/depot/woblink.py create mode 100644 src/documents/migrations/0011_book_woblink_id.py diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 1f07c4fe..346dad4f 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -10,7 +10,7 @@ python-slugify==8.0.1 python-docx==0.8.11 Wikidata==0.7 -librarian==23.07.1 +librarian==23.08 ## Django Django==4.1.9 diff --git a/src/catalogue/models.py b/src/catalogue/models.py index 85757a65..be637083 100644 --- a/src/catalogue/models.py +++ b/src/catalogue/models.py @@ -121,6 +121,16 @@ class Author(WikidataModel): def get_absolute_url(self): return reverse("catalogue_author", args=[self.slug]) + @classmethod + def get_by_literal(cls, literal): + names = literal.split(',', 1) + names = [n.strip() for n in names] + if len(names) == 2: + return cls.objects.filter(last_name=names[0], first_name=names[1]).first() + else: + return cls.objects.filter(last_name=names[0], first_name='').first() or \ + cls.objects.filter(first_name=names[0], last_name='').first() + @property def name(self): return f"{self.last_name}, {self.first_name}" diff --git a/src/catalogue/views.py b/src/catalogue/views.py index 697a5acf..73015802 100644 --- a/src/catalogue/views.py +++ b/src/catalogue/views.py @@ -20,7 +20,7 @@ from rest_framework.permissions import IsAdminUser from rest_framework.response import Response from rest_framework.views import APIView from rest_framework import serializers -from depot.woblink import get_woblink_session +import depot.models @@ -384,11 +384,14 @@ def publish_collection(request, pk): @login_required def woblink_author_autocomplete(request): - session = get_woblink_session() + shop = depot.models.Shop.objects.filter(shop='woblink').first() + if shop is None: + return JsonResponse({}) + woblink = shop.get_publisher() term = request.GET.get('term') if not term: return JsonResponse({}) - response = session.get( + response = woblink.session.get( 'https://publisher.woblink.com/author/autocomplete/' + term ).json() return JsonResponse({ diff --git a/src/depot/admin.py b/src/depot/admin.py index eebf0102..3ad1fa30 100644 --- a/src/depot/admin.py +++ b/src/depot/admin.py @@ -1,3 +1,4 @@ +from admin_ordering.admin import OrderableAdmin from django.contrib import admin from . import models @@ -5,3 +6,21 @@ from . import models @admin.register(models.Package) class PackageAdmin(admin.ModelAdmin): raw_id_fields = ['books'] + + +class MediaInsertTextInline(OrderableAdmin, admin.TabularInline): + model = models.MediaInsertText + extra = 0 + + +class PriceLevelInline(OrderableAdmin, admin.TabularInline): + model = models.PriceLevel + extra = 0 + + +@admin.register(models.Shop) +class ShopAdmin(admin.ModelAdmin): + inlines = [ + MediaInsertTextInline, + PriceLevelInline, + ] diff --git a/src/depot/management/commands/depot.py b/src/depot/management/commands/depot.py index 4b8b99af..6e64e246 100644 --- a/src/depot/management/commands/depot.py +++ b/src/depot/management/commands/depot.py @@ -1,10 +1,10 @@ from django.core.management.base import BaseCommand -from depot.models import LegimiBookPublish +from depot.models import ShopBookPublish class Command(BaseCommand): def handle(self, **options): - for p in LegimiBookPublish.objects.filter(status=0).order_by('created_at'): - print(p, p.book.slug, p.created_at) + for p in ShopBookPublish.objects.filter(status=0).order_by('created_at'): + print(p.id, p.shop, p.book.slug, p.created_at) p.publish() diff --git a/src/depot/migrations/0003_shop_rename_legimibookpublish_shopbookpublish_and_more.py b/src/depot/migrations/0003_shop_rename_legimibookpublish_shopbookpublish_and_more.py new file mode 100644 index 00000000..bcf0af4c --- /dev/null +++ b/src/depot/migrations/0003_shop_rename_legimibookpublish_shopbookpublish_and_more.py @@ -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 index 00000000..d908f75d --- /dev/null +++ b/src/depot/migrations/0004_shopbookpublish_shop.py @@ -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 index 00000000..b50632dc --- /dev/null +++ b/src/depot/migrations/0005_shop_name_shop_publisher_handle.py @@ -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 index 00000000..cecf2441 --- /dev/null +++ b/src/depot/migrations/0006_shop_description_add.py @@ -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), + ), + ] diff --git a/src/depot/models.py b/src/depot/models.py index 23b6d9f3..4b5bf654 100644 --- a/src/depot/models.py +++ b/src/depot/models.py @@ -9,7 +9,8 @@ from django.db import models from django.utils.timezone import now from librarian.cover import make_cover from librarian.builders import EpubBuilder, MobiBuilder -from .legimi import legimi +from .publishers.legimi import Legimi +from .publishers.woblink import Woblink class Package(models.Model): @@ -27,14 +28,14 @@ class Package(models.Model): self.set_status(self.get_status()) except: pass - + try: self.set_definition(self.get_definition()) except: pass super().save(*args, **kwargs) - + def get_status(self): return json.loads(self.status_json) @@ -121,9 +122,10 @@ class Package(models.Model): ) -class LegimiBookPublish(models.Model): +class ShopBookPublish(models.Model): book = models.ForeignKey('documents.Book', models.CASCADE) user = models.ForeignKey(settings.AUTH_USER_MODEL, models.SET_NULL, null=True) + shop = models.ForeignKey('Shop', models.SET_NULL, null=True) created_at = models.DateTimeField() started_at = models.DateTimeField(null=True, blank=True) finished_at = models.DateTimeField(null=True, blank=True) @@ -136,12 +138,12 @@ class LegimiBookPublish(models.Model): error = models.TextField(blank=True) @classmethod - def create_for(cls, book, user): + def create_for(cls, book, user, shop): book.assert_publishable() changes = book.get_current_changes(publishable=True) - me = cls.objects.create(book=book, user=user, created_at=now()) + me = cls.objects.create(book=book, user=user, shop=shop, created_at=now()) for change in changes: - me.legimichunkpublish_set.create(change=change) + me.shopchunkpublish_set.create(change=change) return me def publish(self): @@ -151,10 +153,11 @@ class LegimiBookPublish(models.Model): try: changes = [ p.change for p in - self.legimichunkpublish_set.order_by('change__chunk__number') + self.shopchunkpublish_set.order_by('change__chunk__number') ] - legimi.send_book(self.book, changes=changes) - legimi.edit_sale(self.book) + + self.shop.publish(self.book, changes=changes) + except Exception: self.status = 110 self.error = traceback.format_exc() @@ -165,6 +168,71 @@ class LegimiBookPublish(models.Model): self.save(update_fields=['status', 'finished_at', 'error']) -class LegimiChunkPublish(models.Model): - book_publish = models.ForeignKey(LegimiBookPublish, models.CASCADE) +class ShopChunkPublish(models.Model): + book_publish = models.ForeignKey(ShopBookPublish, models.CASCADE) change = models.ForeignKey('documents.ChunkChange', models.CASCADE) + + +class Shop(models.Model): + name = models.CharField(max_length=255) + shop = models.CharField(max_length=32, choices=[ + ('legimi', 'Legimi'), + ('woblink', 'Woblink'), + ]) + username = models.CharField(max_length=255) + password = models.CharField(max_length=255) + publisher_handle = models.CharField(max_length=255, blank=True) + description_add = models.TextField(blank=True) + + def __str__(self): + return self.shop + + def get_texts(self): + return [t.text for t in self.mediainserttext_set.all()] + + def get_price(self, words, pages): + price_obj = self.pricelevel_set.exclude( + min_pages__gt=pages + ).exclude( + min_words__gt=words + ).order_by('-price').first() + if price_obj is None: + return None + return price_obj.price + + def get_publisher(self): + if self.shop == 'legimi': + pub_class = Legimi + elif self.shop == 'woblink': + pub_class = Woblink + return pub_class(self.username, self.password, self.publisher_handle) + + def publish(self, book, changes): + self.get_publisher().send_book( + self, book, changes=changes, + ) + + def can_publish(self, book): + return self.get_publisher().can_publish(self, book) + + def get_last(self, book): + return self.shopbookpublish_set.filter(book=book).order_by('-created_at').first() + + +class PriceLevel(models.Model): + shop = models.ForeignKey(Shop, models.CASCADE) + min_pages = models.IntegerField(null=True, blank=True) + min_words = models.IntegerField(null=True, blank=True) + price = models.IntegerField() + + class Meta: + ordering = ('price',) + + +class MediaInsertText(models.Model): + shop = models.ForeignKey(Shop, models.CASCADE) + ordering = models.IntegerField() + text = models.TextField() + + class Meta: + ordering = ('ordering',) diff --git a/src/depot/publishers/base.py b/src/depot/publishers/base.py new file mode 100644 index 00000000..b728cf24 --- /dev/null +++ b/src/depot/publishers/base.py @@ -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 += '

' + description += ', '.join( + '{}'.format( + slugify(p.readable()), + p.readable(), + ) + for p in wlbook.meta.authors + ) + '
' + description += '{}
'.format( + wlbook.meta.url.slug, + wlbook.meta.title + ) + if wlbook.meta.translators: + description += 'tłum. ' + ', '.join(p.readable() for p in wlbook.meta.translators) + '
' + description += 'Epoka: ' + ', '.join( + '{}'.format( + slugify(p), + p, + ) + for p in wlbook.meta.epochs + ) + ' ' + description += 'Rodzaj: ' + ', '.join( + '{}'.format( + slugify(p), + p, + ) + for p in wlbook.meta.kinds + ) + ' ' + description += 'Gatunek: ' + ', '.join( + '{}'.format( + slugify(p), + p, + ) + for p in wlbook.meta.genres + ) + '

' + + # TODO: Move away from using audiences for this. + if wlbook.meta.audience in ('L', 'SP1', 'SP2', 'SP3', 'SP4'): + description += '

{} to lektura szkolna.'.format(wlbook.meta.title) + if wlbook.tree.find('//pe') is not None: + description += '
Ebook {title} 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 += '

' + return description + diff --git a/src/depot/legimi.py b/src/depot/publishers/legimi.py similarity index 69% rename from src/depot/legimi.py rename to src/depot/publishers/legimi.py index 464eecad..c7fa26df 100644 --- a/src/depot/legimi.py +++ b/src/depot/publishers/legimi.py @@ -1,28 +1,15 @@ 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.html import transform_abstrakt from librarian.builders import EpubBuilder, MobiBuilder from librarian.covers.marquise import MarquiseCover, LabelMarquiseCover -import requests -from slugify import slugify +from .base import BasePublisher - -fundraising=[ - "Książka, którą czytasz, pochodzi z Wolnych Lektur. 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ą: wolnelektury.pl/towarzystwo/", - "Przyjaciele Wolnych Lektur otrzymują dostęp do prapremier wcześniej niż inni. Zadeklaruj stałą wpłatę i dołącz do Towarzystwa Przyjaciół Wolnych Lektur: wolnelektury.pl/towarzystwo/", - "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: wolnelektury.pl/newsletter/zapisz-sie/", - "Przekaż 1% podatku na Wolne Lektury.
\nKRS: 0000070056
\nNazwa organizacji: Fundacja Nowoczesna Polska
\nKażda wpłacona kwota zostanie przeznaczona na rozwój Wolnych Lektur." -] - -description_add = '

Książkę polecają Wolne Lektury — najpopularniejsza biblioteka on-line.

' - - -class Legimi: - #BASE_URL = 'https://wydawca.legimi.com' +class Legimi(BasePublisher): BASE_URL = 'https://panel.legimi.pl' LOGIN_URL = BASE_URL + '/publishers/membership' UPLOAD_URL = BASE_URL + '/administration/upload/start' @@ -131,30 +118,48 @@ class Legimi: '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 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 {code}".format( + code=escape(meta.thema_main) + ) + if meta.thema: + comment += " oraz: " + ", ".join( + "{code}".format(code=escape(t)) + for t in meta.thema + ) + d['comment'] = mark_safe(comment) + elif meta.thema: + d['comment'] = mark_safe( + "w kategorii " + ", ".join( + "{code}".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, @@ -168,19 +173,20 @@ class Legimi: "url": model['Url'], } - def send_book(self, book, changes=None): + 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=fundraising, + fundraising=texts, base_url='file://' + book.gallery_path() + '/' ).build(wlbook).get_file() mobi_file = MobiBuilder( cover=MarquiseCover, - fundraising=fundraising, + fundraising=texts, base_url='file://' + book.gallery_path() + '/' ).build(wlbook).get_file() @@ -188,7 +194,7 @@ class Legimi: 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), @@ -200,7 +206,7 @@ class Legimi: 'Isbn': '', 'LanguageLocale': lang_code_3to2(meta.language), - 'Description': self.get_description(wlbook), + 'Description': self.get_description(wlbook, shop.description_add), } if meta.isbn_html: isbn = meta.isbn_html @@ -236,7 +242,7 @@ class Legimi: 'BookMobi.Token': mobi_data['token'], 'BookMobi.Name': mobi_data['name'], }) - + if book.legimi_id: self.edit( book.legimi_id, @@ -252,58 +258,7 @@ class Legimi: 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 += '

' - description += ', '.join( - '{}'.format( - slugify(p.readable()), - p.readable(), - ) - for p in wlbook.meta.authors - ) + '
' - description += '{}
'.format( - wlbook.meta.url.slug, - wlbook.meta.title - ) - if wlbook.meta.translators: - description += 'tłum. ' + ', '.join(p.readable() for p in wlbook.meta.translators) + '
' - description += 'Epoka: ' + ', '.join( - '{}'.format( - slugify(p), - p, - ) - for p in wlbook.meta.epochs - ) + ' ' - description += 'Rodzaj: ' + ', '.join( - '{}'.format( - slugify(p), - p, - ) - for p in wlbook.meta.kinds - ) + ' ' - description += 'Gatunek: ' + ', '.join( - '{}'.format( - slugify(p), - p, - ) - for p in wlbook.meta.genres - ) + '

' - - # TODO: Move away from using audiences for this. - if wlbook.meta.audience in ('L', 'SP1', 'SP2', 'SP3', 'SP4'): - description += '

{} to lektura szkolna.'.format(wlbook.meta.title) - if wlbook.tree.find('//pe') is not None: - description += '
Ebook {title} 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 += '

' - return description + self.edit_sale(book) def get_genre(self, wlbook): if wlbook.meta.legimi and wlbook.meta.legimi in self.CATEGORIES: @@ -312,11 +267,11 @@ class Legimi: 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 + 'PublisherId': self.publisher_handle, 'IsLibraryPass': 'False', 'SamplesGenerationType': 'Quantity', @@ -363,7 +318,7 @@ class Legimi: } current.update(data) - + self.session.post( self.EDIT_URL % legimi_id, data=current @@ -392,7 +347,7 @@ class Legimi: }) current.update(files_data) - + response = self.session.post( self.EDIT_FILES_URL % legimi_id, data=current @@ -433,10 +388,3 @@ class Legimi: self.EDIT_SALE_URL % book.legimi_id, data=data ) - - -legimi = Legimi( - settings.LEGIMI_USERNAME, - settings.LEGIMI_PASSWORD, - settings.LEGIMI_PUBLISHER_ID, -) diff --git a/src/depot/publishers/woblink.py b/src/depot/publishers/woblink.py new file mode 100644 index 00000000..00dca5ec --- /dev/null +++ b/src/depot/publishers/woblink.py @@ -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 określonej ceny.', + 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 {author} 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 {code} 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 {code} 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 {author} 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] += '' + 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 index 00000000..0fad5959 --- /dev/null +++ b/src/depot/publishers/woblink_constants.py @@ -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 index 00000000..d1194f05 --- /dev/null +++ b/src/depot/templatetags/depot.py @@ -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 diff --git a/src/depot/urls.py b/src/depot/urls.py index 98045158..4901ca96 100644 --- a/src/depot/urls.py +++ b/src/depot/urls.py @@ -3,9 +3,8 @@ from . import views urlpatterns = [ - path( - 'legimi-publish//', - views.LegimiPublishView.as_view(), - name='depot_legimi_publish' + path('shop-publish///', + views.ShopPublishView.as_view(), + name='depot_shop_publish' ) ] diff --git a/src/depot/views.py b/src/depot/views.py index 5e468b67..79760863 100644 --- a/src/depot/views.py +++ b/src/depot/views.py @@ -5,13 +5,14 @@ from documents.models import Book from . import models -class LegimiPublishView(PermissionRequiredMixin, View): - permission_required = 'depot.add_legimibookpublish' +class ShopPublishView(PermissionRequiredMixin, View): + permission_required = 'depot.add_shopbookpublish' - def post(self, request, book_id): + def post(self, request, shop_id, book_id): + shop = get_object_or_404(models.Shop, pk=shop_id) book = get_object_or_404(Book, pk=book_id) try: - publish = models.LegimiBookPublish.create_for(book, request.user) + publish = models.ShopBookPublish.create_for(book, request.user, shop) except AssertionError: pass return redirect(book.get_absolute_url()) diff --git a/src/depot/woblink.py b/src/depot/woblink.py deleted file mode 100644 index ffd6b4d5..00000000 --- a/src/depot/woblink.py +++ /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 - - - diff --git a/src/documents/forms.py b/src/documents/forms.py index dccf0f09..4c970339 100644 --- a/src/documents/forms.py +++ b/src/documents/forms.py @@ -20,7 +20,9 @@ class DocumentCreateForm(forms.ModelForm): class Meta: model = Book - exclude = ['parent', 'parent_number', 'project'] + fields = [ + 'title', 'slug', 'public', 'gallery' + ] def __init__(self, *args, **kwargs): super(DocumentCreateForm, self).__init__(*args, **kwargs) @@ -151,7 +153,7 @@ class BookForm(forms.ModelForm): class Meta: model = Book - exclude = ['project', 'cover', 'legimi_id'] + exclude = ['project', 'cover', 'legimi_id', 'woblink_id'] def __init__(self, *args, **kwargs): ret = super(BookForm, self).__init__(*args, **kwargs) diff --git a/src/documents/migrations/0011_book_woblink_id.py b/src/documents/migrations/0011_book_woblink_id.py new file mode 100644 index 00000000..a142d84e --- /dev/null +++ b/src/documents/migrations/0011_book_woblink_id.py @@ -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), + ), + ] diff --git a/src/documents/models/book.py b/src/documents/models/book.py index f40af29b..c11862ba 100644 --- a/src/documents/models/book.py +++ b/src/documents/models/book.py @@ -57,6 +57,7 @@ class Book(models.Model): related_query_name='document_book', ) legimi_id = models.CharField(max_length=255, blank=True) + woblink_id = models.CharField(max_length=255, blank=True) class NoTextError(BaseException): pass @@ -282,9 +283,6 @@ class Book(models.Model): except IndexError: return None - def last_legimi_publish(self): - return self.legimibookpublish_set.order_by('-created_at').first() - def assert_publishable(self): assert self.chunk_set.exists(), _('No chunks in the book.') try: diff --git a/src/documents/templates/documents/book_detail.html b/src/documents/templates/documents/book_detail.html index caa3de52..a2dea020 100644 --- a/src/documents/templates/documents/book_detail.html +++ b/src/documents/templates/documents/book_detail.html @@ -1,6 +1,7 @@ {% extends "documents/base.html" %} {% load book_list i18n %} {% load bootstrap4 %} +{% load depot %} {% block titleextra %}{{ book.title }}{% endblock %} @@ -17,22 +18,22 @@
-{% if editable %}
{% csrf_token %}{% endif %} - {% bootstrap_form form %} - {% if editable %} - {% buttons %} - + {% if editable %}{% csrf_token %}{% endif %} + {% bootstrap_form form %} + {% if editable %} + {% buttons %} + {% endbuttons %} - {% endif %} -{% if editable %}
{% endif %} + {% endif %} + {% if editable %}{% endif %} -{% if editable %} - {% if book.gallery %} -

{% trans "Edit gallery" %}

- {% endif %} + {% if editable %} + {% if book.gallery %} +

{% trans "Edit gallery" %}

+ {% endif %} -

{% trans "Append to other book" %}

-{% endif %} +

{% trans "Append to other book" %}

+ {% endif %}
@@ -54,166 +55,174 @@
- - {% for chunk in book %} - {% include 'documents/book_list/chunk.html' %} - {% endfor %} -
+ + {% for chunk in book %} + {% include 'documents/book_list/chunk.html' %} + {% endfor %} +
-
+
-
-

{% trans "Publication" %}

-
-
-
-
- - {% if book.dc_cover_image %} - {{ book.dc_cover_image }} - {% endif %} -

- -
- {% csrf_token %} -
- - -
-
- - -

-
- - Pobierz okładkę: -
-
- - - -
-
+
+

{% trans "Publication" %}

-
-

{% trans "Last published" %}: - {% if book.last_published %} - {{ book.last_published }} - {% else %} - — - {% endif %} -

- -{% if publishable %} -

- {% trans "Full XML" %}
- {% trans "HTML version" %}
- {% trans "TXT version" %}
- {% trans "PDF version" %}
- {% trans "PDF version for mobiles" %}
- {% trans "EPUB version" %}
- {% trans "MOBI version" %}
-

- - {% if user.is_authenticated %} - -
{% csrf_token %} - {{ publish_options_form.as_p }} - - - -
- - {% if perms.depot.add_legimibookpublish %} -
- {% with thema_main=doc.book_info.thema_main thema=doc.book_info.thema %} - {% if thema_main or thema %} -
- {% csrf_token %} - +
+ + + +

+
+ + Pobierz okładkę: +
+
+ + + +
+
+
+
+

{% trans "Last published" %}: + {% if book.last_published %} + {{ book.last_published }} + {% else %} + — + {% endif %} +

+ + {% if publishable %} +

+ {% trans "Full XML" %}
+ {% trans "HTML version" %}
+ {% trans "TXT version" %}
+ {% trans "PDF version" %}
+ {% trans "PDF version for mobiles" %}
+ {% trans "EPUB version" %}
+ {% trans "MOBI version" %}
+

+ + {% if user.is_authenticated %} + +
{% csrf_token %} + {{ publish_options_form.as_p }} + + + +
+ + + {% if perms.depot.add_shopbookpublish %} + {% depot_shops book as shops %} + {% for shop in shops %} +
+

{{ shop.name }}

+ {% if not shop.errors %} +
+ {% csrf_token %} + +
+ {% if shop.comment %} +
+ {{ shop.comment }} +
+ {% endif %} + {% else %} + {% for error in shop.errors %} +
+ {{ error }} +
+ {% endfor %} {% endif %} - {% for t in thema %} - {% if forloop.first and thema_main %}oraz: {% endif %} - {{ t }} - {% if not forloop.last %}, {% endif %} + {% for warning in shop.warnings %} +
+ {{ warning }} +
{% endfor %} - {% if not thema_main %} -  * 
+ {% if shop.id %} + id:{{ shop.id }} {% endif %} - - {% with llp=book.last_legimi_publish %} - {% if llp %} - {{ llp.created_at }} → - {{ llp.started_at }} → - {{ llp.finished_at }} - ({{ llp.get_status_display }}) - + {% with last=shop.last %} + {% if last %} + {{ last.created_at }} → + {{ last.started_at }} → + {{ last.finished_at }} + + ({{ last.get_status_display }}) + + {% endif %} - {% endwith %} - + {% endwith %} + {% endfor %} + {% endif %} + {% else %} -
Nie można opublikować na Legimi, ponieważ nie ustalono kategorii Thema.
+ {% trans "Log in to publish." %} {% endif %} - {% endwith %} - {% endif %} - - {% else %} - {% trans "Log in to publish." %} - {% endif %} -{% else %} -

{% trans "This book can't be published yet, because:" %}

-
  • {{ publishable_error }}
-{% endif %} - - - + {% else %} +

{% trans "This book can't be published yet, because:" %}

+
  • {{ publishable_error }}
+ {% endif %} +
+
-
- -{% if doc %} -
-
-

{% trans "Statistics" %}

-
-
- - - - - - - - - - - - - - {% with stats=doc.get_statistics %} - {% include 'documents/book_stats.html' with book=book stats=stats depth=0 %} + {% if doc %} +
+
+

{% trans "Statistics" %}

+
+
+
- {% trans "book" %} - {% trans "characters" %}{% trans "characters (with footnotes)" %}{% trans "words" %}{% trans "words (with footnotes)" %}{% trans "wiersze (+ inne znaki)" %}{% trans "wiersze (+ inne znaki, z przypisami)" %}
+ + + + + + + + + + + + + {% with stats=doc.get_statistics %} + {% include 'documents/book_stats.html' with book=doc stats=stats depth=0 %} {% endwith %}
+ {% trans "book" %} + {% trans "characters" %}{% trans "characters (with footnotes)" %}{% trans "words" %}{% trans "words (with footnotes)" %}{% trans "wiersze (+ inne znaki)" %}{% trans "wiersze (+ inne znaki, z przypisami)" %}
diff --git a/src/documents/templates/documents/book_stats.html b/src/documents/templates/documents/book_stats.html index 5ce14dea..8f652cd1 100644 --- a/src/documents/templates/documents/book_stats.html +++ b/src/documents/templates/documents/book_stats.html @@ -1,11 +1,19 @@ - {{ book.book_info.title|default:"???" }} - {{ stats.self.chars|default_if_none:"?" }} - {{ stats.self.chars_with_fn|default_if_none:"?" }} - {{ stats.self.words|default_if_none:"?" }} - {{ stats.self.words_with_fn|default_if_none:"?" }} - {{ stats.self.verses|default_if_none:"?" }} + {{ stats.self.chars_out_verse|default_if_none:"?" }} zn. - {{ stats.self.verses_with_fn|default_if_none:"?" }} + {{ stats.self.chars_out_verse_with_fn|default_if_none:"?" }} zn. + + {{ book.meta.title|default:"???" }} + + {% if stats.error %} + + {{ stats.error }} + + {% else %} + {{ stats.self.chars|default_if_none:"?" }} + {{ stats.self.chars_with_fn|default_if_none:"?" }} + {{ stats.self.words|default_if_none:"?" }} + {{ stats.self.words_with_fn|default_if_none:"?" }} + {{ stats.self.verses|default_if_none:"?" }} + {{ stats.self.chars_out_verse|default_if_none:"?" }} zn. + {{ stats.self.verses_with_fn|default_if_none:"?" }} + {{ stats.self.chars_out_verse_with_fn|default_if_none:"?" }} zn. + {% endif %} {% if stats.parts %} {% for part, partstats in stats.parts %} diff --git a/src/documents/views.py b/src/documents/views.py index 225ad559..f4c85612 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -363,10 +363,10 @@ def book(request, slug): publishable = publish_error is None try: - doc = book.wldocument() + doc = book.wldocument(librarian2=True) except: doc = None - + return render(request, "documents/book_detail.html", { "book": book, "doc": doc, diff --git a/src/wlxml/views.py b/src/wlxml/views.py index 64a49dc8..3375b0d9 100644 --- a/src/wlxml/views.py +++ b/src/wlxml/views.py @@ -8,7 +8,7 @@ from librarian.document import WLDocument from librarian.builders import StandaloneHtmlBuilder from librarian.meta.types.wluri import WLURI from librarian.meta.types.text import LegimiCategory, Epoch, Kind, Genre, Audience, ThemaCategory, MainThemaCategory -from depot.legimi import legimi +from depot.publishers.legimi import Legimi class XslView(TemplateView): @@ -48,7 +48,7 @@ class TagView(DetailView): VALUE_TYPES = { LegimiCategory: { 'widget': 'select', - 'options': [''] + list(legimi.CATEGORIES.keys()), + 'options': [''] + list(Legimi.CATEGORIES.keys()), }, Audience: { 'autocomplete': { -- 2.20.1