extra = 1
-class SourcesInline(admin.TabularInline):
+class EditorNoteInline(admin.TabularInline):
model = models.EditorNote
extra = 1
class BookAdmin(WikidataAdminMixin, NumericFilterModelAdmin, VersionAdmin):
- inlines = [SourcesInline]
+ inlines = [EditorNoteInline, SourcesInline]
list_display = [
"smart_title",
"authors_str",
def by_slug(self, slug):
print(slug)
- return BytesIO(Book.objects.get(catalogue_book_id=slug
+ return BytesIO(Book.objects.get(dc_slug=slug
).materialize(publishable=self.publishable
).encode('utf-8'))
+++ /dev/null
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-import sys
-
-from datetime import date
-from lxml import etree
-
-from django.core.management import BaseCommand
-
-from documents.models import Book
-from librarian import RDFNS, DCNS
-
-TEMPLATE = '''<utwor>
-<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
-<rdf:Description rdf:about="http://redakcja.wolnelektury.pl/documents/book/%(slug)s/">
-%(dc)s
-</rdf:Description>
-</rdf:RDF>
-
-</utwor>
-'''
-
-DC_TEMPLATE = '<dc:%(tag)s xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">%(value)s</dc:%(tag)s>'
-
-DC_TAGS = (
- 'creator',
- 'title',
- 'relation.hasPart',
- 'contributor.translator',
- 'contributor.editor',
- 'contributor.technical_editor',
- 'contributor.funding',
- 'contributor.thanks',
- 'publisher',
- 'subject.period',
- 'subject.type',
- 'subject.genre',
- 'description',
- 'identifier.url',
- 'source',
- 'source.URL',
- 'rights.license',
- 'rights',
- 'date.pd',
- 'format',
- 'type',
- 'date',
- 'audience',
- 'language',
-)
-
-IDENTIFIER_PREFIX = 'http://wolnelektury.pl/katalog/lektura/'
-
-
-def dc_desc_element(book):
- xml = book.materialize()
- tree = etree.fromstring(xml)
- return tree.find(".//" + RDFNS("Description"))
-
-
-def distinct_dc_values(tag, desc_elements):
- values = set()
- for desc in desc_elements:
- values.update(elem.text for elem in desc.findall(DCNS(tag)))
- return values
-
-
-class Command(BaseCommand):
- args = 'slug'
-
- def handle(self, slug, **options):
- children_slugs = [line.strip() for line in sys.stdin]
- children = Book.objects.filter(catalogue_book_id__in=children_slugs)
- desc_elements = [dc_desc_element(child) for child in children]
- title = u'Utwory wybrane'
- own_attributes = {
- 'title': title,
- 'relation.hasPart': [IDENTIFIER_PREFIX + child_slug for child_slug in children_slugs],
- 'identifier.url': IDENTIFIER_PREFIX + slug,
- 'date': date.today().isoformat(),
- }
- dc_tags = []
- for tag in DC_TAGS:
- if tag in own_attributes:
- values = own_attributes[tag]
- if not isinstance(values, list):
- values = [values]
- else:
- values = distinct_dc_values(tag, desc_elements)
- for value in values:
- dc_tags.append(DC_TEMPLATE % {'tag': tag, 'value': value})
- xml = TEMPLATE % {'slug': slug, 'dc': '\n'.join(dc_tags)}
- Book.create(
- text=xml,
- creator=None,
- slug=slug,
- title=title,
- gallery=slug)
+++ /dev/null
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-import csv
-
-import sys
-from django.contrib.auth.models import User
-from lxml import etree
-from collections import defaultdict
-from django.core.management import BaseCommand
-
-from documents.models import Book
-from librarian import RDFNS, DCNS
-
-CONTENT_TYPES = {
- 'pdf': 'application/pdf',
- 'epub': 'application/epub+zip',
- 'mobi': 'application/x-mobipocket-ebook',
- 'txt': 'text/plain',
- 'html': 'text/html',
-}
-
-
-ISBN_TEMPLATES = (
- r'<dc:relation.hasFormat id="%(format)s" xmlns:dc="http://purl.org/dc/elements/1.1/">%(url)s'
- r'</dc:relation.hasFormat>',
- r'<meta refines="#%(format)s" id="%(format)s-id" property="dcterms:identifier">ISBN-%(isbn)s</meta>',
- r'<meta refines="#%(format)s-id" property="identifier-type">ISBN</meta>',
- r'<meta refines="#%(format)s" property="dcterms:format">%(content_type)s</meta>',
-)
-
-
-def url_for_format(slug, format):
- if format == 'html':
- return 'https://wolnelektury.pl/katalog/lektura/%s.html' % slug
- else:
- return 'http://wolnelektury.pl/media/book/%(format)s/%(slug)s.%(format)s' % {'slug': slug, 'format': format}
-
-
-class Command(BaseCommand):
- args = 'csv_file'
-
- def add_arguments(self, parser):
- self.add_argument(
- '-u', '--username', dest='username', metavar='USER',
- help='Assign commits to this user (required, preferably yourself).')
-
- def handle(self, csv_file, **options):
- username = options.get('username')
-
- if username:
- user = User.objects.get(username=username)
- else:
- print('Please provide a username.')
- sys.exit(1)
-
- csvfile = open(csv_file, 'rb')
- isbn_lists = defaultdict(list)
- for slug, format, isbn in csv.reader(csvfile, delimiter=','):
- isbn_lists[slug].append((format, isbn))
- csvfile.close()
-
- for slug, isbn_list in isbn_lists.iteritems():
- print('processing %s' % slug)
- book = Book.objects.get(catalogue_book_id=slug)
- chunk = book.chunk_set.first()
- old_head = chunk.head
- src = old_head.materialize()
- tree = etree.fromstring(src)
- isbn_node = tree.find('.//' + DCNS("relation.hasFormat"))
- if isbn_node is not None:
- print('%s already contains ISBN metadata, skipping' % slug)
- continue
- desc = tree.find(".//" + RDFNS("Description"))
- for format, isbn in isbn_list:
- for template in ISBN_TEMPLATES:
- isbn_xml = template % {
- 'format': format,
- 'isbn': isbn,
- 'content_type': CONTENT_TYPES[format],
- 'url': url_for_format(slug, format),
- }
- element = etree.XML(isbn_xml)
- element.tail = '\n'
- desc.append(element)
- new_head = chunk.commit(
- etree.tostring(tree, encoding='unicode'),
- author=user,
- description='automatyczne dodanie isbn'
- )
- print('committed %s' % slug)
- if old_head.publishable:
- new_head.set_publishable(True)
- else:
- print('Warning: %s not publishable' % slug)
--- /dev/null
+# Generated by Django 4.1.9 on 2024-10-15 14:21
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("documents", "0016_project_can_sell_project_private_notes"),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name="book",
+ old_name="catalogue_book",
+ new_name="dc_slug",
+ ),
+ ]
--- /dev/null
+# Generated by Django 4.1.9 on 2024-10-15 14:22
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("catalogue", "0055_book_parent_book_parent_number_editornote"),
+ ("documents", "0017_rename_catalogue_book"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="book",
+ name="catalogue_book",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ editable=False,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="document_books",
+ related_query_name="document_book",
+ to="catalogue.book",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="book",
+ name="dc_slug",
+ field=models.ForeignKey(
+ blank=True,
+ db_constraint=False,
+ editable=False,
+ null=True,
+ on_delete=django.db.models.deletion.DO_NOTHING,
+ related_name="document_books_by_slug",
+ related_query_name="document_book_by_slug",
+ to="catalogue.book",
+ to_field="slug",
+ ),
+ ),
+ ]
--- /dev/null
+# Generated by Django 4.1.9 on 2024-10-15 14:23
+
+from django.db import migrations
+
+
+def copy_slug_to_fk(apps, schema_editor):
+ cBook = apps.get_model('catalogue', 'Book')
+ dBook = apps.get_model('documents', 'Book')
+
+ for db in dBook.objects.all():
+ try:
+ cb = db.dc_slug
+ except cBook.DoesNotExist:
+ pass
+ else:
+ if cb is not None:
+ db.catalogue_book = cb
+ db.save()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("documents", "0018_book_catalogue_book_alter_book_catalogue_book_slug"),
+ ]
+
+ operations = [
+ migrations.RunPython(
+ copy_slug_to_fk,
+ migrations.RunPython.noop,
+ )
+ ]
--- /dev/null
+# Generated by Django 4.1.9 on 2024-10-15 14:39
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("documents", "0019_fk_uses_id"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="book",
+ name="dc_slug",
+ field=models.CharField(
+ blank=True, editable=False, max_length=2048, null=True
+ ),
+ ),
+ ]
db_index=True, on_delete=models.SET_NULL, editable=False)
dc = models.JSONField(null=True, editable=False)
cover = models.FileField(blank=True, upload_to='documents/cover')
+
+ dc_slug = models.CharField(
+ max_length=2048,
+ null=True, blank=True,
+ editable=False,
+ )
catalogue_book = models.ForeignKey(
'catalogue.Book',
- models.DO_NOTHING,
- to_field='slug',
+ models.PROTECT,
null=True, blank=True,
- db_constraint=False,
- editable=False, db_index=True,
+ editable=False,
related_name='document_books',
related_query_name='document_book',
)
def refresh_dc_cache(self):
update = {
- 'catalogue_book_id': None,
+ 'dc_slug': None,
'dc_cover_image': None,
}
info = self.book_info()
if info is not None:
- update['catalogue_book_id'] = info.url.slug
+ update['dc_slug'] = info.url.slug
if info.cover_source:
try:
image = Image.objects.get(pk=int(info.cover_source.rstrip('/').rsplit('/', 1)[-1]))
</div>
<div class="card-body">
<div class="row">
- <div class="col-md-8">
+ <div class="col-md-6">
{% if editable %}<form method='POST'>{% csrf_token %}{% endif %}
</div>
- <div class="col-md-4">
+ <div class="col-md-6">
{% with cbook=book.catalogue_book %}
{% if cbook %}
- W katalogu:
- <a href="{% url 'catalogue_book' cbook.slug %}">{{ cbook }}</a>
+ <p>
+ W katalogu:
+ <a href="{% url 'catalogue_book' cbook.slug %}">{{ cbook }}</a>
+ </p>
+
+ {% if cbook.slug != book.dc_slug %}
+ <p class="alert alert-warning">
+ Slug książki w metadanych tekstu nie zgadza się ze slugiem książki w katalogu.</p>
+ <p>
+ Upewnij się że tekst jest przypisany do właściwego rekordu w katalogu:<br>
+ <tt>{{ book.dc_slug }}</tt> (tekst)<br>
+ <tt>{{ cbook.slug }}</tt> (<a href="/admin/catalogue/book/{{ cbook.pk }}/change/">katalog</a>)<br>
+ </p>
+ {% endif %}
+
+ {% else %}
+ <p class="alert alert-warning">Tekst nie przypisany do rekordu książki w katalogu.</p>
+
+ {% if book.dc_slug %}
+ {% if cbook_by_slug %}
+ W katalogue znajduje się
+ <a href="/admin/catalogue/book/{{ cbook_by_slug.id }}/change/">rekord książki</a> o slugu:<br>
+ {{ book.dc_slug }}<br><br>
+ {% if request.user.is_staff %}
+ <form method="POST" action="{% url 'documents_book_attach_to_catalogue' book.id %}">
+ {% csrf_token %}
+ <button class="btn btn-primary">Przypisz tekst do tego rekordu w katalogu</button>
+ </form>
+ {% endif %}
+ {% else %}
+ <p>
+ W katalogu brak rekordu o slugu:<br><tt>{{ book.dc_slug }}</tt>
+ </p>
+ {% endif %}
+ {% else %}
+ <p>
+ Ten tekst nie ma slugu określonego w metadanych.
+ </p>
+ {% endif %}
{% endif %}
{% endwith %}
</div>
{{ error }}
</div>
{% endfor %}
+ {% for info in site.info %}
+ <div class="alert alert-info">
+ {{ info }}
+ </div>
+ {% endfor %}
{% endif %}
{% for warning in site.warnings %}
<div class="alert alert-warning">
id:{{ site.id }}
{% endif %}
{% with last=site.last %}
- {% if last %}
- {{ last.created_at }} →
- {{ last.started_at }} →
- {{ last.finished_at }}
- <span title="{{ last.error }}">
- ({{ last.get_status_display }})
- </span>
- <!-- {{ site.last.id }} -->
- {% endif %}
+ {% if last %}
+ {{ last.created_at }} →
+ {{ last.started_at }} →
+ {{ last.finished_at }}
+ <span title="{{ last.error }}">
+ ({{ last.get_status_display }})
+ </span>
+ <!-- {{ site.last.id }} -->
+ {% endif %}
{% endwith %}
{% endfor %}
{% endif %}
path('book/<slug:slug>/pdf', views.book_pdf, name="documents_book_pdf"),
path('book/<slug:slug>/pdf-mobile', views.book_pdf, kwargs={'mobile': True}, name="documents_book_pdf_mobile"),
path('book/<slug:slug>/synchro', views.synchro, name="documents_book_synchro"),
+ path('book/<int:pk>/attach/', views.attach_book_to_catalogue, name="documents_book_attach_to_catalogue"),
path('chunk_add/<slug:slug>/<slug:chunk>/',
views.chunk_add, name="documents_chunk_add"),
from .helpers import active_tab
from .models import (Book, Chunk, Image, BookPublishRecord,
ChunkPublishRecord, ImagePublishRecord, Project)
+import catalogue.models
from fileupload.views import UploadView
#
@never_cache
def book_xml_dc(request, slug):
- book = get_object_or_404(Book, catalogue_book_id=slug)
+ book = get_object_or_404(Book, dc_slug=slug)
return serve_xml(request, book, slug)
except:
pass
- if book.catalogue_book_id:
- audio_items = requests.get(f'https://audio.wolnelektury.pl/archive/book/{book.catalogue_book_id}.json').json()['items']
+ cbook_by_slug = None
+ if book.dc_slug:
+ audio_items = requests.get(f'https://audio.wolnelektury.pl/archive/book/{book.dc_slug}.json').json()['items']
has_audio = bool(audio_items)
can_sell_audio = has_audio and all(x['project']['can_sell'] for x in audio_items)
+
+ if book.catalogue_book is None or book.dc_slug != book.catalogue_book.slug:
+ cbook_by_slug = catalogue.models.Book.objects.filter(slug=book.dc_slug).first()
else:
has_audio = None
can_sell_audio = None
-
+
return render(request, "documents/book_detail.html", {
"book": book,
"doc": doc,
"editable": editable,
"has_audio": has_audio,
"can_sell_audio": can_sell_audio,
+ "cbook_by_slug": cbook_by_slug,
})
'table': table,
'error': error,
})
+
+
+@permission_required('documents.change_book')
+def attach_book_to_catalogue(request, pk):
+ dbook = get_object_or_404(Book, pk=pk)
+ if dbook.dc_slug:
+ cbook = get_object_or_404(catalogue.models.Book, slug=dbook.dc_slug)
+ dbook.catalogue_book = cbook
+ dbook.save()
+ return http.HttpResponseRedirect(dbook.get_absolute_url())
return f'{self.source} -> {self.book}'
def get_absolute_url(self):
- return reverse('source_book_prepare', args=[self.pk])
+ return reverse('source_book_prepare', args=[self.book.pk])
def get_view_files(self):
# TODO: won't work for PDFs.
if dbook is None:
dbook = DBook.create(
user, texts[0],
+ catalogue_book=book,
title=book.title,
slug=str(uuid.uuid4()),
)