1 # -*- coding: utf-8 -*-
2 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
3 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
6 from urlparse import urljoin
8 from django.contrib.syndication.views import Feed
9 from django.core.urlresolvers import reverse
10 from django.shortcuts import get_object_or_404
11 from django.utils.feedgenerator import Atom1Feed
12 from django.conf import settings
13 from django.http import Http404
14 from django.contrib.sites.models import Site
15 from django.utils.functional import lazy
17 from basicauth import logged_in_or_basicauth, factory_decorator
18 from catalogue.models import Book, Tag
20 from search.views import Search
25 log = logging.getLogger('opds')
27 from stats.utils import piwik_track
32 u"link": u"opds_user",
34 u"title": u"Moje półki",
35 u"description": u"Półki użytkownika dostępne po zalogowaniu"
38 u"category": u"author",
39 u"link": u"opds_by_category",
40 u"link_args": [u"author"],
42 u"description": u"Utwory wg autorów"
46 u"link": u"opds_by_category",
47 u"link_args": [u"kind"],
49 u"description": u"Utwory wg rodzajów"
52 u"category": u"genre",
53 u"link": u"opds_by_category",
54 u"link_args": [u"genre"],
56 u"description": u"Utwory wg gatunków"
59 u"category": u"epoch",
60 u"link": u"opds_by_category",
61 u"link_args": [u"epoch"],
63 u"description": u"Utwory wg epok"
68 current_domain = lazy(lambda: Site.objects.get_current().domain, str)()
70 return urljoin("http://%s" % current_domain, url)
73 class OPDSFeed(Atom1Feed):
74 link_rel = u"subsection"
75 link_type = u"application/atom+xml"
77 _book_parent_img = lazy(lambda: full_url(os.path.join(settings.STATIC_URL, "img/book-parent.png")))()
79 _book_parent_img_size = unicode(os.path.getsize(os.path.join(settings.STATIC_ROOT, "img/book-parent.png")))
81 _book_parent_img_size = ''
83 _book_img = lazy(lambda: full_url(os.path.join(settings.STATIC_URL, "img/book.png")))()
85 _book_img_size = unicode(os.path.getsize(os.path.join(settings.STATIC_ROOT, "img/book.png")))
90 def add_root_elements(self, handler):
91 super(OPDSFeed, self).add_root_elements(handler)
92 handler.addQuickElement(u"link", None,
93 {u"href": reverse("opds_authors"),
95 u"type": u"application/atom+xml"})
96 handler.addQuickElement(u"link", None,
97 {u"href": full_url(os.path.join(settings.STATIC_URL, "opensearch.xml")),
99 u"type": u"application/opensearchdescription+xml"})
102 def add_item_elements(self, handler, item):
103 """ modified from Atom1Feed.add_item_elements """
104 handler.addQuickElement(u"title", item['title'])
106 # add a OPDS Navigation link if there's no enclosure
107 if item['enclosure'] is None:
108 handler.addQuickElement(u"link", u"", {u"href": item['link'], u"rel": u"subsection", u"type": u"application/atom+xml"})
109 # add a "green book" icon
110 handler.addQuickElement(u"link", '',
111 {u"rel": u"http://opds-spec.org/thumbnail",
112 u"href": self._book_parent_img,
113 u"length": self._book_parent_img_size,
114 u"type": u"image/png"})
115 if item['pubdate'] is not None:
116 # FIXME: rfc3339_date is undefined, is this ever run?
117 handler.addQuickElement(u"updated", rfc3339_date(item['pubdate']).decode('utf-8'))
119 # Author information.
120 if item['author_name'] is not None:
121 handler.startElement(u"author", {})
122 handler.addQuickElement(u"name", item['author_name'])
123 if item['author_email'] is not None:
124 handler.addQuickElement(u"email", item['author_email'])
125 if item['author_link'] is not None:
126 handler.addQuickElement(u"uri", item['author_link'])
127 handler.endElement(u"author")
130 if item['unique_id'] is not None:
131 unique_id = item['unique_id']
133 # FIXME: get_tag_uri is undefined, is this ever run?
134 unique_id = get_tag_uri(item['link'], item['pubdate'])
135 handler.addQuickElement(u"id", unique_id)
138 # OPDS needs type=text
139 if item['description'] is not None:
140 handler.addQuickElement(u"summary", item['description'], {u"type": u"text"})
142 # Enclosure as OPDS Acquisition Link
143 if item['enclosure'] is not None:
144 handler.addQuickElement(u"link", '',
145 {u"rel": u"http://opds-spec.org/acquisition",
146 u"href": item['enclosure'].url,
147 u"length": item['enclosure'].length,
148 u"type": item['enclosure'].mime_type})
149 # add a "red book" icon
150 handler.addQuickElement(u"link", '',
151 {u"rel": u"http://opds-spec.org/thumbnail",
152 u"href": self._book_img,
153 u"length": self._book_img_size,
154 u"type": u"image/png"})
157 for cat in item['categories']:
158 handler.addQuickElement(u"category", u"", {u"term": cat})
161 if item['item_copyright'] is not None:
162 handler.addQuickElement(u"rights", item['item_copyright'])
165 class AcquisitionFeed(Feed):
167 link = u'http://www.wolnelektury.pl/'
168 item_enclosure_mime_type = "application/epub+zip"
169 author_name = u"Wolne Lektury"
170 author_link = u"http://www.wolnelektury.pl/"
172 def item_title(self, book):
175 def item_description(self):
178 def item_link(self, book):
179 return book.get_absolute_url()
181 def item_author_name(self, book):
183 return book.tags.filter(category='author')[0].name
187 def item_author_link(self, book):
189 return book.tags.filter(category='author')[0].get_absolute_url()
193 def item_enclosure_url(self, book):
194 return full_url(book.epub_file.url) if book.epub_file else None
196 def item_enclosure_length(self, book):
197 return book.epub_file.size if book.epub_file else None
200 class RootFeed(Feed):
202 title = u'Wolne Lektury'
203 link = u'http://wolnelektury.pl/'
204 description = u"Spis utworów na stronie http://WolneLektury.pl"
205 author_name = u"Wolne Lektury"
206 author_link = u"http://wolnelektury.pl/"
211 def item_title(self, item):
214 def item_link(self, item):
215 return reverse(item['link'], args=item['link_args'])
217 def item_description(self, item):
218 return item['description']
221 class ByCategoryFeed(Feed):
223 link = u'http://wolnelektury.pl/'
224 description = u"Spis utworów na stronie http://WolneLektury.pl"
225 author_name = u"Wolne Lektury"
226 author_link = u"http://wolnelektury.pl/"
228 def get_object(self, request, category):
229 feed = [feed for feed in _root_feeds if feed['category'] == category]
237 def title(self, feed):
240 def items(self, feed):
241 return Tag.objects.filter(category=feed['category']).exclude(book_count=0)
243 def item_title(self, item):
246 def item_link(self, item):
247 return reverse("opds_by_tag", args=[item.category, item.slug])
249 def item_description(self):
253 class ByTagFeed(AcquisitionFeed):
255 return tag.get_absolute_url()
257 def title(self, tag):
260 def description(self, tag):
261 return u"Spis utworów na stronie http://WolneLektury.pl"
263 def get_object(self, request, category, slug):
264 return get_object_or_404(Tag, category=category, slug=slug)
266 def items(self, tag):
267 books = Book.tagged.with_any([tag])
268 l_tags = Tag.objects.filter(category='book', slug__in=[book.book_tag_slug() for book in books.iterator()])
269 descendants_keys = [book.pk for book in Book.tagged.with_any(l_tags)]
271 books = books.exclude(pk__in=descendants_keys)
276 @factory_decorator(logged_in_or_basicauth())
278 class UserFeed(Feed):
280 link = u'http://www.wolnelektury.pl/'
281 description = u"Półki użytkownika na stronie http://WolneLektury.pl"
282 author_name = u"Wolne Lektury"
283 author_link = u"http://wolnelektury.pl/"
285 def get_object(self, request):
288 def title(self, user):
289 return u"Półki użytkownika %s" % user.username
291 def items(self, user):
292 return Tag.objects.filter(category='set', user=user).exclude(book_count=0)
294 def item_title(self, item):
297 def item_link(self, item):
298 return reverse("opds_user_set", args=[item.slug])
300 def item_description(self):
303 # no class decorators in python 2.5
304 #UserFeed = factory_decorator(logged_in_or_basicauth())(UserFeed)
307 @factory_decorator(logged_in_or_basicauth())
309 class UserSetFeed(AcquisitionFeed):
311 return tag.get_absolute_url()
313 def title(self, tag):
316 def description(self, tag):
317 return u"Spis utworów na stronie http://WolneLektury.pl"
319 def get_object(self, request, slug):
320 return get_object_or_404(Tag, category='set', slug=slug, user=request.user)
322 def items(self, tag):
323 return Book.tagged.with_any([tag])
325 # no class decorators in python 2.5
326 #UserSetFeed = factory_decorator(logged_in_or_basicauth())(UserSetFeed)
330 class SearchFeed(AcquisitionFeed):
331 description = u"Wyniki wyszukiwania na stronie WolneLektury.pl"
332 title = u"Wyniki wyszukiwania"
334 QUOTE_OR_NOT = r'(?:(?=["])"([^"]+)"|([^ ]+))'
335 INLINE_QUERY_RE = re.compile(
336 r"author:" + QUOTE_OR_NOT +
337 "|translator:" + QUOTE_OR_NOT +
338 "|title:" + QUOTE_OR_NOT +
339 "|categories:" + QUOTE_OR_NOT +
340 "|description:" + QUOTE_OR_NOT +
341 "|text:" + QUOTE_OR_NOT
345 'translator': (2, 3),
347 'categories': (6, 7),
348 'description': (8, 9),
354 'translator': 'translators',
356 'categories': 'tag_name_pl',
357 'description': 'text',
361 ATOM_PLACEHOLDER = re.compile(r"^{(atom|opds):\w+}$")
363 def get_object(self, request):
365 For OPDS 1.1 We should handle a query for search terms
366 and criteria provided either as opensearch or 'inline' query.
367 OpenSearch defines fields: atom:author, atom:contributor (treated as translator),
368 atom:title. Inline query provides author, title, categories (treated as book tags),
369 description (treated as content search terms).
371 if search terms are provided, we shall search for books
372 according to Hint information (from author & contributror & title).
374 but if search terms are empty, we should do a different search
375 (perhaps for is_book=True)
379 query = request.GET.get('q', '')
381 inline_criteria = re.findall(self.INLINE_QUERY_RE, query)
383 remains = re.sub(self.INLINE_QUERY_RE, '', query)
384 remains = re.sub(r'[ \t]+', ' ', remains)
386 def get_criteria(criteria, name):
388 for p in self.MATCHES[name]:
391 return c[p].replace('+', ' ')
396 lambda cn: (cn, get_criteria(inline_criteria, cn)),
397 ['author', 'translator', 'title', 'categories',
398 'description', 'text']))
400 # empty query and text set case?
401 log.debug("Inline query = [%s], criteria: %s" % (query, criteria))
403 def remove_dump_data(val):
404 """Some clients don't get opds placeholders and just send them."""
405 if self.ATOM_PLACEHOLDER.match(val):
409 criteria = dict([(cn, remove_dump_data(request.GET.get(cn, '')))
410 for cn in self.MATCHES.keys()])
411 # query is set above.
412 log.debug("Inline query = [%s], criteria: %s" % (query, criteria))
416 book_hit_filter = srch.index.Q(book_id__any=True)
417 filters = [book_hit_filter] + [srch.index.Q(
418 **{self.PARAMS_TO_FIELDS.get(cn, cn): criteria[cn]}
419 ) for cn in self.MATCHES.keys() if cn in criteria
423 q = srch.index.query(
425 [srch.index.Q(**{self.PARAMS_TO_FIELDS.get(cn, cn): query})
426 for cn in self.MATCHES.keys()],
429 q = srch.index.query(srch.index.Q())
431 q = srch.apply_filters(q, filters).field_limit(score=True, fields=['book_id'])
432 results = q.execute()
434 book_scores = dict([(r['book_id'], r['score']) for r in results])
435 books = Book.objects.filter(id__in=set([r['book_id'] for r in results]))
437 books.sort(reverse=True, key=lambda book: book_scores[book.id])
440 def get_link(self, query):
441 return "%s?q=%s" % (reverse('search'), query)
443 def items(self, books):