--- /dev/null
+FROM python:3.8 AS base
+
+ARG UID=1000
+ARG GID=1000
+
+RUN apt-get update && apt-get install -y \
+ git \
+ calibre \
+ texlive-xetex texlive-lang-polish \
+ libespeak-dev
+
+COPY requirements/requirements.txt requirements.txt
+
+# numpy -> aeneas
+RUN pip install numpy
+RUN pip install aeneas
+
+RUN pip install --no-cache-dir -r requirements.txt
+RUN pip install --no-cache-dir \
+ psycopg2-binary \
+ django-debug-toolbar==3.2.2 \
+ python-bidi
+
+RUN addgroup --gid $GID app
+RUN adduser --gid $GID --home /app --uid $UID app
+
+RUN apt-get install -y \
+ texlive-extra-utils \
+ texlive-lang-greek \
+ texlive-lang-other \
+ texlive-luatex \
+ texlive-fonts-extra \
+ texlive-fonts-extra-links \
+ fonts-noto-core fonts-noto-extra
+
+
+USER app
+
+# fonts
+RUN cp -a /usr/local/lib/python*/site-packages/librarian/fonts /app/.fonts
+RUN fc-cache
+
+WORKDIR /app/src
+
+
+FROM base AS dev
+
+#RUN pip install --no-cache-dir coverage
+USER app
+
+
+FROM base AS prod
+
+RUN pip install --no-cache-dir gunicorn
+
+USER app
+COPY src /app/src
-.PHONY: deploy test
+.PHONY: deploy test shell
+
+
+UID != id -u
+GID != id -g
deploy: src/wolnelektury/localsettings.py
mv ../htmlcov.new ../htmlcov
coverage report
rm .coverage
+
+
+shell:
+ UID=$(UID) GID=$(GID) docker-compose run --rm dev bash
+
+
+build:
+ UID=$(UID) GID=$(GID) docker-compose build dev
--- /dev/null
+services:
+ dev:
+ build:
+ context: .
+ target: dev
+ args:
+ - "UID=${UID}"
+ - "GID=${GID}"
+ volumes:
+ - ./src:/app/src
+ - ./var/media:/app/var/media
+ - ./var/static:/app/var/static
+ - ./var/counters/:/app/var/counters
+ depends_on:
+ - db
+ db:
+ image: postgres
+ container_name: db
+ env_file:
+ - .env
+ volumes:
+ - ./var/postgresql-data/:/var/lib/postgresql/data/
--- /dev/null
+#!/bin/sh
+export UID=`id -u`
+export GID=`id -g`
+
+if [ "$1" = "runserver" ]
+then
+ PORT="$2"
+ [ -z "$PORT" ] && PORT=8000
+ EXPOSED=127.0.0.1:"$PORT"
+ echo "expose as: $EXPOSED"
+ exec docker-compose run --rm -p "$EXPOSED":"$PORT" dev python $PYARGS manage.py runserver 0.0.0.0:"$PORT"
+else
+ exec docker-compose run --rm dev python $PYARGS manage.py "$@"
+fi
sorl-thumbnail==12.10.0
# home-brewed & dependencies
-librarian==24.5.8
+librarian==24.5.10
# celery tasks
celery[redis]==5.4.0
def get_size(self, obj):
return obj.file.size
+
class BookDetailSerializer(LegacyMixin, serializers.ModelSerializer):
url = AbsoluteURLField()
piwik_track_view(views.BookFragmentView.as_view()),
name='catalogue_api_book_fragment'
),
- path('books/<slug:slug>/media/<slug:type>/', views.BookMediaView.as_view()),
+ path('books/<slug:slug>/media/<slug:type>/',
+ views.BookMediaView.as_view()
+ ),
path('books/<slug:slug>.json',
views.BookJsonView.as_view()),
pagination_class = None
def get_queryset(self):
- return BookMedia.objects.filter(book__slug=self.kwargs['slug'], type=self.kwargs['type']).order_by('index')
+ return BookMedia.objects.filter(
+ book__slug=self.kwargs['slug'],
+ type=self.kwargs['type']
+ ).order_by('index')
from .tojson import conv
book = get_object_or_404(Book, slug=slug)
js = conv(etree.parse(book.xml_file.path))
return JsonResponse(js, json_dumps_params={'ensure_ascii': False})
-
--- /dev/null
+# Generated by Django 4.0.8 on 2025-08-12 09:37
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('catalogue', '0049_book_html_nonotes_file_book_html_nonotes_file_etag_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='book',
+ name='narrators',
+ field=models.ManyToManyField(blank=True, related_name='narrated', to='catalogue.tag'),
+ ),
+ ]
--- /dev/null
+# Generated by Django 4.0.8 on 2025-08-12 10:28
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('catalogue', '0050_book_narrators'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='book',
+ name='has_audio',
+ field=models.BooleanField(default=False),
+ ),
+ ]
from random import randint
import os.path
import re
+from slugify import slugify
+from sortify import sortify
from urllib.request import urlretrieve
from django.apps import apps
from django.conf import settings
tags = managers.TagDescriptor(Tag)
tag_relations = GenericRelation(Tag.intermediary_table_model, related_query_name='tagged_book')
translators = models.ManyToManyField(Tag, blank=True)
+ narrators = models.ManyToManyField(Tag, blank=True, related_name='narrated')
+ has_audio = models.BooleanField(default=False)
html_built = django.dispatch.Signal()
published = django.dispatch.Signal()
return sibling.get_first_text()
return self.parent.get_next_text(inside=False)
- def get_child_audiobook(self):
- BookMedia = apps.get_model('catalogue', 'BookMedia')
- if not BookMedia.objects.filter(book__ancestor=self).exists():
- return None
- for child in self.children.order_by('parent_number').all():
- if child.has_mp3_file():
- return child
- child_sub = child.get_child_audiobook()
- if child_sub is not None:
- return child_sub
-
def get_siblings(self):
if not self.parent:
return []
else:
return self.media.filter(type=type_).exists()
- def has_audio(self):
- return self.has_media('mp3')
-
def get_media(self, type_):
if self.has_media(type_):
if type_ in Book.formats:
def has_sync_file(self):
return settings.FEATURE_SYNCHRO and self.has_media("sync")
+ def build_sync_file(self):
+ from lxml import html
+ from django.core.files.base import ContentFile
+ with self.html_file.open('rb') as f:
+ h = html.fragment_fromstring(f.read().decode('utf-8'))
+
+ durations = [
+ m['mp3'].duration
+ for m in self.get_audiobooks()[0]
+ ]
+ if settings.MOCK_DURATIONS:
+ durations = settings.MOCK_DURATIONS
+
+ sync = []
+ ts = None
+ sid = 1
+ dirty = False
+ for elem in h.iter():
+ if elem.get('data-audio-ts'):
+ part, ts = int(elem.get('data-audio-part')), float(elem.get('data-audio-ts'))
+ ts = str(round(sum(durations[:part - 1]) + ts, 3))
+ # check if inside verse
+ p = elem.getparent()
+ while p is not None:
+ # Workaround for missing ids.
+ if 'verse' in p.get('class', ''):
+ if not p.get('id'):
+ p.set('id', f'syn{sid}')
+ dirty = True
+ sid += 1
+ sync.append((ts, p.get('id')))
+ ts = None
+ break
+ p = p.getparent()
+ elif ts:
+ cls = elem.get('class', '')
+ # Workaround for missing ids.
+ if 'paragraph' in cls or 'verse' in cls or elem.tag in ('h1', 'h2', 'h3', 'h4'):
+ if not elem.get('id'):
+ elem.set('id', f'syn{sid}')
+ dirty = True
+ sid += 1
+ sync.append((ts, elem.get('id')))
+ ts = None
+ if dirty:
+ htext = html.tostring(h, encoding='utf-8')
+ with open(self.html_file.path, 'wb') as f:
+ f.write(htext)
+ try:
+ bm = self.media.get(type='sync')
+ except:
+ bm = BookMedia(book=self, type='sync')
+ sync = (
+ '27\n' + '\n'.join(
+ f'{s[0]}\t{sync[i+1][0]}\t{s[1]}' for i, s in enumerate(sync[:-1])
+ )).encode('latin1')
+ bm.file.save(
+ None, ContentFile(sync)
+ )
+
+
def get_sync(self):
with self.get_media('sync').first().file.open('r') as f:
sync = f.read().split('\n')
def media_audio_epub(self):
return self.get_media('audio.epub')
- def get_audiobooks(self):
+ def get_audiobooks(self, with_children=False, processing=False):
ogg_files = {}
for m in self.media.filter(type='ogg').order_by().iterator():
ogg_files[m.name] = m
media['ogg'] = ogg
audiobooks.append(media)
- projects = sorted(projects)
- total_duration = '%d:%02d' % (
- total_duration // 60,
- total_duration % 60
- )
+ if with_children:
+ for child in self.get_children():
+ ch_audiobooks, ch_projects, ch_duration = child.get_audiobooks(
+ with_children=True, processing=True)
+ audiobooks.append({'part': child})
+ audiobooks += ch_audiobooks
+ projects.update(ch_projects)
+ total_duration += ch_duration
+
+ if not processing:
+ projects = sorted(projects)
+ total_duration = '%d:%02d' % (
+ total_duration // 60,
+ total_duration % 60
+ )
+
return audiobooks, projects, total_duration
+ def get_audiobooks_with_children(self):
+ return self.get_audiobooks(with_children=True)
+
def wldocument(self, parse_dublincore=True, inherit=True):
from catalogue.import_utils import ORMDocProvider
from librarian.parser import WLDocument
def references(self):
return self.reference_set.all().select_related('entity')
+ def update_has_audio(self):
+ self.has_audio = False
+ if self.media.filter(type='mp3').exists():
+ self.has_audio = True
+ if self.descendant.filter(has_audio=True).exists():
+ self.has_audio = True
+ self.save(update_fields=['has_audio'])
+ if self.parent is not None:
+ self.parent.update_has_audio()
+
+ def update_narrators(self):
+ narrator_names = set()
+ for bm in self.media.filter(type='mp3'):
+ narrator_names.update(set(
+ a.strip() for a in re.split(r',|\si\s', bm.artist)
+ ))
+ narrators = []
+
+ for name in narrator_names:
+ if not name: continue
+ slug = slugify(name)
+ try:
+ t = Tag.objects.get(category='author', slug=slug)
+ except Tag.DoesNotExist:
+ sort_key = sortify(
+ ' '.join(name.rsplit(' ', 1)[::-1]).lower()
+ )
+ t = Tag.objects.create(
+ category='author',
+ name_pl=name,
+ slug=slug,
+ sort_key=sort_key,
+ )
+ narrators.append(t)
+ self.narrators.set(narrators)
+
@classmethod
@transaction.atomic
def repopulate_ancestors(cls):
return f'{name}.{ext}'
def save(self, parts_count=None, *args, **kwargs):
+ if self.type in ('daisy', 'audio.epub'):
+ return super().save(*args, **kwargs)
from catalogue.utils import ExistingFile, remove_zip
if not parts_count:
self.extra_info = json.dumps(extra_info)
self.source_sha1 = self.read_source_sha1(self.file.path, self.type)
self.duration = self.read_duration()
- return super(BookMedia, self).save(*args, **kwargs)
+ super(BookMedia, self).save(*args, **kwargs)
+ self.book.update_narrators()
+ self.book.update_has_audio()
def read_duration(self):
try:
}
$("#paginator").on('click', 'a', function() {
- get_page_by_url(url=$(this).attr('href'));
+ get_page_by_url(url=$(this).attr('href'), () => {
+ $("html").animate({
+ scrollTop: $("#book-list").offset().top - 50
+ });
+ });
return false;
});
$(".c-media__caption .license", $root).html($(".license", elem).html());
$(".c-media__caption .project-logo", $root).html($(".project-icon", elem).html());
- console.log('sm 1');
doesUpdateSynchro = false;
if (!$currentMedia || $currentMedia[0] != elem[0]) {
- console.log('set', player.jPlayer("setMedia", media))
+ player.jPlayer("setMedia", media);
player.jPlayer("option", "playbackRate", speed);
}
doesUpdateSynchro = true;
player.jPlayer(cmd, time);
$currentMedia = elem;
- $(".play-next", $root).prop("disabled", !elem.next().length);
+ $(".play-next", $root).prop("disabled", !elem.nextAll('li').length);
let du = parseFloat(elem.data('duration'));
currentDuration = du;
- elem.nextAll().each(function() {
+ elem.nextAll('li').each(function() {
du += parseFloat($(this).data('duration'));
});
totalDurationLeft = du;
let pdu = 0;
- elem.prevAll().each(function() {
+ elem.prevAll('li').each(function() {
pdu += parseFloat($(this).data('duration'));
});
totalDurationBefore = pdu;
- console.log('sm 3', du, pdu);
return player;
};
// TODO: if snap then roll
locator.removeClass('up').removeClass('down');
if (locator.hasClass('snap')) {
- console.log('SCROLL!');
scrollTo();
} else {
if (y < miny) {
});
$('.play-next', $root).click(function() {
- let p = $currentMedia.next();
+ let p = $currentMedia.nextAll('li').first();
if (p.length) {
setMedia(p).jPlayer("play");
_paq.push(['trackEvent', 'audiobook', 'next']);
}
});
$('.play-prev', $root).click(function() {
- let p = $currentMedia.prev();
+ let p = $currentMedia.prevAll('li').first();
if (p.length) {
setMedia(p).jPlayer("play");
_paq.push(['trackEvent', 'audiobook', 'prev']);
_paq.push(['trackEvent', 'audiobook', 'chapter']);
});
- console.log('READY 3!');
var initialElem = $('.jp-playlist li', $root).first();
var initialTime = 0;
- console.log('READY 4!');
if (true || Modernizr.localstorage) {
try {
let speedStr = localStorage['audiobook-speed'];
initialTime = last[2];
}
}
- console.log('READY 5!', initialElem, initialTime);
setMedia($(initialElem), initialTime);
- console.log('READY 6!');
},
timeupdate: function(event) {
ended: function(event) {
- let p = $currentMedia.next();
+ let p = $currentMedia.nextAll('li');
if (p.length) {
setMedia(p).jPlayer("play");
}
{% if book.is_book %}
<span class="icon icon-book-alt" title="{% trans 'książka' %}"></span>
{% endif %}
- {% if book.has_mp3_file %}
+ {% if book.has_audio %}
<span class="icon icon-audio" title="{% trans 'audiobook' %}"></span>
{% endif %}
{% if book.is_picture %}
<div class="c-media__btn">
<button class="l-button l-button--media" id="ebook"><i class="icon icon-book"></i> {% trans "pobierz książkę" %}</button>
</div>
+ <div class="c-media__btn">
+ {% if first_text %}
+ <a href="https://elevenreader.io/audiobooks/wolnelektury:{{ first_text.slug }}" target="_blank" class="l-button l-button--media"><img src="{% static 'img/elevenreader-21.png' %}" title="{% trans "Posłuchaj w ElevenReader" %}" alt="{% trans "Posłuchaj w ElevenReader" %}"></a>
+ {% endif %}
+ </div>
<div class="c-media__btn">
{% if first_text %}
<a href="{% url 'book_text' first_text.slug %}" class="l-button l-button--media l-button--media--full"><i class="icon icon-eye"></i> {% trans "czytaj online" %}</a>
</div>
-
- {% if book.has_mp3_file %}
+ {% if book.has_audio %}
{% include 'catalogue/snippets/jplayer.html' %}
- {% else %}
- {% with ch=book.get_child_audiobook %}
- {% if ch %}
- {% include 'catalogue/snippets/jplayer_link.html' with book=ch %}
- {% endif %}
- {% endwith %}
-
{% endif %}
-
-
<div class="c-media__popup" data-popup="ebook">
<div class="c-media__popup__box">
<div class="c-media__popup__box__lead">
</div>
{% endif %}
+ {% if narrated %}
+ <div class="l-section l-section--col">
+ <h2 class="header">Audiobooki</h2>
+ <div class="l-books__grid" id="book-list">
+ {% for book in narrated %}
+ {% include "catalogue/book_box.html" %}
+ {% endfor %}
+ </div>
+ </div>
+ {% endif %}
+
{% if main_tag %}
<section class="l-section">
<div class="l-author">
{% load i18n l10n catalogue_tags %}
-{% with audiobooks=book.get_audiobooks %}
+{% with audiobooks=book.get_audiobooks_with_children %}
<div class="c-media__player" id="jp_container_{{ book.pk }}" data-book-slug="{{ book.slug }}">
<div class="jp-jplayer" data-player="jp_container_{{ book.pk }}"
data-supplied="oga,mp3"></div>
<ul class="jp-playlist">
{% localize off %}
{% for i in audiobooks.0 %}
+ {% if i.part %}
+ <div class="title"><strong>{{ i.part.title }}</strong></div>
+ {% else %}
<li
data-mp3='{{ i.mp3.file.url }}'
data-ogg='{{ i.ogg.file.url }}'
</span>
{% endwith %}
</li>
+ {% endif %}
{% endfor %}
{% endlocalize %}
</ul>
+++ /dev/null
-{% load i18n catalogue_tags %}
-
-<div class="c-media__player" id="jp_container_{{ book.pk }}">
- <div class="c-player__head">
- <span> </span>
- </div>
-
- <div class="c-player">
- <div class="c-player__btns">
- <button disabled class="play-prev"><i class="icon icon-prev"></i></button>
- <button disabled class="c-player__btn--md"><i class="icon icon-play"></i></button>
- <form action='{{ book.get_absolute_url }}'>
- <button class="play-next"><i class="icon icon-next"></i></button>
- </form>
- </div>
-
- <div class="c-player__timeline">
- <div class="c-player__info">{{ book.pretty_title }}</div>
- </div>
-
- </div>
- <div class="c-media__caption">
- </div>
-</div>
]
if len(self.ctx['tags']) == 1 and self.ctx['main_tag'] is not None and self.ctx['main_tag'].category == 'author':
self.ctx['translation_list'] = self.ctx['main_tag'].book_set.all()
+ self.ctx['narrated'] = self.ctx['main_tag'].narrated.all()
def get_queryset(self):
if self.ctx['work_tags']:
d = response.json()
return d
- def create_or_update_contact(self, email, key=None):
+ def create_or_update_contact(self, email, fields=None):
contact_id = self.get_contact_id(email)
if contact_id is None:
- contact_id = self.create_contact(email, key)
- elif key:
- self.update_contact(contact_id, key)
+ contact_id = self.create_contact(email, fields)
+ elif fields:
+ self.update_contact(contact_id, fields)
return contact_id
def get_contact_id(self, email):
if result:
return result[0]['id']
- def create_contact(self, email, key=None):
+ def create_contact(self, email, fields):
data = {
'values': {},
'chain': {
]
}
}
- if key:
- data['values']['WL.TPWL_key'] = key
+ if fields:
+ data['values'].update(fields)
result = self.request('Contact', 'create', data)
return result['values'][0]['id']
-
- def update_contact(self, contact_id, key):
+
+ def update_phone(self, contact_id, phone):
+ if self.request('Phone', 'get', {'where': [['phone', "=", phone], ['contact_id', "=", contact_id]]})['count']:
+ return
+ self.request('Phone', 'create', {'values': {'phone': phone, 'contact_id': contact_id}})
+
+ def update_contact(self, contact_id, fields):
return self.request(
'Contact',
'update',
{
- 'values': {
- 'WL.TPWL_key': key,
- },
+ 'values': fields,
'where': [
['id', '=', contact_id]
]
if not self.enabled:
return
- contact_id = self.create_or_update_contact(email, tpwl_key)
+ fields = {'WL.TPWL_key': tpwl_key}
+ contact_id = self.create_or_update_contact(email, fields)
activity_id = self.get_activity_id(key)
if activity_id is None:
def populate_from_wikidata(self, wikidata_id):
client = Client()
+ client.opener.addheaders = [(
+ 'User-Agent', 'Wolne Lektury / https://wolnelektury.pl / Python-wikidata'
+ )]
entity = client.get(wikidata_id)
self.label = entity.label.get('pl', entity.label) or ''
# ====================
-@require_POST
+@login_required
def like_book(request, slug):
- if not request.user.is_authenticated:
- return HttpResponseForbidden('Login required.')
book = get_object_or_404(Book, slug=slug)
+ if request.method != 'POST':
+ return redirect(book)
+
models.UserList.like(request.user, book)
if is_ajax(request):
form_class = forms.RemoveSetForm
-@require_POST
+@login_required
def unlike_book(request, slug):
- if not request.user.is_authenticated:
- return HttpResponseForbidden('Login required.')
book = get_object_or_404(Book, slug=slug)
+ if request.method != 'POST':
+ return redirect(book)
+
models.UserList.unlike(request.user, book)
if is_ajax(request):
# This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
#
-from os import path
-from .paths import PROJECT_DIR
+import os
DEBUG = True
CONTACT_EMAIL = 'fundacja@wolnelektury.pl'
+ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split()
+
CACHE_MIDDLEWARE_SECONDS = 3 * 60
DATABASES = {
'default': {
- 'ENGINE': 'django.db.backends.sqlite3', # 'postgresql_psycopg2'
- 'NAME': path.join(PROJECT_DIR, 'dev.db'),
- 'USER': '', # Not used with sqlite3.
- 'PASSWORD': '', # Not used with sqlite3.
- 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
+ 'ENGINE': 'django.db.backends.postgresql',
+ 'HOST': 'db',
+ 'USER': os.environ.get('POSTGRES_USER'),
+ 'PASSWORD': os.environ.get('POSTGRES_PASSWORD'),
+ 'NAME': os.environ.get('POSTGRES_USER'),
}
}
}
-HAYSTACK_CONNECTIONS = {
- 'default': {
- 'ENGINE': 'haystack.backends.simple_backend.SimpleEngine',
- },
-}
-
-
FORMS_BUILDER_USE_SITES = False
FORMS_BUILDER_EDITABLE_FIELD_MAX_LENGTH = True
FORMS_BUILDER_EDITABLE_SLUGS = True
LATEST_BLOG_POSTS = "https://fundacja.wolnelektury.pl/feed/?cat=-135"
-CATALOGUE_COUNTERS_FILE = os.path.join(VAR_DIR, 'catalogue_counters.p')
+CATALOGUE_COUNTERS_FILE = os.path.join(VAR_DIR, 'counters/catalogue_counters.p')
+LESMIANATOR_PICKLE = os.path.join(VAR_DIR, 'counters/lesmianator.p')
+
+NO_SEARCH_INDEX = False
CATALOGUE_MIN_INITIALS = 60
VARIANTS = {
}
-EPUB_FUNDRAISING = []
-
CIVICRM_BASE = None
CIVICRM_KEY = None
.c-media__actions {
display: flex;
- column-gap: 38px;
+ column-gap: 20px;
row-gap: 10px;
}
display: inline;
}
}
+ div.title {
+ font-size: 12px;
+ line-height: 140%;
+ letter-spacing: 0.05em;
+ list-style-type: decimal;
+ margin: 5px 0;
+ color: white;
+ cursor: pointer;
+ }
}
}
@include rwd($break-flow) {
@include font-size(16px);
line-height: 20px;
- padding: 19px 20px;
+ padding: 19px 0;
}
.icon {
font-size: 21px;
- margin-right: 15px;
+ margin-right: 10px;
+ }
+
+ img {
+ height: 21px;
}
&:hover {
}
#other {
+ display: none;
+ @include rwd($break-wide) {
+ display: block;
+ }
ul {
list-style: none;
margin: 0;
#menu {
- padding-bottom: 50px;
* {
box-sizing: content-box;
}
function upd_t() {
$text = $('#main-text #book-text');
+ if (!$text.length) return;
+
texttop = $text.offset().top;
$footnotes = $('#footnotes', $text);
}
var map_enabled = false;
- var marker = L.circleMarker([0,0]);
+ var marker = null;
var map = null;
function enable_map() {
+ if (!$("#reference-map").length) return;
+
$("#reference-map").show('slow');
if (map_enabled) return;
L.tileLayer('https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=a8a97f0ae5134403ac38c1a075b03e15', {
attribution: 'Maps © <a href="http://www.thunderforest.com">Thunderforest</a>, Data © <a href="http://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>'
}).addTo(map);
+ marker = L.circleMarker([0,0]);
map_enabled = true;
}
$.post({
url: '/ludzie/lektura/' + $(this).attr('data-book-slug') + '/nie_lubie/',
data: {'csrfmiddlewaretoken': $('[name=csrfmiddlewaretoken]').val()},
+ dataType: 'json',
success: function() {
delete state.liked[$btn.attr('data-book')];
updateLiked($btn);
+ },
+ error: function() {
+ window.location.href = $('#login-link').attr('href');
}
})
} else {
$.post({
url: '/ludzie/lektura/' + $(this).attr('data-book-slug') + '/lubie/',
data: {'csrfmiddlewaretoken': $('[name=csrfmiddlewaretoken]').val()},
+ dataType: 'json',
success: function() {
state.liked[$btn.attr('data-book')] = [];
updateLiked($btn);
},
- error: function(e) {
- if (e.status == 403) {
- $('#login-link').click();
- }
- },
+ error: function() {
+ window.location.href = $('#login-link').attr('href')
+ }
});
}
})
<form action="{% url 'import_book' %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
<p>
- <input type="file" id="id_book_xml_file" name="book_xml_file"/>
+ <input type="file" id="id_book_xml_file" name="book_xml_file" required/>
<input type="submit" value="Import książki" />
</p>
</form>