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
16 from basicauth import logged_in_or_basicauth, factory_decorator
17 from catalogue.models import Book, Tag
19 from search.views import Search, SearchResult
20 from lucene import Term, QueryWrapperFilter, TermQuery
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"
69 return urljoin("http://%s" % Site.objects.get_current().domain, url)
72 class OPDSFeed(Atom1Feed):
73 link_rel = u"subsection"
74 link_type = u"application/atom+xml"
76 _book_parent_img = full_url(os.path.join(settings.STATIC_URL, "img/book-parent.png"))
78 _book_parent_img_size = unicode(os.path.getsize(os.path.join(settings.STATIC_ROOT, "img/book-parent.png")))
80 _book_parent_img_size = ''
82 _book_img = full_url(os.path.join(settings.STATIC_URL, "img/book.png"))
84 _book_img_size = unicode(os.path.getsize(os.path.join(settings.STATIC_ROOT, "img/book.png")))
89 def add_root_elements(self, handler):
90 super(OPDSFeed, self).add_root_elements(handler)
91 handler.addQuickElement(u"link", None,
92 {u"href": reverse("opds_authors"),
94 u"type": u"application/atom+xml"})
95 handler.addQuickElement(u"link", None,
96 {u"href": full_url(os.path.join(settings.STATIC_URL, "opensearch.xml")),
98 u"type": u"application/opensearchdescription+xml"})
101 def add_item_elements(self, handler, item):
102 """ modified from Atom1Feed.add_item_elements """
103 handler.addQuickElement(u"title", item['title'])
105 # add a OPDS Navigation link if there's no enclosure
106 if item['enclosure'] is None:
107 handler.addQuickElement(u"link", u"", {u"href": item['link'], u"rel": u"subsection", u"type": u"application/atom+xml"})
108 # add a "green book" icon
109 handler.addQuickElement(u"link", '',
110 {u"rel": u"http://opds-spec.org/thumbnail",
111 u"href": self._book_parent_img,
112 u"length": self._book_parent_img_size,
113 u"type": u"image/png"})
114 if item['pubdate'] is not None:
115 handler.addQuickElement(u"updated", rfc3339_date(item['pubdate']).decode('utf-8'))
117 # Author information.
118 if item['author_name'] is not None:
119 handler.startElement(u"author", {})
120 handler.addQuickElement(u"name", item['author_name'])
121 if item['author_email'] is not None:
122 handler.addQuickElement(u"email", item['author_email'])
123 if item['author_link'] is not None:
124 handler.addQuickElement(u"uri", item['author_link'])
125 handler.endElement(u"author")
128 if item['unique_id'] is not None:
129 unique_id = item['unique_id']
131 unique_id = get_tag_uri(item['link'], item['pubdate'])
132 handler.addQuickElement(u"id", unique_id)
135 # OPDS needs type=text
136 if item['description'] is not None:
137 handler.addQuickElement(u"summary", item['description'], {u"type": u"text"})
139 # Enclosure as OPDS Acquisition Link
140 if item['enclosure'] is not None:
141 handler.addQuickElement(u"link", '',
142 {u"rel": u"http://opds-spec.org/acquisition",
143 u"href": item['enclosure'].url,
144 u"length": item['enclosure'].length,
145 u"type": item['enclosure'].mime_type})
146 # add a "red book" icon
147 handler.addQuickElement(u"link", '',
148 {u"rel": u"http://opds-spec.org/thumbnail",
149 u"href": self._book_img,
150 u"length": self._book_img_size,
151 u"type": u"image/png"})
154 for cat in item['categories']:
155 handler.addQuickElement(u"category", u"", {u"term": cat})
158 if item['item_copyright'] is not None:
159 handler.addQuickElement(u"rights", item['item_copyright'])
162 class AcquisitionFeed(Feed):
164 link = u'http://www.wolnelektury.pl/'
165 item_enclosure_mime_type = "application/epub+zip"
166 author_name = u"Wolne Lektury"
167 author_link = u"http://www.wolnelektury.pl/"
169 def item_title(self, book):
172 def item_description(self):
175 def item_link(self, book):
176 return book.get_absolute_url()
178 def item_author_name(self, book):
180 return book.tags.filter(category='author')[0].name
184 def item_author_link(self, book):
186 return book.tags.filter(category='author')[0].get_absolute_url()
190 def item_enclosure_url(self, book):
191 return full_url(book.epub_file.url) if book.epub_file else None
193 def item_enclosure_length(self, book):
194 return book.epub_file.size if book.epub_file else None
197 class RootFeed(Feed):
199 title = u'Wolne Lektury'
200 link = u'http://wolnelektury.pl/'
201 description = u"Spis utworów na stronie http://WolneLektury.pl"
202 author_name = u"Wolne Lektury"
203 author_link = u"http://wolnelektury.pl/"
208 def item_title(self, item):
211 def item_link(self, item):
212 return reverse(item['link'], args=item['link_args'])
214 def item_description(self, item):
215 return item['description']
218 class ByCategoryFeed(Feed):
220 link = u'http://wolnelektury.pl/'
221 description = u"Spis utworów na stronie http://WolneLektury.pl"
222 author_name = u"Wolne Lektury"
223 author_link = u"http://wolnelektury.pl/"
225 def get_object(self, request, category):
226 feed = [feed for feed in _root_feeds if feed['category']==category]
234 def title(self, feed):
237 def items(self, feed):
238 return Tag.objects.filter(category=feed['category']).exclude(book_count=0)
240 def item_title(self, item):
243 def item_link(self, item):
244 return reverse("opds_by_tag", args=[item.category, item.slug])
246 def item_description(self):
250 class ByTagFeed(AcquisitionFeed):
252 return tag.get_absolute_url()
254 def title(self, tag):
257 def description(self, tag):
258 return u"Spis utworów na stronie http://WolneLektury.pl"
260 def get_object(self, request, category, slug):
261 return get_object_or_404(Tag, category=category, slug=slug)
263 def items(self, tag):
264 books = Book.tagged.with_any([tag])
265 l_tags = Tag.objects.filter(category='book', slug__in=[book.book_tag_slug() for book in books.iterator()])
266 descendants_keys = [book.pk for book in Book.tagged.with_any(l_tags)]
268 books = books.exclude(pk__in=descendants_keys)
273 @factory_decorator(logged_in_or_basicauth())
275 class UserFeed(Feed):
277 link = u'http://www.wolnelektury.pl/'
278 description = u"Półki użytkownika na stronie http://WolneLektury.pl"
279 author_name = u"Wolne Lektury"
280 author_link = u"http://wolnelektury.pl/"
282 def get_object(self, request):
285 def title(self, user):
286 return u"Półki użytkownika %s" % user.username
288 def items(self, user):
289 return Tag.objects.filter(category='set', user=user).exclude(book_count=0)
291 def item_title(self, item):
294 def item_link(self, item):
295 return reverse("opds_user_set", args=[item.slug])
297 def item_description(self):
300 # no class decorators in python 2.5
301 #UserFeed = factory_decorator(logged_in_or_basicauth())(UserFeed)
304 @factory_decorator(logged_in_or_basicauth())
306 class UserSetFeed(AcquisitionFeed):
308 return tag.get_absolute_url()
310 def title(self, tag):
313 def description(self, tag):
314 return u"Spis utworów na stronie http://WolneLektury.pl"
316 def get_object(self, request, slug):
317 return get_object_or_404(Tag, category='set', slug=slug, user=request.user)
319 def items(self, tag):
320 return Book.tagged.with_any([tag])
322 # no class decorators in python 2.5
323 #UserSetFeed = factory_decorator(logged_in_or_basicauth())(UserSetFeed)
327 class SearchFeed(AcquisitionFeed):
328 description = u"Wyniki wyszukiwania na stronie WolneLektury.pl"
329 title = u"Wyniki wyszukiwania"
331 INLINE_QUERY_RE = re.compile(r"(author:(?P<author>[^ ]+)|title:(?P<title>[^ ]+)|categories:(?P<categories>[^ ]+)|description:(?P<description>[^ ]+))")
333 def get_object(self, request):
335 For OPDS 1.1 We should handle a query for search terms
336 and criteria provided either as opensearch or 'inline' query.
337 OpenSearch defines fields: atom:author, atom:contributor (treated as translator),
338 atom:title. Inline query provides author, title, categories (treated as book tags),
339 description (treated as content search terms).
341 if search terms are provided, we shall search for books
342 according to Hint information (from author & contributror & title).
344 but if search terms are empty, we should do a different search
345 (perhaps for is_book=True)
349 query = request.GET.get('q', '')
351 inline_criteria = re.findall(self.INLINE_QUERY_RE, query)
353 def get_criteria(criteria, name, position):
354 e = filter(lambda el: el[0][0:len(name)] == name, criteria)
355 log.info("get_criteria: %s" % e)
359 log.info("get_criteria: %s" % c)
360 if c[0] == '"' and c[-1] == '"':
362 c = c.replace('+', ' ')
365 author = get_criteria(inline_criteria, 'author', 1)
366 title = get_criteria(inline_criteria, 'title', 2)
368 categories = get_criteria(inline_criteria, 'categories', 3)
369 query = get_criteria(inline_criteria, 'description', 4)
371 author = request.GET.get('author', '')
372 title = request.GET.get('title', '')
373 translator = request.GET.get('translator', '')
375 # Our client didn't handle the opds placeholders
376 if author == '{atom:author}': author = ''
377 if title == '{atom:title}': title = ''
378 if translator == '{atom:contributor}': translator = ''
385 # Scenario 1: full search terms provided.
386 # Use auxiliarry information to narrow it and make it better.
391 log.info("narrow to author %s" % author)
392 hint.tags(srch.search_tags(srch.make_phrase(srch.get_tokens(author, field='authors'), field='authors'),
393 filt=srch.term_filter(Term('tag_category', 'author'))))
396 log.info("filter by translator %s" % translator)
397 filters.append(QueryWrapperFilter(
398 srch.make_phrase(srch.get_tokens(translator, field='translators'),
399 field='translators')))
402 filters.append(QueryWrapperFilter(
403 srch.make_phrase(srch.get_tokens(categories, field="tag_name_pl"),
404 field='tag_name_pl')))
406 flt = srch.chain_filters(filters)
408 log.info("hint by book title %s" % title)
409 q = srch.make_phrase(srch.get_tokens(title, field='title'), field='title')
410 hint.books(*srch.search_books(q, filt=flt))
412 toks = srch.get_tokens(query)
413 log.info("tokens for query: %s" % toks)
415 results = SearchResult.aggregate(srch.search_perfect_book(toks, fuzzy=fuzzy, hint=hint),
416 srch.search_perfect_parts(toks, fuzzy=fuzzy, hint=hint),
417 srch.search_everywhere(toks, fuzzy=fuzzy, hint=hint))
418 results.sort(reverse=True)
423 except Book.DoesNotExist:
425 log.info("books: %s" % books)
428 # Scenario 2: since we no longer have to figure out what the query term means to the user,
429 # we can just use filters and not the Hint class.
434 'translators': translator,
438 for fld, q in fields.items():
440 filters.append(QueryWrapperFilter(
441 srch.make_phrase(srch.get_tokens(q, field=fld), field=fld)))
443 flt = srch.chain_filters(filters)
444 books = srch.search_books(TermQuery(Term('is_book', 'true')), filt=flt)
447 def get_link(self, query):
448 return "%s?q=%s" % (reverse('search'), query)
450 def items(self, books):